Building a GraphQL API with ASP.NET Core 2 and Entity Framework Core

GraphQL is a relatively new technology developed initially at Facebook and open-sourced to the world in 2015. In 2017, it really took off and made the leap from a cool, niche technology to one of the primary ways companies like Walmart and IBM are starting to work with their APIs and data.

If you're new to GraphQL, here's a definition that sums it up nicely.

GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data. GraphQL isn't tied to any specific database or storage engine and is instead backed by your existing code and data.

Source: graphql.org

The last sentence is an important one to be mindful of when getting started with GraphQL. So, I'll repeat it by saying GraphQL is not any specific framework or database but rather a specification that describes the capabilities and requirements of data models in client‐server applications. There's a growing number of libraries out there to help you implement the GraphQL spec in your app for a number of different languages.

In this post, we'll learn about designing and building a GraphQL powered API using ASP.NET Core 2, Entity Framework Core and Joe Mcbrides's excellent graphql-dotnet library.

GraphQL dynamic query.
A GraphQL service built with ASP.NET Core 2, Entity Framework Core and graphql-dotnet.

Benefits of GraphQL

Before we dig into the code, it's worth pointing a few fundamental issues with REST that GraphQL solves and why we may want to choose it over a traditional REST approach in certain scenarios.

1. GraphQL Is Less Chatty than REST

One fact of life with REST is that it requires multiple roundtrips between the client and individual resource endpoints on the server to fetch all the data needed to render a view or page in our app.

GraphQL solves the roundtrip problem by allowing the client to create a single query which calls several related functions (or resolvers) on the server to construct a response with multiple resources - in a single request. This is a much more efficient method of data delivery requiring fewer resources than multiple roundtrips.

Get notified on new posts

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.

2. Endpoint Overload

As our application grows, the number of REST endpoints we must build and maintain starts to explode. A growing number of endpoints demands more and more time and effort to test and support - on both the client and server. And if you eventually need to version your API, you've got a lot more to consider.

GET /students/:id
GET /courses/:id
GET /instructors/:id
GET /students/:id/courses
POST /students/:id/courses
/* and on and on... */

In GraphQL, we can define Query types in our Schema to retrieve (or mutate) any combination of types in our system through one endpoint i.e /api/graphql.

In our schema, we might have Student and Course types.

type Student {
  id: ID
  firstName: String
  lastName: String
  birthDate: Date
  email: String
  courses: [Course]
}

type Course {
  id: ID
  title: String
  description: String
  startDate: Date
  endDate: Date  
}

Next, we can create a Query type in our schema to fetch any Student or Course.

type Query {
  student(id: ID!): Student
  course(id: ID!): Course
}

With types and a query defined in the schema, we can now issue requests via GraphQL similar to the RESTful ones listed above. However, we only need to send them to one endpoint. 😎

GET /api/graphql?query={ student(id: "10") { firstName, lastName, courses { title } } }
{
  "firstName": "Mark",
  "lastName": "Macneil",
  "email": "mark@fullstackmark.com",
  "courses": [
    {"title": "Advanced ASPNET Core" },
    {"title": "Introduction to GraphQL" }
  ]
}

3. Overfetching/Underfetching Data

Because REST APIs return rigid data structures it's very hard to design an API flexible enough to fulfill every client's precise data needs. This can lead to two conditions known as Overfetching and Underfetching.

Overfetching simply means the client ends up retrieving more information in some circumstances than is necessary. Consider the Student type above. In a master-detail scenario where we're displaying a list of students, we likely only need their id and name. However, if the endpoint for /students responds with more info than that ie. their birthDate, email or a list of their courses then this unnecessary data creates overhead on the server, network, and client - not ideal.

On the flipside, when an endpoint doesn't provide all of the required data the client is forced to make additional requests to fetch everything it needs. This is known as underfetching or the n+1 request problem. Let's pretend we have a requirement to display a list of students and their courses. We can obtain the main student list by hitting the /students endpoint but unfortunately, that response doesn't contain any course information. So now, for every student retrieved from the initial call we must hit /students/:id/courses to obtain their course data (n+1).

GraphQL at its simplest is about asking for specific fields on objects. This core principle solves both the over- and underfetching problem by giving the client complete control over the data it receives.

In our example, we can get the data in a shape we require on the client by submitting a query that looks something like this.

query StudentsQuery {
  students {
    firstName
    lastName
    courses {
      title
    }
  } 
}

The GraphQL service will return the exact structure we specified - no more no less.

{
  "data": {
    "students": [
     {
      "firstName": "Mark",
      "lastName": "Macneil",
      "courses": [
        {
          "title": "Advanced ASPNET Core"
        },
        {
          "title": "Introduction to GraphQL"
        }
      ]
     }
    ]
  }
}

Notice how the response looks very similar to the query? With GraphQL, the client knows exactly what to expect based on what it requests. There are no surprises or mystery objects to interrogate.

4. Versioning is Easier

Versioning REST APIs is a tricky topic. However, there are a few common approaches for acheiving this.

URL versioning is probably the most popular method and simply involves publishing the unique version of the API as part of the resource URL:

moviedatabase.com/api/v1/movies/90889
moviedatabase.com/api/v2/movies/90889

Another approach is for the client to specify the API version as part of the request header:

GET /api/movies/90889 HTTP/1.1
Accept: application/vnd.api.movie+json; version=2.0

While both of these approaches work, they can create a less than ideal scenario for some applications, teams and customers. For both methods, there is nothing explicitly enforcing the version on the server - this means additional development effort is required to validate and process these versioned requests on the backend. After that, changes must be communicated to customers and supported by the development team as things are deprecated or added between versions of the API.

One key difference in GraphQL is that the client dictates the shape of the response it expects. This attribute eases the pain of versioning because new properties can easily be added to a query with no impact to existing consumers. Since they're not requesting it, it can't interfere or break their code. As much as possible, existing properties should not be changed to maintain backward compatibility.

These are just a few of the key benefits GraphQL offers. Next, we'll take a look at how to achieve some of them by implementing a simple GraphQL service in ASP.NET Core 2.


Building a GraphQL Service in ASP.NET Core

At its core a GraphQL service on any stack will likely include the following:

  1. A modern web framework to build and host a service accessible over HTTP
  2. A data layer comprised of a database and optional OR/M for conveniently working with the database in code
  3. A GraphQL library implementation in a compatible programming language

Considering this, here's a high-level diagram of our GraphQL demo solution. As you can see, there's nothing radically different here.

A pretty standard ASP.NET Core-based web app with a data layer consisting of a Sql Server database and Entity Framework Core. The only other special thing about it is, of course, the inclusion of the graphql-dotnet library which is really just an abstraction over our existing data layer.

The Demo Project

We've explored a little bit of the theory and benefits behind GraphQL. Time to get down to the brass tacks of actually building something here. This repo contains the demo NHLStats GraphQL service which we'll explore in detail. If you couldn't guess by the project name, I like hockey and statistics seemed like an interesting thing to build a data-driven GraphQL API with - so let's do it.

Development Environment

  • .NET Core 2.0 SDK
  • Visual Studio Code v1.21.1 (Should also work in VS2017)
  • Sql Server Management Studio 2017

Project Structure

Our solution consists of 3 projects.

  • NHLStats.Api This is the ASP.NET Core Web API project containing our controllers and GraphQL implementation.

  • NHLStats.Core Contains shared models and interface definitions used in the other projects.

  • NHLStats.Data The data layer is encapsulated here - contains the Entity Framework Core DbContext, Migrations etc.

Project Setup

The project is configured to create and seed the database at runtime if necessary so there shouldn't be any initial setup required. Simply start the debugger from the IDE or run it directly using the CLI dotnet run command from the root of the \NHLStats.Api folder.

After running the project, verify the database was created in your local Sql Server LocalDB instance.

Alternatively, you can also apply the existing migrations to create the database by running dotnet ef database update from the \NHLStats.Data folder. The seed data will be inserted on first run of the application.

The API is configured to run on port 5000, if this conflicts with some other service on your computer you can change it here.

Hello World

With the above setup taken care of and the application fired up and running on localhost:5000 we can test it out by issuing a query for the service to validate and execute.

I used Postman to send a simple query to fetch a random player.

And received back a response - our GraphQL API works. 🤘

GraphQL for .NET

I mentioned that GraphQL isn't really tied to any specific backend framework or programming language. However, there are a number of libraries to help you implement a GraphQL service in your language/framework of choice. GraphQL for .NET is the package we'll add to our Web API project to bootstrap it with GraphQL superpowers.

NHLStats.Api>dotnet add package GraphQL -version 2.0.0-alpha-870

A Typed Schema

Based on what we've seen so far, it's fair to say that GraphQL is primarily about selecting fields on objects. A GraphQL service facilitates this by defining types and fields on those types, then providing underlying functions to populate each field on each type. To govern what types and fields are valid and determine the shape of the queries and mutations the server will accept we add fields to the root types of the Schema: Query, Mutation and Subscription. These types make up the entry point for our GraphQL API while the schema overall provides a clear contract for client-server interaction.

Exploring the Schema with GraphiQL

While you can certainly use Postman (or any other tool that speaks HTTP) as we did above to issue queries to the GraphQL API there is another developer tool in GraphiQL which does a better job. GraphiQL is an interactive, in-browser GraphQL IDE to test queries and explore your schema. There is a port of GraphiQL for ASP.NET Core which I added to the Web API project's middleware.

Adding the package is pretty straight forward using the .NET Core CLI command:

NHLStats.Api>dotnet add package graphiql

Next, it needs to be wired up in the Configure() method in Startup.cs.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, NHLStatsContext db)
{
   if (env.IsDevelopment())
   {
       app.UseDeveloperExceptionPage();
   }

   app.UseGraphiQl();
   app.UseMvc();
   db.EnsureSeedData();
}

With the GraphiQL middleware in place, I fired up the project and hit localhost:5000/graphql in the browser to load the GraphQL IDE.

GraphQL's introspection system is a very useful feature for discovering the Types an API supports.

If you plug the following query into the GraphiQL IDE we can obtain a complete list of all the Types the service provides.

{
  __schema {
    types {
      name
    }
  }
}

The response is fairly long, there are 3 categories of types here.

Familar, built-in scalars containing types for String, Boolean, Float etc.

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "String"
        },
        {
          "name": "Boolean"
        },
...

Types preceded with a double underscore represent parts of the introspection system.

...
        {
          "name": "__Schema"
        },
        {
          "name": "__Type"
        },
        {
          "name": "__TypeKind"
        },
        {
          "name": "__Field"
        },
...

And finally, the custom Types at the heart of our GraphQL API. We'll take a closer look at these now.

...
        {
          "name": "NHLStatsQuery"
        },
        {
          "name": "PlayerType"
        },
        {
          "name": "SkaterStatisticType"
        },
        {
          "name": "CreatePlayerMutation"
        },
        {
          "name": "PlayerInput"
        }
...

We can continue to use introspection to explore the API further by asking for more detailed information about the fields available on individual types. Let's ask the introspection system about NHLStatsQuery:

__type(name: "NHLStatsQuery") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}

If you've peeked at the C# model code for NHLStatsQuery this response isn't that surprising but it would be very helpful for clients and consumers with no knowledge or access to the backend code. Based on this result, we know there are three fields currently available: player and randomPlayer which both return a PlayerType OBJECT. players on the other hand, looks a bit odd. It has no name but we do know it returns a LIST of some type.

{
  "data": {
    "__type": {
      "name": "NHLStatsQuery",
      "fields": [
        {
          "name": "player",
          "type": {
            "name": "PlayerType",
            "kind": "OBJECT"
          }
        },
        {
          "name": "randomPlayer",
          "type": {
            "name": "PlayerType",
            "kind": "OBJECT"
          }
        },
        {
          "name": "players",
          "type": {
            "name": null,
            "kind": "LIST"
          }
        }
      ]
    }
  }
}

We can query for the underlying type information of a LIST field by extending our query to use ofType.

{
  __type(name: "NHLStatsQuery") {
    name
    fields {
      name      
      type {
        name
        kind
        ofType {
          name
          kind
        }         
      }
    }
  }
}

With the additional type information coming back we can now see that — surprise, surprise — players is a list of PlayerType.

...
       {
          "name": "players",
          "type": {
            "name": null,
            "kind": "LIST",
            "ofType": {
              "name": "PlayerType",
              "kind": "OBJECT"
            }
          }
        }
...
  

Dynamic Queries

We just saw how to discover the types and data provided by a GraphQL API. Now, let's issue a real query to retrieve some meaningful data. The player field allows us to retrieve specific player data by way of the id argument. Fields on GraphQL objects can have zero or more arguments which can be required or optional. The Int! indicates that id cannot be null. You might notice in the lower-left of the GraphiQL IDE the Query Variables section which allows us to factor dynamic argument values out of the query and pass them in a separate dictionary. Dynamically manipulating the id and the list of requested fields lets us quickly modify the query to our exact needs. The ability to fetch just the data we want in this fashion is the essence of GraphQL.

The Backend

We've explored and fetched data from the GraphQL service on the frontend, now let's see how the backend service is implemented.

All the of the types for the GraphQL service live in the Models folder within the demo project.

NHLStatsSchema defines the root Query and Mutation objects for the service. Queries only fetch data and should never modify it. You can only have a single root Query object.

public class NHLStatsSchema : Schema
{
   public NHLStatsSchema(IDependencyResolver resolver): base(resolver)
   {
       Query = resolver.Resolve<NHLStatsQuery>();
       Mutation = resolver.Resolve<NHLStatsMutation>();
   }
}

The Schema requires a dependency resolver: IDependencyResolver which allows us to use regular constructor injection in any other GraphType in the graph. All dependencies are registered in the built-in services container provided by ASP.NET Core and configured via ConfigureServices method in the API project's Startup class.

public void ConfigureServices(IServiceCollection services)
{
   services.AddMvc();

   services.AddDbContext<NHLStatsContext>(options => options.UseSqlServer(Configuration["ConnectionStrings:NHLStatsDb"]));
   services.AddTransient<IPlayerRepository, PlayerRepository>();
   services.AddTransient<ISkaterStatisticRepository, SkaterStatisticRepository>();
   services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
   services.AddSingleton<NHLStatsQuery>();
   services.AddSingleton<NHLStatsMutation>();
   services.AddSingleton<PlayerType>();
   services.AddSingleton<PlayerInputType>();
   services.AddSingleton<SkaterStatisticType>();
   var sp = services.BuildServiceProvider();
   services.AddSingleton<ISchema>(new NHLStatsSchema(new FuncDependencyResolver(type => sp.GetService(type))));
}

Note that NHLStatsSchema is registered with a FuncDependencyResolover that uses the built-in container to resolve dependencies from then on. This seems to be the pattern for using DI in the current version of graphql-dotnet.

The Web API Controller

I mentioned earlier that a common issue in large REST-based applications can be the overwhelming number of endpoints that we must manage and support. Our demo project contains a single controller in GraphQLController with a single action method/endpoint that handles every incoming query and mutation that our schema supports.

[HttpPost]
public async Task<IActionResult> Post([FromBody] GraphQLQuery query)
{
  if (query == null) { throw new ArgumentNullException(nameof(query)); }
  var inputs = query.Variables.ToInputs();
  var executionOptions = new ExecutionOptions
      {
        Schema = _schema,
        Query = query.Query,
        Inputs = inputs
      };

      var result = await _documentExecuter.ExecuteAsync(executionOptions).ConfigureAwait(false);

      if (result.Errors?.Count > 0)
      {
          return BadRequest(result);
      }

  return Ok(result);
}

This action is invoked with an HTTP POST to the graphql controller. In REST, as a rule, we use the HTTP GET verb to fetch data. In GraphQL we can certainly still use GET requests for fetching data however that means we must store our "query" in the query string of the URL ie. myapi/graphql?query=.... A cleaner approach is to send the query in the JSON-encoded body of a POST request.

Based on the size of the method, on the surface, there isn't a lot we need to do to process a GraphQL query. The graphql-dotnet lib is doing all the heavy lifting here. The incoming POSTed data is deserialized into a GraphQLQuery object which is just a simple type representing the structure of a GraphQL query. Next, we assemble executionOptions which is another simple object holding the context required to process the request and consists of our schema (injected via constructor), the actual query submitted by the client and any input arguments. There's a number of other properties belonging to ExecutionOptions but for this demo, we're only concerned with these 3.

The real work happens during:

_documentExecuter.ExecuteAsync(executionOptions)

Here the incoming query is parsed, validated and executed asynchronously against the type system defined in the schema. Finally, the result of the execution is returned in the response in an ExecutionResult object. The Data property is the result of the executing query and Errors is null if no error occurred and is a non-empty array if there was a problem.

Resolvers

At the very top level of every GraphQL service is a type that represents all of the possible entry points into the GraphQL API.

This is usually referred to as the Root type or Query type. You may have noticed above when we looked at NHLStatsSchema that the Query property for our API is assigned to NHLStatsQuery. This is the entry point for fetching data and contains the queryable fields exposed by our API.

public class NHLStatsQuery : ObjectGraphType
{
   public NHLStatsQuery(IPlayerRepository playerRepository)
   {
     Field<PlayerType>(
          "player",
          arguments: new QueryArguments(new QueryArgument<IntGraphType> { Name = "id" }),
          resolve: context =>  playerRepository.Get(context.GetArgument<int>("id")));

     Field<PlayerType>(
          "randomPlayer",
          resolve: context => playerRepository.GetRandom());

     Field<ListGraphType<PlayerType>>(
          "players",
          resolve: context => playerRepository.All());
   }
}

Up until now, I haven't mentioned much about data or specifically how and where our data meets the GraphQL layer. Well, I won't keep in you suspense any longer - field Resolvers are the functions responsible for supplying the data requested by the query and is the integration point between our application's data source and the GraphQL infrastructure.

In the player field, for example, I'm simply calling playerRepository.Get() and passing in the id from the query arguments to fetch the player data. PlayerRepository is a simple Entity Framework Core-based repository class defined in the solution's data layer and injected into NHLStatsQuery by the service container we saw earlier. The NHLStats.Data project is completely unaware of GraphQL, there are no dependencies on graphql-dotnet or any other libraries. In this demo, we happen to be using Sql Server with repositories but this pattern of injecting data dependencies for use in resolver functions can be used for whatever your persistence layer or data source looks like - even calling another REST API. This works because GraphQL is an abstraction on top of your existing data layer - whatever it may be.

Mutations

We've focused a lot in this guide on fetching data with GraphQL however our last topic will discuss modifying server-side data via mutations. REST guidelines suggest developers should not use GET requests to modify data on the server. These operations should be requested via POST, PUT, PATCH or DELETE. Similarly, GraphQL has its own convention for writing data and suggests these operations be explictily defined in Mutations.

Mutations are defined in our GraphQL schema in the exact same fashion as queries. Starting with the schema, you may have noticed we have a Mutation property to set the root mutation type: NHLStatsMutation.

public class NHLStatsSchema : Schema
{
  public NHLStatsSchema(IDependencyResolver resolver): base(resolver)
  {
     Query = resolver.Resolve<NHLStatsQuery>();
     Mutation = resolver.Resolve<NHLStatsMutation>();
  }
}

NHLStatsMutation is structured similarly to the NHLStatsQuery we looked at earlier. It has a single field defining the createPlayer mutation. The new player data is retrieved by the resolved QueryArguments and deserialized into a PlayerInputType. When working with mutations it's important to use separate types for input and not attempt to re-use query graph types. Input objects must be serializable while output objects can contain fields that contain circular references or references to interfaces and unions which aren't appropriate for input arguments.

The resolver function adds the player to the database using the PlayerRepository and returns the newly added player in the result meaning this operation is a write followed by a fetch.

public class NHLStatsMutation : ObjectGraphType
{
   public NHLStatsMutation(IPlayerRepository playerRepository)
   {
     Name = "Mutation";

     Field<PlayerType>(
           "createPlayer",
           arguments: new QueryArguments(
           new QueryArgument<NonNullGraphType<PlayerInputType>> { Name = "player" }
     ),
     resolve: context =>
     {
        var player = context.GetArgument<Player>("player");
        return playerRepository.Add(player);
     });
   }
}

A mutation request is nearly identical to a regular query. We pass the data in via the player input argument and specify the fields we'd like returned. You can see in the GraphiQL response window the id and name of the newly added player are returned. If you send multiple requests you can see the id value increment as each new row is added to the player table.

Finally, no new controller or action is required to process mutations. These requests are sent to and handled by the same GraphQLController we looked at earlier.

Wrapping Up

I hope this post has helped you gain some insight into the power and flexibility GraphQL can provide developers building and delivering data-rich APIs. There's a number of topics we didn't cover here so as always I encourage you to explore further and expand on what we covered in this post. If you're building something cool, have any questions or feedback I'd love to hear it in the comments below - thanks!

source code here

Get notified on new posts

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.

Get notified on new posts
X

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.