Coder Social home page Coder Social logo

graphql-dotnet-dataloader's Introduction

DataLoader for .NET

A port of Facebook's DataLoader for .NET.

Originally began as a solution to the select N+1 problem for GraphQL in .NET but found that most of the (small amount of) code was independent and could be generalized for use in other scenarios.

If anyone finds use for this in other areas, please let me know... I'd love to know whether the solution could be expanded to cater for other uses.

Caveats

Facebook's implementation runs in Javascript and takes advantage of the event loop to fire any pending requests for ID's collected during the previous frame. Unfortunately not all .NET applications run in an event loop.

For this reason, we have our own DataLoaderContext to house DataLoader instances. Any instances should be called within a particular DataLoaderContext - which essentially represents a frame in Javascript - using the static DataLoaderContext.Run method. This method will run the user-supplied delegate before calling Start on the created context, which then fires any pending fetches and processes the results.

Loaders may be called again as the results are processed, which would cause them to be requeued. This effectively turns the context into a kind of asynchronous loader pump.

Usage

public void GetPersonsManually()
{
    var personLoader = new DataLoader<int, Person>(ids =>
    {
        using (var db = new StarWarsContext())
            return db.Person.Where(p => ids.Contains(p.Id)).ToListAsync();
    });

    var person1 = personLoader.LoadAsync(1);
    var person2 = personLoader.LoadAsync(2);
    var person3 = personLoader.LoadAsync(3);
    var task = Task.WhenAll(person1, person2, person3);

    // Do some stuff when they're all loaded
    task.ContinueWith(_ => Console.WriteLine("Hello there " + string.Join(', ', _.Result.Select(p => p.Name))));

    // Actually trigger the load
    personLoader.ExecuteAsync();

    return task;
}

public void GetPersonsContextual()
{
    // The collect/fire cycle can be managed implicitly using
    // the static DataLoaderContext.Run() method.
    var result = await DataLoaderContext.Run(() =>
    {
        // Implicit context here... DataLoaderContext.Current != null
        // and is used by the loader during a call to LoadAsync.
        var task1 = loader.LoadAsync(1);
        var task2 = loader.LoadAsync(2);
        var task3 = loader.LoadAsync(3);
        return await Task.WhenAll(task1, task2, task3).ConfigureAwait(false);
    });

    Console.WriteLine(result[0]);
    Console.WriteLine(result[1]);
    Console.WriteLine(result[2]);
}

GraphQL example

// Example 1 - Create and use loaders directly.
//   This approach does not require the GraphQL.DataLoader package
//   and can instead depend on only the main DataLoader package.
var friendsLoader = new DataLoader<int, Droid>(ids => {
    using (var db = new StarWarsContext())
        return db.Friendships
            .Where(f => ids.Contains(f.HumanId))
            .Select(f => new {Key = f.HumanId, f.Droid})
            .ToLookup(f => f.Key, f => f.Droid);
});

Field<ListGraphType<CharacterInterface>>()
    .Name("friends2")
    .Resolve(ctx => friendsLoader.LoadAsync(ctx.Source.HumanId));

// Example 2 - resolve field context extension method.
//   Creates or reuses a loader for the given field, keyed by the
//   field definition object. This approach could be implemented
//   using only the DataLoader package
Field<ListGraphType<CharacterInterface>>()
    .Name("friends3")
    .Resolve(ctx => ctx.GetDataLoader(ids => {
        using (var db = new StarWarsContext())
            return db.Friendships
                .Where(f => ids.Contains(f.HumanId))
                .Select(f => new {Key = f.HumanId, f.Droid})
                .ToLookup(f => f.Key, f => f.Droid);
    }));

// Example 3 - field builder BatchResolve extension method
//   Creates a 
Field<ListGraphType<CharacterInterface>>()
    .Name("friends4")
    .BatchResolve((ids/*, ctx*/) => {
        using (var db = new StarWarsContext())
            return db.Friendships
                .Where(f => ids.Contains(f.HumanId))
                .Select(f => new {Key = f.HumanId, f.Droid})
                .ToLookup(f => f.Key, f => f.Droid);
    });

Running the sample app

cd example/
dotnet ef migrations add InitialSetup
dotnet ef database update
dotnet run

To do

[x] Basic support [x] Support async fetches [ ] Cancellation [ ] Benchmarks [ ] Multithreaded performance

graphql-dotnet-dataloader's People

Contributors

dlukez avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

graphql-dotnet-dataloader's Issues

Nested DataLoader Calls Never Return

We have been working with GraphQL DotNet and we started working with this DataLoader. Seemed like a pretty good tool but we have run into an issue that I believe I can recreate in the sample project.

When you have nested calls that invoke the IDataLoader LoadAsync methods it looks like those calls never return. You can run the below query to see the issue first hand. This will eventually timeout.
{humans{name, appearsIn{id, characters{name}}}}

After much digging it appears to happen after going more than 1 level deep. These queries do work:
{humans{name, appearsIn{id}}}
{episodes{characters{name}}}

At first I was sure that I was doing something wrong but upon being able to recreate using the code here I think there may be a problem with the code.

I did some debugging and it looks like the Fetch Method is never being called for the nested characters query.

Any help would be greatly appreciated.

Async threading issues

Began playing with this in a Web API project and came across some async threading issues.

From what I can see, nested continuations (code after await) are being executed asynchronously on another thread, which means Flush continues running in parallel, the loop condition is checked the the method returns as subsequent fetches haven't yet been queued.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.