Start modelling your app today.

Get started for free

What's this?

GraphQL Server-side in C#Bot

An overview of the GraphQL implementation on the C#Bot server-side


GraphQL Server-side in C#Bot

In this article we will go over the flow of data when a request is sent to the GraphQL controller. For this article, we are going to be using the Learning Management System (LMS) - Example project.

Introduction

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.

File By File Breakdown

GraphQLController

serverside/src/Controllers/GraphQlController.cs

This is the entry point into the application and 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 you can see we retrieve the user who made the request, parse the post body and then execute the query in the GraphQL service.

GraphQLService

serverside/src/Services/GraphQlService.cs

This file mostly contains the construction of an options object and then calling 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.

Schema

serverside/src/Graphql/Schema.cs

This is where the GraphQL schema is defined on the server-side. A schema is a basic class which contains 2 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 principals. 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");
            AddModelQueryField<LessonEntityFormVersionType, LessonEntityFormVersion>("LessonEntityFormVersion");
            AddModelQueryField<AdministratorEntityType, AdministratorEntity>("AdministratorEntity");
            AddModelQueryField<BookEntityType, BookEntity>("BookEntity");
            AddModelQueryField<ArticleEntityType, ArticleEntity>("ArticleEntity");
            AddModelQueryField<ContentFileEntityType, ContentFileEntity>("ContentFileEntity");
            AddModelQueryField<TagEntityType, TagEntity>("TagEntity");
            AddModelQueryField<WorkflowEntityType, WorkflowEntity>("WorkflowEntity");
            AddModelQueryField<WorkflowStateEntityType, WorkflowStateEntity>("WorkflowStateEntity");
            AddModelQueryField<WorkflowTransitionEntityType, WorkflowTransitionEntity>("WorkflowTransitionEntity");
            AddModelQueryField<WorkflowVersionEntityType, WorkflowVersionEntity>("WorkflowVersionEntity");
            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 the 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 shall be 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 the 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 ...
}

To break down one of these calls, the first argument is the name of the query that can be called from the GraphQL API. The second argument is the query resolver (basically a callback function) that will be run when the query is executed. The final argument is the GraphQL model type that shall be used as the return type for the query.

When a query is executed, it will run the resolve function and then use the model type to return a value to the executor which is then returned by the API.

GraphQL Query Resolvers

serverside/src/Graphql/Helpers/QueryHelpers.cs

The specific resolver we are going to look at 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 of our different entities. The first 3 lines are 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.

Model Types and Relations

serverside/src/Models/SpeciesEntity/SpeciesEntityType.cs

A GraphQL type represents the values used as arguments and return types for the GraphQL functions. Taking a look at the model type for the species entity we can see there are 2 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);

    // GraphQL reference to entity LessonSubmissionEntity via reference LessonSubmissions
    IEnumerable<LessonSubmissionEntity> LessonSubmissionssResolveFunction(ResolveFieldContext<UserEntity> context)
    {
        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 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 will allow the related entities to queried in one request. A navigation field is translated into a .Include call in Entity Framework.

Notes and Further Information

Even though there is a large amount of bot written code, most of it is a configuration for the 2 different GraphQL libraries that we use:

  • GraphQL.Net - This library is used to provide the GraphQL executor that processes GraphQL queries as well as the schema that defines the entire API.
  • GraphQL.EntityFramework - This library builds upon the previous one to enable native Entity Framework queries based on the defined model types.

For a more in depth look to the GraphQL.NET library we recommend looking at the Lynda course API Development in .NET with GraphQL, that steps through creating an ASP.NET GraphQL application from scratch.


Start modelling your app today.