Developer Docs

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:

Learn more:

On this page