Migrating from v2.x to v3.x
New Features
Support for additional scalar types and conversions built-in
These .Net types now are automatically mapped to corresponding built-in custom scalar types:
- Byte
- SByte
- Short
- UShort
- UInt
- ULong
- Guid (maps to
IdGraphType
by default; also supportsGuidGraphType
) - BigInt
There is also support for converting base-64 encoded strings to byte arrays. See
the FAQ
and the ValueConverter
class for more details.
See Schema Types for more details.
Other new features
- Based on .Net Standard 2.0, supporting .Net Core applications
- Supports scoped services (see below under Dependency Injection)
- Supports the
System.Text.Json
package for JSON serialization, in addition to theNewtonsoft.Json
package - Data loaders work with serial execution strategies and can be chained together
- Name converters can be configured to use a different function for field names versus argument names
- Field builders can take an optional configuration action parameter
- Support for auto-registering input object graph types via
AutoRegisteringInputObjectGraphType
- Added codes to
ExecutionError
s - Document processing exceptions can be logged or modified (see below under Exception Handling)
- Enhanced validation of graphs built-in
- Supports filtering of schema introspection requests - see details here
- Supports federated schemas - see details here
- Supports schema 'description' property - see details here
- Supports comment nodes - see details here
- Supports result 'extensions' - see details here
- Ability to limit maximum number of asynchronous field resolvers executing simultaneously - see details here
Breaking Changes
.NET compatibility
This project now requires the .NET Standard 2.0 and breaks compatibility with applications based on .NET Framework 4.6 and earlier. See https://docs.microsoft.com/en-us/dotnet/standard/net-standard for a list of frameworks that support .Net Standard 2.0.
Naming
By default, the GraphType.Name
property now is initialized in constructor and is the same as the name of the .NET type
unless the name of .NET type ends in GraphType. In this case, the GraphType suffix is truncated and GraphType.Name
is set to this value.
Examples:
.NET GraphType Name | Default Name |
---|---|
MoneyType | MoneyType |
MoneyGraphType | Money |
You can always set your own type name either in the constructor or externally. The result of this change is that it is no
longer possible to have Graph Types with unspecified (null
) or invalid names. This allows to avoid a number of errors
and incomprehensible behavior at runtime.
Dependency Injection
The previous IDependencyResolver
interface and FuncDependencyResolver
class have been replaced by the
.Net Standard IServiceProvider
interface and the new FuncServiceProvider
class.
//public Schema(IDependencyResolver dependencyResolver)
public Schema(IServiceProvider serviceProvider)
{
...
}
Also, the Schema.DependencyResolver
property has been removed and not replaced. If you need to access the service provider
from your graphs, you can include IServiceProvider
in the constructor of the graph type. Your DI container will pass
a reference to the service provider. For singleton schemas, this will be the root service provider, from which you can
obtain other singleton or transient services. For scoped schemas with scoped graph types, this will be the service provider
for the current executing scope. Casting Schema
to IServiceProvider
is also possible, but not recommended, and will
yield similar results.
If you wish to access a scoped service from within a resolver and want to use a singleton schema (as is recommended), you
can pass a scoped service provider to ExecutionOptions.RequestServices
, which can then be used to resolve scoped
services. For ASP.NET Core projects, you can set this equal to HttpContext.RequestServices
. Be aware that if you
are using a parallel execution strategy (default for 'query' requests), using scoped services within field resolvers can
introduce thread safety issues; you may need to use a serial execution strategy or manually create a scope within each
field resolver.
See the Dependency Injection documentation for more details, including service lifetime guidelines and restrictions when registering your schema and graph types.
Json parsing and serialization
Version 2.x relied on the Newtonsoft.Json library for parsing of input variables and serialization of response data. As the Newtonsoft.Json dependency has now been removed, a third-party library will be required within your application to parse and serialize json data. The GraphQL.SystemTextJson and GraphQL.NewtonsoftJson nuget packages include the necessary components to assist in this regard. Below are examples of the changes required:
Version 3.0 sample, using the Newtonsoft.Json
package:
using Newtonsoft.Json;
private static async Task ExecuteAsync(HttpContext context, ISchema schema)
{
GraphQLRequest request;
using (var reader = new StreamReader(context.Request.Body))
using (var jsonReader = new JsonTextReader(reader))
{
var ser = new JsonSerializer();
request = ser.Deserialize<GraphQLRequest>(jsonReader);
}
var executer = new DocumentExecuter();
var result = await executer.ExecuteAsync(options =>
{
options.Schema = schema;
options.Query = request.Query;
options.OperationName = request.OperationName;
options.Inputs = request.Variables.ToInputs();
});
context.Response.ContentType = "application/json";
context.Response.StatusCode = result.Errors?.Any() == true ? (int)HttpStatusCode.BadRequest : (int)HttpStatusCode.OK;
var writer = new GraphQL.NewtonsoftJson.DocumentWriter();
await writer.WriteAsync(context.Response.Body, result);
}
public class GraphQLRequest
{
public string OperationName { get; set; }
public string Query { get; set; }
public Newtonsoft.Json.Linq.JObject Variables { get; set; }
}
Version 3.0 sample, using the System.Text.Json
package:
using System.Text.Json;
private static async Task ExecuteAsync(HttpContext context, ISchema schema)
{
var request = await JsonSerializer.DeserializeAsync<GraphQLRequest>
(
context.Request.Body,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
var executer = new DocumentExecuter();
var result = await executer.ExecuteAsync(options =>
{
options.Schema = schema;
options.Query = request.Query;
options.OperationName = request.OperationName;
options.Inputs = request.Variables.ToInputs();
});
context.Response.ContentType = "application/json";
context.Response.StatusCode = 200; // OK
var writer = new GraphQL.SystemTextJson.DocumentWriter();
await writer.WriteAsync(context.Response.Body, result);
}
public class GraphQLRequest
{
public string OperationName { get; set; }
public string Query { get; set; }
[JsonConverter(typeof(GraphQL.SystemTextJson.ObjectDictionaryConverter))]
public Dictionary<string, object> Variables { get; set; }
}
This is just a simplified example of the changes necessary. Note that typically the DocumentExecuter
and DocumentWriter
are registered as singletons within the dependency injection container, as they can safely be shared between requests.
If you continue to use the Newtonsoft.Json
converter, please note that ASP.NET Core 3.0 disallows synchronous IO by default,
which is required by the converter. You will need to make a change in the ConfigureServices
section of Startup.cs
as
follows:
// kestrel
services.Configure<KestrelServerOptions>(options =>
{
options.AllowSynchronousIO = true;
});
// IIS
services.Configure<IISServerOptions>(options =>
{
options.AllowSynchronousIO = true;
});
Please note that if you use a NameConverter
other than the default CamelCaseNameConverter
, you may need to configure
your json serializer also to not convert object properties to camel case. For example, with the System.Text.Json
converter,
you need to set JsonSerializerOptions.PropertyNamingPolicy = null;
as follows:
var writer = new GraphQL.SystemTextJson.DocumentWriter(options => {
options.PropertyNamingPolicy = null;
});
UserContext
The definition of the UserContext
object throughout the library has changed to a IDictionary<string, object>
.
Custom user context classes will need to inherit from Dictionary<string, object>
or otherwise support the interface.
//class MyContext
class MyContext : Dictionary<string, object>
{
public DbContext MyDbContext { get; set; }
}
If you are going to use GraphQL.NET in an ASP.NET Core application, then you may be interested in the server project. It already implements all the necessary integration with GraphQL.NET via the custom ASP.NET Core middleware.
Document Listeners
The DocumentExecutionListenerBase<T>
class and IDocumentExecutionListener<T>
interface have been removed;
please implement the IDocumentExecutionListener
interface when creating a custom document listener. You
can also inherit from the DocumentExecutionListenerBase
class to provide default implementations of events.
The methods definitions have also changed from passing userContext
and cancellationToken
parameters to a
single parameter context
of type IExecutionContext
. The context has properties for accessing the user context,
cancellation token, metrics, execution errors, and other information about the executing request.
//class MyListener : DocumentExecutionListenerBase<MyContext>
class MyListener : DocumentExecutionListenerBase
{
//public virtual Task AfterValidationAsync(MyContext userContext, IValidationResult validationResult, CancellationToken token)
public Task AfterValidationAsync(IExecutionContext context, IValidationResult validationResult)
{
var myContext = (MyContext)context.userContext;
// log validation error
return Task.CompletedTask;
}
...
}
IResolveFieldContext and IResolveConnectionContext
Field resolver methods now pass a reference to an IResolveFieldContext
interface, rather than a
ResolveFieldContext
class. Inline lambda functions are typically unaffected, but if you define your
resolvers separately, you will need to change the function signature.
class MyGraphType : ObjectGraphType
{
public MyGraphType()
{
Field("Name", resolve: x => "John Doe");
Field("Children", resolve: GetChildren);
}
//public IEnumerable<string> GetChildren(ResolveFieldContext context)
public IEnumerable<string> GetChildren(IResolveFieldContext context)
{
return new [] { "Jack", "Jill" };
}
}
Also please note that all IResolveFieldContext
and similar interfaces and classes have moved from
the GraphQL.Types
namespace to the GraphQL
namespace. You may need to add a using GraphQL;
statement to some of your files.
There are also several new properties of IResolveFieldContext
that were not present on the prior
ResolveFieldContext
class, such as RequestServices
and ResponsePath
, and the constructor of
the ResolveFieldContext
class has changed. Custom implementations will need to implement the
new properties. Please see the IResolveFieldContext
interface for a complete list of properties
that are required to be implemented.
Connection Builders
The connection builders have changed slightly. Please see https://graphql-dotnet.github.io/docs/getting-started/relay for current implementation details.
Field Middleware
Field Middleware must be registered in the dependency injection container in order to be instantiated.
You also need to implement the IFieldMiddleware
interface on your custom middleware classes, and change
the signature for the Resolve
method to accept IResolveFieldContext
.
//class MyMiddleware
class MyMiddleware : IFieldMiddleware
{
//public async Task<object> Resolve(ResolveFieldContext context, FieldMiddlewareDelegate next)
public async Task<object> Resolve(IResolveFieldContext context, FieldMiddlewareDelegate next)
{
// your code here
var ret = await next(context);
// your code here
return ret;
}
}
Please note that the delegate definition for FieldMiddlewareDelegate
has changed as follows:
// version 2.4.0
public delegate Task<object> FieldMiddlewareDelegate(ResolveFieldContext context);
// version 3.0
public delegate Task<object> FieldMiddlewareDelegate(IResolveFieldContext context);
You also must ensure that your schema implements IServiceProvider
. This is handled
automatically if you inherit from Schema
.
See Field Middleware for more details, including guidelines and restrictions on service lifetimes of middleware registered through your DI framework.
Data Loaders
Data loaders now return an IDataLoaderResult<T>
rather than a Task<T>
. Field resolver signatures may need to change
as a result. Lambda functions passed to field builders' ResolveAsync
method should not need to change.
public class OrderType : ObjectGraphType<Order>
{
private readonly IDataLoaderContextAccessor _accessor;
private readonly IUsersStore _users;
// Inject the IDataLoaderContextAccessor to access the current DataLoaderContext
public OrderType(IDataLoaderContextAccessor accessor, IUsersStore users)
{
_accessor = accessor;
_users = users;
...
Field<UserType, User>()
.Name("User")
.ResolveAsync(ResolveUser);
}
//public Task<User> ResolveUser(IResolveFieldContext context)
public IDataLoaderResult<User> ResolveUser(IResolveFieldContext context)
{
// Get or add a batch loader with the key "GetUsersById"
// The loader will call GetUsersByIdAsync for each batch of keys
var loader = _accessor.Context.GetOrAddBatchLoader<int, User>("GetUsersById", users.GetUsersByIdAsync);
// Add this UserId to the pending keys to fetch
// The task will complete once the GetUsersByIdAsync() returns with the batched results
return loader.LoadAsync(context.Source.UserId);
}
}
If you need to process the data loader result before it is returned, additional refactoring will need to be done. The data loader also now supports chained data loaders, and asynchronous code prior to queuing the data loader. See Data loader documentation for more details.
DateGraphType parsing changes
In 2.x, the DateGraphType
will always serialize to a date in the format of yyyy-MM-dd
, ignoring any time
component of the DateTime
value. As an input type it would accept any date/time format accepted by the
DateTime.Parse
method. This allowed for ambiguous dates such as '09-10-2015', which has a different meaning
depending on locale.
In 3.x, the output date format has not changed, but will throw an error if the DateTime
value has a time
component. The input date format now only accepts a format of yyyy-MM-dd
. Any other format will trigger
an input error. Note that the DateTime.Kind
property for returned dates is set to DateTimeKind.Utc
.
ExecutionStrategy changes
If you utilize data loaders along with a custom implementation of IExecutionStrategy
(typically inheriting
from ExecutionStrategy
), you must change the implementation to monitor for IDataLoaderResult
returned
values, and execute GetResultAsync
at the appropriate time to retrieve the actual value asynchronously.
If the field resolver returns a Task<IDataLoaderResult>
, the execution strategy should start the task as
usual, only queuing the data loader once the IDataLoaderResult
has been returned. Note that
await IDataLoaderResult.GetResultAsync()
may return another IDataLoaderResult
which must again
be queued to execute at the proper time.
To accommodate these changes, the ExecuteNodeAsync
method has been split into three methods; the new methods
are CompleteDataLoaderNodeAsync
and CompleteNode
. Please refer to the reference implementation of
ParallelExecutionStrategy
for an example.
Exception Handling
Exceptions have been split into three categories: schema errors, input errors, and processing errors. For instance,
if an invalid query was passed to the DocumentExecuter
, it would be considered an input error, and a SyntaxError
would be thrown. Or if an invalid enum string was passed as a variable to a query, an InvalidValueError
would be
thrown. All validation rules that fail their respective tests are treated as input errors.
In 2.x, most schema errors would throw an exception of the ExecutionError
type. This has been changed; now
the thrown error is a native exception -- for instance, an ArgumentOutOfRange
exception would be thrown when
trying to add a field to a type with the same name as one that already exists. Many of these exceptions
occur prior to executing a document; however, some occur during the schema initialization within the
DocumentExecuter
and are then treated as processing errors.
In 3.x, ExecutionError
(and derived classes) are returned directly from the DocumentExecuter
for parsing
and validation errors (input errors). Field resolvers and middleware can also return input errors by
throwing an ExecutionError
exception.
Processing errors are now able to be caught within an optional UnhandledExceptionDelegate
and processed,
logged or masked as desired. By default, processing errors are masked and encapsulated in an UnhandledError
and returned within the ExecutionResult
. Alternatively, processing errors can be thrown back to the caller
of the DocumentExecuter
by setting ThrowOnUnhandledException
to true
.
See Error handling documentation for more details, samples, and a full list of exception types and codes.
SubscriptionExecuter removal
The SubscriptionExecuter
class, previously marked as obsolete, has been removed. Use the DocumentExecuter
in its place.
NameConverter
The ExecutionOptions.FieldNameConverter
property has been replaced by the NameConverter
property, and the
corresponding class names have changed as well. Static instance members have been added to the included
name converters. Below is a sample of the required change when using PascalCaseFieldNameConverter
:
var executer = new DocumentExecuter();
var result = executer.ExecuteAsync(options => {
...
//options.FieldNameConverter = new PascalCaseFieldNameConverter();
options.NameConverter = PascalCaseNameConverter.Instance;
});
If you have written a custom name converter, you must now implement INameConverter
rather than
IFieldNameConverter
, which has two methods, NameForField
and NameForArgument
, rather than
only NameFor
. There is also no need to check for introspection types, as GraphQL.NET will
handle this automatically. Below is a sample of the changes required:
// version 2.4.x
public class MyConverter : IFieldNameConverter
{
private static readonly Type[] IntrospectionTypes = { typeof(SchemaIntrospection) };
public string NameFor(string field, Type parentType) => isIntrospectionType(parentType) ? field.ToCamelCase() : field.ToPascalCase();
private bool isIntrospectionType(Type type) => IntrospectionTypes.Contains(type);
}
// version 3.0
public class MyConverter : INameConverter
{
public string NameForField(string fieldName, IComplexGraphType graphType) => fieldName.ToPascalCase();
public string NameForArgument(string argumentName, IComplexGraphType graphType, FieldType field) => argumentName.ToPascalCase();
}
Global references to the three introspection fields are now properties on ISchema
The three introspection field definitions for __schema
, __type
, and __typename
have moved from static properties on the SchemaIntrospection
class
to properties of the ISchema
interface, typically provided by the Schema
class. Custom implementations of ISchema
must implement three new properties:
SchemaMetaFieldType
, TypeMetaFieldType
, and TypeNameMetaFieldType
. These can be provided by the GraphTypesLookup
class.
ISchema
now implements IProvideMetadata
(only for GraphQL.NET >= v3.2.0)
Formally it is a breaking change but practically clients shouldn't run into problems, as hardly anyone creates their own ISchema
implementations preferring
to inherit from Schema
class.