Hot Chocolatev13
This is documentation for v13, which is currently in preview.
See the latest stable version instead.

Migrate from Hot Chocolate GraphQL server 12 to 13

This guide will walk you through the manual migration steps to update your Hot Chocolate GraphQL server to version 13.

Breaking changes

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.

@authorize on types

If you previously annotated a type with @authorize, either directly in the schema or via [Authorize] or descriptor.Authorize(), the authorization rule was copied to each field of this type. This meant the authorization rule would be evaluated for each selected field beneath the annotated type in a request. This is inefficient, so we switched to evaluating the authorization rule once on the field that returns the "authorized" type instead.

Let's imagine you currently have the following GraphQL schema:

GraphQL
type Query {
user: User
}
type User @authorize {
field1: String
field2: Int
}

This is how the authorization rule would be evaluated previously and now:

Before

GraphQL
{
user {
# The authorization rule is evaluated here since this field is beneath
# the `User` type, which is annotated with @authorize
field1
# The authorization rule is evaluated here since this field is beneath
# the `User` type, which is annotated with @authorize
field2
}
}

After

GraphQL
{
# The authorization rule is now evaluated here since the `user` field
# returns the `User` type, which is annotated with @authorize
user {
field1
field2
}
}

We observed a common pattern to put a '@authorize' directive on the root types and secure all their fields.

With the new default behavior of authorization, this would now fail since annotating the type will ensure that all fields returning instances of this type will be validated. Since there is no field returning the root types in most cases, these authorization rules will have no effect.

With the Authorization overhaul, we also introduced a way to more efficiently implement such a pattern by moving parts of the authorization into the validation.

GraphQL
type Query @authorize(apply: VALIDATION) {
user: User
}
type User {
field1: String
field2: Int
}

The ‘apply‘ argument defines when an authorization rule is applied. In the above case, the validation ensures that the GraphQL request documents authorization rules are fulfilled. We do that by collecting all authorization directives with ‘apply‘ set to ‘Validation‘ and running them before we start the execution.

RegisterDbContext

We changed the default DbContextKind from DbContextKind.Synchronized to DbContextKind.Resolver. If the instance of your DbContext doesn't need to be the same for each executed resolver during a request, this should lead to a performance improvement.

To restore the v12 default behavior, pass the DbContextKind.Synchronized to the RegisterDbContext<T> call.

Before

C#
services.AddGraphQLServer()
.RegisterDbContext<DbContext>()

After

C#
services.AddGraphQLServer()
.RegisterDbContext<DbContext>(DbContextKind.Synchronized)

Note: Only add this if your application requires it. You're better off with the new default otherwise.

DataLoaderAttribute

Previously you might have annotated DataLoaders in your resolver method signature with the [DataLoader] attribute. This attribute has been removed in v13 and can be safely removed from your code.

Before

C#
public async Task<User> GetUserByIdAsync(string id, [DataLoader] UserDataLoader loader)
=> await loader.LoadAsync(id);

After

C#
public async Task<User> GetUserByIdAsync(string id, UserDataLoader loader)
=> await loader.LoadAsync(id);

ITopicEventReceiver / ITopicEventSender

Previously you could use any type as the topic for an event stream. In this release we are requiring the topic to be a string.

Before

C#
ITopicEventReceiver.SubscribeAsync<TTopic, TMessage>(TTopic topic,
CancellationToken cancellationToken);
ITopicEventSender.SendAsync<TTopic, TMessage>(TTopic topic, TMessage message,
CancellationToken cancellationToken)

After

C#
ITopicEventReceiver.SubscribeAsync<TMessage>(string topicName,
CancellationToken cancellationToken);
ITopicEventSender.SendAsync<TMessage>(string topicName, TMessage message,
CancellationToken cancellationToken)

@defer / @stream

@defer and @stream have now been disabled per default. If you want to continue using them, you have to opt-in now:

C#
services.AddGraphQLServer()
// ...
.ModifyOptions(o =>
{
o.EnableDefer = true;
o.EnableStream = true;
});
Warning

The spec of these features is still evolving, so expect more changes on how the incremental payloads are being delivered.

Deprecations

Things that will continue to function this release, but we encourage you to move away from.

ScopedServiceAttribute

In this release, we are deprecating the [ScopedService] attribute and encourage you to use RegisterDbContext<T>(DbContextKind.Pooled) instead.

Checkout this part of our Entity Framework documentation to learn how to register your DbContext with DbContextKind.Pooled.

Afterward you just need to update your resolvers:

Before

C#
[UseDbContext]
public IQueryable<User> GetUsers([ScopedService] MyDbContext dbContext)
=> dbContext.Users;

After

C#
public IQueryable<User> GetUsers(MyDbContext dbContext)
=> dbContext.Users;

If you've been using [ScopedService] without a pooled DbContext, you can recreate its behavior by switching it out for [LocalState("FullName")] (where FullName is the full name of the method argument type).

SubscribeAndResolve

Before

C#
public class Subscription
{
[SubscribeAndResolve]
public ValueTask<ISourceStream<Book>> BookPublished(string author,
[Service] ITopicEventReceiver receiver)
{
var topic = $"{author}_PublishedBook";
return receiver.SubscribeAsync<string, Book>(topic);
}
}

After

C#
public class Subscription
{
public ValueTask<ISourceStream<Book>> SubscribeToPublishedBooks(
string author, ITopicEventReceiver receiver)
{
var topic = $"{author}_PublishedBook";
return receiver.SubscribeAsync<Book>(topic);
}
[Subscribe(With = nameof(SubscribeToPublishedBooks))]
public Book BookPublished(string author, [EventMessage] Book book)
=> book;
}

LocalValue / ScopedValue / GlobalValue

We aligned the naming of state related APIs:

IResolverContext

  • IResolverContext.GetGlobalValue --> IResolverContext.GetGlobalStateOrDefault
  • IResolverContext.GetOrAddGlobalValue --> IResolverContext.GetOrSetGlobalState
  • IResolverContext.SetGlobalValue --> IResolverContext.SetGlobalState
  • IResolverContext.RemoveGlobalValue --> Removed
  • IResolverContext.GetScopedValue --> IResolverContext.GetScopedStateOrDefault
  • IResolverContext.GetOrAddScopedValue --> IResolverContext.GetOrSetScopedState
  • IResolverContext.SetScopedValue --> IResolverContext.SetScopedState
  • IResolverContext.RemoveScopedValue --> IResolverContext.RemoveScopedState
  • IResolverContext.GetLocalValue --> IResolverContext.GetLocalStateOrDefault
  • IResolverContext.GetOrAddLocalValue --> IResolverContext.GetOrSetLocalState
  • IResolverContext.SetLocalValue --> IResolverContext.SetLocalState
  • IResolverContext.RemoveLocalValue --> IResolverContext.RemoveLocalState

IQueryRequestBuilder

  • IQueryRequestBuilder.SetProperties --> IQueryRequestBuilder.InitializeGlobalState
  • IQueryRequestBuilder.SetProperty --> IQueryRequestBuilder.SetGlobalState
  • IQueryRequestBuilder.AddProperty --> IQueryRequestBuilder.AddGlobalState
  • IQueryRequestBuilder.TryAddProperty --> IQueryRequestBuilder.TryAddGlobalState
  • IQueryRequestBuilder.TryRemoveProperty --> IQueryRequestBuilder.RemoveGlobalState