Query And Mutation In GraphQL - Part Two

Introduction

 
In the previous article of the GraphQL series, we discussed the introduction of GraphQL and Pros & Cons of using GraphQL. If you haven’t read my previous articles, then I highly encourage you to do so.
I believe you have a prior understanding of EF Core and we'll be concentrating only on GraphQL concepts. If not, please refer to the below article.
 

Coding

 
Let's directly jump into coding section by adding NuGet package
  1. Microsoft.EntityFrameworkCore.SqlServer  
  2. Microsoft.EntityFrameworkCore.Design  
  3. Microsoft.EntityFrameworkCore.Tools  
  4. GraphQL  
  5. GraphQL.Server.Transports.AspNetCore  
  6. GraphQL.Server.UI.Playground  
In this article, we are illustrating a model that is concerned with books and their authors.
  1. public class Author  
  2.     {  
  3.         public int AuthorId { getset; }  
  4.         public string FirstName { getset; }  
  5.         public string LastName { getset; }  
  6.         public ICollection<Book> Books { getset; }  
  7.     }  

  8.     public class Book  
  9.     {  
  10.         public int BookId { getset; }  
  11.         public string Title { getset; }  
  12.         public int PublicationYear { getset; }  
  13.         public int AuthorId { getset; }  
  14.         public Author Author { getset; }  
  15.     }  
Now, let's include an Author's context class and add data seeding for initial data in the database.
  1. public class AuthorContext:DbContext  
  2.     {  
  3.         public DbSet<Author> Authors { getset; }  
  4.   
  5.         public DbSet<Book> Books { getset; }  
  6.   
  7.         public AuthorContext(DbContextOptions dbContextOptions) :base(dbContextOptions)  
  8.         {  
  9.         }  
  10.   
  11.         protected override void OnModelCreating(ModelBuilder modelBuilder)  
  12.         {  
  13.             modelBuilder.Entity<Author>().HasData(new Author  
  14.             {  
  15.                 FirstName= "Agatha",  
  16.                 LastName="Christry",  
  17.                 AuthorId = 1  
  18.             });  
  19.   
  20.             modelBuilder.Entity<Book>().HasData(  
  21.                 new Book { Title = "The Mysterious Affair at Styles", PublicationYear = 1921,AuthorId=1,BookId=1 },  
  22.                 new Book { Title = "The Secret Adversary", PublicationYear = 1922,AuthorId = 1,BookId=2 }  
  23.                 );  
  24.         }  
  25.     }  
Note
I highly encourage you to use IEntityTypeConfiguration for data seeding rather than using OnModelCreating method. You can refer to the article here.
  1. public interface IAuthorRepository  
  2.     {  
  3.         Task<IEnumerable<Author>> GetAuthorsAsync();  
  4.   
  5.         Task<Author> GetAuthorAsync(int authorId);  
  6.   
  7.         Task<Author> InsertAuthorAsync(Author author);  
  8.   
  9.         Task<Author> GetAuthorByFirstNameAsync(string firstName);  
  10.     }  
  11.   
  12. public class AuthorRepository: IAuthorRepository  
  13.     {  
  14.         private readonly AuthorContext authorContext;  
  15.   
  16.         public AuthorRepository(AuthorContext authorContext)  
  17.         {  
  18.             this.authorContext = authorContext;  
  19.         }  
  20.   
  21.         public async Task<Author> GetAuthorAsync(int authorId)  
  22.         {  
  23.             return await authorContext.Authors.FirstOrDefaultAsync(x => x.AuthorId == authorId);  
  24.         }  
  25.   
  26.         public async Task<Author> GetAuthorByFirstNameAsync(string firstName)  
  27.         {  
  28.             return await authorContext.Authors.FirstOrDefaultAsync(x => x.FirstName == firstName);  
  29.         }  
  30.   
  31.         public async Task<IEnumerable<Author>> GetAuthorsAsync()  
  32.         {  
  33.             return await authorContext.Authors.ToListAsync();  
  34.         }  
  35.   
  36.         public async Task<Author> InsertAuthorAsync(Author author)  
  37.         {  
  38.             var result = (await authorContext.Authors.AddAsync(author)).Entity;  
  39.             await authorContext.SaveChangesAsync();  
  40.             return result;  
  41.         }  
  42.     }  
  1. public interface IBookRepository  
  2.     {  
  3.         Task<IEnumerable<Book>> GetBooksAsync();  
  4.   
  5.         Task<IEnumerable< Book>> GetBooks(int authorId);  
  6.   
  7.         Task<Book> InsertBook(Book book);  
  8.     }  
  1. public class BookRepository: IBookRepository  
  2.     {  
  3.         private readonly AuthorContext authorContext;  
  4.   
  5.         public BookRepository(AuthorContext authorContext)  
  6.         {  
  7.             this.authorContext = authorContext;  
  8.         }  
  9.   
  10.         public async Task<IEnumerable< Book>> GetBooks(int authorId)  
  11.         {  
  12.             var result = await (from author in authorContext.Set<Author>()  
  13.                                 join book in authorContext.Set<Book>() on author.AuthorId equals book.AuthorId  
  14.                                 where author.AuthorId==authorId  
  15.                                 select book).ToListAsync();  
  16.             return result;  
  17.         }  
  18.   
  19.         public async Task<IEnumerable<Book>> GetBooksAsync()  
  20.         {  
  21.             return await authorContext.Books.ToListAsync();  
  22.         }  
  23.   
  24.         public async Task<Book> InsertBook(Book book)  
  25.         {  
  26.             var result = (await authorContext.Books.AddAsync(book)).Entity;  
  27.             await authorContext.SaveChangesAsync();  
  28.             return result;  
  29.         }  
  30.     }  
Now register the DbContext and repositories to the dependency injection in the startup class
  1. services.AddDbContext<AuthorContext>(x => x.UseSqlServer(Configuration.GetConnectionString("SqlConnectionString")));  
  2. services.AddScoped<IAuthorRepository, AuthorRepository>();  
  3. services.AddScoped<IBookRepository, BookRepository>();  
Perform database migration using the following commands
  1. //Adding migration  
  2. dotnet ef migrations add InitialMigration  
  3. //Update Database  
  4. dotnet ef database update  
GraphQL Middleware
 
You can add GraphQL middleware to the configure method of the Startup class
  1. app.UseGraphQL<AuthorSchema>("/api/author");  
  2. app.UseGraphQL<BookSchema>("/api/book");  
  3. app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());  
  4. app.UseAuthorization();  
GraphQL Query
 
A GraphQL operation can be either read or write, however, Query is used to read or fetch values, whereas Mutation is used to write or post values.
 
Since you want to get data about authors and books, you need to query the author and book classes. To make the author & book class in GraphQL queryable, you should create a new type and extend it from custom ObjectGraphType<T>.
  1. public class AuthorType:ObjectGraphType<Author>  
  2.     {  
  3.         public AuthorType(IBookRepository bookRepository)  
  4.         {  
  5.             Field(x => x.AuthorId).Description("Author Id");  
  6.             Field(x => x.FirstName).Description("Author's first name");  
  7.             Field(x => x.LastName);  
  8.             Field<ListGraphType<BookType>>("books",  
  9.                 arguments: new QueryArguments(new QueryArgument<IntGraphType> { Name = "authorId" }),  
  10.                 resolve: context =>  
  11.                  {  
  12.                      return bookRepository.GetBooks(context.Source.AuthorId);  
  13.                  });   
  14.         }  
  15.     }  
  1. public class BookType:ObjectGraphType<Book>  
  2.     {  
  3.         public BookType()  
  4.         {  
  5.             Field(x => x.BookId);  
  6.             Field(x => x.Title);  
  7.             Field(x => x.PublicationYear);  
  8.         }  
  9.     }  
You can now write a GraphQL query that will handle fetching an author or list of authors.
  1. public class AuthorQuery:ObjectGraphType<object>  
  2.     {  
  3.         public AuthorQuery(IAuthorRepository authorRepository)  
  4.         {  
  5.             Field<ListGraphType<AuthorType>>("authors", resolve:context =>  
  6.             {  
  7.                 return authorRepository.GetAuthorsAsync();  
  8.             });  
  9.   
  10.             Field<AuthorType>("author",  
  11.                 arguments: new QueryArguments(new QueryArgument<IntGraphType> { Name = "authorId" }),  
  12.                 resolve: context =>  
  13.                  {  
  14.                      return authorRepository.GetAuthorAsync(context.GetArgument<int>("authorId"));  
  15.                  });  
  16.         }  
  17.     }  
Similarly, you can write a GraphQL query that will handle fetching a book or list of books.
  1. public class BookQuery:ObjectGraphType  
  2.     {  
  3.         public BookQuery(IBookRepository bookRepository)  
  4.         {  
  5.             Field<ListGraphType<BookType>>("books", resolve: context =>  
  6.             {  
  7.                 return bookRepository.GetBooksAsync();  
  8.             });  
  9.         }  
  10.     }  
A GraphQL Schema is at the core of Server implementation. The Schema is written in Graph Schema language and it can be used to define object types and fields to represent data that can be retrieved from API.
  1. public class AuthorSchema:Schema  
  2.     {  
  3.         public AuthorSchema(IDependencyResolver resolver):base(resolver)  
  4.         {  
  5.             Query = resolver.Resolve<AuthorQuery>();  
  6.         }  
  7.     }  
  1. public class BookSchema:Schema  
  2.     {  
  3.         public BookSchema(IDependencyResolver resolver):base(resolver)  
  4.         {  
  5.             Query = resolver.Resolve<BookQuery>();  
  6.         }  
  7.     }  
 Add dependency injection for author and book schema in ConfigureService method of Startup class.
  1. services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));  
  2.             services.AddScoped<AuthorSchema>();  
  3.             services.AddScoped<BookSchema>();  
  4.             services.AddGraphQL(o => o.ExposeExceptions = false)  
  5.                 .AddGraphTypes(ServiceLifetime.Scoped);  
  6.   
  7.             services.Configure<KestrelServerOptions>(options =>  
  8.             {  
  9.                 options.AllowSynchronousIO = true;  
  10.             });  
Now, run the application and open the playground UI. You can use the following queries to run author and book details from the data.
  1. query {  
  2.   author(authorId:1){  
  3.     firstName  
  4.     lastName  
  5.     authorId  
  6.     books {  
  7.       publicationYear  
  8.       title  
  9.       bookId  
  10.     }  
  11.   }  
  12. }  
Remember one of the pros of GraphQL; over-fetching and under-fetching. In the above query, we can retrieve any types defined in author and book types.
 
 
 
GraphQL Mutation
 
As discussed before, Mutation is used to write or post values. You have to define InputObjectGraphType for author and book types.
  1. public class AuthorInputType: InputObjectGraphType  
  2.     {  
  3.         public AuthorInputType()  
  4.         {  
  5.             Name = "authorInput";  
  6.             Field<NonNullGraphType<StringGraphType>>("firstName");  
  7.             Field<NonNullGraphType<StringGraphType>>("lastName");  
  8.         }  
  9.     }  
  1. public class BookInputType:InputObjectGraphType  
  2.     {  
  3.         public BookInputType()  
  4.         {  
  5.             Name = "bookInput";  
  6.             Field<NonNullGraphType<StringGraphType>>("title");  
  7.             Field<NonNullGraphType<IntGraphType>>("publicationYear");  
  8.             Field<IntGraphType>("authorId");  
  9.         }  
  10.     }  
 You can define mutation for author and book using ObjectGraphType
  1. public class AuthorMutation:ObjectGraphType  
  2.     {  
  3.         public AuthorMutation(IAuthorRepository authorRepository,IBookRepository bookRepository)  
  4.         {  
  5.             Field<AuthorType>("insertAuthor",  
  6.                 arguments: new QueryArguments(new QueryArgument<AuthorInputType> { Name = "author" }),  
  7.                 resolve:context=>  
  8.                 {  
  9.                     return authorRepository.InsertAuthorAsync(context.GetArgument<Author>("author"));  
  10.                 });  
  11.         }  
  12.     }  
  1. public class BookMutation:ObjectGraphType  
  2.     {  
  3.         private readonly IBookRepository bookRepository;  
  4.         private readonly IAuthorRepository authorRepository;  
  5.   
  6.         public BookMutation(IBookRepository bookRepository,IAuthorRepository authorRepository)  
  7.         {  
  8.             this.bookRepository = bookRepository;  
  9.             this.authorRepository = authorRepository;  
  10.   
  11.             Field<BookType>("insertAuthorAndBook",  
  12.               arguments: new QueryArguments(  
  13.                   new QueryArgument<AuthorInputType> { Name = "author" },  
  14.                   new QueryArgument<BookInputType> { Name = "book" }),  
  15.               resolve: context =>  
  16.               {  
  17.                   var author = context.GetArgument<Author>("author");  
  18.                   var book = context.GetArgument<Book>("book");  
  19.                   return InsertBook(author, book);  
  20.               });  
  21.   
  22.             Field<BookType>("insertBook",  
  23.                arguments: new QueryArguments(new QueryArgument<BookInputType> { Name = "book" }),  
  24.                resolve: context =>  
  25.                {  
  26.                    return bookRepository.InsertBook(context.GetArgument<Book>("book"));  
  27.                });  
  28.         }  
  29.   
  30.         private async Task<Book> InsertBook(Author author,Book book)  
  31.         {  
  32.             if (author == null)  
  33.                 return null;  
  34.              
  35.             var existingAuthor =await authorRepository.GetAuthorByFirstNameAsync(author.FirstName);  
  36.             if (existingAuthor == null)  
  37.             {  
  38.                 var newAuthor = await authorRepository.InsertAuthorAsync(author);  
  39.                 book.AuthorId = newAuthor.AuthorId;  
  40.                 return await bookRepository.InsertBook(book);  
  41.             }  
  42.             else  
  43.             {  
  44.                 book.AuthorId = existingAuthor.AuthorId;  
  45.                 return await bookRepository.InsertBook(book);  
  46.             }  
  47.         }  
  48.     }  
Finally, you need to define mutation in both the author and book schema classes
  1. public class AuthorSchema:Schema  
  2.     {  
  3.         public AuthorSchema(IDependencyResolver resolver):base(resolver)  
  4.         {  
  5.             Query = resolver.Resolve<AuthorQuery>();  
  6.             Mutation = resolver.Resolve<AuthorMutation>();  
  7.         }  
  8.     }   
  1. public class BookSchema:Schema  
  2.     {  
  3.         public BookSchema(IDependencyResolver resolver):base(resolver)  
  4.         {  
  5.             Query = resolver.Resolve<BookQuery>();  
  6.             Mutation = resolver.Resolve<BookMutation>();  
  7.         }  
  8.     }  
You can use the following query to insert a record
  1. mutation($author:authorInput!,$book:bookInput!){  
  2.   insertAuthor(author:$author){  
  3.     firstName  
  4.     lastName  
  5.   }  
  6.   insertBook(book:$book){  
  7.     title,  
  8.     publicationYear  
  9.   }  
  10. }  
You need an input variable for both author and book
  1. {  
  2.   "author":{  
  3.     "firstName":"Charles",  
  4.     "lastName":"Darwin"  
  5.   },  
  6.   "book": {  
  7.     "title""On the origin of Species",  
  8.     "publicationyear": 1856  
  9.   }  
  10. }  
Everything seems to be working fine, but there are still issues with GraphQL endpoint. GraphQL should have a single endpoint and currently, there are 2 endpoints for author and book.
 
In the next article, we will discuss fixing the endpoint issue by introducing GraphQLController and will also see how the client has to invoke the API.
 
I hope you liked the article. In case you find the article interesting, please kindly like and share it.