Getting started with GraphQL in Asp.Net Core

GraphQL is a powerful query language and runtime that efficiently fetches and manipulates data from APIs. This blog post will explore how to get started with GraphQL in Asp.Net Core.

What is GraphQL?

GraphQL is a query language and runtime for APIs that Facebook developed. It was designed to overcome some of the limitations of REST (Representational State Transfer) APIs by allowing clients to request only the data they need in the exact format they need and reducing the number of round-trips to the server.

In GraphQL, clients can specify the exact data they need, including nested relationships and fields, and the server will return exactly that data in a single response. This avoids over-fetching (getting more data than needed) and under-fetching (not getting enough data), which are common issues with REST APIs.

GraphQL also provides a type system for APIs, allowing developers to define the types of data that can be queried and returned and the relationships between them. This allows for more efficient and reliable API development and consumption.

GraphQL provides a more flexible and efficient way to build and consume APIs than traditional REST APIs, especially in complex or dynamic applications with changing data requirements.

Core concepts of GraphQL

Here are the core concepts of GraphQL:

  1. Schema: The schema defines the types of data that can be queried, as well as the queries, mutations, and subscriptions that are available. The schema is written in the GraphQL Schema Definition Language (SDL) and describes the shape and relationships of the data.

  2. Types: GraphQL defines its own type system, which includes scalars (like String, Int, Float, and Boolean) and complex types (like Object, Interface, Union, and Enum). Types are used to define the shape and relationships of the data in the schema.

  3. Fields: Fields are the basic unit of data in GraphQL. Each field has a name and a type and represents a single piece of data that can be queried. Fields can also have arguments which filter or modify the data returned by the field.

  4. Queries: Queries are used to fetch data from the server. A query specifies the fields to be retrieved and their associated arguments, and the server responds with the requested data.

  5. Mutations: Mutations are used to modify data on the server. Like queries, mutations specify the fields to be modified and their associated arguments, but the server responds with the updated data.

  6. Subscriptions: Subscriptions are used to receive real-time updates from the server. A subscription specifies the fields to be subscribed to and their associated arguments, and the server pushes updates to the client as they occur.

  7. Directives: Directives are used to modify the behavior of fields or operations. They can conditionally include or exclude fields based on variables, paginate results, or apply custom logic to fields.

  8. Fragments: Fragments are used to reuse common selections of fields in queries. They allow us to define a set of fields once and then reference them from multiple places in our query.

  9. Variables: Variables are used to parameterize queries and mutations. They allow us to pass values into the query at runtime, making queries more dynamic and reusable.

These are the core concepts of GraphQL, and understanding them is the key to using GraphQL effectively. We can create powerful, flexible APIs that provide a better user experience by leveraging these concepts.

Operations in GraphQL

GraphQL supports three main types of operations: queries, mutations, and subscriptions.

  1. Queries: Queries are used to retrieve data from the server. A query specifies the fields to be retrieved and their associated arguments, and the server responds with the requested data. Queries are executed using the query keyword.
query {
  getUser(id: "123") {
    name
    email
    age
  }
}
  1. Mutations: Mutations are used to modify data on the server. Like queries, mutations specify the fields to be modified and their associated arguments, but the server responds with the updated data. Mutations are executed using the mutation keyword.
mutation {
  updateUser(id: "123", name: "John Doe", email: "john@example.com", age: 30) {
    name
    email
    age
  }
}
  1. Subscriptions: Subscriptions are used to receive real-time updates from the server. A subscription specifies the fields to be subscribed to and their associated arguments, and the server pushes updates to the client as they occur. Subscriptions are executed using the subscription keyword.
subscription {
  newMessages(channelId: "123") {
    id
    content
    createdAt
    user {
      name
      avatarUrl
    }
  }
}

In addition to these core operations, GraphQL supports directives, which modify the behavior of fields or operations, and fragments, which reuse a common selection of fields in queries.

GraphQL vs REST

GraphQL and REST are popular approaches to building APIs but have some key differences. Here are some of the main differences between GraphQL and REST:

  1. Data Fetching: In REST, multiple endpoints often fetch different resource parts. This can result in over-fetching or under-fetching of data, as the client has limited control over the returned data. With GraphQL, clients can fetch exactly the data they need, and the server responds with only that data, eliminating over-fetching and under-fetching.

  2. Response Structure: In REST, the server defines the structure of the response, and the client has limited control over the returned data. With GraphQL, the client defines the structure of the response by specifying the fields they want to fetch, and the server responds with only those fields.

  3. Versioning: In REST, versioning is typically done by creating new endpoints with different URLs or adding version numbers to the URL. This can result in a proliferation of endpoints and increased complexity. With GraphQL, versioning is handled at the schema level, and clients can request specific schema versions.

  4. Schema Validation: In REST, there is no standard for schema validation, and clients can receive unexpected responses if the server changes its response structure. With GraphQL, the schema is defined in advance, and clients can validate their queries against the schema to ensure they will receive the expected response.

  5. Tooling: GraphQL has a rich ecosystem of tools and libraries for query validation, auto-completion, and debugging. REST has a more limited tooling ecosystem, and many tools are specific to particular frameworks or languages.

GraphQL provides more flexibility and control over data fetching and response structure, while REST has a more straightforward and well-established approach to building APIs. The choice between GraphQL and REST depends on the application's specific needs, the resources available, and the development team's preferences.

GraphQL Frameworks in .NET

GraphQL is becoming increasingly popular in .NET development, and several frameworks are available that make it easier to build GraphQL APIs in .NET. Here are some of the most popular GraphQL frameworks for .NET:

  1. Hot Chocolate: Hot Chocolate is a high-performance GraphQL server for .NET that supports the latest GraphQL features and integrates with ASP.NET Core. It includes features like schema stitching, data loaders, and real-time subscriptions and provides a clean and intuitive API for building GraphQL APIs.

  2. GraphQL.NET: GraphQL.NET is a popular GraphQL framework for .NET that implements the GraphQL specification completely. It includes features like schema validation, query parsing, and execution and provides a powerful query execution engine that supports complex queries and data fetching strategies.

  3. Strawberry Shake: Strawberry Shake is a GraphQL client library for .NET that provides a strongly-typed API for querying GraphQL APIs. It includes features like query validation, auto-completion, and caching and integrates with popular .NET tools like Visual Studio and .NET Core.

  4. GraphQL for .NET: GraphQL for .NET is a lightweight GraphQL framework for .NET that provides a simple and intuitive API for building GraphQL APIs. It includes features like schema validation, query parsing, and execution and provides a flexible data fetching mechanism that allows using any data source.

  5. GraphQL.Conventions: GraphQL.Conventions are a convention-based GraphQL framework for .NET that provides a simple and intuitive API for building GraphQL APIs. It includes features like schema validation, query parsing, and execution and automatically uses conventions to generate resolvers and data types from the .NET classes.

Implementation

Nuget packages

We will use the following nuget packages:

  1. HotChocolate.AspNetCore: this is a library that provides integration between the Hot Chocolate GraphQL server and ASP.NET Core.

  2. GraphQL.Server.Ui.Voyager: this tool allows us to explore and interact with a GraphQL API visually. Voyager is a popular tool used to visualize GraphQL APIs and their schemas. It provides a user-friendly interface to explore the API, understand its structure and relationships, and test queries and mutations.

  3. HotChocolate.Data.EntityFramework: this extension to the Hot Chocolate GraphQL server provides seamless integration with the Entity Framework for data access. With this package, we can define a GraphQL schema that maps to Entity Framework models and expose them as queryable types in the GraphQL API. We can also leverage the package's powerful filtering, sorting, and pagination capabilities to make complex queries on our data.

After installing the packages, it is necessary to register some services and configure the endpoint.

builder.Services
    .AddGraphQLServer();
app.MapGraphQL();

app.UseGraphQLVoyager(options: new VoyagerOptions()
{
    GraphQLEndPoint = "/graphql",
});

Queries

To define the queries, adding a class that exposes the models is necessary. The implementation of the method has a service that calls an Entity Framework repository to get the data.

public class Query
{
    public async Task<IEnumerable<MuscleDto>> GetMuscles(MuscleService service)
    {
        return await service.GetAll();
    }

    public async Task<IEnumerable<ExerciseDto>> GetExercises(ExerciseService service)
    {
        return await service.GetAll();
    }
}

The services and the query need to be registered into the services so the app can recognize them.

builder.Services
    .AddGraphQLServer()
    .RegisterService<MuscleService>()
    .RegisterService<ExerciseService>()
    .AddQueryType<Query>();

Voyager View

Accessing the endpoint /ui/voyager will show the GraphQL APIs and their schemas. We can see in the following image that it shows the two queries and the models that are being used. We can also define descriptions for each type and property, which will be displayed on this page.

Executing Queries

To execute a query, it is necessary to use the reserved word query, the model that the data will be fetched and the properties.

To execute the queries, we can navigate to the endpoint /graphql/ where an environment will be loaded, and we will be able to run the queries and see the schema definition. The following image shows how the page looks like.

Or we can use an application like Insomnia to execute the queries.

The following codes show the query to get the properties from the Muscle model and the query result.

query {
    muscles{
        id,
        name,
        exercises {
            id,
            name
        }
    }
}
{
    "data": {
        "muscles": [
            {
                "id": 1,
                "name": "Back",
                "exercises": null
            },
            {
                "id": 2,
                "name": "Leg",
                "exercises": null
            },
            {
                "id": 3,
                "name": "Biceps",
                "exercises": null
            },
            {
                "id": 4,
                "name": "Triceps",
                "exercises": null
            }
        ]
    }
}

GraphQL allows parallel queries like the following query. To allow the app to execute this type of query, it is necessary to change how DBContext is registered and how to resolve it inside the repositories.

query {
    a: muscles {
        id
        name
    }

        b: muscles {
        id
        name
    }
}

When injecting a DbContext using the DbContextKind.Pool, Hot Chocolate will retrieve one DbContext instance from the pool for each invocation of a resolver. Once the resolver has finished executing, the instance will be returned to the pool.

builder.Services.AddPooledDbContextFactory<CoachPlanContext>(options =>
       options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

When creating services/repositories, they must inject the IDbContextFactory<T> instead of the DbContext directly. The services/repositories also need to be of a transient lifetime. Otherwise, we could be faced with the DbContext concurrency issue if the same DbContext instance is accessed by two resolvers through our service in parallel.

builder.Services.AddTransient<IMuscleRepository, MuscleRepository>();
builder.Services.AddTransient<MuscleService>();

public class MuscleRepository: IMuscleRepository, IAsyncDisposable
{
    private readonly CoachPlanContext _coachPlanContext;

    public MuscleRepository(IDbContextFactory<CoachPlanContext> dbContextFactory)
    {
        _coachPlanContext = dbContextFactory.CreateDbContext();
    }
    public  IEnumerable<Muscle> GetAll()
    {
        return _coachPlanContext.Muscles;
    }

    public ValueTask DisposeAsync()
    {
        return _coachPlanContext.DisposeAsync();
    }
}

After the changes, the parallel query can be executed without errors, and the following code shows the result of it.

{
    "data": {
        "a": [
            {
                "id": 1,
                "name": "Back"
            },
            {
                "id": 2,
                "name": "Leg"
            },
            {
                "id": 3,
                "name": "Biceps"
            },
            {
                "id": 4,
                "name": "Triceps"
            }
        ],
        "b": [
            {
                "id": 1,
                "name": "Back"
            },
            {
                "id": 2,
                "name": "Leg"
            },
            {
                "id": 3,
                "name": "Biceps"
            },
            {
                "id": 4,
                "name": "Triceps"
            }
        ]
    }
}

Object Type

GraphQL Objects represent a list of named fields, and each yield a specific type value. Object values should be serialized as ordered maps, where the selected field names (or aliases) are the keys, and the result of evaluating the field is the value, ordered by the order in which they appear in the selection set. All fields defined within an Object type must not have a name which begins with "__" (two underscores), as this is used exclusively by GraphQL’s introspection system.

Creating an ObjectType, we can add a resolver for a specific property of the model. For example, we can resolve the values for the Exercises property for the Muscle model.

The following codes show how to create the ObjectTye and register it. For the Exercises property, a method will be called to retrieve all the exercises by MuscleId.

public class MuscleType: ObjectType<MuscleDto>
{
   protected override void Configure(IObjectTypeDescriptor<MuscleDto> descriptor)
    {
        descriptor
            .Field(c=> c.Exercises)
            .ResolveWith<Resolvers>(p=> p.GetExercises(default!, default!));
    }
}

public class Resolvers
{
    public async Task<IEnumerable<ExerciseDto>> GetExercises([Parent] MuscleDto muscle, ExerciseService service)
    {
        return await service.GetByMuscleId(muscle.Id);
    }
}
builder.Services
    .AddGraphQLServer()
    .RegisterService<MuscleService>()
    .RegisterService<ExerciseService>()
    .AddType<MuscleType>()
    .AddQueryType<Query>();

Now executing the query to bring all muscles, we can also have the exercises related to each.

{
    "data": {
        "muscles": [
            {
                "id": 1,
                "name": "Back",
                "exercises": [
                    {
                        "id": 1,
                        "name": "Deadlifts"
                    },
                    {
                        "id": 10,
                        "name": "Dumbbell Row"
                    }
                ]
            },
            {
                "id": 2,
                "name": "Leg",
                "exercises": [
                    {
                        "id": 2,
                        "name": "Air Squat"
                    },
                    {
                        "id": 3,
                        "name": "Chair Squat"
                    },
                    {
                        "id": 4,
                        "name": "Leg Press"
                    }
                ]
            },
            {
                "id": 3,
                "name": "Biceps",
                "exercises": [
                    {
                        "id": 5,
                        "name": "Barbell Curl"
                    },
                    {
                        "id": 6,
                        "name": "Cable Curl With Bar"
                    },
                    {
                        "id": 7,
                        "name": "Spider Curl"
                    }
                ]
            },
            {
                "id": 4,
                "name": "Triceps",
                "exercises": [
                    {
                        "id": 8,
                        "name": "Bench Dip"
                    },
                    {
                        "id": 9,
                        "name": "Close-Grip Push-Up"
                    }
                ]
            }
        ]
    }
}

We also can use the ObjectType to add a description for the model and its properties. The descriptions will be displayed on the Voyager page.

public class MuscleType: ObjectType<MuscleDto>
{
   protected override void Configure(IObjectTypeDescriptor<MuscleDto> descriptor)
    {
        descriptor.Description("Represents the muscles of the body.");

        descriptor
            .Field(c=> c.Id)
            .Description("Represents the Id of the entry in the table");

        descriptor
            .Field(c=> c.Name)
            .Description("Represents the name of the muscle");    


        descriptor
            .Field(c=> c.Exercises)
            .Description("Represents the exercises for the muscle")
            .ResolveWith<Resolvers>(p=> p.GetExercises(default!, default!));
    }
}

Filtering

With Hot Chocolate filters, we can expose complex filter objects through the GraphQL API that translates to native database queries. The default filter implementation translates filters to expression trees that are applied to IQueryable. Hot Chocolate, by default, will inspect the model and infer its possible filter operations. Filters use IQueryable (IEnumerable) by default, but we can also easily customize them to use other interfaces.

First, it is necessary to register the filtering option.

builder.Services
    .AddGraphQLServer()
    .RegisterService<MuscleService>()
    .RegisterService<ExerciseService>()
    .AddType<MuscleType>()
    .AddQueryType<Query>()
    .AddFiltering();

Hot Chocolate will infer the filters directly from the model and then use a Middleware to apply filters to IQueryable<T> or IEnumerable<T> on execution.

public class Query
{
    [UseFiltering]
    public IEnumerable<MuscleDto> GetMuscles(MuscleService service)
    {
        return service.GetAll();
    }

    [UseFiltering]
    public IEnumerable<ExerciseDto> GetExercises(ExerciseService service)
    {
        return service.GetAll();
    }
}

The following codes show the query and the results using filters to fetch the data.

query {  
    a: muscles(
        where: { id: {eq: 1} }) 
      {
            id,
            name,
            exercises {
                id,
                name
            }
        }

    b: muscles(
        where: {
            or: [{name: {startsWith: "L"}}, {id: {gte: 3}}]
        })
      {
            id,
            name,
            exercises {
                id,
                name
            }
      }
}
{
    "data": {
        "a": [
            {
                "id": 1,
                "name": "Back",
                "exercises": [
                    {
                        "id": 1,
                        "name": "Deadlifts"
                    },
                    {
                        "id": 10,
                        "name": "Dumbbell Row"
                    }
                ]
            }
        ],
        "b": [
            {
                "id": 2,
                "name": "Leg",
                "exercises": [
                    {
                        "id": 2,
                        "name": "Air Squat"
                    },
                    {
                        "id": 3,
                        "name": "Chair Squat"
                    },
                    {
                        "id": 4,
                        "name": "Leg Press"
                    }
                ]
            },
            {
                "id": 3,
                "name": "Biceps",
                "exercises": [
                    {
                        "id": 5,
                        "name": "Barbell Curl"
                    },
                    {
                        "id": 6,
                        "name": "Cable Curl With Bar"
                    },
                    {
                        "id": 7,
                        "name": "Spider Curl"
                    }
                ]
            },
            {
                "id": 4,
                "name": "Triceps",
                "exercises": [
                    {
                        "id": 8,
                        "name": "Bench Dip"
                    },
                    {
                        "id": 9,
                        "name": "Close-Grip Push-Up"
                    }
                ]
            }
        ]
    }
}

Sorting

Ordering results of a query dynamically is a common case. With Hot Chocolate sorting, we can expose a sorting argument that abstracts the complexity of ordering logic. With a little configuration, the GraphQL API has sorting capabilities, translating to native database queries.

It is also necessary to register it and then use the attribute in the method inside of the query.

builder.Services
    .AddGraphQLServer()
    .RegisterService<MuscleService>()
    .RegisterService<ExerciseService>()
    .AddType<MuscleType>()
    .AddQueryType<Query>()
    .AddFiltering()
    .AddSorting();
public class Query
{
    [UseFiltering]
    [UseSorting]
    public IEnumerable<MuscleDto> GetMuscles(MuscleService service)
    {
        return service.GetAll();
    }

    [UseFiltering]
    [UseSorting]
    public IEnumerable<ExerciseDto> GetExercises(ExerciseService service)
    {
        return service.GetAll();
    }
}

We use the tag order to define the list of fields and the ordering we want to order a query.

query {
    muscles(order: [{ name: ASC }]) {
        id,
        name
    }
}
{
    "data": {
        "muscles": [
            {
                "id": 1,
                "name": "Back"
            },
            {
                "id": 3,
                "name": "Biceps"
            },
            {
                "id": 2,
                "name": "Leg"
            },
            {
                "id": 4,
                "name": "Triceps"
            }
        ]
    }
}

Mutation

As we saw, mutations are used to modify data on the server. GraphQL defines mutations as top-level fields on the mutation type. Meaning only the fields on the mutation root type itself are mutations. Everything that is returned from a mutation field represents the changed state of the server.

In GraphQL, it is best practice to have a single argument on mutations called input, and each mutation should return a payload object. The payload object allows to read the changes of the mutation or to access the domain errors caused by a mutation.

The following code shows the mutation type for Muscle and the methods that we will allow to be executed. It also defined the Input and the Payload types for each one of the methods.

public partial class Mutation
{
    public async Task<MusclePayload> AddMuscle(AddMuscleInput input, MuscleService service)
    {
        var muscle = new MuscleDto
        {
            Name = input.Name
        };

        var savedMuscle = await service.Create(muscle);

        return new MusclePayload(savedMuscle);
    }

    public async Task<MusclePayload> UpdateMuscle(UpdateMuscleInput input, MuscleService service)
    {
        var muscle = new MuscleDto
        {
            Id = input.Id,
            Name = input.Name            
        };

        var updateMuscle = await service.Update(muscle);

        return new MusclePayload(updateMuscle);
    }

    public async Task<MusclesPayload> DeleteMuscle(DeleteMuscleInput input, MuscleService service)
    {
        var muscle = new MuscleDto
        {
            Id = input.Id          
        };

        await service.Delete(muscle);

        var muscles = service.GetAll();

        return new MusclesPayload(muscles);
    }
}
namespace CoachPlan.GraphQL.Features.Muscle;

public record AddMuscleInput(string Name); 

public record UpdateMuscleInput(int Id, string Name); 

public record DeleteMuscleInput(int Id); 

public record MusclePayload(MuscleDto muscle);

public record MusclesPayload(IEnumerable<MuscleDto> muscle);

Like the other functionalities, it is necessary to register the Mutation type.

builder.Services
    .AddGraphQLServer()
    .RegisterService<MuscleService>()
    .RegisterService<ExerciseService>()
    .AddType<MuscleType>()
    .AddType<ExerciseType>()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()
    .AddFiltering()
    .AddSorting();

The following images show the execution of the Mutation's methods.

Wrapping Up

There are more features we can use with GraphQL, and this blog post was just an introduction to learn the basics of it.

You can find the complete code on my GitHub.