Dependency Injection
GraphQL.NET supports dependency injection through a IServiceProvider
interface that is passed to the Schema class. Internally when trying to resolve a type the library will call the methods on this interface.
The library resolves a
GraphType
only once and caches that type for the lifetime of theSchema
.
The default implementation of IServiceProvider
uses Activator.CreateInstance
. Activator.CreateInstance
requires that an object have a public parameterless constructor.
public sealed class DefaultServiceProvider : IServiceProvider
{
public object GetService(Type serviceType)
{
if (serviceType == null)
throw new ArgumentNullException(nameof(serviceType));
try
{
return Activator.CreateInstance(serviceType);
}
catch (Exception exception)
{
throw new Exception($"Failed to call Activator.CreateInstance. Type: {serviceType.FullName}", exception);
}
}
}
You can override the default implementation by passing a IServiceProvider
to the constructor of your Schema
.
public class StarWarsSchema : GraphQL.Types.Schema
{
public StarWarsSchema(IServiceProvider provider, StarWarsQuery query, StarWarsMutation mutation)
: base(provider)
{
Query = query;
Mutation = mutation;
}
}
How you integrate this into your system will depend on the dependency injection framework you are using. FuncServiceProvider
is provided for easy integration with multiple containers.
Dependency Injection Registration Helpers
GraphQL.NET provides an IGraphQLBuilder
interface which encapsulates the configuration methods of a dependency injection framework, to provide an
abstract method of configuring a dependency injection framework to work with GraphQL.NET. This interface is provided through a configuration delegate
from a DI-provider-specific setup method (typically called AddGraphQL()
), at which point you can call extension methods on the interface to
configure this library. A simple example is below:
services.AddGraphQL(builder => builder
.AddSystemTextJson()
.AddSchema<MySchema>());
The interface also allows configuration of the schema during initialization, and configuration of the execution at runtime. In this manner, adding
middleware, for example, is as simple as calling .AddMiddleware<MyMiddlware>()
and does not require the middleware to be added into the schema
configuration.
The AddGraphQL()
method will register default implementations of the following services within the dependency injection framework:
IDocumentExecuter
IDocumentBuilder
IDocumentValidator
IErrorInfoProvider
IExecutionStrategySelector
- which does not support subscriptions by default
A list of the available extension methods is below:
Method | Description / Notes | Library |
---|---|---|
AddAutoClrMappings |
Configures unmapped CLR types to use auto-registering graph types | |
AddAutoSchema |
Registers a schema based on CLR types | |
AddClrTypeMappings |
Scans the specified assembly for graph types intended to represent CLR types and registers them within the schema | |
AddComplexityAnalyzer |
Enables the complexity analyzer and configures its options | |
AddDataLoader |
Registers classes necessary for data loader support | GraphQL.DataLoader |
AddDocumentCache<> |
Registers the specified document caching service | |
AddDocumentExecuter<> |
Registers the specified document executer; useful when needed to change the execution strategy utilized | |
AddDocumentListener<> |
Registers the specified document listener and configures execution to use it | |
AddErrorInfoProvider |
Registers a custom error info provider or configures the default error info provider | |
AddExecutionStrategy |
Registers an ExecutionStrategyRegistration for the selected execution strategy and operation type |
|
AddExecutionStrategySelector |
Registers the specified execution strategy selector | |
AddFederation |
Registers the federation types and configures the schema to support Apollo Federation | |
AddGraphTypes |
Scans the specified assembly for graph types and registers them within the DI framework | |
AddGraphTypeMappingProvider |
Registers a graph type mapping provider for unmapped CLR types | |
AddLegacyComplexityAnalyzer |
Enables the v7 complexity analyzer and configures its options | |
AddNewtonsoftJson |
Registers the serializer that uses Newtonsoft.Json as its underlying JSON serialization engine | GraphQL.NewtonsoftJson |
AddSchema<> |
Registers the specified schema | |
AddSchemaVisitor<> |
Registers the specified schema visitor and configures it to be used at schema initialization | |
AddSelfActivatingSchema<> |
Registers the specified schema which will create instances of unregistered graph types during initialization | |
AddSerializer<> |
Registers the specified serializer | |
AddSystemTextJson |
Registers the serializer that uses System.Text.Json as its underlying JSON serialization engine | GraphQL.SystemTextJson |
AddUnhandledExceptionHandler |
Configures the unhandled exception handler | |
AddValidationRule<> |
Registers the specified validation rule and configures it to be used at runtime | |
ConfigureExecution |
Configures execution middleware to monitor or modify both options and the result | |
ConfigureExecutionOptions |
Configures execution options at runtime | |
ConfigureSchema |
Configures schema options when the schema is initialized | |
Configure<TOptions> |
Used by extension methods to configures an options class within the DI framework | |
UseApolloTracing |
Registers and enables metrics depending on the supplied arguments, and adds Apollo Tracing data to the execution result | |
UseAutomaticPersistedQueries |
Enables Automatic Persisted Queries support | GraphQL.MemoryCache |
UseMemoryCache |
Registers the memory document cache and configures its options | GraphQL.MemoryCache |
UseMiddleware<> |
Registers the specified middleware and configures it to be installed during schema initialization | |
UsePersistedDocuments |
Registers the persisted document handler and configures its options | |
UseTelemetry |
Creates telemetry events based on the System.Diagnostics.Activity API, primarily for use with OpenTelemetry | .NET 5+ |
WithTimeout |
Configures the execution timeout |
The above methods will register the specified services typically as singletons unless otherwise specified. Graph types and middleware are registered as transients so that they will match the schema lifetime. So with a singleton schema, all services are effectively singletons.
Calls to ConfigureExecutionOptions
and methods that start with Add
will execute first, in the order they
appear, followed by calls to ConfigureExecution
and methods that start with Use
. The order of the calls
may be important. For instance, calling UseMemoryCache
prior to UseAutomaticPersistedQueries
would result in
the memory cache being unable to cache any APQ queries.
Custom IGraphQLBuilder
extension methods typically rely on the Services
property of the builder in order to register services
with the underlying dependency injection framework. The Services
property returns a IServiceRegister
interface which has these methods:
Method | Description |
---|---|
Register |
Registers a service within the DI framework replacing existing registration if needed |
TryRegister |
Registers a service within the DI framework if it has not already been registered |
To use the AddGraphQL
method, you will need to install the proper nuget package for your DI provider. See list below:
DI Provider | Nuget Package |
---|---|
Microsoft.Extensions.DependencyInjection | GraphQL.MicrosoftDI |
ASP.NET Core
Microsoft.Extensions.DependencyInjection
package used in ASP.NET Core already has support for resolving IServiceProvider
interface so no additional settings are required - just add your required dependencies:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
services.AddSingleton<IGraphQLSerializer, GraphQLSerializer>();
services.AddSingleton<StarWarsData>();
services.AddSingleton<StarWarsQuery>();
services.AddSingleton<StarWarsMutation>();
services.AddSingleton<HumanType>();
services.AddSingleton<HumanInputType>();
services.AddSingleton<DroidType>();
services.AddSingleton<CharacterInterface>();
services.AddSingleton<EpisodeEnum>();
services.AddSingleton<ISchema, StarWarsSchema>();
}
To avoid having to register all of the individual graph types in your project, you can
import the GraphQL.MicrosoftDI NuGet package
package and utilize the SelfActivatingServiceProvider
wrapper as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ISchema, StarWarsSchema>(services => new StarWarsSchema(new SelfActivatingServiceProvider(services)));
}
If you previously pulled in your query, mutation and/or subscription classes via dependency injection, you will need
to manually pull in those dependencies from the SelfActivatingServiceProvider
via GetRequiredService
as follows:
public class StarWarsSchema : Schema
{
public StarWarsSchema(IServiceProvider serviceProvider) : base(serviceProvider)
{
Query = serviceProvider.GetRequiredService<StarWarsQuery>();
Mutation = serviceProvider.GetRequiredService<StarWarsMutation>();
}
}
No other graph types will need to be registered. Graph types will only be instantiated once, during schema initialization as usual. Graph types can also pull in any services registered with dependency injection as usual.
Note that if any of the graph types directly or indirectly implement IDisposable
, be sure to register those types with your dependency
injection provider, or their Dispose
methods will not be called. Any dependencies of graph types that implement
IDisposable
will be disposed of properly, regardless of whether the graph type is registered within the service provider.
You can also use the .AddGraphTypes()
builder method to scan the calling or specified assembly for classes that implement
IGraphType
and register them all as transients within the service provider. Mark your class with DoNotRegisterAttribute
if you
want to skip registration.
Nancy TinyIoCContainer
protected override void ConfigureApplicationContainer(TinyIoCContainer container)
{
base.ConfigureApplicationContainer(container);
container.Register((c, overloads) =>
{
return new StarWarsSchema(new FuncServiceProvider(c.Resolve));
});
}
SimpleContainer
var container = new SimpleContainer();
container.Singleton(new StarWarsSchema(new FuncServiceProvider(container.Get)));
Autofac
protected override void Load(ContainerBuilder builder)
{
builder
.Register(c => new FuncServiceProvider(c.Resolve<IComponentContext>().Resolve))
.As<IServiceProvider>()
.InstancePerDependency();
}
Castle Windsor
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(
Component
.For<IServiceProvider>()
.UsingFactoryMethod(k => k.Resolve)
);
}
Schema Service Lifetime
Most dependency injection frameworks allow for specifying different service lifetimes for different services. Although they may have different names with different frameworks, the three most common lifetimes are as follows:
- Transient services are created every time they are injected or requested.
- Scoped services are created per scope. In a web application, every web request creates a new unique service scope. That means scoped services are generally created per web request.
- Singleton services are created per DI container. That generally means that they are created only one time per application and then used for whole the application life time.
It is highly recommended that the schema is registered as a singleton. As all graph types are constructed at the
same time as the schema, all graph types will effectively have a singleton lifetime, regardless of how it is registered with the DI framework. This is most performant approach. Having a scoped schema can degrade performance by a huge margin. For instance, even a small schema execution can slow down by 100x, and much more with a large schema.
Scoped lifetime can be used to allow the schema and all its graph types access to the current DI scope. This is not recommended; please see Scoped Services below. With scoped schemas, it is required that all its graph types are registered within the DI framework as scoped or transient services.
Transient lifetime is also not recommended due to performance degradation. For schemas having a transient lifetime, it is required that all its graph types are also registered within the DI framework as transient services.
Scoped services with a singleton schema lifetime
For reasons described above, it is recommended that the schema is registered as a singleton within the dependency injection framework. However, this prevents including scoped services within the constructor of the schema or your custom graph types.
To use scoped services (e.g. HttpContext scoped services in ASP.NET Core) you will need to pass
the scoped service provider into the ExecutionOptions.RequestServices
property. Then within
any field resolver or field middleware, you can access the IResolveFieldContext.RequestServices
property to resolve types via the scoped service provider. Typical integration with ASP.NET Core
might look like this:
var result = await _executer.ExecuteAsync(options =>
{
options.Schema = _schema;
options.Query = request.Query;
options.Variables = _serializer.Deserialize<Inputs>(request.Variables); // IGraphQLTextSerializer from DI
options.RequestServices = context.RequestServices;
});
You could then call scoped services from within field resolvers as shown in the following example:
public class StarWarsQuery : ObjectGraphType
{
public StarWarsQuery()
{
Field<DroidType>("hero")
.Resolve(context => context.RequestServices.GetRequiredService<IDroidRepo>().GetDroid("R2-D2"));
}
}
Thread safety with scoped services
When using scoped services, be aware that most scoped services are not thread-safe. Therefore you will likely
need to use the SerialExecutionStrategy
execution strategy, or write code to create a service scope
for the duration of the execution of the field resolver that requires a scoped service. For instance, with
Entity Framework Core, typically the database context is registered as a scoped service and obtained via
dependency injection. To continue to use the database context in the same manner with a singleton schema,
you would need to use a serial execution strategy, or create a scope within each field resolver that
requires database access, as shown in the following example:
public class StarWarsQuery : ObjectGraphType
{
public StarWarsQuery()
{
Field<DroidType>("hero")
.Resolve(context =>
{
using var scope = context.RequestServices.CreateScope();
var services = scope.ServiceProvider;
return services.GetRequiredService<MyDbContext>().Droids.Find(1);
});
}
}
There are classes to assist with this within the GraphQL.MicrosoftDI NuGet package. Sample usage is as follows:
public class MyGraphType : ObjectGraphType<Category>
{
public MyGraphType()
{
Field("Name").Resolve(context => context.Source.Name);
Field<ListGraphType<ProductGraphType>>("Products")
.ResolveScopedAsync(context => {
var db = context.RequestServices.GetRequiredService<MyDbContext>();
return db.Products.Where(x => x.CategoryId == context.Source.Id).ToListAsync();
});
}
}
In this case context.RequestServices
will be an IServiceProvider
in a newly created scope.
Be aware that using the service locator in this fashion described in this section could be considered an Anti-Pattern. See Service Locator is an Anti-Pattern. However, the performance benefits far outweigh the anti-pattern idealogy, when compared to creating a scoped schema.
Within the GraphQL.MicrosoftDI
package, there is also a builder approach to adding scoped dependencies.
This makes for a concise and declarative approach. Each field clearly states the services it needs
and thereby, the anti-pattern argument does not apply anymore.
public class MyGraphType : ObjectGraphType<Category>
{
public MyGraphType()
{
Field("Name").Resolve(context => context.Source.Name);
Field<ListGraphType<ProductGraphType>>("Products")
.Resolve()
.WithScope() // creates a service scope as described above; not necessary for serial execution
.WithService<MyDbContext>()
.ResolveAsync((context, db) => db.Products.Where(x => x.CategoryId == context.Source.Id).ToListAsync());
}
}
Another approach to resolve scoped services is to use the SteroidsDI project, as described below.
Using SteroidsDI
To use SteroidsDI with ASP.NET Core, add Defer<>
and IScopeProvider
in your Startup.ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
...
// Add SteroidsDI Open Generic Defer<> Factory Class
services.AddDefer();
// Add SteroidsDI IScopeProvider to use the AspNetCoreHttpScopeProvider
// which internally uses the IHttpContextAccessor.HttpContext.RequestServices;
services.AddHttpScope();
...
}
Then in your query graph types you can request services using Defer<T>
to be injected via DI,
which will be evaluated at runtime to their relevant Scoped services, T
has been registered with a Scoped
DI lifetime:
public class StarWarsQuery : ObjectGraphType
{
// #1 - Add dependencies using Defer<T>
public StarWarsQuery(Defer<IDroidRepo> repoFactory)
{
Field<DroidType>("hero")
// #2 Resolve dependencies using current scope provider
.Resolve(context => repoFactory.Value.GetDroid("R2-D2"));
}
}
- Add
Defer<T>
to be injected by the dependency injection container. This is a factory which upon callingDefer.Value
will resolve the requested service using any currently registered scope provider (e.g.AspNetCoreHttpScopeProvider
) - Use the
Defer<T>
factory class to resolve the requested dependency using any currently registered scope provider. In our case it will attempt to use theIHttpContextAccessor.HttpContext.RequestServices
which is the ASP.NET Core ScopedIServiceProvider
in order to resolve the dependency.