In this post, we will see how to create and consume GraphQL API query using .NET Core.
But first of first, what is GraphQL?
From GraphQL website:
“GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.“
A GraphQL query is used to read or fetch values while a mutation is used to write or post values. In either case, the operation is a simple string that a GraphQL server can parse and respond to with data in a specific format. The popular response format that is usually used for mobile and web applications is JSON
We start creating a Web API project called SchoolsService where, we will add the HotChocolate library and EntityFrameworkCore.InMemory library using these commands:
Install-Package HotChocolate.AspNetCore
Install-Package HotChocolate.Data
Install-Package Microsoft.EntityFrameworkCore.InMemory
For everyone doesn’t know HotChocolate, it is a .NET GraphQL platform that can help us build a GraphQL layer over our existing and new applications.
Now, we create two entities called School and Student:
[SCHOOL.CS]
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace SchoolsService.Model
{
public class School
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int SchooldId { get; set; }
public string SchoolName { get; set; }
public ICollection<Student> Students { get; set; }
}
}
[STUDENT.CS]
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace SchoolsService.Model
{
public class Student
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int StudentId { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Surname { get; set; }
[Required]
public string Email { get; set; }
[Required]
public int Age { get; set; }
public int SchoolID { get; set; }
public School School { get; set; }
}
}
Then, we define the DBContext and we will create the repositories for both entities:
[SERVICEDBCONTEXT.CS]
using Microsoft.EntityFrameworkCore;
using SchoolsService.Model;
namespace SchoolsService.DataAccess
{
public class ServiceDbContext:DbContext
{
public ServiceDbContext(DbContextOptions<ServiceDbContext> options) : base(options)
{
}
public DbSet<School> Schools => Set<School>();
public DbSet<Student> Students => Set<Student>();
}
}
[ISCHOOLCORE.CS]
using SchoolsService.Model;
using System.Linq;
namespace SchoolsService.DataAccess.Commands
{
public interface ISchoolCore
{
IQueryable<School> GetAllSchools();
School AddSchool(School school);
}
}
[SCHOOLCORE.CS]
using Microsoft.EntityFrameworkCore;
using SchoolsService.Model;
using System.Linq;
namespace SchoolsService.DataAccess.Commands
{
public class SchoolCore : ISchoolCore
{
private readonly ServiceDbContext _dbContext;
public SchoolCore(ServiceDbContext dbContext)
{
_dbContext = dbContext;
FeedDbInMemory();
}
public School AddSchool(School school)
{
_dbContext.Schools.Add(school);
_dbContext.SaveChanges();
return school;
}
public IQueryable<School> GetAllSchools()
{
return _dbContext.Schools.Include(i => i.Students);
}
private void FeedDbInMemory()
{
if (!_dbContext.Schools.Any())
{
var school1 = new School { SchooldId = 1, SchoolName = "School_1"};
var school2 = new School { SchooldId = 2, SchoolName = "School_2" };
_dbContext.Schools.Add(school1);
_dbContext.Schools.Add(school2);
_dbContext.SaveChanges();
}
}
}
}
[ISTUDENTCORE.CS]
using SchoolsService.Model;
using System.Linq;
namespace SchoolsService.DataAccess.Commands
{
public interface IStudentCore
{
IQueryable<Student> GetAllStudents();
Student AddStudent(Student student);
}
}
[STUDENTCORE.CS]
using SchoolsService.Model;
using System.Linq;
namespace SchoolsService.DataAccess.Commands
{
public class StudentCore : IStudentCore
{
private readonly ServiceDbContext _dbContext;
public StudentCore(ServiceDbContext dbContext)
{
_dbContext = dbContext;
FeedDbInMemory();
}
public Student AddStudent(Student student)
{
_dbContext.Students.Add(student);
_dbContext.SaveChanges();
return student;
}
public IQueryable<Student> GetAllStudents()
{
return _dbContext.Students.AsQueryable();
}
private void FeedDbInMemory()
{
if (!_dbContext.Students.Any())
{
for (int i = 1; i < 10; i++)
{
var student = new Student { Age = i % 2 == 0 ? 16 : 15, Email = $"email{i}@test.com", Name = $"NameStudent{i}", Surname = $"SernameStudent{i}", SchoolID = i % 2 == 0 ? 1 : 2 };
_dbContext.Students.Add(student);
_dbContext.SaveChanges();
}
}
}
}
}
Now, we define a class called Query that will contain queries we want to run:
[QUERY.CS]
using HotChocolate;
using HotChocolate.Data;
using SchoolsService.DataAccess.Commands;
using SchoolsService.Model;
using System.Linq;
namespace SchoolsService.GraphQL
{
public class Query
{
[UseProjection]
public IQueryable<Student> AllStudents([Service] IStudentCore studentCore)
{
return studentCore.GetAllStudents();
}
[UseProjection]
public IQueryable<School> AllSchool([Service] ISchoolCore schoolCore)
{
return schoolCore.GetAllSchools();
}
}
}
Finally, we modify Startup file to define Dependency Injections and configure the GraphQL Middleware:
[STARTUP.CS]
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SchoolsService.DataAccess;
using SchoolsService.DataAccess.Commands;
using SchoolsService.GraphQL;
namespace SchoolsService
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ServiceDbContext>(opt => opt.UseInMemoryDatabase("DbService"));
services.AddScoped<IStudentCore, StudentCore>();
services.AddScoped<ISchoolCore, SchoolCore>();
services.AddControllers();
services.AddGraphQLServer()
.AddQueryType<Query>()
.AddProjections();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
});
}
}
}
Now, if we run the application and we go to localhost/graphql, this will be the result:
Banana Cake Pop is a UI makes easy and enjoyable to test our GraphQL server implementations.
We can see the two methods in Query file and the two entities:
In order to run some queries, we can use this UI:
otherwise, we can use any other API clients as for example Postman or Insomnia
IMPORTANT!
In order to call a query in graphQL, we have to use the POST http verb :
SORTING
For sorting a query in GraphQL, we have to add the attribute [UseSorting] in the Query class and then, we have to add AddSorting() in the Startup file:
[QUERY.CS]
using HotChocolate;
using HotChocolate.Data;
using SchoolsService.DataAccess.Commands;
using SchoolsService.Model;
using System.Linq;
namespace SchoolsService.GraphQL
{
public class Query
{
[UseProjection]
[UseSorting]
public IQueryable<Student> AllStudents([Service] IStudentCore studentCore)
{
return studentCore.GetAllStudents();
}
[UseProjection]
[UseSorting]
public IQueryable<School> AllSchool([Service] ISchoolCore schoolCore)
{
return schoolCore.GetAllSchools();
}
}
}
[STARTUP.CS]
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ServiceDbContext>(opt => opt.UseInMemoryDatabase("DbService"));
services.AddScoped<IStudentCore, StudentCore>();
services.AddScoped<ISchoolCore, SchoolCore>();
services.AddControllers();
services.AddGraphQLServer()
.AddQueryType<Query>()
.AddProjections()
.AddSorting();
}
Now, if we run some queries, these will be the results:
It works fine but, if we try to run this query, we will receive an error message:
In order to fix this problem, we have to add the attribute [UseSorting] in the School class as well:
using HotChocolate.Data;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace SchoolsService.Model
{
public class School
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int SchooldId { get; set; }
public string SchoolName { get; set; }
[UseSorting]
public ICollection<Student> Students { get; set; }
}
}
Now, if we run again the query, this will be the result: