DEV Community

loading...
Cover image for .Net 5 API with GraphQL - Step by Step

.Net 5 API with GraphQL - Step by Step

moe23 profile image Mohamad Lawand ・12 min read

In this article we will be learning about GraphQL with Asp.Net Core 5, we will be building a Todo List functionality

You can watch the full video on Youtube

And you can get the source from GitHub using this link:
https://github.com/mohamadlawand087/v28-Net5-GraphQL

So what we will cover today:

  • The Problem we want to solve
  • What is GraphQL
  • REST vs GraphQL
  • Core Concepts
  • Ingredients
  • Coding

As always you will find the source code in the description down below. Please like, share and subscribe if you like the video. It will really help the channel

The Problem and the Solution

in order to understand graphQL let us understand the problem that it solves.

It was originally built by facebook to solve their data fetching needs. back in 2012 when facebook released their application it had a lot of criticism about the performance, the lag, the battery drainage as it was doing a lot of API calls to fetch the users data

Alt Text

To solve this facebook introduced GraphQL which turns all of these request into a single request. With that single API endpoint we can retrieve any data from the server using the GraphQL query language. Where we tell the API exactly the information we want and the API will return it, this will solve for a bunch of the problems.

Alt Text

So let us analyse the query we have, we are actually nesting objects inside the same request we are requesting more information through the nested obj. We get back the result in json, so in Rest API it could have been multiple requests to get this information while in GraphQL it was a single call.

We are taking what was a different requests and instead making the client the responsible for figuring out the logic of which the data needs to be processed to get the information we are delegating all of these requests to the server and requesting the server to handle the information binding based on the GraphQL query that we sent.

What is GraphQL

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.

Get many resources in a single request: GraphQL queries access not just the properties of one resource but also smoothly follow references between them. While typical REST APIs require loading from multiple URLs, GraphQL APIs get all the data your app needs in a single request.

GraphQL is about data trees, which allow us to get into relations. push back the data aggregation to the server side

  • One Endpoint
  • One Request
  • Type System

GraphQL vs REST

REST

  • Multiple endpoints for different data type
  • Chained requests to get the data we need
  • Over-fetch: We get more information from what we need.
  • Under-fetch: we get less data so we need to make a lot of requests to get the info

GraphQL

  • One Endpoint
  • One Request with different mapping
  • No Over-fetch
  • No Under-fetch
  • Type System
  • Predictable

When to use it

REST

  • Non-Interactive (System to System)
  • Microservices
  • Simple Object Hierarchy
  • Repeated Simple queries
  • Easier to develop
  • more complex to consume by clients

GraphQL

  • real time applications
  • mobile applications
  • complex object hierarchy
  • complex query
  • Complicated to develop
  • Easier to consume by clients

Core Concepts

Schema: Describe the api in full, query, objects, datatypes and description. Some of its properties are

  • Self-documenting
  • Formed of Types
  • Must have a Root Query Type

Types: It can be anything some of the types are

  • Query
  • Mutation
  • Subscription
  • Objects
  • Enumeration
  • Scalar

Resolvers: returns data for a given field

Data Source:

  • Data Source
  • Microservice
  • Rest API

Mutation: will allow us to edit and add data

Subscription: a web socket base connection which will allow us to send real time messages once an action is executed.

Ingredients

VS Code (https://code.visualstudio.com/download)
.Net 5 (https://dotnet.microsoft.com/download)
Insomnia (https://insomnia.rest/download)
Dbeaver (https://dbeaver.io/download/)

HotChocolate

is an implementation of GraphQL and a framework for writing GraphQL servers in .Net Core.

We need to check the version of dotnet

dotnet --version
Enter fullscreen mode Exit fullscreen mode

Now we need to install the entity framework tool

dotnet tool install --global dotnet-ef
Enter fullscreen mode Exit fullscreen mode

Once thats finish let us create our application

dotnet new web -n TodoListGQL
Enter fullscreen mode Exit fullscreen mode

Now we need to install the packages that we need

dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data.EntityFramework
dotnet add package GraphQL.Server.Ui.Voyage
Enter fullscreen mode Exit fullscreen mode

Now lets check our application and check the source code, lets build the application and see if its running

dotnet build
dotnet run
Enter fullscreen mode Exit fullscreen mode

Now let us start developing, the first thing we need to do is create our models and build our DbContext. Inside the root directory of our application let us create a new folder called Models and inside the Models folder let us create a 2 new classes called ItemData.cs and ItemList.cs

public class ItemData
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public bool Done { get; set; }
    public int ListId { get; set; }
    public virtual ItemList ItemList { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
public class ItemList
{
    public ItemList()
    {
        ItemDatas = new HashSet<ItemData>();
    }

    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<ItemData> ItemDatas { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to create our application db context so inside the root directory we create a new folder called Data and inside the Data folder will create a new class called ApiDbContext

public class ApiDbContext : DbContext
{
    public virtual DbSet<ItemData> Items {get;set;}
    public virtual DbSet<ItemList> Lists {get;set;}

    public ApiDbContext(DbContextOptions<ApiDbContext> options)
        : base(options)
    { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ItemData>(entity =>
        {
            entity.HasOne(d => d.ItemList)
                .WithMany(p => p.ItemDatas)
                .HasForeignKey(d => d.ListId)
                .OnDelete(DeleteBehavior.Restrict)
                .HasConstraintName("FK_ItemData_ItemList");
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to update our appsettings.json as well the startup class.

First let us open the appsettings and add the following code

"ConnectionStrings": {
    "DefaultConnection" : "DataSource=app.db; Cache=Shared"
  },
Enter fullscreen mode Exit fullscreen mode

Now let us update the startup class to connect our application to the database

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApiDbContext>(options =>
        options.UseSqlite(
            Configuration.GetConnectionString("DefaultConnection")
        ));
}
Enter fullscreen mode Exit fullscreen mode

Now we want to build our database, to do that we need utilise the ef core migrations

dotnet ef migrations add "Initial Migrations"
dotnet ef database update
Enter fullscreen mode Exit fullscreen mode

After the database update has completed successfully we can see we have a new folder called migrations which will contain the C# script which will be responsible on creating the database and its table Item.

we can verify that the database has been created since we can see the app.db file in our root directory as well we can see that use the SQLite browser (dbeaver) to verify that the table has been created successfully.

Now we need to start integrating GraphQL to our application, the first thing we are going to do is to add a new folder into the root of our applications which is called GraphQL.

Now inside the GraphQL folder we are going to create a new class called Query.cs

Our Query class will contain some methods which will return an IQueryable Result and this query class will be the endpoint we are going to utilise to get information back from the api

public class Query
{
    // Will return all of our todo list items
    // We are injecting the context of our dbConext to access the db
    public IQueryable<ItemData> GetItem([Service] ApiDbContext context)
    {
        return context.Items;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to to update our startup class inside our ConfigureServices Method to utilise GraphQL and create an entry point for GraphQL and provide us with a schema construction

services.AddGraphQLServer()
        .AddQueryType<Query>();
Enter fullscreen mode Exit fullscreen mode

Now we need to update our endpoints

app.UseEndpoints(endpoints =>
{
    endpoints.MapGraphQL();
});

app.UseGraphQLVoyager(new VoyagerOptions()
{
    GraphQLEndPoint = "/graphql"
}, "/graphql-voyager");
Enter fullscreen mode Exit fullscreen mode

Let us build our application and run it

dotnet build
dotnet run

// http://localhost:5000/graphql
Enter fullscreen mode Exit fullscreen mode

When we navigate to http://localhost:5000/graphql we can see that we are utilising a UI which is provided for us by the HotChocolate nuget package that we installed. if we click on the schema button (book icon) we can see the main query that we have added called items based on the one we added in the query class.

Let us open our database with dbeaver and add some information manually there then lets go back to our url http://localhost:5000/graphql Now let us test our application

query{
  items
  {
    id
    title
  }
}
Enter fullscreen mode Exit fullscreen mode

so now let us update our query and introduce aliases which mean that we want to execute different commands in the same query similar to below

query {
  a:items{
    id
    title
  }
  b:items{
    id
    title
  }
   c:items{
    id
    title
  }
}
Enter fullscreen mode Exit fullscreen mode

In this request we are only getting 1 of the requests back while the others are generating errors why is that happening.

The main reason behind this is that our application db context does not work in parallel which means when GraphQL try to execute the commands simultaneously it fails as the db context can only work single threaded.

To resolve this issue we need to use a new feature introduced in .Net 5 which is PooledDbContextFactory which we can use to resolve this error.

The first place we need to change is our startup class inside our ConfigureServices Method

services.AddPooledDbContextFactory<ApiDbContext>(options =>
                options.UseSqlite(
                    Configuration.GetConnectionString("DefaultConnection")
                ));
Enter fullscreen mode Exit fullscreen mode

The AddPooledDbContextFactory is basically creating instance of the ApiDbContext and putting them in a pool, when ever the db context is needed we can take an instance from the pool then return it once we finish using it.

The next step we need to update the Query.cs

// So basically this attribute is pulling a db context from a pool
// using the db context 
// returning the db context to the pool
[UseDbContext(typeof(ApiDbContext))]
public IQueryable<ItemData> GetItems([ScopedService] ApiDbContext context)
{
    return context.Items;
}
Enter fullscreen mode Exit fullscreen mode

Now let us run the application again and run the parallel queries again.

dotnet run
Enter fullscreen mode Exit fullscreen mode

Now we can see everything is running as it should be, next we are going to try to pul the information from the list so we are going to get the parent list and all the items that belong to the list

let us add a new query in the query class

[UseDbContext(typeof(ApiDbContext))]
public IQueryable<ItemList> GetLists([ScopedService] ApiDbContext context)
{
    return context.Lists;
}
Enter fullscreen mode Exit fullscreen mode

Now let us try querying the data and check what do we get so inside insomnia we create a new request

query{
  lists
  {
    name
    itemDatas {
      id
      title
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And we can see that the data which is being returned is not complete

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9518142d-cfdf-4863-bcbc-f314f3fa9c3b/Screenshot_2021-05-03_at_20.23.40.png

So how do we solve this we need to enable projections inside our query to enable us to get obj children. Let us update the Query.cs class to the following

[UseDbContext(typeof(ApiDbContext))]
[UseProjection]
public IQueryable<ItemList> GetLists([ScopedService] ApiDbContext context)
{
    return context.Lists;
}
Enter fullscreen mode Exit fullscreen mode

Next we need to update the startup class inside the ConfigureServices method

// This will be the entry point and will provide us with a schema 
// construction
services.AddGraphQLServer()
        .AddQueryType<Query>()
        .AddProjections();
Enter fullscreen mode Exit fullscreen mode

Documentation

Now let us document our API, to accomplish this we need to update our models class first let us update the ItemData model

[GraphQLDescription("Used to define todo item for a specific list")]
public class ItemData
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }

    [GraphQLDescription("If the user has completed this item")]
    public bool Done { get; set; }

    [GraphQLDescription("The list which this item belongs to")]
    public int ListId { get; set; }

    public virtual ItemList ItemList { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now let us update our ItemList

[GraphQLDescription("Used to group the do list item into groups")]
public class ItemList
{
    public ItemList()
    {
        ItemDatas = new HashSet<ItemData>();
    }

    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<ItemData> ItemDatas { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now where can we see these documentation, since we have already added the GraphQL voyager nuget and we configured it in our startup class we need to visit this url: http://localhost:5000/graphql-voyager

And we can see a graphical representation of our API and we can see the documentation that we added.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ab28c866-07af-468e-8f2f-01f7e0e3a314/Screenshot_2021-05-03_at_20.42.37.png

Now we need to do some breakup between our Models and the documentation and in order for us to achieve this we will use types

Inside the GraphQL folder we need to create a new folders called Items and Lists. Once we create these folders let us create our first Type ListType.cs inside the Lists folder.

public class ListType : ObjectType<ItemList>
{
     // since we are inheriting from objtype we need to override the functionality
    protected override void Configure(IObjectTypeDescriptor<ItemList> descriptor)
    {
        descriptor.Description("Used to group the do list item into groups");

        descriptor.Field(x => x.ItemDatas).Ignore();

        descriptor.Field(x => x.ItemDatas)
                    .ResolveWith<Resolvers>(p => p.GetItems(default!, default!))
                    .UseDbContext<ApiDbContext>()
                    .Description("This is the list of to do item available for this list");
    }

     private class Resolvers
    {
        public IQueryable<ItemData> GetItems(ItemList list, [ScopedService] ApiDbContext context)
        {
            return context.Items.Where(x => x.ListId == list.Id);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to create the ItemType inside GraphQL ⇒ Items folder

// since we are inheriting from objtype we need to override the functionality
protected override void Configure(IObjectTypeDescriptor<ItemData> descriptor)
{
    descriptor.Description("Used to define todo item for a specific list");

      descriptor.Field(x => x.ItemList)
                .ResolveWith<Resolvers>(p => p.GetList(default!, default!))
                .UseDbContext<ApiDbContext>()
                .Description("This is the list that the item belongs to");
}

private class Resolvers
{
    public ItemList GetList(ItemData item, [ScopedService] ApiDbContext context)
    {
        return context.Lists.FirstOrDefault(x => x.Id == item.ListId);
    }
}
Enter fullscreen mode Exit fullscreen mode

Once the types has been added we need to update our startup class to take advantage of the types so inside our Startup class in the ConfigureServices method we need to update to the following

services.AddGraphQLServer()
                        .AddQueryType<Query>()
                        .AddType<ItemType>()
                        .AddType<ListType>()
                        .AddProjections();
Enter fullscreen mode Exit fullscreen mode

Now let us build and run our application and check the schema

dotnet build
dotnet run
Enter fullscreen mode Exit fullscreen mode

Now we need to add filtering and sorting we need to update the Query class to the below

public class Query
{
    // Will return all of our todo list items
    // We are injecting the context of our dbConext to access the db
    // this is called a resolver

    // So basically this attribute is pulling a db context from a pool
    // using the db context 
    // returning the db context to the pool
    [UseDbContext(typeof(ApiDbContext))]
    [UseProjection] //=> we have remove it since we have used explicit resolvers
    [UseFiltering]
    [UseSorting]
    public IQueryable<ItemData> GetItems([ScopedService] ApiDbContext context)
    {
        return context.Items;
    }

    [UseDbContext(typeof(ApiDbContext))]
    [UseProjection] //=> we have remove it since we have used explicit resolvers
    [UseFiltering]
    [UseSorting]
    public IQueryable<ItemList> GetLists([ScopedService] ApiDbContext context)
    {
        return context.Lists;
    }
}
Enter fullscreen mode Exit fullscreen mode

Then we need to update the startup class ConfigureServices method

services.AddGraphQLServer()
        .AddQueryType<Query>()
        .AddType<ListType>()
        .AddType<ItemType>()
        .AddProjections()
        .AddSorting()
        .AddFiltering();
Enter fullscreen mode Exit fullscreen mode

Now let us create a new query with filtering to see what how it works filtering

query {
  lists(where: {id: {eq: 1} })
  {
    id
    name
    itemDatas {
      title
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

sorting query

query{
  lists(order: {name: DESC})
  {
    id
    name
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we want to cover mutation, and Mutation is when we want to add, edit and delete data.

To implement mutation we need to add a new class inside our GraphQL folder and this mutation class will contain 2 methods 1 for adding Lists and 1 for adding list item

Will start by adding the input and output model so inside the GraphQL ⇒ List we add 2 files AddListInput and AddListPayload

public record AddListPayload(ListType list);
Enter fullscreen mode Exit fullscreen mode
public record AddListInput(string name);
Enter fullscreen mode Exit fullscreen mode

And now we need to add our mutation class inside the GraphQL folder we add a new a class called Mutation

// this attribute will help us utilise the multi threaded api db context
[UseDbContext(typeof(ApiDbContext))]
public async Task<AddListPayload> AddListAsync(AddListInput input, [ScopedService] ApiDbContext context)
{
    var list = new ItemList
    {
        Name = input.name
    };

    context.Lists.Add(list);
    await context.SaveChangesAsync();

    return new AddListPayload(list);
}
Enter fullscreen mode Exit fullscreen mode

Now we need to update our startup class

services.AddGraphQLServer()
        .AddQueryType<Query>()
        .AddType<ListType>()
        .AddType<ItemType>()
        .AddMutationType<Mutation>()
        .AddProjections()
        .AddSorting()
        .AddFiltering();
Enter fullscreen mode Exit fullscreen mode

Now let us test it, we create a new request in insomnia and utilise mutation instead of query

mutation{
  addList(input: {
    name: "Food"
  })
  {
    list
    {
      name
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we are going to add a second mutation to add list item so we start by adding the models inside the GraphQL ⇒ Items will add AddItemInput and AddItemPayload

public record AddItemInput(string title, string description, bool done, int listId);
Enter fullscreen mode Exit fullscreen mode
public record AddItemPayload(ItemData item);
Enter fullscreen mode Exit fullscreen mode

and we need now to update the mutation class

[UseDbContext(typeof(ApiDbContext))]
public async Task<AddItemPayload> AddItemAsync(AddItemInput input, [ScopedService] ApiDbContext context)
{
    var item = new ItemData
    {
        Description = input.description,
        Done = input.done,
        Title = input.title,
        ListId = input.listId
    };

    context.Items.Add(item);
    await context.SaveChangesAsync();

    return new AddItemPayload(item);
}
Enter fullscreen mode Exit fullscreen mode

Now let us test it

mutation{
  addItem(input: {
    title: "Bring laptop",
    description: "Bring the laptop with charger",
    done: true,
    listId: 1
  })
  {
    item
    {
      id
      title
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we are going to go through subscriptions

A subscription is a real time event notification, we utilise a websocket to achieve this.

Inside the GraphQL folder will create a new class called Subscription.cs

[Subscribe]
[Topic]
public ItemList OnListAdded([EventMessage] ItemList list) => list;
Enter fullscreen mode Exit fullscreen mode

And we need to update the startup class to take advantage of websockets we update the Configure method with the following

app.UseWebSockets();
Enter fullscreen mode Exit fullscreen mode

The second part we need to update in startup class is the ConfigureServices method

services.AddGraphQLServer()
        .AddQueryType<Query>()
        .AddType<ListType>()
        .AddType<ItemType>()
        .AddMutationType<Mutation>()
        .AddSubscriptionType<Subscription>()
        .AddProjections()
        .AddSorting()
        .AddFiltering()
        .AddInMemorySubscriptions();
Enter fullscreen mode Exit fullscreen mode

Now we need to update our mutations so once we add a new list item we are sending an update to the subscriptions

// this attribute will help us utilise the multi threaded api db context
[UseDbContext(typeof(ApiDbContext))]
public async Task<AddListPayload> AddListAsync(
    AddListInput input, 
    [ScopedService] ApiDbContext context,
    [Service] ITopicEventSender eventSender,
    CancellationToken cancellationToken)
{
    var list = new ItemList
    {
        Name = input.name
    };

    context.Lists.Add(list);
    await context.SaveChangesAsync(cancellationToken);

    // we emit our subscription
    await eventSender.SendAsync(nameof(Subscription.OnListAdded), list, cancellationToken);

    return new AddListPayload(list);
}
Enter fullscreen mode Exit fullscreen mode

Discussion (0)

Forem Open with the Forem app