Query and Mutation in GraphQL- Part two

Recap

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

https://dotnetintellect.com/2020/07/29/introduction-to-graphql/

I believe you have prior understanding of EF Core and we’ll be concentrating only on GraphQL concepts. If not, please refer to the below article

https://dotnetintellect.com/2020/06/09/exploring-azure-functions-http-trigger-using-ef-core/

Coding

Let’s directly jump into coding section by adding nuget package

Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.Tools
GraphQL
GraphQL.Server.Transports.AspNetCore
GraphQL.Server.UI.Playground

In this article, we are illustrating the model that is concerned with books and their authors:

public class Author
    {
        public int AuthorId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public ICollection<Book> Books { get; set; }
    }
    public class Book
    {
        public int BookId { get; set; }
        public string Title { get; set; }
        public int PublicationYear { get; set; }
        public int AuthorId { get; set; }
        public Author Author { get; set; }
    }

Now, let’s include an Author’s context class and add data seeding for initial data in the database.

public class AuthorContext:DbContext
    {
        public DbSet<Author> Authors { get; set; }

        public DbSet<Book> Books { get; set; }

        public AuthorContext(DbContextOptions dbContextOptions) :base(dbContextOptions)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Author>().HasData(new Author
            {
                FirstName= "Agatha",
                LastName="Christry",
                AuthorId = 1
            });

            modelBuilder.Entity<Book>().HasData(
                new Book { Title = "The Mysterious Affair at Styles", PublicationYear = 1921,AuthorId=1,BookId=1 },
                new Book { Title = "The Secret Adversary", PublicationYear = 1922,AuthorId = 1,BookId=2 }
                );
        }
    }

Note: I highly encourage you to use IEntityTypeConfiguration for data seeding rather than using OnModelCreating method. You can refer to the article https://dotnetintellect.com/2020/07/05/azure-cosmos-db-sql-api-using-ef-core-part-two/

public interface IAuthorRepository
    {
        Task<IEnumerable<Author>> GetAuthorsAsync();

        Task<Author> GetAuthorAsync(int authorId);

        Task<Author> InsertAuthorAsync(Author author);

        Task<Author> GetAuthorByFirstNameAsync(string firstName);
    }
public class AuthorRepository: IAuthorRepository
    {
        private readonly AuthorContext authorContext;

        public AuthorRepository(AuthorContext authorContext)
        {
            this.authorContext = authorContext;
        }

        public async Task<Author> GetAuthorAsync(int authorId)
        {
            return await authorContext.Authors.FirstOrDefaultAsync(x => x.AuthorId == authorId);
        }

        public async Task<Author> GetAuthorByFirstNameAsync(string firstName)
        {
            return await authorContext.Authors.FirstOrDefaultAsync(x => x.FirstName == firstName);
        }

        public async Task<IEnumerable<Author>> GetAuthorsAsync()
        {
            return await authorContext.Authors.ToListAsync();
        }

        public async Task<Author> InsertAuthorAsync(Author author)
        {
            var result = (await authorContext.Authors.AddAsync(author)).Entity;
            await authorContext.SaveChangesAsync();
            return result;
        }
    }
 public interface IBookRepository
    {
        Task<IEnumerable<Book>> GetBooksAsync();

        Task<IEnumerable< Book>> GetBooks(int authorId);

        Task<Book> InsertBook(Book book);
    }
public class BookRepository: IBookRepository
    {
        private readonly AuthorContext authorContext;

        public BookRepository(AuthorContext authorContext)
        {
            this.authorContext = authorContext;
        }

        public async Task<IEnumerable< Book>> GetBooks(int authorId)
        {
            var result = await (from author in authorContext.Set<Author>()
                                join book in authorContext.Set<Book>() on author.AuthorId equals book.AuthorId
                                where author.AuthorId==authorId
                                select book).ToListAsync();
            return result;
        }

        public async Task<IEnumerable<Book>> GetBooksAsync()
        {
            return await authorContext.Books.ToListAsync();
        }

        public async Task<Book> InsertBook(Book book)
        {
            var result = (await authorContext.Books.AddAsync(book)).Entity;
            await authorContext.SaveChangesAsync();
            return result;
        }
    }

Now register the DbContext and repositories to the dependency injection in the startup class

services.AddDbContext<AuthorContext>(x => x.UseSqlServer(Configuration.GetConnectionString("SqlConnectionString")));
            services.AddScoped<IAuthorRepository, AuthorRepository>();
            services.AddScoped<IBookRepository, BookRepository>();

Perform database migration using the following commands

//Adding migration
dotnet ef migrations add InitialMigration
//Update Database
dotnet ef database update

GraphQL Middleware

You can add GraphQL middleware to the configure method of the Startup class

app.UseGraphQL<AuthorSchema>("/api/author");
            app.UseGraphQL<BookSchema>("/api/book");
            app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
            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 author & book class GraphQL queryable, you should create a new type and extend it from custom ObjectGraphType<T>.

public class AuthorType:ObjectGraphType<Author>
    {
        public AuthorType(IBookRepository bookRepository)
        {
            Field(x => x.AuthorId).Description("Author Id");
            Field(x => x.FirstName).Description("Author's first name");
            Field(x => x.LastName);
            Field<ListGraphType<BookType>>("books",
                arguments: new QueryArguments(new QueryArgument<IntGraphType> { Name = "authorId" }),
                resolve: context =>
                 {
                     return bookRepository.GetBooks(context.Source.AuthorId);
                 }); 
        }
    }
public class BookType:ObjectGraphType<Book>
    {
        public BookType()
        {
            Field(x => x.BookId);
            Field(x => x.Title);
            Field(x => x.PublicationYear);
        }
    }

You can now write GraphQL query that will handle fetching an author or list of authors.

public class AuthorQuery:ObjectGraphType<object>
    {
        public AuthorQuery(IAuthorRepository authorRepository)
        {
            Field<ListGraphType<AuthorType>>("authors", resolve:context =>
            {
                return authorRepository.GetAuthorsAsync();
            });

            Field<AuthorType>("author",
                arguments: new QueryArguments(new QueryArgument<IntGraphType> { Name = "authorId" }),
                resolve: context =>
                 {
                     return authorRepository.GetAuthorAsync(context.GetArgument<int>("authorId"));
                 });
        }
    }

Similarly, you can write GraphQL query that will handle fetching a book or list of books.

public class BookQuery:ObjectGraphType
    {
        public BookQuery(IBookRepository bookRepository)
        {
            Field<ListGraphType<BookType>>("books", resolve: context =>
            {
                return bookRepository.GetBooksAsync();
            });
        }
    }

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.

public class AuthorSchema:Schema
    {
        public AuthorSchema(IDependencyResolver resolver):base(resolver)
        {
            Query = resolver.Resolve<AuthorQuery>();
        }
    }
public class BookSchema:Schema
    {
        public BookSchema(IDependencyResolver resolver):base(resolver)
        {
            Query = resolver.Resolve<BookQuery>();
        }
    }

Add dependency injection for author and book schema in ConfigureService method of Startup class.

services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
            services.AddScoped<AuthorSchema>();
            services.AddScoped<BookSchema>();
            services.AddGraphQL(o => o.ExposeExceptions = false)
                .AddGraphTypes(ServiceLifetime.Scoped);

            services.Configure<KestrelServerOptions>(options =>
            {
                options.AllowSynchronousIO = true;
            });

Now, run the application and open the playground UI. You can use the following queries to run author and book details from the data

query {
  author(authorId:1){
    firstName
    lastName
    authorId
    books {
      publicationYear
      title
      bookId
    }
  }
}

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.

 public class AuthorInputType: InputObjectGraphType
    {
        public AuthorInputType()
        {
            Name = "authorInput";
            Field<NonNullGraphType<StringGraphType>>("firstName");
            Field<NonNullGraphType<StringGraphType>>("lastName");
        }
    }
public class BookInputType:InputObjectGraphType
    {
        public BookInputType()
        {
            Name = "bookInput";
            Field<NonNullGraphType<StringGraphType>>("title");
            Field<NonNullGraphType<IntGraphType>>("publicationYear");
            Field<IntGraphType>("authorId");
        }
    }

You can define mutation for author and book using ObjectGraphType

public class AuthorMutation:ObjectGraphType
    {
        public AuthorMutation(IAuthorRepository authorRepository,IBookRepository bookRepository)
        {
            Field<AuthorType>("insertAuthor",
                arguments: new QueryArguments(new QueryArgument<AuthorInputType> { Name = "author" }),
                resolve:context=>
                {
                    return authorRepository.InsertAuthorAsync(context.GetArgument<Author>("author"));
                });
        }
    }
public class BookMutation:ObjectGraphType
    {
        private readonly IBookRepository bookRepository;
        private readonly IAuthorRepository authorRepository;

        public BookMutation(IBookRepository bookRepository,IAuthorRepository authorRepository)
        {
            this.bookRepository = bookRepository;
            this.authorRepository = authorRepository;

            Field<BookType>("insertAuthorAndBook",
              arguments: new QueryArguments(
                  new QueryArgument<AuthorInputType> { Name = "author" },
                  new QueryArgument<BookInputType> { Name = "book" }),
              resolve: context =>
              {
                  var author = context.GetArgument<Author>("author");
                  var book = context.GetArgument<Book>("book");
                  return InsertBook(author, book);
              });

            Field<BookType>("insertBook",
               arguments: new QueryArguments(new QueryArgument<BookInputType> { Name = "book" }),
               resolve: context =>
               {
                   return bookRepository.InsertBook(context.GetArgument<Book>("book"));
               });
        }

        private async Task<Book> InsertBook(Author author,Book book)
        {
            if (author == null)
                return null;
           
            var existingAuthor =await authorRepository.GetAuthorByFirstNameAsync(author.FirstName);
            if (existingAuthor == null)
            {
                var newAuthor = await authorRepository.InsertAuthorAsync(author);
                book.AuthorId = newAuthor.AuthorId;
                return await bookRepository.InsertBook(book);
            }
            else
            {
                book.AuthorId = existingAuthor.AuthorId;
                return await bookRepository.InsertBook(book);
            }
        }
    }

Finally, you need to define mutation in both author and book schema classes

public class AuthorSchema:Schema
    {
        public AuthorSchema(IDependencyResolver resolver):base(resolver)
        {
            Query = resolver.Resolve<AuthorQuery>();
            Mutation = resolver.Resolve<AuthorMutation>();
        }
    }
public class BookSchema:Schema
    {
        public BookSchema(IDependencyResolver resolver):base(resolver)
        {
            Query = resolver.Resolve<BookQuery>();
            Mutation = resolver.Resolve<BookMutation>();
        }
    }

You can use the following query to insert record

mutation($author:authorInput!,$book:bookInput!){
  insertAuthor(author:$author){
    firstName
    lastName
  }
  insertBook(book:$book){
    title,
    publicationYear
  }
}

You need input variable for both author and book

{
  "author":{
    "firstName":"Charles",
    "lastName":"Darwin"
  },
  "book": {
    "title": "On the origin of Species",
    "publicationyear": 1856
  }
}

Everything seems to be working fine but still there are issues with GraphQL endpoint. GraphQL should have single endpoint and currently there are 2 endpoints for author and book.

In the next article, we will discuss about fixing the endpoint issue by introducing GraphQLController and will also see how client has to invoke the API.

I hope you like the article. In case, you find the article as interesting then kindly like and share it.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s