website/src/docs/hotchocolate/v14/migrating/migrate-from-13-to-14.md
This guide will walk you through the manual migration steps to update your Hot Chocolate GraphQL server to version 14.
Start by installing the latest 14.x.x version of all of the HotChocolate.* packages referenced by your project.
This guide is still a work in progress with more updates to follow.
Things that have been removed or had a change in behavior that may cause your code not to compile or lead to unexpected behavior at runtime if not addressed.
| Old | New | Notes |
|---|---|---|
| AddBananaCakePopExporter | AddNitroExporter | |
| AddBananaCakePopServices | AddNitro | |
| BananaCakePop.Middleware | ChilliCream.Nitro.App | |
| BananaCakePop.Services | ChilliCream.Nitro | |
| BananaCakePop.Services.Azure | ChilliCream.Nitro.Azure | |
| BananaCakePop.Services.Fusion | ChilliCream.Nitro.Fusion | |
| barista | nitro | CLI executable |
| Barista | ChilliCream.Nitro.CLI | CLI NuGet package |
| BARISTA_API_ID | NITRO_API_ID | |
| BARISTA_API_KEY | NITRO_API_KEY | |
| BARISTA_CLIENT_ID | NITRO_CLIENT_ID | |
| BARISTA_OPERATIONS_FILE | NITRO_OPERATIONS_FILE | |
| BARISTA_OUTPUT_FILE | NITRO_OUTPUT_FILE | |
| BARISTA_SCHEMA_FILE | NITRO_SCHEMA_FILE | |
| BARISTA_STAGE | NITRO_STAGE | |
| BARISTA_SUBGRAPH_ID | NITRO_SUBGRAPH_ID | |
| BARISTA_SUBGRAPH_NAME | NITRO_SUBGRAPH_NAME | |
| BARISTA_TAG | NITRO_TAG | |
| bcp | nitro | Key in subgraph-config.json |
| bcp-config.json | nitro-config.json | |
| BCP_API_ID | NITRO_API_ID | |
| BCP_API_KEY | NITRO_API_KEY | |
| BCP_STAGE | NITRO_STAGE | |
| eat.bananacakepop.com | nitro.chillicream.com | |
| MapBananaCakePop | MapNitroApp | |
| @chillicream/bananacakepop-express-middleware | @chillicream/nitro-express-middleware | |
| @chillicream/bananacakepop-graphql-ide | @chillicream/nitro-embedded | mode: "self" is now mode: "embedded" |
It is no longer necessary to use the [Service] attribute unless you're using keyed services, in which case the attribute is used to specify the key.
Support for the [FromServices] attribute has been removed.
[Service] attribute above, this attribute is no longer necessary.Since the RegisterService method is no longer required, it has been removed, along with the ServiceKind enum.
Scoped services injected into query resolvers are now resolver-scoped by default (not request scoped). For mutation resolvers, services are request-scoped by default.
The default scope can be changed in two ways:
Globally, using ModifyOptions:
builder.Services
.AddGraphQLServer()
.ModifyOptions(o =>
{
o.DefaultQueryDependencyInjectionScope =
DependencyInjectionScope.Resolver;
o.DefaultMutationDependencyInjectionScope =
DependencyInjectionScope.Request;
});
On a per-resolver basis, with the [UseRequestScope] or [UseResolverScope] attribute.
[UseServiceScope] attribute has been removed.For more information, see the Dependency Injection documentation.
RegisterDbContext method is no longer required, and has therefore been removed, along with the DbContextKind enum.RegisterDbContextFactory to register a DbContext factory.For more information, see the Entity Framework integration documentation.
This release introduces a more performant GID serializer, which also simplifies the underlying format of globally unique IDs.
By default, the new serializer will be able to parse both the old and new ID format, while only emitting the new format.
This change is breaking if your consumers depend on the format of the GIDs, by for example parsing them (which they shouldn't). If possible, strive to decouple your consumers from the internal ID format and exposing the underlying ID as a separate field on your type if necessary.
If you don't want to switch to the new format yet, you can register the legacy serializer, which only supports parsing and emitting the old ID format:
builder.Services
.AddGraphQLServer()
.AddLegacyNodeIdSerializer()
.AddGlobalObjectIdentification();
Note:
AddLegacyNodeIdSerializer()needs to be called beforeAddGlobalObjectIdentification().
None of your services can start to emit the new ID format, as long as there are services that can't parse the new format.
Therefore, you'll first want to make sure that all of your services support parsing both the old and new format, while still emitting the old format.
This can be done, by configuring the new default serializer to not yet emit the new format:
builder.Services
.AddGraphQLServer()
.AddDefaultNodeIdSerializer(outputNewIdFormat: false)
.AddGlobalObjectIdentification();
Note:
AddDefaultNodeIdSerializer()needs to be called beforeAddGlobalObjectIdentification().
Once all of your services have been updated to this, you can start emitting the new format service-by-service, by removing the AddDefaultNodeIdSerializer() call and switching to the new default behavior:
builder.Services
.AddGraphQLServer()
.AddGlobalObjectIdentification();
Previously, you could grab the IIdSerializer from your dependency injection container to manually parse and serialize globally unique identifiers (GID).
As part of the changes to the GID format mentioned above, the IIdSerializer interface has been renamed to INodeIdSerializer.
The methods used for parsing and serialization have also been renamed:
| Before | After |
|---|---|
.Deserialize("<gid-value>") | .Parse("<gid-value>", typeof(string)) where string is the underlying type of the GID |
.Serialize("MyType", "<raw-id>") | .Format("MyType", "<raw-id>") |
The Parse() (previously Deserialize()) method has also changed its return type from IdValue to NodeId. The parsed Id value can now be accessed through the NodeId.InternalId instead of the IdValue.Value property.
The ability to encode the schema name in the GID via .Serialize("SchemaName", "MyType", "<raw-id>") has been dropped and is no longer supported.
We now enforce that each object type implementing the Node interface also defines a resolver, so that the object can be refetched through the node(id: ID!) field.
You can opt out of this new behavior by setting the EnsureAllNodesCanBeResolved option to false.
builder.Services
.AddGraphQLServer()
.ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false)
Previously, the LoadAsync method on a DataLoader was typed as non-nullable, even though null could be returned.
This release changes the return type of LoadAsync to always be nullable.
We have aligned all builder APIs to be more consistent and easier to use. Builders can now be created by using the static method Builder.New() and the Build() method to create the final object.
The interface IQueryRequestBuilder and its implementations were replaced with OperationRequestBuilder which now supports building standard GraphQL operation requests as well as variable batch requests.
The Build() method now returns a IOperationRequest which is implemented by OperationRequest and VariableBatchRequest.
We've also renamed and consolidated some methods on the OperationRequestBuilder:
| Before | After |
|---|---|
SetQuery("{ __typename }") | SetDocument("{ __typename }") |
AddVariableValue("name", "value") | AddVariableValues(new Dictionary<string, object?> { ["name"] = "value" }) |
The interface IQueryResultBuilder and its implementations were replaced with OperationResultBuilder which produces an OperationResult on Build().
The interface IQueryResult was replaced with IOperationResult.
In your unit tests you might have been using result.ExpectQueryResult() to assert that a result is not a streamed response and rather a completed result.
This assertion method has been renamed to ExpectOperationResult().
The Operation Complexity Analyzer in v13 has been replaced by Cost Analysis in v14, based on the draft IBM Cost Analysis specification.
Complexity property on RequestExecutorOptions (accessed via ModifyRequestOptions) has been removed.Please see the documentation for further information.
The DateTime scalar will now enforce a specific format. The time and offset are now required, and fractional seconds are limited to 7. This aligns it with the DateTime Scalar spec (https://www.graphql-scalars.com/date-time/), with the one difference being that fractions of a second are optional, and 0-7 digits may be specified.
Please ensure that your clients are sending date/time strings in the correct format to avoid errors.
You can opt out of the format check with the following code:
builder.Services
.AddGraphQLServer()
.AddType(new DateTimeType(disableFormatCheck: true));
| Old package name | New package name |
|---|---|
| HotChocolate.PersistedQueries.FileSystem | HotChocolate.PersistedOperations.FileSystem |
| HotChocolate.PersistedQueries.InMemory | HotChocolate.PersistedOperations.InMemory |
| HotChocolate.PersistedQueries.Redis | HotChocolate.PersistedOperations.Redis |
| Old interface name | New interface name |
|---|---|
| IPersistedQueryOptionsAccessor | IPersistedOperationOptionsAccessor |
| Old method name | New method name |
|---|---|
| UsePersistedQueryPipeline | UsePersistedOperationPipeline |
| UseAutomaticPersistedQueryPipeline | UseAutomaticPersistedOperationPipeline |
| AddFileSystemQueryStorage | AddFileSystemOperationDocumentStorage |
| AddInMemoryQueryStorage | AddInMemoryOperationDocumentStorage |
| AddRedisQueryStorage | AddRedisOperationDocumentStorage |
| AllowNonPersistedQuery | AllowNonPersistedOperation |
| UseReadPersistedQuery | UseReadPersistedOperation |
| UseAutomaticPersistedQueryNotFound | UseAutomaticPersistedOperationNotFound |
| UseWritePersistedQuery | UseWritePersistedOperation |
| Old option name | New option name |
|---|---|
| OnlyAllowPersistedQueries | PersistedOperations.OnlyAllowPersistedDocuments |
| OnlyPersistedQueriesAreAllowedError | PersistedOperations.OperationNotAllowedError |
| Parameter | Old default | New default |
|---|---|---|
| cacheDirectory | "persisted_queries" | "persisted_operations" |
| Old name | New name |
|---|---|
| MutationResult<TResult> | FieldResult<TResult> |
| IMutationResult | IFieldResult |
IReadStoredQueries and IWriteStoredQueries have been merged into a single interface named IOperationDocumentStorage.
Renamed interface methods:
| Old name | New name |
|---|---|
| TryReadQueryAsync | TryReadAsync |
| WriteQueryAsync | SaveAsync |
Accessing a keyed service that has not been registered will now throw, instead of returning null. The return type is now non-nullable.
This change aligns the API with the regular (non-keyed) service access API.
Before
ModifyRequestOptions(o => o.OnlyAllowPersistedOperations = true);
After
ModifyRequestOptions(o => o.PersistedOperations.OnlyAllowPersistedDocuments = true);
Previously, you could supply an async method to the getTotalCount constructor argument when instantiating a Connection<T>. This method would only be evaluated to calculate the total count, if the totalCount field was selected on that Connection in a query.
return new Connection<MyType>(
edges: [/* ... */],
info: new ConnectionPageInfo(/* ... */),
getTotalCount: async cancellationToken => 123)
In this release the constructor argument was renamed to totalCount and now only accepts an int for the total count, no longer a method to compute the total count.
If you want to re-create the old behavior, you can use the new [IsSelected] attribute to conditionally compute the total count.
public Connection<MyType> GetMyTypes(
[IsSelected("totalCount")] bool hasSelectedTotalCount,
CancellationToken cancellationToken)
{
var totalCount = 0;
if (hasSelectedTotalCount)
{
totalCount = /* ... */;
}
return new Connection<MyType>(
edges: [/* ... */],
info: new ConnectionPageInfo(/* ... */),
totalCount: totalCount)
}
SingleOrDefaultMiddlewareAs a side-effect of fixing a bug in the SingleOrDefaultMiddleware, usage of this middleware along with EF Core may result in a warning being logged, as follows:
The query uses a row limiting operator ('Skip'/'Take') without an 'OrderBy' operator. This may lead to unpredictable results. If the 'Distinct' operator is used after 'OrderBy', then make sure to use the 'OrderBy' operator after 'Distinct' as the ordering would otherwise get erased.
We are looking at fixing this in a different way in the future (see #8070), but for now you can work around this by returning an IExecutable from your resolver by calling AsDbContextExecutable() on your IQueryable or DbSet, or by using Executable.From(...).
Things that will continue to function this release, but we encourage you to move away from.
In an effort to align our configuration APIs, we're now also offering a delegate based configuration API for pagination options.
Before
builder.Services
.AddGraphQLServer()
.SetPagingOptions(new PagingOptions
{
MaxPageSize = 100,
DefaultPageSize = 25
});
After
builder.Services
.AddGraphQLServer()
.ModifyPagingOptions(opt =>
{
opt.MaxPageSize = 100;
opt.DefaultPageSize = 25;
});