Building Web Application with React + Apollo Client + GraphQL + .NET Core WebAPI

This article is really about showing you, how we can build a web application using React, GraphQL and .NET Core WebAPI. I would like to think of this as a Tutorial cum Sharing/Post-Mortem session after working through all those frustrated moments throughout the entire course. The demo application’s source codes can be found on GitHub https://github.com/DriLLFreAK100/graphql-dotnet-poc

Background

Back when I was building the demo application (somewhere back in early August 2020), the GraphQL.NET version 3.x was still in the preview stages. Many complimentary packages were not up supporting it yet. Therefore, after many trials and errors, I have decided to work with its stable release, version 2.4.0 instead. So yes, if you are following through this tutorial, you need to have .NET Core 2.2 SDK being installed on your machine first as the demo is developed with .NET Core 2.2 as Traget framework.

Motivation

The real motivation for me to write this article, is really to help the folks out there struggling to find a COMPLETE guide or tutorial on working with GraphQL.NET. Not to mention, the resources found on the web are also very limited on this topic. It’s either the tutorial/example is not complete or there isn’t any source code repo provided for us to refer to.

Also, GraphQL Subscription topic is something that’s almost missing in the entire web (For .NET only of course!). There are some real sneaky CAVEATS to it. So, just for the benefits of all the other .NET folks out there struggling to get this part up just like I did, don’t worry, I’ve got you covered here mates! 😉

Topics Coverage

Please feel free to skip to the sections of your interest.

  1. What is GraphQL?
  2. What’s NOT GraphQL?
  3. Where does GraphQL sits in a Software Architecture?
  4. Configuring GraphQL Server
  5. Building GraphQL Schema
  6. Write and Test GraphQL queries with GraphiQL
  7. React + Apollo Client Integration
  8. Demo – Query, Mutation and Subscription in Action!
  9. Caveat!

1. What is GraphQL

Based on the official graphql.org site’s definition, GraphQL is

A query language for your API

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, makes it easier to evolve APIs over time, and enables powerful developer tools.

As mentioned in https://graphql.org/

If I were to rephrase and lamen-ize it, it is basically a Query Language or a Payload format that a Client Application has to conform to when making a request to the GraphQL Server. In a HTTP Context, this is referring to the Payloads contained in a POST Request body.

A Query Language or a Payload format that a Client Application has to conform to when making a request to the GraphQL Server.

My lamen half-baked take on its definition

2. What’s NOT GraphQL?

If you don’t bother to even do a basic read up on this topic, GraphQL could possibly be a technology related to Database since there is the QL in its name. Truth is that, GraphQL has zero thing to do with Database at all. I will show you that in the next section, where I explain its placement in a Software Architecture stack.

Another common misconception is that GraphQL enables Client Apps to query the API “however it likes” and things will happen magically. Truth is that, it is not a new protocol and there are still works needed to be done in order for magic to happen. In order for Client Apps to make a query to backend successfully, GraphQL Server has to be coded, programmed, expose or make those set of resources (graph) available to the clients first!

3. Where does GraphQL sits in a Software Architecture?

Let us use the classic 3-tier architecture as an example – Presentation (Client), Domain (Server) and Data layers. Where does GraphQL actually sits in here?

Well, it actually sits between Client and Server. The diagram below pretty much sums it all up.

GraphQL’s position in the classic 3-tier architecture

The GraphQL Layer highlighted in the Server tier, is actually a layer that provides capability to the API Server to make it a GraphQL Server. Read that again, let that sink.

Usually, we do not reinvent the wheel. Chances is, there is already package available for whichever language or stack that you are using. For Node.js, people usually use express-graphql. For .NET Core, people usually use graphql-dotnet. It contains all those heavy liftings like parsing the GraphQL queries sent from Client applications, etc. You can refer here for more information on other languages implementing GraphQL Server.

4. Configuring GraphQL Server

Alright, enough introduction from me! Let’s get into the practical stuffs! We will start by setting up our GraphQL Server in a .NET Core WebAPI application.

I will only mention the gist of it here. If you are just starting out or are confused with which packages to be installed where, I will strongly suggest that you clone the repo to your local machine and open the .sln solution file in the Demo.Server folder with Visual Studio. That will give you a working example to begin with, rather than digging the solutions elsewhere.

I have setup 2 projects in my solution, a WebAPI and a Class Library project, both targeting .NET Core 2.2 as Target framework in their properties.

I’m actually taking reference to the Clean Architecture here for the code organization. And for the simplicity sake of this demo, I have excluded the Infrastructure layer as the demonstration of GraphQL’s capabilities doesn’t really require that.

Code organization. WebAPI and a Class Library project

Let’s start with the Startup.cs file. There are 2 things that are crucial to get things work properly here. First and foremost, it would the ConfigureServices method. We need to include the dependency injection configs like how we usually do it for a .NET Core web application. To make things more clear cut, I have grouped all the GraphQL related DI into a method called ConfigureGraphQlScopes. Do pay attention to that AddWebSockets() extension method here. That is crucial to get the GraphQL Subscription to work as it relies on WebSocket protocol.

public void ConfigureGraphQlScopes(IServiceCollection services)
{
    services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
    services.AddSingleton<IDocumentWriter, DocumentWriter>();
    services.AddSingleton<ClientType>();
    services.AddSingleton<FeedbackInputType>();
    services.AddSingleton<FeedbackType>();
    services.AddSingleton<FeedbackEventType>();
    services.AddSingleton<TransactionType>();
    services.AddSingleton<MonthType>();
    services.AddSingleton<TransactionCategoryType>();
    services.AddSingleton<TransactionSummaryByMonthType>();
    services.AddSingleton<ClientTransactionSummaryDtoType>();
    services.AddSingleton<DemoQuery>();
    services.AddSingleton<DemoMutation>();
    services.AddSingleton<DemoSubscription>();
    services.AddSingleton<ISchema, DemoSchema>();
    services.AddSingleton<IDependencyResolver>(d => new FuncDependencyResolver(d.GetRequiredService));

    services
        .AddGraphQL(new GraphQLOptions()
        {
            EnableMetrics = true,
            ExposeExceptions = true
        })
        .AddWebSockets()
        .AddDataLoader()
        .AddGraphTypes();
}

Second part would be the Configure method where the HTTP middleware is configured. Over here, we need to configure the WebSockets middleware (for Subscription to work) and also the endpoint configurations for the GraphQL API and GraphiQL (Test tool for GraphQL queries).

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseCors("AllowAllOrigins");
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseWebSockets();
    app.UseGraphQLWebSockets<ISchema>("/graphql");

    app.UseGraphiQLServer(new GraphiQLOptions()
    {
        GraphiQLPath = "/graphiql",
        GraphQLEndPoint = "/graphql"
    });
    app.UseHttpsRedirection();
    app.UseMvc();
}

Next would be the single HTTP POST endpoint that handles all the GraphQL requests. For that, I have setup a GraphQlController with a single POST method in it. The method basically invokes the GraphQL.NET package’s DocumentExecuter function along with the request payloads.

[Route("graphql")]
public class GraphQlController : ControllerBase
{
    private readonly ISchema _schema;
    private readonly IDocumentExecuter _executer;
    private readonly IDocumentWriter _writer;

    public GraphQlController(IDocumentExecuter executer, IDocumentWriter writer, ISchema schema)
    {
        _executer = executer;
        _writer = writer;
        _schema = schema;
    }

    public async Task<IActionResult> Post([FromBody] GraphQLQuery query)
    {
        var result = await _executer.ExecuteAsync(_ =>
        {
            _.Schema = _schema;
            _.Query = query.Query;
            _.OperationName = query.OperationName;
            _.Inputs = query.Variables.ToInputs();
        });

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

        return Ok(result);
    }
}

Cool, we are now done with all the configurations and setups required for our .NET Core WebAPI GraphQL Server! Up next, we will focus on writing the Schema for our GraphQL Server.

5. Building GraphQL Schema

As mentioned on GraphQL.NET Docs, there are 2 ways to build Schema, i.e. Schema-first or Code-first (GraphType-first). Sounds familiar eh? Yep, the semantics are taken from ORMs like Entity Framework. In my demo, the schema is built via Code-first approach, as I felt more natural that way. I have no comment on which is better over the other. To me, getting things done and feeling comfortable is more important. So yea, whichever method that works best for you!

I have included all my GraphQL Schema related classes in Demo.Server.Core project > GraphQl folder. There are 2 subfolders within it, the Schema and Types folder. I would like to think of it this way, classes in the Schema folder are the blueprint classes of my GraphQL Server, the roots of my graphs – the Query, Mutation and Subscription.

The DemoSchema.cs is like my registrar for Query, Mutation and Subscription. Let’s take a look at it together. It is inheriting the GraphQL.Types.Schema class, which enables us to specify the Query, Mutation, Subscription, DependencyResolver, which are all injected based on the configuration in Startup.cs

using GraphQL;

namespace Demo.Server.Core.GraphQl.Schema
{
    public class DemoSchema: GraphQL.Types.Schema
    {
        public DemoSchema(DemoQuery query, DemoMutation mutation, DemoSubscription subscription, IDependencyResolver resolver)
        {
            Query = query;
            Mutation = mutation;
            Subscription = resolver.Resolve<DemoSubscription>();
            DependencyResolver = resolver;
        }
    }
}

How things are hooked up? Well, there really are no magics to it. If you take a closer look at the Startup.cs, in the DI section, ISchema is configured with DemoSchema as its implementation class. And in the GraphQlController class, ISchema is injected into it and being passed into the ExecuteAsync function from DocumentWriter. That is how it identifies the Schema.

There is one thing to be kept in mind, in order to expose something in the Schema, it has to inherit GraphType. So, some rule goes for the Query, Mutation and Subscription root classes. In the example I provided, they are all inheriting the ObjectGraphType.

Query

using Demo.Server.Core.GraphQl.Types;
using Demo.Server.Core.Service.Interface;
using GraphQL.Types;

namespace Demo.Server.Core.GraphQl.Schema
{
    public class DemoQuery : ObjectGraphType<object>
    {
        public DemoQuery(ITransactionService transactionService, IClientService clientService)
        {
            Name = "Query";
            Field<ListGraphType<TransactionType>>(
                "transactions",
                arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "year" }),
                resolve: context => transactionService.GetTransactions(context.GetArgument<int>("year")),
                description: "Get all transactions for the specified year"
            );
            Field<ListGraphType<ClientType>>(
                "clients",
                resolve: _ => clientService.GetClients(),
                description: "Get all available clients"
            );
            Field<ListGraphType<ClientType>>(
                "client",
                arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "id" }),
                resolve: context => clientService.GetClient(context.GetArgument<int>("id")),
                description: "Get client with specified ID"
            );
            Field<ListGraphType<ClientTransactionSummaryDtoType>>(
                "topPerformingClient",
                arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "count" }, new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "year" }),
                resolve: context => clientService.GetTopPerformingClients(context.GetArgument<int>("count"), context.GetArgument<int>("year")),
                description: "Get top performing clients. Max limit count = 5"
            );
            Field<ListGraphType<ClientTransactionSummaryDtoType>>(
                "worstPerformingClient",
                arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "count" }, new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "year" }),
                resolve: context => clientService.GetWorstPerformingClients(context.GetArgument<int>("count"), context.GetArgument<int>("year")),
                description: "Get worst performing clients. Max limit count = 5"
            );
            Field<ListGraphType<IntGraphType>>(
                "availableTransactionYears",
                resolve: context => transactionService.GetAvailableTransactionYears(),
                description: "Get the list of years where transactions happened"
            );
            Field<ListGraphType<TransactionSummaryByMonthType>>(
                "transactionSummaryByMonth",
                arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "year" }),
                resolve: context => transactionService.GetTransactionSummaryByMonth(context.GetArgument<int>("year")),
                description: "Get transaction summary for a year by month"
            );
        }
    }
}

Mutation

using Demo.Server.Core.BusinessEntity;
using Demo.Server.Core.GraphQl.Types;
using Demo.Server.Core.Service.Interface;
using GraphQL.Types;
using System;

namespace Demo.Server.Core.GraphQl.Schema
{
    public class DemoMutation : ObjectGraphType<object>
    {
        public DemoMutation(IFeedbackService feedbackService)
        {
            Name = "Mutation";
            Field<FeedbackType>(
                "addFeedback",
                description: "Submit feedback to the application",
                arguments: new QueryArguments(new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "text" }),
                resolve: context =>
                {
                    var feedbackInput = context.GetArgument<string>("text");
                    return feedbackService.AddFeedback(new Feedback(Guid.NewGuid().ToString(), feedbackInput));
                }
            );
        }
    }
}

Subscription

using Demo.Server.Core.BusinessEntity;
using Demo.Server.Core.GraphQl.Types;
using Demo.Server.Core.Service.Interface;
using GraphQL.Resolvers;
using GraphQL.Subscription;
using GraphQL.Types;
using System;

namespace Demo.Server.Core.GraphQl.Schema
{
    public class DemoSubscription: ObjectGraphType<object>
    {
        private readonly IFeedbackEventService _feedbackEventService;
        public DemoSubscription(IFeedbackEventService feedbackEventService)
        {
            _feedbackEventService = feedbackEventService;

            Name = "Subscription";
            AddField(new EventStreamFieldType
            {
                Name = "feedbackEvent",
                Type = typeof(FeedbackEventType),
                Resolver = new FuncFieldResolver<FeedbackEvent>(ResolveEvent),
                Subscriber = new EventStreamResolver<FeedbackEvent>(Subscribe)
            });
        }

        private FeedbackEvent ResolveEvent(ResolveFieldContext context)
        {
            return context.Source as FeedbackEvent;
        }

        private IObservable<FeedbackEvent> Subscribe(ResolveEventStreamContext context)
        {
            return _feedbackEventService.EventStream();
        }
    }
}

It is a graph basically. So, all the types exposed in Query subsequently will also have to inherit a GraphType. If we take a look at the first field in the Query type for example, it is a ListGraphType<TransactionType>.

Field<ListGraphType<TransactionType>>
(
    "transactions",
    arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "year" }),
    resolve: context => transactionService.GetTransactions(context.GetArgument<int>("year")),
    description: "Get all transactions for the specified year"
);

As you can see, the fields are defined in the constructor of a class. In my query example, the fields are constructed via Field method. In my subscription example, the field is constructed via AddField method. There are also other methods available to construct the fields. The library probably uses Assembly info and Reflection methods to construct the Graph in runtime. Just my rough guess though. Would definitely dig into their codes in future, if I find the time to do it 🙂

That’s about it for the Schema constructions!

6. Write and Test GraphQL queries with GraphiQL

GraphiQL. What a cheeky and creative name! It’s pronounced as “Graphical”. Fair enough, it is the IDE for GraphQL, the place where you wanna construct, execute and test your GraphQL queries.

Think of it this way. In REST, we usually test our APIs using tools like Swagger or Postman. In GraphQL, we test it using GraphiQL. Though, there are also other tools available in the market like GraphQL Voyager, GraphQL Playground, etc. But baseline is, they are all essentially GraphQL test clients.

Let’s take a look at it! It’s a pretty simple and straight-forward UI. From the left, we have the history list of queries that we have executed. Followed by the Query Editor and Variables section underneath, then the results pane. We can just simply start by typing something on the editor, press Ctrl + Space to get some intellisense and autofill capabilities. When you are done, just press Ctrl + Enter or click the play button on top, the query will then be executed against the GraphQL Server to retrieve results. Pretty cool eh? 😛

This works and feels pretty much the same like using some SQL test clients, where you write your queries, hit the execute button and wait for the results to be returned. I thought it was a pretty cool paradigm shift as a web developer, from having to key in endpoints and parameters, to writing queries and variables.

On the right most pane, we have the document explorer. It contains all the information that we have written in our Schema earlier on, i.e. the field names, descriptions, etc. are all display here.

GraphiQL in action!

If you think about it, it’s a really good collaboration tool if you are in a situation where you have a Frontend team and a Backend team. Backend team can focus on providing data, elaborate information in the “graph”, where else Frontend team can then use GraphiQL to test and find the data that suits their needs. Sweet!

Not just that. After the Frontend dev is satisfied with his/her query construction. He/She can then copy and paste the query into the Frontend project directly. This is especially true if we are using a GraphQL Client package like Apollo Client, which really helps streamlining the dev process and makes the dev experience a breeze!

Great, let’s get on with the next part, enter GraphQL Client!

I thought it was a pretty cool paradigm shift as a web developer, from having to key in endpoints and parameters, to writing queries and variables.

7. React + Apollo Client Integration

GraphQL Client is no more than a helper for a Client application to communicate with a GraphQL Server. It should abstract away all the “infrastructure” codes, especially things like endpoint names, enabling developers to focus just on writing GraphQL queries and fulfilling their application’s needs.

In my demo example, I am using Apollo Client, which is a very popular GraphQL Client package. It is normally used along with ReactJS, but make no mistake, it is framework/library agnostic and actually works well with others framework/library like Angular, Vue, etc. too.

To setup ApolloClient, we just need an instance of it (configurations are defined here, e.g. GraphQL Server address, etc.), then wrap the portion of React tree that you want to use Apollo with ApolloProvider, with the instance as a Props.

ApolloClient Instance

import {
  ApolloClient,
  InMemoryCache,
  HttpLink,
  split,
} from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities'
import { WebSocketLink } from '@apollo/client/link/ws';
import { OperationDefinitionNode } from 'graphql';


const httpLink = new HttpLink({
  uri: "https://localhost:44391/graphql",
});

// Create a WebSocket link:
const wsLink = new WebSocketLink({
  uri: "wss://localhost:44391/graphql",
  options: {
    reconnect: true
  }
});

const link = split(
  // split based on operation type
  ({ query }) => {
    const definition = getMainDefinition(query);
    return definition.kind === 'OperationDefinition' && (definition as OperationDefinitionNode).operation === 'subscription';
  },
  wsLink,
  httpLink,
);

// Instantiate client
const ApolloClientInstance = new ApolloClient({
  link,
  cache: new InMemoryCache()
})

export default ApolloClientInstance;

ApolloProvider

import Admin from './Pages/Admin';
import ApolloClientInstance from './Utils/ApolloClientInstance';
import Dashboard from './Pages/Dashboard';
import Feedback from './Pages/Feedback';
import Header from './Components/Header';
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from '@apollo/client';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { SnackbarProvider } from 'notistack';
import './index.scss';

ReactDOM.render(
  <ApolloProvider client={ApolloClientInstance}>
    <SnackbarProvider maxSnack={3}>
      <Router>
        <Header />
        <div className="pageContainer">
          <Switch>
            <Route exact path="/">
              <Dashboard />
            </Route>
            <Route path="/feedback">
              <Feedback />
            </Route>
            <Route path="/admin">
              <Admin />
            </Route>
          </Switch>
        </div>
      </Router>
    </SnackbarProvider>
  </ApolloProvider>,
  document.getElementById('root')
);

Cool, we have now finished setting up our GraphQL Client. Let’s take a look at some GraphQL queries in action!

Let’s take the Dashboard page as an example here. After I am done testing with GraphiQL, I proceeded to copy the query over and assign it to GET_DASHBOARD_DATA const. The gql function is provided by the @apollo/client package.

const GET_DASHBOARD_DATA = gql`
  query dashboardData($year: Int!) {
    transactionSummaryByMonth(year: $year){
      month,
      totalRevenue,
      totalProfit,
    }
    topPerformingClient(count: 4, year: $year){
      id
      clientName
      totalRevenue
      totalServiceCost
      totalOtherOperationCost
      totalProfit
      totalProfitMargin
    }
  }
`;

The query const is then used as a parameter for the useQuery/useLazyQuery hook, depending on your usage purposes. Simple as that 😉

const Dashboard = (): JSX.Element => {
  const classes = useStyles();
  const [year, setYear] = React.useState<number>(0);
  const { data, loading, error } = useQuery<IDashboardInitQueryRes>(GET_DASHBOARD_INIT);
  const [getData, { data: queryData }] = useLazyQuery<IDashboardDataQueryRes>(GET_DASHBOARD_DATA);

  const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
    setYear(event.target.value as number);
    getData({ variables: { year: event.target.value } });
  };

  const renderDataDisplay = (): JSX.Element[] => {
    const elements = [];

    if (queryData) {
      if (queryData.transactionSummaryByMonth && queryData.transactionSummaryByMonth.length > 0) {
        elements.push(<TransactionSummary key="transaction-summary" data={queryData.transactionSummaryByMonth} />);
      }
      if (queryData.topPerformingClient && queryData.topPerformingClient.length > 0) {
        elements.push(<TopPerformingClients key="top-performing-client" data={queryData.topPerformingClient} />);
      }
    }

    return elements;
  };

  const renderNoRecord = () => {
    return (
      <Typography variant="subtitle2" className={classes.noRecords}>
        No Records
      </Typography>
    )
  }

  if (loading) return <p>LOADING...</p>
  if (error) return <p>ERROR</p>;
  if (!data) return <p>No Records</p>;

  return (
    <div className={styles['dashboard']}>
      <FormControl className={classes.formControl}>
        <InputLabel id="demo-simple-select-label">Year</InputLabel>
        <Select
          labelId="dashboard-year-select"
          id="dashboard-year-select-id"
          value={year}
          onChange={handleChange}
        >
          {data.availableTransactionYears.map((year, index) => {
            return <MenuItem key={index} value={year}>{year}</MenuItem>;
          })}
        </Select>
      </FormControl>
      {!queryData && renderNoRecord()}
      {renderDataDisplay()}
    </div>
  );
};

Okay! I have covered most of important stuffs by now

8. Demo – Query, Mutation and Subscription in Action!

Alright, demo time! I have setup 3 pages for this demo, covering different parts of GraphQL

  • Dashboard -> Query
  • Feedback -> Mutation
  • Admin -> Subscription
Demo in action!

9. Caveat!

If you using Windows, you will probably run into the problem of not being able to get the Subscription to work. There could be either 2 reasons to this. You are using Windows 7 (or any version before 8) or you did not turn on the WebSocket protocol on your Windows Feature.

If you are using Windows 7, then you probably won’t be able to get it works since Windows 7 does not support WebSocket by default. There could be workarounds but I would not dive into it unless strictly necessary, cause it’s just a real pain in the ass! 😛

If you are using Windows 8 and above, lucky you! You just need to go to Windows Feature > Internet Information Services > World Wide Web Services > Application Development Features and turn on the “WebSocket Protocol”.

It took me some time to realize this. A silly mistake indeed. So, please don’t make the same mistake like I do!


Hope that you enjoyed this one! The longest article that I have written to date. Hahaha..