Green Donut is a port of facebook's DataLoader utility, written in C# for .NET Core and .NET Framework.
DataLoader is a generic utility to be used as part of your application's data fetching layer to provide a consistent API over various backends and reduce requests to those backends via batching and caching. -- facebook
DataLoader are perfect in various client-side and server-side scenarios. Although, they are
usually know for solving the N+1
problem in GraphQL APIs. DataLoader decouple any kind of
request in a simplified way to a backend resource like a database or a web service to reduce the
overall traffic to those resources by using two common techniques in computer science namely
batching and caching. With batching we decrease the amount of requests to a backend resource by
grouping single requests into one batch request. Whereas with caching we avoid requesting a backend
resource at all.
First things first, install the package via NuGet.
For .NET Core we use the dotnet
CLI, which is perhaps the preferred way doing this.
dotnet add package GreenDonut
And for .NET Framework we still use the following line.
Install-Package GreenDonut
People who prefer a UI to install packages might wanne use the NuGet Package Manager, which is provided by Visual Studio.
After we have installed the package, we should probably start using it, right. We really tried to keep the API of DataLoader congruent to the original facebook implementation which is written in JavaScript, but without making the experience for us .NET developers weird.
The simplest way to get started is to create an instance of the default DataLoader implementation, which might be the right choice if you need just one type of DataLoader. However, if you need a bunch of individual DataLoader and/or using DI, which is an abbreviation for Dependency Injection, you might wanne also take a look at the Custom DataLoader section.
Creating a new instance is easy as you will see in the following example. The tricky part here is to
implement our data fetching logic - here shown as FetchUsers
- which depends on our backend
resource. Once we have done that, we just pass our fetch function into the DataLoader constructor.
That's actually everything so far.
var userLoader = new DataLoader<string, User>(FetchUsers);
In order to change the default behavior of a DataLoader
, we have to create a new instance of
DataLoaderOptions
and pass it right into the DataLoader
constructor. Lets see how that looks
like.
var options = new DataLoaderOptions<string>
{
SlidingExpiration = TimeSpan.FromHours(1)
};
var userLoader = new DataLoader<string, User>(keys => FetchUsers(keys), options);
So, what we see here is that we have changed the SlidingExpiration
from its default value, which
is 0
to 1 hour
. 0
means the cache entries will live forever in the cache as long as the
maximum cache size does not exceed. Whereas 1 hour
means a single cache entry will stay in the
cache as long as the entry gets touched within one hour. This is an additional feature that does not
exist in the original facebook implementation.
Fetching data consists of two parts. First part is declaring your need in one or more data items by providing one or more keys.
await userLoader.LoadAsync("Foo", "Bar", "Baz");
The second part is dispatching our requested data items. There are two options. First option is
manual dispatching the default behavior as of version 2.0.0
. As the name says,
manual dispatching means we have to trigger the dispatching process manually; otherwise no data is
being fetched. This is actually an important difference to facebook's original implementation,
which is written in JavaScript. Facebook's implementation is using a trick in NodeJs to
dispatch automatically. If you're interested how that works, click
here
to learn more about that. But now lets see how we trigger the dispatching process manually.
await userLoader.DispatchAsync();
The second option is, we enable auto dispatching which dispatches permanently in the background. This process starts immediately after creating a new instance of the DataLoader. Lets see how that looks like.
var options = new DataLoaderOptions<string>
{
AutoDispatching = true
};
var userLoader = new DataLoader<string, User>(FetchUsers, options);
In this case we wouldn't need to call DispatchAsync
at all.
A custom DataLoader is especially useful in DI scenarios.
public interface IUserDataLoader
: IDataLoader<string, User>
{ }
Although the extra interface IUserDataLoader
isn't necessarily required, we strongly recommend to
create an extra interface in this particular case because of several reasons. One reason is you
might have a handful of DataLoader which implemanting a completely different data fetching logic,
but from the outside they look identic due to their identic type parameter list. That's why we
should always create a separate interface for each DataLoader. We just mentioned one reason here
because the explanation would go beyond the scope of custom DataLoader.
public class UserDataLoader
: DataLoaderBase<string, User>
, IUserDataLoader
{
protected override Task<IReadOnlyList<Result<User>>> Fetch(IReadOnlyList<string> keys)
{
// Here goes our data fetching logic
}
}
The API shown here is simplified. Means we have omitted some information for brevity purpose like type information, method overloads, return values and so on. If you're interested in those kind of information - and we bet you're - then click here for being transferred to our documentation.
Name | Description |
---|---|
RequestBuffered |
Raises when an incoming data request is added to the buffer. Will never be raised if batching is disabled. |
Name | Description |
---|---|
BufferedRequests |
Gets the current count of buffered data requests waiting for being dispatched as batches. Will always return 0 if batching is disabled. |
CachedValues |
Gets the current count of cached values. Will always return 0 if caching is disabled. |
Name | Description |
---|---|
Clear() |
Empties the complete cache. |
DispatchAsync() |
Dispatches one or more batch requests. In case of auto dispatching we just trigger an implicit dispatch which could mean to interrupt a wait delay. Whereas in a manual dispatch scenario it could mean to dispatch explicitly. |
LoadAsync(key) |
Loads a single value by key. This call may return a cached value or enqueues this single request for bacthing if enabled. |
LoadAsync(keys) |
Loads multiple values by keys. This call may return cached values and enqueues requests which were not cached for bacthing if enabled. |
Remove(key) |
Removes a single entry from the cache. |
Set(key, value) |
Adds a new entry to the cache if not already exists. |
- Consider using a
DataLoader
instance per request if the results may differ due to user privileges for instance.
Click here for more documentation.