When we at ChilliCream talked the other day, we reflected on the progress on version 11, where we are at this point, and how we got there. We are now working for almost one year on version 11 and will probably need a couple more months to polish it and get all the features in. When talked about this, we reflected that the actual version 11 was perhaps the 10.3 release when all the pure code-first goodness came.
With version 11, we are re-envisioning what we want Hot Chocolate to be. How we want the API to feel and how extensibility works. We have looked at the things that are difficult for users to understand and made these better accessible. We also looked at how we can take things to the next level with a new execution engine that will support execution plans.
Developer Preview
Today we are releasing a first developer preview of version 11 with our new configuration API. We call this a developer preview to make it clear that this should not be used in production. This preview is missing a lot of components included in version 10.x like filtering, schema stitching, and many others. As we go forward, we will slowly integrate these missing components and refine the new APIs further.
In order to get started with the developer preview first create a new ASP.NET Core project.
dotnet new web -n Demo
Next, add the ASP.NET Core server package.
cd Demodotnet add package HotChocolate.AspNetCore --version 11.0.0-dev.1
Configuration API
OK, after all these disclaimers, let us get into some code and talk features.
The first feature that I want to walk you through is the one that everybody will have to use to set up their GraphQL server, and it is also the first breaking change compared to version 11. When setting up a GraphQL server, we start with an ASP.NET Core web project. Our main configuration is located in the Startup.cs
.
Before we look at how we do it, version 11, let us see how we usually would start in version 10.
public void ConfigureServices(IServiceCollection services){ services.AddGraphQL(sp => SchemaBuilder.New() .AddQueryType<Query>());}
The code looks nice and simple. Also, the schema builder is a great API that lets us chain configuration. The main issue that we found with this or where we saw that people had problems was when schema stitching came into play or when you wanted to configure request services or change the execution pipeline and so on. Whenever it got a little more complicated, and we had to add more services and integrate other things that were not available on the SchemaBuilder
, it got complicated. The pity here is also that the SchemaBuilder
is difficult to extend. This means that components like schema stitching cannot easily add an extension method that brings new configuration functionality to the SchemaBuilder
.
After long nights we came up with a new approach that brings everything together into one API that is very easy to extend.
public void ConfigureServices(IServiceCollection services){ services .AddGraphQLServer() .AddQueryType<Query>();}
This little example does not look so much different, but the new API can do a lot more.
First, when in a server context like ASP.NET Core or Azure Functions, we now have this new AddGraphQLServer()
method that sets up a new schema and executor with additional services the server needs. This API also does not allow just one schema but multiple.
public void ConfigureServices(IServiceCollection services){ services .AddGraphQLServer() .AddQueryType<Query>() .AddGraphQLServer("internal") .AddQueryType<Query>() .AddTypeExtension<InternalQueryExtension>();}
The above code sets up two schemas. One is our default schema and adds a Query
type. The other schema is called internal
and adds the same Query
type, and extends the Query
type with some internal queries.
I can put each of these schemas on a different route and for that, we also now support Microsoft`s new routing API.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env){ app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapGraphQL(); endpoints.MapGraphQL("/internal", schemaName: "internal"); });}
The new configuration API, in combination with Microsoft`s new routing, makes it easy to map various schemas to various routes and secure and limit them as one pleases.
But there is even more to that. Since we also have some new schema stitching features in mind that will use the unique capabilities of this API. The new configuration API allows to hot reload schema configurations. Meaning you can push schema configurations to a running server. We will have more on this with the next few previews.
Another part that I mentioned is that we can more seamlessly configure a schema. If we wanted, for instance, to add apollo tracing support to our internal schema but not the default schema we can do that now with one line of code.
public void ConfigureServices(IServiceCollection services){ services .AddGraphQLServer() .AddQueryType<Query>() .AddGraphQLServer("internal") .AddQueryType<Query>() .AddTypeExtension<InternalQueryExtension>() .AddApolloTracing();}
Having configuration bound to specific schemas also means that the performance impact from components like apollo tracing effects only the schema it is applied to. We could also add this globally by adding apollo tracing to the service collection instead of the request builder. In this case, apollo tracing would be applied to all schemas.
public void ConfigureServices(IServiceCollection services){ services .AddApolloTracing() .AddGraphQLServer() .AddQueryType<Query>() .AddGraphQLServer("internal") .AddQueryType<Query>() .AddTypeExtension<InternalQueryExtension>();}
We are still bringing more APIs over to the new configuration API, and it will take us some time to have everything in here.
Subscriptions
Another area that is now super simple to set up is subscriptions. To use in-memory subscriptions, we configure our schema like the following.
public void ConfigureServices(IServiceCollection services){ services .AddGraphQLServer() .AddQueryType<Query>() .AddMutationType<Mutation>() .AddSubscriptionType<Subscription>() .AddInMemorySubscriptions();}
Again, I can have in-memory subscriptions on one schema and Redis subscriptions on another.
Next, we need to define our Mutation
type to trigger subscriptions whenever something happens on our schema.
public class Mutation{ public string SendMessage( string userId string message, [Service] ITopicEventSender eventSender) { eventSender.SendAsync(userId, message); return message; }}
In our example, we have a mutation that can send a text message to a user represented by the user API. To send a message to our subscription bus, we use the userId
argument as a topic and the message
argument as the payload of our subscription event. We also injected ITopicEventSender
, which allows us to send events to our internal event stream. Events are topic-based, and a subscription can subscribe to a topic.
From a GraphQL standpoint, we would like to subscribe to a specific user to receive the messages for that user.
subscription onMessage { onMessage(userId: "123");}
This subscription will then pass down to us the message text for user 123
whenever the mutation is invoked with the userId 123
.
Let us have a look at how we would create our subscription type for that.
public class Subscription{ [Subscribe] public string OnMessage( [Topic] string userId, [EventMessage] string message) => message;}
If you look at the code above you, do not see any specific code that subscribes to the event system itself. We added an argument userId
and annotated it to be our topic. The topic argument tells our system what events we would like to receive. Next, we added another argument message
, which we annotated as our event message or payload. The message
argument is where the system shall inject us the payload of the events whenever our subscription resolver is invoked.
There are many more variants with the new subscriptions, but I will cover that in a later blog post that only looks at subscriptions and what we can do with them.
Extensibility
One of our most significant investments was making the type system even more flexible to allow more complex features. We want to allow for very complex features to become fully transparent. Meaning, features like relay support should not dictate how you build your types. You should not need to handle id serialization or things like that. The system should understand your types and rewrite them into what you want them to be.
To this, there is an even better example. With version 11, we want to bring schema-first or SDL-first up to par with code-first. In SDL-first integrating paging is quite tedious at the moment, since you have to write all the paging and connection types and so forth. But if we could have a feature that can rewrite a schema, we could let people specify a schema like the following.
type Query { users: [User] @paging}
type User { # removed for brevity}
The configuration would take this initial schema and rewrite it to the following schema that includes all those types necessary for relay pagination.
type Query { users(first: Int, last: Int, after: String, before: String): [UserConnection]}
type UserConnection { pageInfo: PageInfo edges: [User]}
type PageInfo { # removed for brevity}
type User { # removed for brevity}
To allow cross-cutting features like this, we are now allowing to intercept type configurations and rewrite them.
public void ConfigureServices(IServiceCollection services){ services .AddGraphQLServer("hello") .AddQueryType(d => d .Name("Query") .Field("hello") .Resolver("world")) .OnBeforeCompleteType<ObjectTypeDefinition>( (context, definition, contextData) => { if(definition.Name.Equals("Query")) { ObjectTypeDescriptor.From(context.DescriptorContext, definition) .Field("foo") .Type<StringType>() .Resolver(resolverContext => "say hello"); } });}
In the above example, I am intercepting the configuration of the Query
type and add a simple foo field to it. This feature allows for so much more since you can write very sophisticated interceptors that scope types and branch them into separate type trees. We will rewrite and decouple a lot of our current features with this.
Execution Engine
With this first dev preview, we are bringing the first part of our new execution engine in. It does not yet contain the execution plan bits but has a lot of the memory optimizations built-in. The execution engine now also is much easier to extend with features. All of the execution configurations are as-well backed into our new configuration API.
If I, for instance, wanted to use the persisted queries execution flow, I could do so by using the following configuration.
public void ConfigureServices(IServiceCollection services){ services .AddGraphQLServer("hello") .AddQueryType(d => d .Name("Query") .Field("hello") .Resolver("world")) .UsePersistedQueryPipeline();}
Summary
There are a ton more features in this preview, so many really that it is to much to go into every one of them. Also, we have just begun to bring our various bits together and hope to integrate those now more quickly. Over the next weeks, we will share with every new preview more new features with you and will drill down more specifically into those. This first post is meant to kick things off. Give us feedback on how you like the feel of the new configuration API.