:warning: For the best upgrade experience, please upgrade to 7.9 and use the included analyzers to apply automatic code fixes to obsolete code patterns before upgrading to 8.0. :warning:
See issues and pull requests done in v8.
OverviewGraphQL.NET v8 is a major release that includes many new features, including:
ToObject
is dynamically compiledComplexScalarGraphType
added, allowing flexible input/output typesIMetadataWriter
and similar interfaces added to allow better intellisenseField
methodsListGraphType<IntGraphType>
)Some of these features require changes to the infrastructure, which can cause breaking changes during upgrades. Most notably, if your server uses any of the following features, you are likely to encounter migration issues:
IGraphType
implementations not based on an included class)AddGraphQL
builder methods)ExecutionHelper.GetArguments
Below we have documented each new feature and breaking change, outlining the steps you need to take to upgrade your application to v8.0. When possible, we have provided code examples to help you understand the changes required, and options to revert behavior to v7.x if necessary.
Please keep in mind that methods and classes marked as obsolete in v8.x may be removed in v9.0. Each obsolete member will include a message indicating the expected removal version.
For best results through the migration, we recommend following these steps:
GlobalSwitches.UseLegacyTypeNaming
to false
and verify your type names have not changed (or override them), or set GlobalSwitches.UseLegacyTypeNaming
to true
to maintain v7.x behavior during the migrationGlobalSwitches.InferFieldNullabilityFromNRTAnnotations
to true
and verify your inferred field types have not changed, or set GlobalSwitches.InferFieldNullabilityFromNRTAnnotations
to false
to maintain v7.x behaviorIMetadataReader
, IMetadataWriter
and IFieldMetadataWriter
interfaces added
This makes it convenient to add extension methods to graph types or fields that can be used to read or write metadata such as authentication information. Methods for IMetadataWriter
types will appear on both field builders and graph/field types, while methods for IMetadataReader
types will only appear on graph and field types. You can also access the IMetadataReader
reader from the IMetadataWriter.MetadataReader
property. Here is an example:
public static TMetadataBuilder RequireAdmin<TMetadataBuilder>(this TMetadataBuilder builder)
where TMetadataBuilder : IMetadataWriter
{
if (builder.MetadataReader.GetRoles?.Contains("Guests"))
throw new InvalidOperationException("Cannot require admin and guest access at the same time.");
return builder.AuthorizeWithRoles("Administrators");
}
Both interfaces extend IProvideMetadata
with read/write access to the metadata contained within the graph or field type. Be sure not to write metadata during the execution of a query, as the same graph/field type instance may be used for multiple queries and you would run into concurrency issues.
In addition, the IFieldMetadataWriter
interface has been added to allow scoping extension methods to fields only. For example:
public static TMetadataWriter Requires<TMetadataWriter>(this TMetadataWriter fieldType, string fields)
where TMetadataWriter : IFieldMetadataWriter
=> fieldType.ApplyDirective(PROVIDES_DIRECTIVE, d => d.AddArgument(new(FIELDS_ARGUMENT) { Value = fields }));
2. Built-in scalars may be overridden via DI registrations
For GraphQL.NET built-in scalars (such as IntGraphType
or GuidGraphType
), a dervied class may be registered within the DI engine to facilitate replacement of the graph type throughout the schema versus calling RegisterType
.
services.AddSingleton<BooleanGraphType, MyBooleanGraphType>();
See https://graphql-dotnet.github.io/docs/getting-started/custom-scalars/#3-register-the-custom-scalar-within-your-schema for more details.
3. AddedComplexScalarGraphType
This new scalar can be used to send or receive arbitrary objects or values to or from the server. It is functionally equivalent to the AnyGraphType
used for GraphQL Federation, but defaults to the name of Complex
rather than _Any
.
Parser
delegates added to input field and argument definitions
This allows for custom parsing of input values. The Parser
delegate is used to convert the input value to the expected type, and can be set via the ParseValue
method on the FieldBuilder
or QueryArgument
.
The auto-registering graph types will automatically configure the Parser
delegate appropriately, and the Field(x => x.Property)
and Field("FieldName", x => x.Property)
syntax will as well.
The most common use case for this is when using the ID graph type (passed via a string) for a numeric or GUID identifier. For example, consider the following code:
Field("Id", x => x.Id, type: typeof(NonNullGraphType<IdGraphType>));
class MyInputObject
{
public int Id { get; set; }
}
This will now cause an error when the client sends a string value for the Id field that cannot be coerced to an int
during the validation stage, rather than during the execution stage. Supplying an invalid value will produce a response similar to the following:
{
"errors": [
{
"message": "Invalid value for argument 'id' of field 'testMe'. The input string 'abc' was not in a correct format.",
"locations": [
{
"line": 1,
"column": 14
}
],
"extensions": {
"code": "INVALID_VALUE",
"codes": [
"INVALID_VALUE",
"FORMAT"
],
"number": "5.6"
}
}
]
}
This now is a validation error and not passed to the unhandled exception handler. Previously, this would have been considered a server exception and processed by the unhandled exception handler, returning an error similar to the following:
{
"errors": [
{
"message": "Error trying to resolve field 'testMe'.",
"locations": [
{
"line": 1,
"column": 3
}
],
"path": [
"testMe"
],
"extensions": {
"code": "FORMAT",
"codes": [
"FORMAT"
]
}
}
],
"data": null
}
You can also define a custom parser when appropriate to convert an input value to the expected type. This is typically unnecessary when using the Field(x => x.Property)
syntax, but when matching via property name, it may be desired to define a custom parser. For example:
Field<StringGraphType>("website")
.ParseValue(value => new Uri((string)value));
class MyInputObject
{
public Uri? Website { get; set; }
}
Without adding a parser the coercion will occur within the resolver during GetArgument<Uri>("abc")
as occured in previous versions of GraphQL.NET. This will result in a server exception being thrown and processed by the unhandled exception handler if the value cannot be coerced to a Uri
. Note that the parser function need not check for null values.
For type-first schemas in v8.1 and later, you may use the [Parser]
attribute to define a custom parser as shown in the below example:
public class OutputClass1
{
public static string Hello1([Parser(nameof(ParseHelloArgument))] string value) => value;
public static string Hello2([Parser(typeof(ParserClass))] string value) => value;
public static string Hello3([Parser(typeof(HelperClass), nameof(HelperClass.ParseHelloArgument))] string value) => value;
private static object ParseHelloArgument(object value) => (string)value + "test1";
}
public class InputClass1
{
[Parser(nameof(ParseHelloArgument))]
public string? Field1 { get; set; }
private static object ParseHelloArgument(object value) => (string)value + "test1";
}
Note that this will replace the default parser configured by the auto-registering graph type.
5.Validator
delegates added to input field and argument definitions
This allows for custom validation of input values. It can be used to easily validate input values such as email addresses, phone numbers, or to validate a value is within a specific range. The Validator
delegate is used to validate the input value, and can be set via the Validate
method on the FieldBuilder
or QueryArgument
. Here are some examples:
Field(x => x.FirstName)
.Validate(value =>
{
if (((string)value).Length >= 10)
throw new ArgumentException("Length must be less than 10 characters.");
});
Field(x => x.Age)
.Validate(value =>
{
if ((int)value < 18)
throw new ArgumentException("Age must be 18 or older.");
});
Field(x => x.Password)
.Validate(value =>
{
VerifyPasswordComplexity((string)value);
});
The Validator
delegate is called during the validation stage, prior to execution of the request. Null values are not passed to the validation function. Supplying an invalid value will produce a response similar to the following:
{
"errors": [
{
"message": "Invalid value for argument 'firstName' of field 'testMe'. Length must be less than 10 characters.",
"locations": [
{
"line": 1,
"column": 14
}
],
"extensions": {
"code": "INVALID_VALUE",
"codes": [
"INVALID_VALUE",
"ARGUMENT"
],
"number": "5.6"
}
}
]
}
For type-first schemas, you may define your own attributes to perform validation, either on input fields or on output field arguments. For example:
public class MyClass
{
public static string TestMe([MyMaxLength(5)] string value) => value;
}
private class MyMaxLength : GraphQLAttribute
{
private readonly int _maxLength;
public MyMaxLength(int maxLength)
{
_maxLength = maxLength;
}
public override void Modify(ArgumentInformation argumentInformation)
{
if (argumentInformation.TypeInformation.Type != typeof(string))
{
throw new InvalidOperationException("MyMaxLength can only be used on string arguments.");
}
}
public override void Modify(QueryArgument queryArgument)
{
queryArgument.Validate(value =>
{
if (((string)value).Length > _maxLength)
{
throw new ArgumentException($"Value is too long. Max length is {_maxLength}.");
}
});
}
}
In version 8.1 and newer, you can use the [Validator]
attribute to define a custom validator as shown in the below example:
public class OutputClass2
{
public static string Hello1([Validator(nameof(ValidateHelloArgument))] string value) => value;
public static string Hello2([Validator(typeof(ValidatorClass))] string value) => value;
public static string Hello3([Validator(typeof(HelperClass), nameof(HelperClass.ValidateHelloArgument))] string value) => value;
private static void ValidateHelloArgument(object value)
{
if ((string)value != "hello")
throw new ArgumentException("Value must be 'hello'.");
}
}
public class InputClass2
{
[Validator(nameof(ValidateHelloArgument))]
public string? Field1 { get; set; }
private static void ValidateHelloArgument(object value)
{
if ((string)value != "hello")
throw new ArgumentException("Value must be 'hello'.");
}
}
Similar to the Parser
delegate, the Validator
delegate is called during the validation stage, and will not unnecessarily trigger the unhandled exception handler due to client input errors.
At this time GraphQL.NET does not directly support the MaxLength
and similar attributes from System.ComponentModel.DataAnnotations
, but this may be added in a future version. You can implement your own attributes as shown above, or call the Validate
method to set a validation function.
ArgumentValidator
delegate added to input object/interface field definitions (since 8.1)
This allows for custom validation of the coerced argument values. It can be used when you must enforce certain relationships between arguments, such as ensuring that at least one of multiple arguments are provided, or that a specific argument is provided when another argument is set to a specific value. It can be set by configuring the ValidateArguments
delegate on the FieldType
class or calling the ValidateArguments
method on the field builder. Here is an example:
Field<string>("example")
.Argument<string>("str1", true)
.Argument<string>("str2", true)
.ValidateArguments(ctx =>
{
var str1 = ctx.GetArgument<string>("str1");
var str2 = ctx.GetArgument<string>("str2");
if (str1 == null && str2 == null)
throw new ValidationError("Must provide str1 or str2");
});
Please throw a ValidationError
exception or call ctx.ReportError
to return a validation error to the client. Throwing ExecutionError
will prevent further validation rules from being executed, and throwing other exceptions will be caught by the unhandled exception handler. This is different than the Parser
and Validator
delegates, or scalar coercion methods, which will not trigger the unhandled exception handler.
In version 8.1 and newer, you may use the [ValidateArguments]
attribute to define a custom argument validator as shown in the below example:
public class OutputClass3
{
public static string Hello1(string str1, string str2) => str1 + str2;
[ValidateArguments(nameof(ValidateHelloArguments))]
public static string Hello2(string str1, string str2) => str1 + str2;
private static ValueTask ValidateHelloArguments(FieldArgumentsValidationContext context)
{
var str1 = context.GetArgument<string>("str1");
var str2 = context.GetArgument<string>("str2");
if (str1 == null && str2 == null)
context.ReportError("Must provide str1 or str2");
return default;
}
}
7. @pattern
custom directive added for validating input values against a regular expression pattern
This directive allows for specifying a regular expression pattern to validate the input value. It can also be used as sample code for designing new custom directives, and is now the preferred design over the older InputFieldsAndArgumentsOfCorrectLength
validation rule. This directive is not enabled by default, and must be added to the schema as follows:
services.AddGraphQL(b => b
.AddSchema<MyQuery>()
.ConfigureSchema(s =>
{
s.Directives.Register(new PatternMatchingDirective());
s.RegisterVisitor(new PatternMatchingVisitor());
}));
You can then apply the directive to any input field or argument as follows:
Field(x => x.FirstName)
.ApplyDirective("pattern", "regex", "[A-Z]+");
8. DirectiveAttribute added to support applying directives to type-first graph types and fields
For example:
private class Query
{
public static string Hello(
[Directive("pattern", "regex", "[A-Z]+")]
string arg)
=> arg;
}
9. Validation rules can read or validate field arguments and directive arguments
Validation rules can now execute validation code either before or after field arguments have been read. This is useful for edge cases, such as when a complexity analyzer needs to read the value of a field argument to determine the complexity of the field.
The ValidateAsync
method on IValidationRule
has been changed to GetPreNodeVisitorAsync
, and a new method GetPostNodeVisitorAsync
has been added. Also, the IVariableVisitorProvider
interface has been combined with IValidationRule
and now has a new method GetVariableVisitorAsync
. So the new IValidationRule
interface looks like this:
public interface IValidationRule
{
ValueTask<INodeVisitor?> GetPreNodeVisitorAsync(ValidationContext context);
ValueTask<IVariableVisitor?> GetVariableVisitorAsync(ValidationContext context);
ValueTask<INodeVisitor?> GetPostNodeVisitorAsync(ValidationContext context);
}
This allows for a single validation rule to validate AST structure, validate variable values, and/or validate coerced field and directive arguments.
To simplify the creation of validation rules, the abstract ValidationRuleBase
class has been added, which implements the IValidationRule
interface and provides default implementations for all three methods.
Documentation has been added to the Query Validation section of the documentation to explain how to create custom validation rules using the revised IValidationRule
interface and related classes.
Previously only specific list types were natively supported, such as List<T>
and IEnumerable<T>
, and list types that implemented IList
. Now, any list-like type such as HashSet<T>
or Queue<T>
which has either a public parameterless constructor along with an Add(T value)
method, or a constructor that takes an IEnumerable<T>
is supported. This allows for more flexibility in the types of lists that can be used as CLR input types.
You can also register a custom list coercion provider to handle custom list types. For instance, if you wish to use a case-insensitive comparer for HashSet<string>
types, you can register a custom list coercion provider as follows:
ValueConverter.RegisterListConverter<HashSet<string>, string>(
values => new HashSet<string>(values, StringComparer.OrdinalIgnoreCase));
ValueConverter.RegisterListConverter<ISet<string>, string>(
values => new HashSet<string>(values, StringComparer.OrdinalIgnoreCase));
The RegisterListProvider
method is also useful in AOT scenarios to provide ideal performance since dynamic code generation is not possible, and to prevent trimming of necessary list types.
You can also register a custom list coercion provider for an open generic type. For instance, if you wish to provide a custom list coercion provider for IImmutableList<T>
, you can register it as follows:
public class ImmutableListConverterFactory : ListConverterFactoryBase
{
public override Func<object?[], object> Create<T>()
=> list => ImmutableList.CreateRange(list.Cast<T>());
}
ValueConverter.RegisterListConverterFactory(typeof(IImmutableList<>), new ImmutableListConverterFactory());
Finally, if you simply need to map an interface list type to a concrete list type, you can do so as follows:
ValueConverter.RegisterListConverterFactory(typeof(IList<>), typeof(List<>));
11. IGraphType.IsPrivate
and IFieldType.IsPrivate
properties added
Allows to set a graph type or field as private within a schema visitor, effectively removing it from the schema. Introspection queries will not be able to query the type/field, and queries will not be able to reference the type/field. Exporting the schema as a SDL (or printing it) will not include the private types or fields.
Private types are fully 'resolved' and validated; you can obtain references to these types or fields in a schema validation visitor before they are removed from the schema. After initialization is complete, these types and fields will not be present within SchemaTypes or TypeFields. The only exception for validation is that private types are not required have any fields or, for interfaces and unions, possible types.
This makes it possible to create a private type used within the schema but not exposed to the client. For instance, it is possible to dynamically create input object types to deserialize GraphQL Federation entity representations, which are normally sent via the _Any
type.
IObjectGraphType.SkipTypeCheck
property added
Allows to skip the type check for a specific object graph type during resolver execution. This is useful for schema-first schemas where the CLR type is not defined while the resolver is built, while allowing IsTypeOf
to be set automatically for other use cases. Schema-first schemas will automatically set this property to true
for all object graph types to retain the existing behavior.
ISchemaNodeVisitor.PostVisitSchema
method added
Allows to revisit the schema after all other methods (types/fields/etc) have been visited.
14. GraphQL Federation v2 graph types addedThese graph types have been added to the GraphQL.Federation.Types
namespace:
AnyScalarType
(moved from GraphQL.Utilities.Federation
)EntityGraphType
FieldSetGraphType
LinkImportGraphType
LinkPurpose
enumerationLinkPurposeGraphType
ServiceGraphType
These extension methods and attributes simplify the process of applying GraphQL Federation directives:
Directive Extension Method Attribute Description@external
External()
[External]
Indicates that this subgraph usually can't resolve a particular object field, but it still needs to define that field for other purposes. @requires
Requires(fields)
[Requires(fields)]
Indicates that the resolver for a particular entity field depends on the values of other entity fields that are resolved by other subgraphs. This tells the router that it needs to fetch the values of those externally defined fields first, even if the original client query didn't request them. @provides
Provides(fields)
[Provides(fields)]
Specifies a set of entity fields that a subgraph can resolve, but only at a particular schema path (at other paths, the subgraph can't resolve those fields). @key
Key(fields)
[Key(fields)]
Designates an object type as an entity and specifies its key fields. Key fields are a set of fields that a subgraph can use to uniquely identify any instance of the entity. @override
Override(from)
[Override(from)]
Indicates that an object field is now resolved by this subgraph instead of another subgraph where it's also defined. This enables you to migrate a field from one subgraph to another. @shareable
Shareable()
[Shareable]
Indicates that an object type's field is allowed to be resolved by multiple subgraphs (by default in Federation 2, object fields can be resolved by only one subgraph). @inaccessible
Inaccessible()
[Inaccessible]
Indicates that a definition in the subgraph schema should be omitted from the router's API schema, even if that definition is also present in other subgraphs. This means that the field is not exposed to clients at all. 16. OneOf Input Object support added
OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null, all others being omitted. This is useful for representing situations where an input may be one of many different options.
See: https://github.com/graphql/graphql-spec/pull/825
To use this feature:
IsOneOf
property on your InputObjectGraphType
to true
.@oneOf
directive on the input type in your schema definition.[OneOf]
directive on the CLR class.Note: the feature is still a draft and has not made it into the official GraphQL spec yet. It is expected to be added once it has been implemented in multiple libraries and proven to be useful. It is not expected to change from the current draft.
17. Federation entity resolver configuration methods and attributes added for code-first and type-first schemasExtension methods have been added for defining entity resolvers in code-first and type-first schemas for GraphQL Federation.
Code-first sample 1: (uses entity type for representation)
public class WidgetType : ObjectGraphType<Widget>
{
public WidgetType()
{
this.Key("id");
this.ResolveReference(async (context, widget) =>
{
var id = widget.Id;
var widgetData = context.RequestServices!.GetRequiredService<WidgetRepository>();
return await widgetData.GetWidgetByIdAsync(id, context.CancellationToken);
});
Field(x => x.Id, type: typeof(NonNullGraphType<IdGraphType>));
Field(x => x.Name);
}
}
public class Widget
{
public string Id { get; set; }
public string Name { get; set; }
}
Code-first sample 2: (uses custom type for representation)
public class WidgetType : ObjectGraphType<Widget>
{
public WidgetType()
{
this.Key("id");
this.ResolveReference<WidgetRepresentation, Widget>(async (context, widget) =>
{
var id = widget.Id;
var widgetData = context.RequestServices!.GetRequiredService<WidgetRepository>();
return await widgetData.GetWidgetByIdAsync(id, context.CancellationToken);
});
Field(x => x.Id, type: typeof(NonNullGraphType<IdGraphType>));
Field(x => x.Name);
}
}
public class Widget
{
public string Id { get; set; }
public string Name { get; set; }
}
public class WidgetRepresentation
{
public string Id { get; set; }
}
Type-first sample 1: (static method; uses method arguments for representation)
[Key("id")]
public class Widget
{
[Id]
public string Id { get; set; }
public string Name { get; set; }
[FederationResolver]
public static async Task<Widget> ResolveReference([FromServices] WidgetRepository widgetData, [Id] string id, CancellationToken token)
{
return await widgetData.GetWidgetByIdAsync(id, token);
}
}
Type-first sample 2: (instance method; uses instance for representation)
[Key("id")]
public class Widget
{
[Id]
public string Id { get; set; }
public string Name { get; set; }
[FederationResolver]
public async Task<Widget> ResolveReference([FromServices] WidgetRepository widgetData, CancellationToken token)
{
var id = Id;
return await widgetData.GetWidgetByIdAsync(id, token);
}
}
Note that you may apply the [Key]
attribute multiple times to define multiple sets of key fields, pursuant to the GraphQL Federation specification. You may define multiple resolvers when using static methods in a type-first schema. Otherwise your method will need to decide which set of key fields to use for resolution, as demonstrated in the code-first sample below:
public class WidgetType : ObjectGraphType<Widget>
{
public WidgetType()
{
this.Key("id");
this.Key("sku");
this.ResolveReference(async (context, widget) =>
{
var id = widget.Id;
var sku = widget.Sku;
var widgetData = context.RequestServices!.GetRequiredService<WidgetRepository>();
if (id != null)
return await widgetData.GetWidgetByIdAsync(id, context.CancellationToken);
else
return await widgetData.GetWidgetBySkuAsync(sku, context.CancellationToken);
});
Field(x => x.Id, type: typeof(NonNullGraphType<IdGraphType>));
Field(x => x.Sku);
Field(x => x.Name);
}
}
public class Widget
{
public string Id { get; set; }
public string Sku { get; set; }
public string Name { get; set; }
}
18. Applied directives may contain metadata
AppliedDirective
now implements IProvideMetadata
, IMetadataReader
and IMetadataWriter
to allow for reading and writing metadata to applied directives.
@link
directive
This directive indicates that some types and/or directives are to be imported from another schema. Types and directives can be explicitly imported, either with their original name or with an alias. Any types or directives that are not explicitly imported will be assumed to be named with a specified namespace, which is derived from the URL of the linked schema if not set explicitly. Visit https://specs.apollo.dev/link/v1.0/ for more information.
To link another schema, use code like this in your schema constructor or ConfigureSchema
call:
schema.LinkSchema("https://specs.apollo.dev/federation/v2.3", o =>
{
o.Namespace = "fed";
o.Imports.Add("@key", "@key");
o.Imports.Add("@shareable", "@share");
});
In addition to applying a @link
directive to the schema, it will also import the @link
directive and configure the necessary types and directives to support the @link
specification. Your schema will then look like this:
schema
@link(url: "https://specs.apollo.dev/link/v1.0", import: ["@link"])
@link(url: "https://specs.apollo.dev/federation/v2.3", as: "fed", import: ["@key", {name:"@shareable", as:"@share"}]) {
}
directive @link(url: String!, as: String, import: [link__Import], purpose: link__Purpose) repeatable on SCHEMA
scalar link__Import
enum link__Purpose {
EXECUTION
SECURITY
}
You will still be required to add the imported schema definitions to your schema, such as @key
, @share
, and @fed__requires
in the above example. You may also print the schema without imported definitions. To print the schema without imported definitions, set the IncludeImportedDefinitions
option to false
when printing:
var sdl = schema.Print(new() { IncludeImportedDefinitions = false });
The schema shown above would now print like this:
schema
@link(url: "https://specs.apollo.dev/link/v1.0", import: ["@link"])
@link(url: "https://specs.apollo.dev/federation/v2.3", as: "fed", import: ["@key", {name:"@shareable", as:"@share"}]) {
}
Note that you may call LinkSchema
multiple times with the same URL to apply additional configuration options to the same url, or with a separate URL to link multiple schemas.
FromSchemaUrl
added to AppliedDirective
This property supports using a directive that was separately imported via @link
. After importing the schema as described above, apply imported directives to your schema similar to the example below:
graphType.ApplyDirective("shareable", s => s.FromSchemaUrl = "https://specs.apollo.dev/federation/");
graphType.ApplyDirective("shareable", s => s.FromSchemaUrl = "https://specs.apollo.dev/federation/v2.3");
During schema initialization, the name of the applied directive will be resolved to the fully-qualified name. In the above example, if @shareable
was imported, the directive will be applied as @shareable
, but if not, it will be applied as @federation__shareable
. Aliases are also supported.
AddFederation
GraphQL builder call added to initialize any schema for federation support
This method will automatically add the necessary types and directives to support GraphQL Federation. Simply call AddFederation
with the version number of the Federation specification that you wish to import within your DI configuration. See example below:
services.AddGraphQL(b => b
.AddSchema<MySchema>()
.AddFederation("2.3")
);
This will do the following:
_service
field._Entity
type based on which of the schema's type definitions are marked with @key
._entities
field if there are any resolvable entities.https://specs.apollo.dev/federation/v2.3
.@key
, @requires
, @provides
, @external
, @extends
, @shareable
, @inaccessible
, @override
and @tag
directives.federation
namespace - @federation__interfaceObject
and @federation__composeDirective
for version 2.3.Currently supported are versions 1.0 through 2.8. Note that for version 1.0, you will be required to mark parts of your schema with @extends
. This is not required for version 2.0 and later.
You may add additional configuration to the AddFederation
call to import additional directives or types, remove imports, change import aliases, or change the namespace used for directives that are not explicitly imported.
When defining the field with expression, the graph type nullability will be inferred from Null Reference Types (NRT) by default. To disable the feature, set the GlobalSwitches.InferFieldNullabilityFromNRTAnnotations
to false
.
For example, given the following code
public class Person
{
public string FullName { get; set; }
public string? SpouseName { get; set; }
public IList<string>? Children { get; set; }
}
public class PersonGraphType : ObjectGraphType<Person>
{
public PersonGraphType()
{
Field(p => p.FullName);
Field(p => p.SpouseName);
Field(p => p.Children);
}
}
When InferFieldNullabilityFromNRTAnnotations
is true
(default), the result is:
type Person {
fullName: String!
spouseName: String
children: [String!]
}
When InferFieldNullabilityFromNRTAnnotations
is false
:
type Person {
fullName: String!
spouseName: String!
children: [String]!
}
23. ValidationContext.GetRecursivelyReferencedFragments
updated with @skip
and @include
directive support
When developing a custom validation rule, such as an authorization rule, you may need to determine which fragments are recursively referenced by an operation by calling GetRecursivelyReferencedFragments
with the onlyUsed
argument set to true
. The method will then ignore fragments that are conditionally skipped by the @skip
or @include
directives.
GraphQL.NET now supports persisted documents based on the draft spec listed here. Persisted documents are a way to store a query string on the server and reference it by a unique identifier, typically a SHA-256 hash. When enabled, the default configuration disables use of the query
field in the request body and requires the client to use the documentId
field instead. This acts as a whitelist of allowed queries and mutations that the client may execute, while also reducing the size of the request body.
To configure persisted document support, you must implement the IPersistedDocumentLoader
interface to retrieve the query string based on the document identifier, or set the GetQueryDelegate
property on the PersistedDocumentOptions
class. See typical examples below:
In the below example, regular requests (via the query property) are disabled, and only document identifiers prefixed with sha256:
are allowed where the id is a 64-character lowercase hexadecimal string.
services.AddGraphQL(b => b
.UsePeristedDocuments<MyLoader>(GraphQL.DI.ServiceLifetime.Scoped)
);
public class MyLoader : IPersistedDocumentLoader
{
public async ValueTask<string?> GetQueryAsync(string? documentIdPrefix, string documentIdPayload, CancellationToken cancellationToken)
{
return await _db.QueryDocuments
.Where(x => x.Hash == documentIdPayload)
.Select(x => x.Query)
.FirstOrDefaultAsync(cancellationToken);
}
}
Sample request:
{
"documentId": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"variables": {
"id": "1"
}
}
Example 2 - Configuring persisted documents with the options class
In the below example, regular requests are allowed, and document identifiers are unprefixed GUIDs.
services.AddGraphQL(b => b
.UsePeristedDocuments(options =>
{
options.AllowNonpersistedDocuments = true;
options.AllowedPrefixes.Clear()
options.AllowedPrefixes.Add(null);
options.GetQueryDelegate = async (executionOptions, documentIdPrefix, documentIdPayload) =>
{
if (!Guid.TryParse(documentIdPayload, out var id))
return null;
var db = executionOptions.RequestServices!.GetRequiredService<MyDbContext>();
return await db.QueryDocuments
.Where(x => x.Id == id)
.Select(x => x.Query)
.FirstOrDefaultAsync(executionOptions.CancellationToken);
};
});
);
Sample persisted document request:
{
"documentId": "01234567-89ab-cdef-0123-456789abcdef",
"variables": {
"id": "1"
}
}
Caching (v8.3.0+)
The persisted document handler does not provide caching by default. You may implement your own caching mechanism within the GetQueryAsync
method to cache the query strings based on the document identifier. Alternatively, you may add the .UseMemoryCache()
method from the GraphQL.MemoryCache
package to enable in-memory caching. Be sure to call UseMemoryCache
before calling UsePeristedDocuments
to ensure that the cache is used.
services.AddGraphQL(b => b
.UseMemoryCache()
.UsePeristedDocuments<MyLoader>(GraphQL.DI.ServiceLifetime.Scoped)
);
24. Execution timeout support
ExecutionOptions.Timeout
has been added to allow a maximum time for the execution of a query. If the execution exceeds the timeout, the execution will be cancelled and a timeout error will be returned to the client. The default value is Timeout.InfiniteTimeSpan
, which means no timeout is set. The timeout error is not an 'unhandled' exception and so is not passed through the UnhandledExceptionDelegate
.
Configuration:
services.AddGraphQL(b => b
.WithTimeout(TimeSpan.FromSeconds(30))
);
options.Timeout = TimeSpan.FromSeconds(30);
Example response:
{
"errors":[
{
"message": "The operation has timed out.",
"extensions": { "code": "TIMEOUT", "codes": [ "TIMEOUT" ] }
}
]
}
Note that the timeout triggers immediate cancellation throughout the execution pipeline, including resolvers and middleware. This means that any pending response data will be discarded as the client would otherwise receive an incomplete response.
You may alternatively configure the timeout to throw a TimeoutException
to the caller. Set the TimeoutAction
property as shown here:
services.AddGraphQL(b => b
.WithTimeout(TimeSpan.FromSeconds(30), TimeoutAction.ThrowTimeoutException)
);
options.Timeout = TimeSpan.FromSeconds(30);
options.TimeoutAction = TimeoutAction.ThrowTimeoutException;
If you wish to catch and handle timeout errors, use the delegate overload as demonstrated below:
services.AddGraphQL(b => b
.WithTimeout(TimeSpan.FromSeconds(30), options =>
{
var logger = options.RequestServices!.GetRequiredService<ILogger<MySchema>>();
logger.LogError("The operation has timed out.");
return new ExecutionResult(new TimeoutError());
})
);
Please note that signaling the cancellation token passed to ExecutionOptions.CancellationToken
will always rethrow the OperationCanceledException
to the caller, regardless of the TimeoutAction
setting.
Please review the documentation for the new complexity analyzer to understand how to use it and how to configure it. See the Complexity Analzyer document for more information.
26. Optimization when returning lists of scalarsWhen returning a list of scalars, specifically of intrinsic types such as int
, string
, and bool
, the matching scalar type (e.g. IntGraphType
, StringGraphType
, BooleanGraphType
) will be used to serialize the list as a whole rather than each individual item. This can result in a significant performance improvement when returning large lists of scalars. Be sure the returned list type (e.g. IEnumerable<int>
) matches the scalar type (e.g. IntGraphType
) to take advantage of this optimization. Scalar types that require conversion, such as DateTimeGraphType
are not currently optimized in this way.
Pursuant to the current GraphQL specification, interfaces may implement other interfaces. Add implemented interfaces to your interface type definition as done for object graph types, as shown below:
Schema-first:
interface Node {
id: ID!
}
interface Character implements Node {
id: ID!
name: String!
}
Code-first:
public class NodeGraphType : InterfaceGraphType
{
public NodeGraphType()
{
Field<NonNullGraphType<IdGraphType>>("id");
}
}
public class CharacterGraphType : InterfaceGraphType
{
public CharacterGraphType()
{
Field<NonNullGraphType<IdGraphType>>("id");
Field<NonNullGraphType<StringGraphType>>("name");
Interface<NodeGraphType>();
}
}
Type-first:
public interface Node
{
string Id { get; }
}
[Implements(typeof(Node))]
public interface Character : Node
{
string Name { get; }
}
28. IResolveFieldContext.ExecutionContext
property added
The ExecutionContext
property has been added to the IResolveFieldContext
interface to allow access to the underlying execution context. This is useful for accessing the parsed arguments and directives from the operation via IExecutionContext.GetArguments
and GetDirectives
.
[DefaultAstValue]
attribute added (v8.1 and newer) to set default values for complex types with type-first schemas
When defining an input object or output field argument in a type-first schema, you may now use the [DefaultAstValue]
attribute to specify a default value for the argument. This is useful when the argument is a complex type that cannot be represented as a constant via [DefaultValue]
or in the method signature.
public class MyInputObject1
{
[DefaultValue("value")]
public required string Field1 { get; set; }
}
public class MyInputObject2
{
[DefaultAstValue("{ field1: \"value\" }")]
public MyInputObject1 Json { get; set; }
}
public class MyOutputObject
{
public string Field1(string arg = "abc") => arg;
public string Field2([DefaultAstValue("{ field1: \"sample2\" }")] MyInputObject1 arg) => arg.Field1;
}
30. NoClrMapping Extension for Input Fields with Custom CLR Mapping (from version 8.4.0)
A new extension method, NoClrMapping
, has been introduced to give you fine-grained control over how input object fields are mapped to CLR types. In many scenarios, your input types may include fields that are computed or require custom processing before being set on your CLR model. For example, you might have a CLR type with a single FullName
property but want your input object to accept separate firstName
and lastName
fields. By marking these fields with NoClrMapping()
, they are excluded from the automatic binding process. You can then override the ParseDictionary
method to manually combine these values into the CLR's FullName
property, while letting the base implementation handle any automatically mapped fields (such as Age
). The following example illustrates this pattern:
public class Person
{
public string FullName { get; set; } = string.Empty;
public int Age { get; set; }
}
public class PersonInputType : InputObjectGraphType<Person>
{
public PersonInputType()
{
Name = "PersonInput";
Field<NonNullGraphType<StringGraphType>>("firstName").NoClrMapping();
Field<NonNullGraphType<StringGraphType>>("lastName").NoClrMapping();
Field(x => x.Age);
}
public override object ParseDictionary(IDictionary<string, object?> value)
{
var person = (Person)base.ParseDictionary(value);
if (value.TryGetValue("firstName", out var firstNameObj) && firstNameObj is string firstName &&
value.TryGetValue("lastName", out var lastNameObj) && lastNameObj is string lastName)
{
person.FullName = $"{firstName} {lastName}";
}
return person;
}
}
In this example, the input object defines three fields: firstName
, lastName
, and age
. The firstName
and lastName
fields are marked with NoClrMapping()
so that they are not automatically bound to any CLR properties. Instead, the overridden ParseDictionary
method combines these two fields into the FullName
property of the Person
CLR type. Meanwhile, the Age
field is processed via the standard mapping mechanism by calling base.ParseDictionary
. This approach allows you to seamlessly mix automatic and custom binding, providing greater flexibility in how your input objects are processed and mapped.
Document caching now supports unique cache keys for applications using multiple or dynamic schemas. The cache key has been enhanced to include an extra property computed by the new AdditionalCacheKeySelector
delegate. By default, this delegate distinguishes between schema-first and code-first implementations by returning the schema instance for schema-first and dynamic schemas (those that are direct implementations of Schema
), and the schema type for code-first and type-first schemas (schemas that derive from Schema
). This change ensures that cached documents are uniquely identified even when multiple schemas are configured in the same application, while still supporting caching for scoped code-first and type-first schemas.
To migrate, you may continue using the default behavior. However, if you need further customization -- such as incorporating additional context to identify unique dynamic schemas -- you can override the default selector as shown in the example below:
services.AddGraphQL(b => b
.UseMemoryCache(options =>
{
options.AdditionalCacheKeySelector = execOptions =>
{
if (execOptions.UserContext is IDictionary<string, object> context &&
context.TryGetValue("CustomHeader", out var header))
{
return header;
}
return null;
};
})
);
These changes help ensure that cached documents are correctly associated with the appropriate schema when multiple schemas are in use.
32.IGraphQLBuilder.ValidateServices
added to validate services during schema initialization (8.5.0+)
A new method, ValidateServices
, has been added to the IGraphQLBuilder
interface to allow you to validate field resolver dependencies during schema initialization. Validation occurs during schema initialization, verifying that any field arguments marked with [FromServices]
can be resolved by the DI container, as well as any services referenced via .WithService<T>()
. This method is useful for detecting potential runtime errors caused by missing services or incorrectly registered dependencies. The following example demonstrates how to use this method:
services.AddGraphQL(b => b
.AddSchema<MySchema>()
.ValidateServices()
);
You may also manually mark a FieldType
as requiring a service by calling .DependsOn<T>()
:
Field<StringGraphType>("myField")
.DependsOn<IMyService>()
.Resolve(context =>
{
var service = context.RequestServices!.GetRequiredService<IMyService>();
return service.GetMyValue();
});
33. Added capability to reference object graph types from their implementing interfaces (8.5.0+)
GraphQL.NET allows developers to configure object graph types to reference interfaces that they implement via the Interface<T>()
or AddResolvedInterface(IInterfaceGraphType type)
. Alternatively, you may reference the object graph type from the interface by using AddPossibleType(IObjectGraphType type)
. However, this requires a concrete reference to the instantiated class rather than just the Type
to be resolved during schema initialization.
In version 8.5.0+, you may use Type<TObjectType>()
or Type(Type objectType)
to reference the object type from the interface type. This matches the same methods already available on UnionGraphType
, unifying the API between unions and interfaces. The new methods will be added to IAbstractGraphType
in version 9, which is implemented by both UnionGraphType
and IInterfaceGraphType
. For example:
public class MyInterfaceGraphType : InterfaceGraphType
{
public MyInterfaceGraphType()
{
Type<MyObjectGraphType>();
Field<string>("Name");
}
}
For type-first users, a new attribute [PossibleType(Type objectType)]
has been added which can be marked on interface CLR types to reference an object CLR or graph type that the interface type implements. For instance:
[PossibleType(typeof(MyObject))]
public interface IMyInterface
{
public string Name { get; set; }
}
Breaking Changes 1. Query type is required
Pursuant to the GraphQL specification, a query type is required for any schema. This is enforced during schema validation but may be bypassed as follows:
GlobalSwitches.RequireRootQueryType = false;
Future versions of GraphQL.NET will not contain this property and each schema will always be required to have a root Query type to comply with the GraphQL specification.
2. UseApplyDirective
instead of Directive
on field builders
The Directive
method on field builders has been renamed to ApplyDirective
to better fit with other field builder extension methods.
WithComplexityImpact
instead of ComplexityImpact
on field builders
The ComplexityImpact
method on field builders has been renamed to WithComplexityImpact
to better fit with other field builder extension methods.
Previuosly the Relay graph types were instantiated directly by the SchemaTypes
class. This has been changed so that the types are now pulled from the DI container. No changes are required if you are using the provided DI builder methods, as they automatically register the relay types. Otherwise, you will need to manually register the Relay graph types.
services.AddGraphQL(b => {
b.AddSchema<StarWarsSchema>();
});
services.AddSingleton<StarWarsSchema>();
services.AddSingleton<PageInfoType>();
services.AddSingleton(typeof(EdgeType<>);
services.AddSingleton(typeof(ConnectionType<>);
services.AddSingleton(typeof(ConnectionType<,>);
5. Duplicate GraphQL configuration calls with the same Use
command is ignored
Specifically, this relates to the following methods:
UseMemoryCache()
UseAutomaticPersistedQueries()
UseConfiguration<T>()
with the same T
typeThis change was made to prevent duplicate registrations of the same service within the DI container.
6.ObjectExtensions.ToObject
changes (impacts InputObjectGraphType
)
ObjectExtensions.ToObject<T>
was removed; it was only used by internal tests.ObjectExtensions.ToObject
requires input object graph type for conversion.[GraphQLConstructor]
, it is used.The changes above allow for matching behavior with source-generated or dynamically-compiled functions.
7.AutoRegisteringInputObjectGraphType
changes
ObjectExtensions.ToObject
for deserialization notes.ToObject
behavior. Does not register constructor parameters that are not read-only properties. Any attributes such as [Id]
must be applied to the property, not the constructor parameter.The default graph name of generic types has changed to include the generic type name. This should reduce naming conflicts when generics are in use. To consolidate behavior across different code paths, both Type
and GraphType
are stripped from the end of the class name. See below examples:
PersonType
PersonType
Person
PersonGraphType
Person
Person
AutoRegisteringObjectGraphType<SearchResults<Person>>
SearchResults
PersonSearchResults
LoggerGraphType<Person, string>
Logger
PersonStringLogger
InputObjectGraphType<Person>
InputObject_1
PersonInputObject
To revert to the prior behavior, set the following global switch prior to creating your schema classes:
using GraphQL;
GlobalSwitches.UseLegacyTypeNaming = true;
As usual, you are encouraged to set the name in the constructor of your class, or immediately after construction, or for auto-registering types, via an attribute. You can also set global attributes that will be applied to all auto-registering types if you wish to define your own naming logic.
The UseLegacyTypeNaming
option is deprecated and will be removed in GraphQL.NET v9.
This change simplifies using extension methods for the data loaders. You may need to remove the using GraphQL.DataLoader;
statement from your code to resolve any compiler warnings, and/or add using GraphQL;
.
Please see the v7 migration document regarding the new schema.ToAST()
and schema.Print()
methods available for printing the schema (available since 7.6).
For federated schemas, the ServiceGraphType
's sdl
field will now use the new implementation to print the schema. Please raise an issue if this causes a problem for your federated schema.
The following GraphQL DI builder methods have been removed:
Method ReplacementAddApolloTracing
UseApolloTracing
AddMiddleware
UseMiddleware
AddAutomaticPersistedQueries
UseAutomaticPersistedQueries
AddMemoryCache
UseMemoryCache
The following methods have been removed:
Method CommentTypeExtensions.IsConcrete
Use !type.IsAbstract
GraphQLTelemetryProvider.StartActivityAsync
Use StartActivity
AutoRegisteringInterfaceGraphType.BuildMemberInstanceExpression
Interfaces cannot contain resolvers so this method was unused ValidationContext.GetVariableValuesAsync
Use GetVariablesValuesAsync
The following constructors have been removed:
Class CommentVariable
Use new constructor with definition
argument VariableUsage
Use new constructor with hasDefault
argument 12. IVariableVisitorProvider
removed and IValidationRule
changed
The ValidateAsync
method on IValidationRule
has been changed to GetPreNodeVisitorAsync
, and a new method GetPostNodeVisitorAsync
has been added. Also, the IVariableVisitorProvider
interface has been combined with IValidationRule
and now has a new method GetVariableVisitorAsync
. So the new IValidationRule
interface looks like this:
public interface IValidationRule
{
ValueTask<INodeVisitor?> GetPreNodeVisitorAsync(ValidationContext context);
ValueTask<IVariableVisitor?> GetVariableVisitorAsync(ValidationContext context);
ValueTask<INodeVisitor?> GetPostNodeVisitorAsync(ValidationContext context);
}
It is recommended to inherit from ValidationRuleBase
for custom validation rules and override only the methods you need to implement.
IGraphType
, IFieldType
and IObjectGraphType
See the new features section for details on the new properties added to these interfaces. Unless you directly implement these interfaces, you should not be impacted by these changes.
14.ISchemaNodeVisitor.PostVisitSchema
method added
See the new features section for details on the new method added to this interface. Unless you directly implement this interface, you should not be impacted by this change.
15.AnyScalarType
and ServiceGraphType
moved to GraphQL.Federation.Types
These graph types, previously located within the GraphQL.Utilities.Federation
namespace, have been moved to the GraphQL.Federation.Types
namespace alongside all other federation types.
IFederatedResolver
, FuncFederatedResolver
and ResolveReferenceAsync
replaced
IFederatedResolver
has been replaced with IFederationResolver
.FuncFederatedResolver
has been replaced with FederationResolver
.ResolveReferenceAsync
has been replaced with ResolveReference
.Please note that the new members are now located in the GraphQL.Federation
namespace and may require slight changes to your code to accommodate the new signatures. The old members have been marked as obsolete and will continue to work in v8, but will be removed in v9.
__typename
into requests.
Previously, the __typename
field was automatically injected into the request for entity resolvers. This behavior has been removed as it is not required to meet the GraphQL Federation specification.
For instance, the following sample request:
{
_entities(representations: [{ __typename: "User", id: "1" }]) {
... on User {
id
}
}
}
Should now be written as:
{
_entities(representations: [{ __typename: "User", id: "1" }]) {
__typename
... on User {
id
}
}
}
Please ensure that your client requests are updated to include the __typename
field in the response. Alternatively, you can install the provided InjectTypenameValidationRule
validation rule to automatically inject the __typename
field into the request.
IInputObjectGraphType.IsOneOf
property added
See the new features section for details on the new property added to this interface. Unless you directly implement this interface, you should not be impacted by this change.
19.VariableUsage.IsRequired
property added and VariableUsage
constructor changed
This is required for OneOf Input Object support and is used to determine if a variable is required. Unless you have a custom validation rule that uses VariableUsage
, you should not be impacted by this change.
When defining the field with expression, the graph type nullability will be inferred from Null Reference Types (NRT) by default. See the new features section for more details.
To revert to old behavior set the global switch before initializing the schema
GlobalSwitches.InferFieldNullabilityFromNRTAnnotations = false;
21. The complexity analyzer has been rewritten and functions differently
The complexity analyzer has been rewritten to be more efficient and to support more complex scenarios. Please read the documentation on the new complexity analyzer to understand how it works and how to configure it. To revert to the old behavior, use the LegacyComplexityValidationRule
or GraphQL builder method as follows:
services.AddGraphQL(b => b
.AddSchema<MyQuery>()
.AddLegacyComplexityAnalyzer(c => c.MaxComplexity = 100)
);
The legacy complexity analyzer will be removed in v9.
22. Unhandled exceptions within execution configuration delegates are now handled and wrappedPreviously, unhandled exceptions within execution configuration delegates were not handled and would be thrown directly to the caller. For instance, if a database exception occurred within the delegate used to pull persisted documents, the exception would be thrown directly to the caller. Now, these exceptions are caught and processed through the unhandled exception handler delegate, which allows for logging and other processing of the exception.
Example:
services.AddGraphQL(b => b
.AddSchema<MyQuery>()
.AddUnhandledExceptionHandler(context =>
{
})
.ConfigureExecution(async (options, next) => {
throw new Exception("Database exception occurred");
return await next(options);
})
);
For this to work properly, be sure that any code liable to throw an exception is located inside a Use...
or ConfigureExecution
method, not an Add...
or ConfigureExecutionOptions
method, or else ensure that the call to AddUnhandledExceptionHandler
is first in the chain.
IExecutionContext.ExecutionOptions
property added
Custom IExecutionContext
implementations must now implement the ExecutionOptions
property. In addition, the AddUnhandledExceptionHandler
methods that have an ExecutionOptions
parameter within the delegate have been deprecated in favor of using the IExecutionContext.ExecutionOptions
property.
services.AddGraphQL(b => b
.AddSchema<MyQuery>()
.AddUnhandledExceptionHandler((context, options) =>
{
})
);
services.AddGraphQL(b => b
.AddSchema<MyQuery>()
.AddUnhandledExceptionHandler(context =>
{
var options = context.ExecutionOptions;
})
);
24. ID graph type serialization is culture-invariant
The IdGraphType
now serializes values using the invariant culture.
The DirectiveAttribute
has been moved to the GraphQL
namespace.
Small changes were made to IInterfaceGraphType
and IImplementInterfaces
to support interface inheritance. If you have custom implementations of these interfaces, you may need to update them.
Verfies that the arguments defined on fields of interfaces are also defined on fields of implementing types, pursuant to GraphQL specifications.
28. APIs marked obsolete since v5 have been removedGraphQLMetadataAttribute.InputType
has been replaced with InputTypeAttribute
.GraphQLMetadataAttribute.OutputType
has been replaced with OutputTypeAttribute
.ErrorInfoProviderOptions.ExposeExceptionStackTrace
has been replaced with ExposeExceptionDetails
and ExposeExceptionDetailsMode
.See prior migration documents for more details concerning these changes.
29.IResolveFieldContext.ExecutionContext
added and ExecutionContext.GetArguments
signature has changed
If you are using the ExecutionContext.GetArguments
method to parse arguments directly, for example to retrieve the arguments of child fields in a resolver, we suggest using the new IExecutionContext.GetArguments
and IExecutionContext.GetDirectives
methods instead. These take advantage of the fact that all arguments are parsed during the validation phase and need not be parsed again. See example below:
IResolveFieldContext context;
FieldType fieldDefinition;
GraphQLField fieldAst;
var arguments = ExecutionHelper.GetArguments(fieldDefinition.Arguments, fieldAst.Arguments, context.Variables);
IResolveFieldContext context;
FieldType fieldDefinition;
GraphQLField fieldAst;
var arguments = context.ExecutionContext.GetArguments(fieldDefinition, fieldAst);
To continue to use the ExecutionHelper.GetArguments
method, you may need to refer to the GraphQL.NET source for reference.
If you directly implement IResolveFieldContext
, you must now also implement the ExecutionContext
property.
GraphType.Initialize
method is now called after initialization is complete
The Initialize
method on each GraphType
is now called after the schema has been fully initialized. As such, you cannot add fields to the graph type expecting SchemaTypes
to resolve types and apply name converters. If it is necessary for your graph type to add fields dynamically, you should do so in the constructor or else set the ResolvedType
property for the new fields. Failing to do so will result in a schema validation exception.
Please note that the constructor is the preferred place to add fields to a graph type.
public class MyGraphType : ObjectGraphType
{
public override void Initialize(ISchema schema)
{
AddField(new FieldType {
Name = "Field",
Type = typeof(StringGraphType)
});
}
}
public class MyGraphType : ObjectGraphType
{
public override void Initialize(ISchema schema)
{
AddField(new FieldType {
Name = "field",
ResolvedType = new StringGraphType()
});
}
}
Appendix Schema verification test example
The below example demonstrates how to write a test to verify that the schema does not change during migration.
[Fact]
public void VerifyIntrospection()
{
var services = new ServiceCollection();
var startup = new Startup(new ConfigurationBuilder().Build());
startup.ConfigureServices(services);
var provider = services.BuildServiceProvider();
var schema = provider.GetRequiredService<ISchema>();
schema.Initialize();
var sdl = schema.Print(new() { StringComparison = StringComparison.OrdinalIgnoreCase });
sdl.ShouldMatchApproved(o => o.NoDiff().WithFileExtension("graphql"));
}
This test uses the ShouldMatchApproved
method from the Shouldly
NuGet package to compare the schema generated by the application to the approved schema. If the schema changes, the test will fail, and you will need to update the approved schema file or fix the code that caused the schema to change.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4