GraphQL Server-side in C#Bot
For this article, we are going to be using the Learning Management System (LMS) - Example project.
Introduction
When using GraphQL, all requests are sent to a single endpoint: /api/graphql
(this is in contrast to other API design architectures that use a different endpoint for each different request). When submitting an API request to /api/graphql
, the data will hit the controller found at serverside/src/Controllers/GraphQlController.cs
. The data sent to this controller will follow the standards provided by GraphQL.
C#Bot has an interactive query tool called GraphiQL built into the application that can write, validate and test GraphQL queries. This tool is located at /admin/graphiql
in your application, and requires you to be logged in. To learn more about using Graphiql, check out the following Client/server communication in C#Bot.
An API request in GraphQL might look something like this:
{
courseData {
difficulty
estimatedTime
lessonCount
}
}
Basic definitions for common terms:
Term | Definition |
---|---|
Query | Method of fetching data from the server. |
Mutation | The way to modify server-side data - they’re equivalent to using a CRUD operation (i.e. involve creating, updating or deleting data). |
Resolvers | Collection of functions that generate a response for a GraphQl query. |
Types | The most basic components of a GraphQL schema, which represent the kinds of objects that can be fetched from your service. |
You can learn more about queries and mutations here.
GraphQL vs REST
A well-known alternative to GraphQL that we also support is REST; a comparatively more straight forward method of making API requests. However, at Codebots we prefer to use GraphQL due to its power and efficiency. REST requires a separate endpoint for each action, whereas GraphQL has a single endpoint. GraphQL uses its query language to tailor the request to exactly what information is wanted, which limits the amount of processing required, and removes any over and under fetching.
File-By-File Breakdown
The following sections give an overview of the most important files used by the server-side to complete GraphQL requests. These files can be found in the LMS Example Project.
Please continue reading if you would like a more in-depth understanding of the entire process of sending a request using GraphQL.
Step 1: GraphQLController
File path: serverside/src/Controllers/GraphQlController.cs
This is the entry point into the application. The request will almost always be hitting the Post
function.
public async Task<ExecutionResult> Post(CancellationToken cancellation)
{
// % protected region % [Change post method here] off begin
await _identityService.RetrieveUserAsync();
var parsedRequest = await ParsePostBody(cancellation);
var result = await _graphQlService.Execute(
parsedRequest.Body.Query,
parsedRequest.Body.OperationName,
parsedRequest.Body.Variables.ToInputs(),
parsedRequest.Files,
_identityService.User,
cancellation);
return RenderResult(result);
// % protected region % [Change post method here] end
}
In this function we retrieve the user who made the request, parse the post body and then execute the query in the GraphQL service.
Step 2: GraphQLService
File path: serverside/src/Services/GraphQlService.cs
This file constructs an ‘executionOptions’ object:
var executionOptions = new ExecutionOptions { ...}
It then calls the execute method of the GraphQL executor.
var result = await _executer.ExecuteAsync(executionOptions).ConfigureAwait(false);
The GraphQL executor is a class provided by the library GraphQL.NET. This executor will read the GraphQL schema the bot has defined, and either execute one of the functions inside of it, or fail if the query was invalid.
Step 3: Schema
File path: serverside/src/Graphql/Schema.cs
This is where the GraphQL schema gets defined on the server-side. A schema is a basic class which contains two other classes: a query and mutation. These query and mutation classes provide the functions used to execute a GraphQL request.
public class LmssharpSchema : Schema
{
public LmssharpSchema(IDependencyResolver resolver) : base(resolver)
{
Query = resolver.Resolve<LmssharpQuery>();
Mutation = resolver.Resolve<LmssharpMutation>();
// % protected region % [Add any extra schema constructor options here] off begin
// % protected region % [Add any extra schema constructor options here] end
}
// % protected region % [Add any schema methods here] off begin
// % protected region % [Add any schema methods here] end
}
While this article will look at the query class, it is important to note the mutation class will follow the same principles. If we take a look at the constructor for the query class we can see this.
public LmssharpQuery(IEfGraphQLService<LmssharpDBContext> efGraphQlService) : base(efGraphQlService)
{
// Add query types for each entity
AddModelQueryField<CourseCategoryEntityType, CourseCategoryEntity>("CourseCategoryEntity");
AddModelQueryField<CourseLessonsEntityType, CourseLessonsEntity>("CourseLessonsEntity");
AddModelQueryField<CourseEntityType, CourseEntity>("CourseEntity");
AddModelQueryField<UserEntityType, UserEntity>("UserEntity");
AddModelQueryField<LessonSubmissionEntityType, LessonSubmissionEntity>("LessonSubmissionEntity");
AddModelQueryField<LessonEntityType, LessonEntity>("LessonEntity");
// ... Some lines were removed here
AddModelQueryField<ArticleTimelineEventsEntityType, ArticleTimelineEventsEntity>("ArticleTimelineEventsEntity");
AddModelQueryField<LessonEntityFormTileEntityType, LessonEntityFormTileEntity>("LessonEntityFormTileEntity");
// Add query types for each many to many reference
AddModelQueryField<ArticlesTagsType, ArticlesTags>("ArticlesTags");
AddModelQueryField<ArticleWorkflowStatesType, ArticleWorkflowStates>("ArticleWorkflowStates");
// % protected region % [Add any extra query config here] off begin
// % protected region % [Add any extra query config here] end
}
This will call AddModelQueryField
for each different type of entity in the model to construct the functions defined for the entity in the GraphQL API. The argument provided for this function is the string that is used in the name for the GraphQL functions, and the 2 generic parameters are the GraphQL type class and the Entity Framework model class. We will come back to the GraphQL ‘type class’ later in the article.
If we take a look at AddModelQueryField
we can see it looks like the following:
public void AddModelQueryField<TModelType, TModel>(string name)
where TModelType : ObjectGraphType<TModel>
where TModel : class, IOwnerAbstractModel, new()
{
// % protected region % [Override single query here] off begin
AddQueryField(
$"{name}s",
QueryHelpers.CreateResolveFunction<TModel>(),
typeof(TModelType)).Description = $"Query for fetching multiple {name}s";
// % protected region % [Override single query here] end
// % protected region % [Override multiple query here] off begin
AddSingleField(
name: name,
resolve: QueryHelpers.CreateResolveFunction<TModel>(),
graphType: typeof(TModelType)).Description = $"Query for fetching a single {name}";
// More functions below weren't included ...
}
To break down one of these calls: the first argument is the name of the query that is called from the GraphQL API. The second argument is the query resolver (essentially a callback function) that will run when the query executes. The final argument is the GraphQL model type that is used as the return type for the query.
When a query executes, it will first run the resolve function, and then use the model type to return a value to the executor. This value is then returned by the API.
Step 4: GraphQL Query Resolvers
File path: serverside/src/Graphql/Helpers/QueryHelpers.cs
The specific resolver we are going to discuss for this article is CreateResolveFunction
, which fetches query data.
public static Func<ResolveFieldContext<object>, IQueryable<TModel>> CreateResolveFunction<TModel>()
where TModel : class, IOwnerAbstractModel, new()
{
return context =>
{
var graphQlContext = (LmssharpGraphQlContext) context.UserContext;
var crudService = graphQlContext.CrudService;
var auditFields = AuditReadData.FromGraphqlContext(context);
return crudService.Get<TModel>(auditFields: auditFields).AsNoTracking();
};
}
We can see this function constructs and returns a new function. This is so we can reuse the same resolver for all our different entities. The first three lines retrieving fields we need to execute the query. The final call is to the Get
method of the CrudService
which will create an Entity Framework query for this specific entity type and return it.
Step 5: Model Types and Relations
File path: serverside/src/Models/UserEntity/UserEntityType.cs
A GraphQL ‘type’ represents the values used as arguments and return types. Taking a look at the model type for the user entity we can see there are two different classes inside of it. We will go over both of these.
public class UserEntityType : EfObjectGraphType<LmssharpDBContext, UserEntity>
{
public UserEntityType(IEfGraphQLService<LmssharpDBContext> service) : base(service)
{
Description = @"Users of the library";
// Add model fields to type
Field(o => o.Id, type: typeof(IdGraphType));
Field(o => o.Created, type: typeof(DateTimeGraphType));
Field(o => o.Modified, type: typeof(DateTimeGraphType));
Field(o => o.Email, type: typeof(StringGraphType));
Field(o => o.FirstName, type: typeof(StringGraphType)).Description(@"First name of the user");
Field(o => o.LastName, type: typeof(StringGraphType)).Description(@"Last name of the user");
// % protected region % [Add any extra GraphQL fields here] off begin
// % protected region % [Add any extra GraphQL fields here] end
// Add entity references
// GraphQL reference to entity ArticleEntity via reference UpdatedArticle
IEnumerable<ArticleEntity> UpdatedArticlesResolveFunction(ResolveFieldContext<UserEntity> context)
{
var graphQlContext = (LmssharpGraphQlContext) context.UserContext;
var filter = SecurityService.CreateReadSecurityFilter<ArticleEntity>(graphQlContext.IdentityService, graphQlContext.UserManager, graphQlContext.DbContext, graphQlContext.ServiceProvider);
return context.Source.UpdatedArticles.Where(filter.Compile());
}
AddNavigationListField("UpdatedArticles", (Func<ResolveFieldContext<UserEntity>, IEnumerable<ArticleEntity>>) UpdatedArticlesResolveFunction);
AddNavigationConnectionField("UpdatedArticlesConnection", UpdatedArticlesResolveFunction);
// GraphQL reference to entity ArticleEntity via reference CreatedArticle
IEnumerable<ArticleEntity> CreatedArticlesResolveFunction(ResolveFieldContext<UserEntity> context)
{
var graphQlContext = (LmssharpGraphQlContext) context.UserContext;
var filter = SecurityService.CreateReadSecurityFilter<ArticleEntity>(graphQlContext.IdentityService, graphQlContext.UserManager, graphQlContext.DbContext, graphQlContext.ServiceProvider);
return context.Source.CreatedArticles.Where(filter.Compile());
}
AddNavigationListField("CreatedArticles", (Func<ResolveFieldContext<UserEntity>, IEnumerable<ArticleEntity>>) CreatedArticlesResolveFunction);
AddNavigationConnectionField("CreatedArticlesConnection", CreatedArticlesResolveFunction);File path:
var graphQlContext = (LmssharpGraphQlContext) context.UserContext;
var filter = SecurityService.CreateReadSecurityFilter<LessonSubmissionEntity>(graphQlContext.IdentityService, graphQlContext.UserManager, graphQlContext.DbContext, graphQlContext.ServiceProvider);
return context.Source.LessonSubmissionss.Where(filter.Compile());
}
AddNavigationListField("LessonSubmissionss", (Func<ResolveFieldContext<UserEntity>, IEnumerable<LessonSubmissionEntity>>) LessonSubmissionssResolveFunction);
AddNavigationConnectionField("LessonSubmissionssConnection", LessonSubmissionssResolveFunction);
// % protected region % [Add any extra GraphQL references here] off begin
// % protected region % [Add any extra GraphQL references here] end
}
}
In this class, we can first see the individual fields defined in the entity model for this entity. We can see the first four fields are defining the entity attributes. The second part of the code first defines an inline function for resolving the reference, which is then added as a callback for a navigation field. This allows for the related entities to be queried in one request. A navigation field is translated into a .Include
call in Entity Framework.
Notes and Further Information
A significant amount of our bot-written code is a configuration for the two different GraphQL libraries that we use:
- The GraphQL.Net library provides the GraphQL executor, that processes GraphQL queries, as well as the schema that defines the entire API.
- The GraphQL.EntityFramework library builds upon the previous one to enable native Entity Framework queries based on the defined model types.
Learn more:
- For a more in-depth look at the GraphQL.NET library, we recommend the LinkedIn Learning course API Development in .NET with GraphQL, which steps through creating an ASP.NET GraphQL application from scratch.
Was this article helpful?