Graphql-dotnet: Accessor.Context is always null

Created on 17 Oct 2018  路  34Comments  路  Source: graphql-dotnet/graphql-dotnet

Hi, I followed your Data loader documentation but when my resolver code is executed I always get accessor.Context = null.

Not sure what I did wrong.

data loader

Most helpful comment

I have the same issue, on .Net Core 2.1.

Weirdly, even though it's registered as a singleton, my services are receiving different instances of the DataLoaderContextAccessor.

I don't know if it's a problem with the dependency injection service or graphql-dotnet, but my solution is to manually create an instance of DataLoaderContextAccessor in Startup.cs:

services.AddSingleton<IDataLoaderContextAccessor>(new DataLoaderContextAccessor());

All 34 comments

Nothing comes to mind. Can you share some code that shows the problem? Then I should be able to see what's wrong.

@johnrutherford @joemcbride I am also experiencing the same issue, I have followed the example in the documentation almost exactly without success.
https://graphql-dotnet.github.io/docs/guides/dataloader.

This line:

var loader = accessor.Context.GetOrAddBatchLoader<string, MyModel>(...);

Results in the following error:

Exception has occurred: CLR/System.ArgumentNullException
An exception of type 'System.ArgumentNullException' occurred in GraphQL.dll but was not handled in user code: 'Value cannot be null.'

This is in reference to Context being null, but im unable to determine why. Any insight would be appreciated.

What framework are you running on? .NET Core or Full Framework?

Same issue here. Using the following dependencies:

  <ItemGroup>
    <PackageReference Include="Autofac" Version="4.8.1" />
    <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.3.1" />
    <PackageReference Include="AutoMapper" Version="7.0.1" />
    <PackageReference Include="GraphiQL" Version="1.2.0" />
    <PackageReference Include="GraphQL" Version="2.3.0" />
    <PackageReference Include="MediatR" Version="5.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
    <PackageReference Include="NLog" Version="4.5.11" />
    <PackageReference Include="NLog.Web.AspNetCore" Version="4.7.0" />
  </ItemGroup>

@dietergoetelen What framework (and version) are you running on? .NET Core or .NET Framework?

@joemcbride sorry, it's working now, my issue was because I didn't entirely understand step 3 in the documentation. Once I've added the DataLoaderDocumentListener to the listeners array everything started working fine.

var executionOptions = new ExecutionOptions
{
    Schema = _schema,
    Query = query.Query,
    Inputs = inputs
};

executionOptions.Listeners.Add(_listener);

var result = await _documentExecuter.ExecuteAsync(executionOptions).ConfigureAwait(false);

I'm using the latest .NET Core framework.

@nvhoanganh, @aaronjedwards, does the above fix the issue for you?

If not, it would help if you could provide some sample code that reproduces the issue. I know there could be unexpected issues depending on when the context is created/accessed in the asynchronous control flow. So it would help to see what exactly you're doing so we could provide better guidance in the documentation or make some changes to help others avoid this too.

@johnrutherford I will be sure to post some code when i get the chance here in a few hours. I am running on .NET Core 2.1. Is there by chance a working example using DataLoader and .Net Core for reference?

Edit: Im fairly certain I properly added the listener but will be sure to follow up.

I'm getting the same issue in .Net Core 2.1 also.

I followed the "Setup" section of the docs precisely and have double and triple checked it.

In the Type below, accessor.Context is always null.

public class PlayerType : ObjectGraphType<PlayerModel>
{
    public PlayerType(IPlayerDatastore playerds, IPlayerBoundDatastore<ShipModel> shipDs, IDataLoaderContextAccessor accessor)
    {
        Name = "Player";
        Field(x => x.Id, type: typeof(IdGraphType));
        Field(x => x.Auth0Id);
        Field(x => x.Nickname);
        Field<ListGraphType<ShipType>, IEnumerable<ShipModel>>()
            .Name("Ships")
            .ResolveAsync(ctx =>
            {
                var loader =
                    accessor.Context.GetOrAddBatchLoader<Guid, IEnumerable<ShipModel>>("GetByPlayerId", shipDs.GetByPlayerIdsAsync);
                return loader.LoadAsync(ctx.Source.Id);
            });
    }
}
public async Task<IDictionary<Guid, IEnumerable<TModel>>> GetByPlayerIdsAsync(IEnumerable<Guid> playerIds)
{
    var entities = await Context.Set<TEntity>()
        .Where(x => playerIds.Contains(x.PlayerId))
        .ToListAsync();

    return playerIds.ToDictionary(playerId => playerId, playerId => Mapper.Map<IEnumerable<TModel>>(entities.Where(x => x.PlayerId == playerId)));
}

I also inspected the listener in the graphql controller action... The accessor context was null there also.

My project is blocked at present due to this issue so any help would be appreciated.

@hades200082, can you share the part of your code where the DataLoaderDocumentListener is added to the DocumentExecuter? Maybe a different instance of the IDataLoaderContextAccessor is being used there.

Sure thing....

From startup.cs

services.AddSingleton<IDataLoaderContextAccessor, DataLoaderContextAccessor>();
services.AddSingleton<DataLoaderDocumentListener>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient(s => s.GetService<IHttpContextAccessor>().HttpContext.User);
services.AddTransient<AuthorizationHelper>();
services.AddSingleton<IDocumentExecuter, DocumentExecuter>();

Then in my GraphQlController.cs

public class GraphQLController : BaseController
{
    private readonly IDocumentExecuter _documentExecuter;
    private readonly IDocumentExecutionListener _documentListener;
    private readonly ISchema _schema;

    public GraphQLController(ISchema schema, IDocumentExecuter documentExecuter, DataLoaderDocumentListener documentListener, AuthorizationHelper authHelper)
        : base(authHelper)
    {
        _schema = schema;
        _documentExecuter = documentExecuter;
        _documentListener = documentListener;
    }

    [HttpPost]
    public async Task<IActionResult> Post([FromBody] GraphQLQuery query)
    {

        if (query == null) { throw new ArgumentNullException(nameof(query)); }
        var inputs = query.Variables.ToInputs();

        var options = new ExecutionOptions
        {
            Schema = _schema,
            Query = query.Query,
            Inputs = inputs,
            UserContext = User,
            ComplexityConfiguration = new ComplexityConfiguration { MaxDepth = 15 }
    };
        options.Listeners.Add(_documentListener);

        var result = await _documentExecuter.ExecuteAsync(options).ConfigureAwait(false);

        if (result.Errors?.Count > 0)
        {
            return BadRequest(result);
        }

        return Ok(result);
    }
}

EDIT: Nevermind.

same issue in .Net Core 2.1

Yeah, I don't see anything wrong with that setup. I'll have to investigate more what could be happening.

I have the same issue, on .Net Core 2.1.

Weirdly, even though it's registered as a singleton, my services are receiving different instances of the DataLoaderContextAccessor.

I don't know if it's a problem with the dependency injection service or graphql-dotnet, but my solution is to manually create an instance of DataLoaderContextAccessor in Startup.cs:

services.AddSingleton<IDataLoaderContextAccessor>(new DataLoaderContextAccessor());

@stripeyjumper, thank you.

I think I understand now what's happening. The dependency injection for the GraphQL types is being handled by the dependency resolver on the Schema which just uses Activator.CreateInstance() by default. So two different instances of DataLoaderContextAccesor are being used.

I'm not exactly sure why @stripeyjumper's work-around works, though, because it's still a separate instance being created by the Schema. Maybe something with how AsyncLocal<T> works. But it's a simple workaround, if it does work.

The other option would be to hook up the Schema to the service container. Something like this:

public class MySchema : Schema
{
    public MySchema(IServiceProvider services) : base(new ServiceProviderAdapter(services))
    {

    }

    ...
}

private sealed class ServiceProviderAdapter : IDependencyResolver
{
    private readonly IServiceProvider _services;

    public GraphQLDependencyResolver(IServiceProvider services)
    {
        _services = services;
    }

    public T Resolve<T>() => _services.GetService<T>();

    public object Resolve(Type type) => _services.GetService(type);
}

I will work on making this easier to do and improving the setup documentation.

Quick note...

public T Resolve<T>() => _services.GetService<T>();

should be ...

public T Resolve<T>() => _services.GetRequiredService<T>();

GetService doesn't have a generic implementation.

I'll try the solution and reply back if it works for me.

It worked! Thanks.

I had to make the queries & schema scoped because my data-layer is scoped due to being EF.

services.AddSingleton<IDataLoaderContextAccessor, DataLoaderContextAccessor>();
services.AddSingleton<IDocumentExecutionListener, DataLoaderDocumentListener>();
services.AddSingleton<IDocumentExecuter, DocumentExecuter>();

services.AddSingleton<IDependencyResolver, ServiceProviderAdapter>();

services.AddScoped<Query>();
services.AddScoped<PlayerType>();
services.AddScoped<ShipType>();

services.AddScoped<ISchema, RootSchema>();
public class RootSchema : Schema
{
    public RootSchema(IDependencyResolver resolver) : base(resolver)
    {
        Query = resolver.Resolve<Query>();
    }
}
internal sealed class ServiceProviderAdapter : IDependencyResolver
{
    private readonly IServiceProvider _services;

    public ServiceProviderAdapter(IServiceProvider services)
    {
        _services = services.CreateScope().ServiceProvider;
    }

    public T Resolve<T>() => _services.GetRequiredService<T>();

    public object Resolve(Type type) => _services.GetService(type);
}

I did exactly what @hades200082 did, and now I am getting an error "No service for type 'GraphQL.DataLoader.DataLoaderDocumentListener' has been registered."

I am definitely registering it in startup.cs using
diSvcs.AddSingleton<IDocumentExecutionListener, DataLoaderDocumentListener>();

And calling it here in my GraphQL Controller:

var listener = _provider.GetRequiredService<DataLoaderDocumentListener>();
            executionOptions.Listeners.Add(listener);

This is where I get the exception. Any ideas?

@aoakeson, you're registering DataLoaderDocumentListener as IDocumentExecutionListener and then trying to resolve DataLoaderDocumentListener. You need to resolve IDocumentExecutionListener.

@johnrutherford Thanks, that did help me in finding the issue. Think I was getting confused based on different articles, repos, and docs that were doing things differently.

Was having the same issue, luckily I found this issue as I was beginning to give up hope!

@johnrutherford John has the documentation ever been updated for this? And/or is there a full concrete example of how to get it all working...with a Startup class, Schema, Query, Listeners etc etc?

I've been pulling my hair out for 2 days trying to get this working and it's really confusing trying to figure out exactly what is required. First I followed the official documentation on DataLoader here but that was incomplete and didn't work. Then I tried everything I read in this issue above trying each one by one but no luck I'm still getting a null Context.

This is a key piece of functionality that everyone is going to want so I think we need something conclusive we can point to and say 'this is how you get it setup'

thanks

The same issue with .Net Core 2.2

Had the same Problem, using net core 2.2 and the GraphQL Middleware .
The solution was to simply use AddDataLoader extension:

services.AddGraphQL(options =>
    {
        options.EnableMetrics = true;
        options.ExposeExceptions = this.Environment.IsDevelopment();
    })
    .AddDataLoader();

The difference is that i was manually registering
services.AddSingleton<DataLoaderDocumentListener>();
before, but the extension is registering against the IDocumentExecutionListener:

builder.Services.TryAddSingleton<IDataLoaderContextAccessor, DataLoaderContextAccessor>();
builder.Services.AddSingleton<IDocumentExecutionListener, DataLoaderDocumentListener>();

@amelszg Yes, using graphql-dotnet/server is the easiest way to get everything set up correctly.

For 3.0, I intend on making it much simpler to set up the DataLoader. Like just a boolean property on the ExecutionOptions.

I have the same issue, on .Net Core 2.1.

Weirdly, even though it's registered as a singleton, my services are receiving different instances of the DataLoaderContextAccessor.

I don't know if it's a problem with the dependency injection service or graphql-dotnet, but my solution is to manually create an instance of DataLoaderContextAccessor in Startup.cs:

services.AddSingleton<IDataLoaderContextAccessor>(new DataLoaderContextAccessor());

This helped me to solve my problem.

Making the following replacements from the documentation did the trick for me

// from step 2:
// services.AddSingleton<DataLoaderDocumentListener>();
services.AddSingleton<IDocumentExecutionListener, DataLoaderDocumentListener>();

// from step 3:
// var listener = Services.GetRequiredService<DataLoaderDocumentListener>();
var listener = Services.GetRequiredService<IDocumentExecutionListener>();

@Shane32 can we close this issue or do we need to add something to the documentation?

I didn't touch the accessors. I'd need to research the issue to answer appropriately.

services.AddGraphQL(options =>
    {
        options.EnableMetrics = true;
        options.ExposeExceptions = this.Environment.IsDevelopment();
    })
    .AddDataLoader();

Works on my side

Making the following replacements from the documentation did the trick for me

// from step 2:
// services.AddSingleton<DataLoaderDocumentListener>();
services.AddSingleton<IDocumentExecutionListener, DataLoaderDocumentListener>();

// from step 3:
// var listener = Services.GetRequiredService<DataLoaderDocumentListener>();
var listener = Services.GetRequiredService<IDocumentExecutionListener>();

I don鈥檛 see/understand how this would change functionality.

I would close the issue due to lack of input.

Was this page helpful?
0 / 5 - 0 ratings