facebook

Blog

Stay updated

GraphQL: let’s see what it is, what allows us to do and how we can create an API with ASP.NET Core and Hot Chocolate
Creating our API with GraphQL and Hot Chocolate
Wednesday, March 25, 2020

How often have you called an API and received more data than needed? Or, on the contrary, you obtained less than necessary and therefore have to make more calls to different endpoints to have all the information required?
Those two events are called overfetching and underfetching, and they probably represent the major problem in an API REST.
We need something with greater flexibility, allowing the client to be able to retrieve only the required information with a specific query.

With GraphQL, we can do that.

GraphQL is like a middle layer between our data and our clients, and it can be considered as an alternative to REST API or maybe even an evolution.
It is a query language: it means that the GraphQL queries are more flexible than the other interface architectures, which accept only very strict queries, often addressed to a single resource.
The Client requires exactly the fields it needs and nothing more.

We can find this flexibility also in the improvement phase of the API: in fact, the addition of fields returned to our structure will not impact the existing clients.
GraphQL is the strongly typed: each query level match a given type, and every type describes a set of available fields. Thanks to this feature, GraphQL can validate a query before running it, and in case of error, GraphQL can also return descriptive error messages.

There are four key concepts in GraphQL:

  • Schema
  • Resolver
  • Query
  • Mutation

A GraphQL schema consists of object types that define the type of objects that are possible to receive and the type of fields available.

The resolvers are the collaborators that we can associate with the fields of our scheme and which will take care of recovering data from these fields.

Considering a typical CRUD, we can define the concept of query and mutation.
The first one will deal with the information reading, while the creation, modification, and deletion are tasks managed by mutation.

Let’s try to create an application that is able to execute a CRUD with GraphQL.
We will do it in ASP.NET Core with Hot Chocolate, a library that allows you to create a GraphQL Server implementation.

Let’s create an ASP.NET Core Web application, and add the libraries HotChocolate e HotChocolate.AspNetCore with Nuget package manager. The HotChocolate.AspNetCore.Playground library has also proved very useful since it allows us to test in the browser the API we are developing.

We add the necessary instructions to configure GraphQL to the Startup.cs file

namespace DemoGraphQL
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddGraphQL(s => SchemaBuilder.New()
                .AddServices(s)
                .AddType<AuthorType>()
                .AddType<BookType>()
                .AddQueryType<Query>()
                .Create());
        }
 
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UsePlayground();
            }
 
            app.UseGraphQL("/api");
        }
    }
}

We define AuthorType and BookType, which will be the types exposed by our API.

In those classes, we define which fields of the domain classes Author and Book will be exposed.

public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor.Field(a => a.Id).Type<IdType>();
        descriptor.Field(a => a.Name).Type<StringType>();
        descriptor.Field(a => a.Surname).Type<StringType>();
    }
}
      
public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
}
 
public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor.Field(b => b.Id).Type<IdType>();
        descriptor.Field(b => b.Title).Type<StringType>();
        descriptor.Field(b => b.Price).Type<DecimalType>();
    }
}
  
 
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public decimal Price { get; set; }
}

At this point, we can create the class Query, starting with defining the authors.

public class Query
{
    private readonly IAuthorService _authorService;
    public Query(IAuthorService authorService)
    {
        _authorService = authorService;
    }
    [UsePaging(SchemaType = typeof(AuthorType))]
    public IQueryable<Author> Authors => _authorService.GetAll();
}

With the annotation UsePaging, we are instructing GraphQL so that the authors returned by the service will have to be made available with pagination and in the previously defined AuthorType.
This way, by starting the application and going to the playground, we can make the following query and see the result.

By adding the HotChocolate.Types and HotChocolate.Types.Filters library you can add a new annotation to enable filters.

[UsePaging(SchemaType = typeof(AuthorType))]
[UseFiltering]
public IQueryable<Author> Authors => _authorService.GetAll();

We get the same result with the query on books

[UsePaging(SchemaType = typeof(BookType))]
[UseFiltering]
public IQueryable<Book> Books => _bookService.GetAll();

At the moment, books and authors are not related to each other. Still, in a real application, we want to be able to get the information of authors when we make a query about books, and, in the same way, we want to get the list of books written when we make a query about authors.

Let’s modify the Book class to add the author id:

public class Book
{
    public int Id { get; set; }
    public int AuthorId { get; set; }
    public string Title { get; set; }
    public decimal Price { get; set; }
}

Now, we can use one of the items listed before: the resolvers. We implement the one that allows us to get the author’s information on the book query first.

public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor.Field(b => b.Id).Type<IdType>();
        descriptor.Field(b => b.Title).Type<StringType>();
        descriptor.Field(b => b.Price).Type<DecimalType>();
        descriptor.Field<AuthorResolver>(t => t.GetAuthor(default, default));
    }
}

We added a new field to the BookType schema with descriptor.Field and told him to solve it by the GetAuthor method of AuthorResolver.

public class AuthorResolver
{
    private readonly IAuthorService _authorService;
 
    public AuthorResolver([Service]IAuthorService authorService)
    {
        _authorService = authorService;
    }
 
    public Author GetAuthor(Book book, IResolverContext ctx)
    {
        return _authorService.GetAll().Where(a => a.Id == book.AuthorId).FirstOrDefault();
    }
}

In the same way, we can define a new field with relative resolver in order to add to the authors also the books they wrote.

public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor.Field(a => a.Id).Type<IdType>();
        descriptor.Field(a => a.Name).Type<StringType>();
        descriptor.Field(a => a.Surname).Type<StringType>();
        descriptor.Field<BookResolver>(t => t.GetBooks(default, default));
    }
}
 
 
public class BookResolver
{
    private readonly IBookService _bookService;
 
    public BookResolver([Service]IBookService bookService)
    {
        _bookService = bookService;
    }
    public IEnumerable<Book> GetBooks(Author author, IResolverContext ctx)
    {
        return _bookService.GetAll().Where(b => b.AuthorId == author.Id);
    }
}

To complete the CRUD with creation and deletion operations, we need to implement what is called mutation. It is a class with methods that indicate the possible operations.
It must be recorded using the following statement AddMutationTypez<Mutation>() that must be added to the configuration block of GraphQL in Startup.cs

services.AddGraphQL(s => SchemaBuilder.New()
    .AddServices(s)
    .AddType<AuthorType>()
    .AddType<BookType>()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()
    .Create());

Let’s start by adding a method that allow us to insert a new book:

public class Mutation
{
    private readonly IBookService _bookService;
 
    public Mutation(IBookService bookService)
    {
        _bookService = bookService;
    }
 
    public Book CreateBook(CreateBookInput inputBook)
    {
        return _bookService.Create(inputBook);
    }
}

we create a class of input that we will insert in the query

public class CreateBookInput
{
    public string Title { get; set; }
    public decimal Price { get; set; }
    public int AuthorId { get; set; }
}

The service will only have to create a new instance of Book, add it to its list, and return the created book.
From playground, we can test the new feature by launching the query you can see in the shot

Let’s add now the feature of book removing.
As for creation, we must add a method to mutation.

public Book DeleteBook(DeleteBookInput inputBook)
{
    return _bookService.Delete(inputBook);
}

The class DeleteBookInput has only an int-type property that represents the book’s Id you want to delete.
A possible implementation of service Delete method is as follows:

public Book Delete(DeleteBookInput inputBook)
{
    var bookToDelete = _books.Single(b => b.Id == inputBook.Id);
    _books.Remove(bookToDelete);
    return bookToDelete;
}

Trying to run the query again, we will get an error message about the exception thrown by the Single method. We can make the error message more informative, so that who calls our API will not have doubts about the error.

Let’s create a new Exception

public class BookNotFoundException : Exception
{
    public int BookId { get; internal set; }
}

And let’s make sure that this will be launched, if the book that we want to delete was not found.

public Book Delete(DeleteBookInput inputBook)
{
    var bookToDelete = _books.FirstOrDefault(b => b.Id == inputBook.Id);
    if (bookToDelete == null)
    throw new BookNotFoundException() { BookId = inputBook.Id };
    _books.Remove(bookToDelete);
    return bookToDelete;
}

Now we create a class that implements IErrorFilter made available by HotChocolate that intercepts BookNotFoundException and adds to the error the information we want return to the user.

public class BookNotFoundExceptionFilter : IErrorFilter
{
    public IError OnError(IError error)
    {
         if (error.Exception is BookNotFoundException ex)
             return error.WithMessage($"Book with id {ex.BookId} not found");
             
         return error;
    }
}

Finally, let’s record this class in the Startup

services.AddErrorFilter<BookNotFoundExceptionFilter>();

The API is finally completed, the source code is on my github

At the end of the project, I thought: it all looks great, why didn’t I use GraphQL for all the APIs? Is there some disadvantage I haven’t considered?

In this demo, we have implemented the error management. In API REST, when something goes wrong, we expect a Status Code other than 200. In GraphQL, instead, the Status Code is always 200. That means that the client will have to rely exclusively on the content of the reply without being able to make preventive decisions based on the status received from the request.

Another aspect that in a demo obviously does not come out is the caching.
There’s no built-in cache support, and implementing it is not as simple as the REST API. This aspect should not be underestimated.

I hope I intrigued you.

See you at the next article!