GraphQL is a query language led by Facebook, for API. Many use cases are providing GraphQL based API and GitHub is one of them. GraphQL is useful, especially for front-end applications and mobile apps that cover most user interactions on the client-side. This post and its series discuss building GraphQL server/client using .NET Core based application, including ASP.NET Core.
- Building GraphQL Server on ASP.NET Core
- Building GraphQL Server on ASP.NET Core for Headless Wordpress
- Migrating GraphQL Server to Azure Functions in .NET Core
- Building GraphQL Server on Azure Functions in JavaScript
- Building Simple CMS with Blazor Web Assembly, with GraphQL and Headless Wordpress
The sample code used in this post can be found at this GitHub repository.
How Attractive Is GraphQL, Comparing to REST API?
If I am asked this question,
Well, probably yes? I'm not too sure for now.
Would be my answer. One thing I noticed on GraphQL is that it solves both over-fetching
and under-fetching
issues that REST API has. As REST API has its fixed schema, when I call an API request to a specified endpoint, I expect that the endpoint always returns the data with the same data structure. Here's an example of REST API endpoints for a blogging system.
GET /posts | |
GET /posts/{postId} | |
GET /authors | |
GET /authors/{authorId} | |
GET /tags | |
... |
In many cases, I should call multiple endpoints to aggregate data that I want. For example, sending a request to /posts
returns the list of posts with authorId
, not the author details. Therefore, I should send another request to /authors/{authorId}
with the authorId
value multiple times. This is called under-fetching
because of the API's flat structure. To solve this under-fetching
issue, either BFF (Backends for Frontends) pattern or Gateway Aggregation pattern, or combination of both is used. But still, there are "multiple requests" happening for aggregation.
On the other hand, if the API has nested structures, the payload might be too verbose. For example, sending a request to /posts
doesn't only return authorId
but also includes all author details. I only need the authors' ID and name, but other details are also returned. This is called over-fetching
. GitHub REST API is a typical example of over-fetching
. The problem of over-fetching
is too verbose and expensive due to the high network IO consumption.
What if I can compose an API request that I want to receive? What if the front-end application has an ability to compose data request structure, rather than relying on the API server-side? I think GraphQL changes the controllability from the server-side to client-side.
So, why not building a GraphQL server then? It sounds fun!
Building GraphQL Server on ASP.NET Core Application
As GraphQL is another type of API server, we can use any programming language. We're going to use ASP.NET Core and there are several .NET implementations. Let's use the most popular one, graphql-dotnet
.
If you are not familiar with GraphQL with ASP.NET Core, I would recommend Glenn Block's awesome online lecture on LinkedIn Learning. I reorganised my app based on his instruction.
First of all, let's create a C# class library project. It has all the logic for GraphQL server.
dotnet new classlib -n PostsQL |
Then add a NuGet package. That's it for the project setting. The latest stable version of GraphQL
is 2.4.0
.
dotnet add package GraphQL |
Defining Models
Let's define Post
and Author
like below:
public class Author | |
{ | |
public Author(int id, string name) | |
{ | |
this.Id = id; | |
this.Name = name; | |
} | |
public int Id { get; set; } | |
public string Name { get; set; } | |
} | |
public class Post | |
{ | |
public Post(int id, string title, string slug, DateTimeOffset published, string authorId, PostStatus status) | |
{ | |
this.Id = id; | |
this.Title = title; | |
this.Slug = slug; | |
this.Published = published; | |
this.AuthorId = authorId; | |
this.Status = status; | |
} | |
public int Id { get; set; } | |
public string Title { get; set; } | |
public string Slug { get; set; } | |
public DateTimeOffset Published { get; set; } | |
public string AuthorId { get; set; } | |
public PostStatus Status { get; set; } | |
} | |
public enum PostStatus | |
{ | |
Created = 1, | |
Published = 2, | |
Deleted = 3 | |
} |
Defining Services to Data
Let's write a service layer to access data storage. In fact, data storage can be anything, but this time we just use a hard-coded memory DB, which is sufficient for now. As you can see, both AuthorService
and PostService
are nothing that special.
public interface IAuthorService | |
{ | |
Task<Author> GetAuthorByIdAsync(int id); | |
Task<List<Author>> GetAuthorsAsync(); | |
} | |
public class AuthorService : IAuthorService | |
{ | |
private readonly List<Author> _authors; | |
public AuthorService() | |
{ | |
this._authors = new List<Author>() | |
{ | |
new Author(1, "Natasha Romanoff"), | |
new Author(2, "Carol Danvers"), | |
new Author(3, "Jean Grey"), | |
new Author(4, "Wanda Maximoff"), | |
new Author(5, "Gamora"), | |
}; | |
} | |
public async Task<Author> GetAuthorByIdAsync(int id) | |
{ | |
return await Task.FromResult(this._authors.SingleOrDefault(p => p.Id.Equals(id))); | |
} | |
public async Task<List<Author>> GetAuthorsAsync() | |
{ | |
return await Task.FromResult(this._authors); | |
} | |
} | |
public interface IPostService | |
{ | |
Task<Post> GetPostByIdAsync(int id); | |
Task<List<Post>> GetPostsAsync(); | |
} | |
public class PostService | |
{ | |
private readonly List<Post> _posts; | |
public PostService() | |
{ | |
this._posts = new List<Post>() | |
{ | |
new Post(1, "Post #1", "post-1", DateTimeOffset.UtcNow.AddHours(-4), 1, PostStatus.Deleted), | |
new Post(2, "Post #2", "post-2", DateTimeOffset.UtcNow.AddHours(-3), 2, PostStatus.Published), | |
new Post(3, "Post #3", "post-3", DateTimeOffset.UtcNow.AddHours(-2), 3, PostStatus.Published), | |
new Post(4, "Post #4", "post-4", DateTimeOffset.UtcNow.AddHours(-1), 4, PostStatus.Published), | |
new Post(5, "Post #5", "post-5", DateTimeOffset.UtcNow.AddHours(0), 5, PostStatus.Created), | |
}; | |
} | |
public async Task<Post> GetPostByIdAsync(int id) | |
{ | |
return await Task.FromResult(this._posts.SingleOrDefault(p => p.Id.Equals(id))); | |
} | |
public async Task<List<Post>> GetPostsAsync() | |
{ | |
return await Task.FromResult(this._posts); | |
} | |
} |
Defining GraphQL Schemas
We've got codes for data manipulation. Let's build GraphQL schemas that convert existing data models to GraphQL types. AuthorType
inherits the ObjectGraphType<T>
class with Author
(line #1) and exposes its properties.
public class AuthorType : ObjectGraphType<Author> | |
{ | |
public AuthorType() | |
{ | |
this.Field(p => p.Id); | |
this.Field(p => p.Name); | |
} | |
} |
PostType
also inherits the ObjectGraphType<T>
class with Post
(line #13). We define the PostStatusEnum
class inheriting EnumerationGraphType
(line #1), which converts the PostStatus
enum value (line #26). In addition to this, AuthorType
is combined based on the authorId
value (line #25).
public class PostStatusEnum : EnumerationGraphType | |
{ | |
public PostStatusEnum() | |
{ | |
this.Name = "PostStatus"; | |
this.AddValue(new EnumValueDefinition() { Name = "Created", Description = "Post was created", Value = 1 }); | |
this.AddValue(new EnumValueDefinition() { Name = "Published", Description = "Post has been published", Value = 2 }); | |
this.AddValue(new EnumValueDefinition() { Name = "Deleted", Description = "Post was deleted", Value = 3 }); | |
} | |
} | |
public class PostType : ObjectGraphType<Post> | |
{ | |
private readonly IAuthorService _authorService; | |
public PostType(IAuthorService authorService) | |
{ | |
this._authorService = authorService; | |
this.Field(p => p.Id); | |
this.Field(p => p.Title); | |
this.Field(p => p.Slug); | |
this.Field(p => p.Published); | |
this.FieldAsync<AuthorType>("author", resolve: async c => await this._authorService.GetAuthorByIdAsync(c.Source.AuthorId)); | |
this.Field<PostStatusEnum>("status", resolve: c => c.Source.Status); | |
} | |
} |
Let's build a query object, PostsQuery
. It contains posts
that returns all the list of PostType
as an array (line #11-13). It also contains post
that returns a single PostType
corresponding to a specified ID (line #15-19).
public class PostsQuery : ObjectGraphType<object> | |
{ | |
private readonly IPostService _postService; | |
public PostsQuery(IPostService postService) | |
{ | |
this._postService = postService; | |
this.Name = "Query"; | |
this.FieldAsync<ListGraphType<PostType>>( | |
"posts", | |
resolve: async c => await this._postService.GetPostsAsync()); | |
this.FieldAsync<PostType>( | |
"post", | |
arguments: new QueryArguments( | |
new QueryArgument<NonNullGraphType<IntGraphType>>() { Name = "id", Description = "Post ID" }), | |
resolve: async c => await this._postService.GetPostByIdAsync(c.GetArgument<int>("id"))); | |
} | |
} |
Finally, let's expose all those schemas to UI, using PostsSchema
. It injects the PostsQuery
instance and IDependencyResolver
instance that resolves other instances injected.
public class PostsSchema : Schema | |
{ | |
public PostsSchema(PostsQuery query, IDependencyResolver resolver) | |
{ | |
this.Query = query; | |
this.DependencyResolver = resolver; | |
} | |
} |
We've got all GraphQL data and service contract definitions. Let's build the server with ASP.NET Core!
Building ASP.NET Core UI
Create an empty ASP.NET Core project that hosts the GraphQL UI.
dotnet new web -n Server.WebApp |
Add NuGet packages.
dotnet add package Microsoft.AspNetCore | |
dotnet add package GraphQL.Server.Core | |
dotnet add package GraphQL.Server.Transports.AspNetCore | |
dotnet add package GraphQL.Server.Transports.WebSockets |
The installed GraphQL.Server.Core
of version 3.4.0 contains GraphQL-Parser
of version 3.0.0, but it doesn't get along with each other. Therefore, it should be installed separately. However, the latest version (5.x) of GraphQL-Parser
is incompatible with the current GraphQL.Server.Core
version of 3.4.0, which is the known issue. Therefore, we should install the parser version 4.1.2.
dotnet add package GraphQL-Parser -v 4.1.2 |
The last NuGet package is for UI. While there are other UI libraries, we use GraphiQL.
dotnet add package GraphQL.Server.Ui.GraphiQL |
And add a reference to the PostsQL
project.
dotnet add reference PostsQL |
The ASP.NET Core project settings are over. Let's add dependencies to Startup.cs
. First of all, add dependencies to the ConfigureServices()
method (line #5-11). Make sure to add the IDependencyResolver
instance so that other dependencies can be resolved within GrahpQL (line #13). And finally, add GraphQL schema objects (line #15-17).
public class Startup | |
{ | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddSingleton<IAuthorService, AuthorService>(); | |
services.AddSingleton<IPostService, PostService>(); | |
services.AddSingleton<AuthorType>(); | |
services.AddSingleton<PostType>(); | |
services.AddSingleton<PostStatusEnum>(); | |
services.AddSingleton<PostsQuery>(); | |
services.AddSingleton<PostsSchema>(); | |
services.AddSingleton<IDependencyResolver>(p => new FuncDependencyResolver(type => p.GetRequiredService(type))); | |
services.AddGraphQL() | |
.AddWebSockets() | |
.AddGraphTypes(Assembly.GetAssembly(typeof(PostsSchema))); | |
} | |
} |
We also need to configure the Configure()
method for GraphiQL UI (line #15-18). And finally, add the default routing table to /ui/graphql
(line #11).
public class Startup | |
{ | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
... | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapGet("/", async context => | |
{ | |
await Task.Run(() => context.Response.Redirect("/ui/graphql", permanent: true)); | |
}); | |
}); | |
app.UseWebSockets(); | |
app.UseGraphQLWebSockets<PostsSchema>(); | |
app.UseGraphQL<PostsSchema>(); | |
app.UseGraphiQLServer(new GraphiQLOptions() { GraphiQLPath = "/ui/graphql", GraphQLEndPoint = "/graphql" }); | |
} | |
} |
Done! Let's build and run the app.
dotnet run Server.WebApp |
It seems to be running! Enter the following URL to access to the UI.
https://localhost:5001/ |
It's automatically redirected to https://localhost:5001/ui/graphql
. But it throws an error!
System.InvalidOperationException: Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.
This is because one of the dependencies, Newtonsoft.Json
doesn't support async operations. To solve this issue, add one line of code to the ConfigureServices()
method. If you use IIS, replace KestrelServerOptions
with IISServerOptions
(line #3).
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.Configure<KestrelServerOptions>(o => o.AllowSynchronousIO = true); | |
... | |
} |
Compile all the project again and run the app.
dotnet run Server.WebApp |
And enter the following URL through your browser.
https://localhost:5001/ |
Now, the GraphQL server is up and running as expected!
Let's run some queries. The first query takes one post with details of id
, title
, slug
, author id
and author name
while the second query fetches all posts with id
, title
and published
values.
query { | |
post(id: 3) { | |
id, | |
title, | |
slug, | |
author { | |
id, | |
name | |
} | |
} | |
posts { | |
id, | |
title, | |
published | |
} | |
} |
And here's the result.
Let's recap what we defined in the PostsQL
project – PostType
and AuthorType
. Although both contain everything we want, the GraphQL server only returns what client requests. That's how GraphQL works. I also wonder how the query request works in a browser. Open a developer tool, and you will find out the request details like:
With this information, we don't have to rely on the UI. Let's send an API request through Postman.
{ | |
"query": "query { post(id: 3) { id, title, slug, author { id, name } } posts { id, title, published } }", | |
"variables": null | |
} |
It works as expected.
So far, we have built an ASP.NET Core server for GraphQL. As many libraries exist in the ecosystem, we were able to build the server easily. I hope this post will give you a high-level idea of building GraphQL server on ASP.NET Core. Let's wrap an existing REST API with GraphQL in the next post.