The article and code illustrate usage of GraphQL in .NET 5. GraphQL data access is optimized with data caching. Several libraries were developed to support GraphQL, JWT authentication, TLS, configurable logging.
Table of Contents
Introduction
In this article, two .NET 5 Web services are presented. The first one GraphQlService
supports Create, Retrieve, Update and Delete (CRUD) operations with a database (SQL Server) using GraphQL technology. Transport Layer Security (TLS) protects messages from being read while travelling across the network and JSON Web Token (JWT) is employed for user authentication and authorization. The second LoginService
provides user login mechanism and generates JWT out of user's credentials.
Wiki:
GraphQL is an open-source data query and manipulation language for APIs, and a runtime for fulfilling queries with existing data. GraphQL was developed internally by Facebook in 2012 and was publicly released in 2015. Currently, the GraphQL project is running by GraphQL Foundation. It provides an approach to developing web APIs and has been compared and contrasted with REST and other web service architectures. It allows clients to define the structure of the data required, and the same structure of the data is returned from the server, therefore preventing excessively large amounts of data from being returned.
The code of this article demonstrates the following main features:
- CRUD operations for transactional data repository using GraphQL technology
- Handy Playground and GraphiQL off-the-shelf Web UI applications for GraphQL queries and mutations with no front end code required
- JWT authentication
- OpenApi (a. k. a. Swagger) usage in conjunction with GraphQL
- Flexible configurable logging (currently configured for some minimum output to console only)
- Integration tests using in-memory service
Several open source packages for GraphQL development taken with NuGet, are used.
How Do Services Work?
Work of the services is shown in the figure below:
Figure 1. Work of the services.
To begin her/his work, the user provides credentials (user name and password) to LoginService
(1). The latter generates JWT and returns it to user. Then user sends queries / updates to GraphQlService
(2) and gets response from the service.
The services have separate databases. User database UsersDb
accessed by LoginService
contains one table Users
consisting of user name, password (encrypted in real world) and role of each user. Person database PersonsDb
accessed by GraphQlService
contains several tables related to persons, organization and their relations and affiliation.
GraphQL Usage and Optimization
GraphQL defines contract between client and server for data retrieval (query) and update (mutation). Both query and mutation constitute JSON-like structure. Retrieved data are formatted into much the same structure as request and return to client. Due to hierarchical form of GraphQL query, the process of data retrieving is a sequence of calls to handlers of nested fields.
GraphQL implies usage of resolve functions (resolvers) for every data field. Normally, implementation of GraphQL including one used here, ensures calls of appropriate resolvers during formation of a Web response hierarchical structure. If every resolver issues a SELECT
query to database, then overall number of those queries equals to a sum of return rows on each level of the hierarchy. Consider a query that fetches all persons. In the upmost level, all n persons are fetched. Then on the second level, SELECT
query is executed n times to fetch affiliations and relations for each person. Similar picture is observed on each following level. Obviously, a substantial number of return rows causes large number of queries to database causing serious performance penalty. Number of queries to database in this case is:
database_queries = Σ entries(level - 1)
levels
This problem is commonly referred to as N + 1 query problem.
Efficient GraphQL implementation has to provide a reasonable solution to this problem. Solution implemented in this work may be formulated as follows. In "naive" implementation, handler of every field calls database to retrieve data. In optimized solution, the first call of a field handler on each level retrieves from database data for all fields on this level and stores them in a cache attached to GraphQL context object. The GraphQL context object is available to all field handlers. Subsequent calls of the given level field handler obtain data from the cache and not from database. In optimized case, number of queries to database is:
database_queries = levels
As you can see, number of database calls (SELECT-s) corresponds to number of inner levels of the GraphQL query and independent on number of fetched records on each level.
The difference is illustrated in Figures 2 and 3 below:
Figure 2. Non-optimized data fetch.
Figure 3. Optimized data fetch.
Return values of resolvers will automatically be inserted into response object for GraphQL query. We only need to provide those return values from data that we have already fetched from database in the resolver on this level. The simplest way to achieve this is to place the fetched data into a cache object in memory and attach it to a context object available across all resolvers. The cache is organized as a dictionary with keys according to resolvers. Each resolver returns a piece of data extracted from the cache with appropriate key.
These return values form response object to be sent back to client in fulfillment of GraphQL query. Since cache object is a property of a context object, it will be destroyed along with the context at the end of GraphQL query processing. So cache object is created for each client request and its lifetime is limited by processing of this request.
Data acquisition optimization based on memory cache boosts performance. Its limitation is however in size of available operative memory (RAM). If cache object is too big to accommodate it in a single process memory, then distributed cache solutions such as Redis, Memcached or similar may be used. In this article, we assume simple in-memory cache that satisfy vast majority of the real world cases.
Components and Structure
Component | Project Type | Location | Description |
GraphQlService | Service (Console application) | .\ | The service performs CRUD operations using GraphQL technology. It provides two controllers. GqlController processes all GraphQL requests, whereas PersonController processes parameterless GET request responding with some predefined text, and another GET request with Person id as a parameter. This request is internally processed as an ordinary GraphQL request with hardcoded query. It acts as a “shortcut” to often used GraphQL query. Here, PersonController serves mostly illustrative purpose. |
LoginService | Service (Console application) | .\ | The service supports user login procedure. It has a LoginController creating JWT in response to user's credentials. |
ServicesTest | Test Project (Console application) | .\Tests | Project provides integration tests for both services. The tests are based on the concept of in-memory service. Such an approach allows developer effortlessly test actual service code. |
ConsoleClient | Console application | .\ | A simple client console application for the services. |
PersonModelLib | DLL | .\Model | The project provides code specific for the given domain problem (Persons in our case). |
AsyncLockLib | DLL | .\Libs | Provides locking mechanism for async/await methods, particularly applied for implementation of GraphQL caching. |
AuthRolesLib | DLL | .\Libs | Provides enum UserAuthRole . |
GraphQlHelperLib | DLL | .\Libs | Contains general GraphQL related code including one for data caching to solve N + 1 query problem. |
HttpClientLib | DLL | .\Libs | Used to create HTTP client, implements HttpClientWrapper class. |
JwtAuthLib | DLL | .\Libs | Generates JWT by user's credentials |
JwtLoginLib | DLL | .\Libs | Provides user login handling, uses JwtAuthLib . |
RepoInterfaceLib | DLL | .\Libs | Defines IRepo<T> interface for dealing with transactional data repository. |
RepoLib | DLL | .\Libs | Implements IRepo<T> interface from RepoInterfacesLib for EntityFrameworkCore . It equips data saving procedure with transaction. |
How to Run?
Prerequisites (for Windows)
- Local SQL Server (please see connection string in file appsettings.json of the service)
- Visual Studio 2019 (VS2019) with .NET 5 support
- Postman application to test cases with authentication
Sequence of Actions
-
Open solution GraphQL_DotNet.sln with VS2019 that supports .NET 5 and build the solution.
-
SQL Server is used. For the sake of simplicity, Code First paradigm is adopted. Databases UsersDb
and PersonsDb
are automatically created when either appropriate services or their integration tests run. Please adjust connection string (if required) in appsettings.json services configuration files. On the start, database is filled with several initial records from the code. To ensure proper functioning of identity mechanism, all those records are assigned with negative Id-s except for UsersDb.Users
since this table will not be changed programmatically in this work.
- Configuration file appsetting.json of
GraphQlService
contains object FeatureToggles
:
"FeatureToggles": {
"IsAuthJwt": true,
"IsOpenApiSwagger": true,
"IsGraphIql": true,
"IsGraphQLPlayground": true,
"IsGraphQLSchema": true
}
By default, all options are set to true
. Let's start first without authentication and set "IsAuthJwt"
to false
.
-
Start GraphQlService
. It may be carried out from VS2019 either as a service or under IIS Express. Browser with Playground
Web UI application for GraphQL starts automatically.
In Playground
Web page, you may see GraphQL schema and play with different queries and mutations. Some predefined queries and mutations may be copied from file queries-mutations-examples.txt.
Figure 4. Playground Web application.
You can use similar GraphiQL Web application instead of Playground
: browse on https://localhost:5001/graphiql.
Figure 5. GraphiQL Web application.
-
Playground application uses middleware to get response bypassing GqlController
(it is mostly used during development, but in this project, it is available in all versions). It does not call GqlController
that is used by clients in production. To work with GqlController
, you may use Postman application.
From Postman, make a POST to https://localhost:5001/gql with Body
-> GraphQL
providing in QUERY textbox your actual GraphQL query / mutation.
Figure 6. GraphQL query with Postman.
-
You may also use OpenApi
(a. k. a. Swagger): browse to https://localhost:5001/swagger:
Figure 7. OpenApi (Swagger).
Activate POST /Gql in Swagger Web page.
Then in Postman, press Code link in the upper-right corner:
Figure 8. HTTP request from Postman.
Copy query to Swagger's Request body
textbox and execute method.
Figure 9. POST /Gql request.
Figure 10. POST /Gql response.
-
In all cases, you may use unsafe call to http://localhost:5000 allowed for illustration and debugging.
-
Now let's use JWT authentication. Stop running GraphQlService
(if it was), in configuration file appsetting.json of GraphQlService
in object FeatureToggle
set "IsAuthJwt"
to true
, in VS2019, define LoginService
and GraphQlService
as Multiple startup projects and run them.
Alternatively, the services may be started by activating files LoginService.exe and GraphQlService.exe from corresponding Debug or Release directories. In this case, browser should be started manually navigating on https://localhost:5001/playground when the service is already running.
First, you need from Postman
to make a POST to https://localhost:5011/login providing user's credentials username = "Super", password = "SuperPassword"
. Please note port 5011: as you may see, LoginService
listens on this port.
Figure 11: Login
Then open a new tab in Postman to POST to https://localhost:5001/gql, open Authorization -> Bearer Token, copy the token received in login to Token textbox and make a post by click Send button. You can use OpenApi
with authentication. For this in OpenApi
Web page, press button Authorize (please see Figure 7), insert word "Bearer
" followed by JWT token into Value textbox and press Authorize button .
-
Integration tests may be found in project ServicesTest in directory .\Test .
Queries and Mutations with Playground
Playground
is a Web application that may be activated by GraphQL libraries middleware out-of-the-box (in this case, NuGet package GraphQL.Server.Ui.Playground
is used). It offers convenient and intuitive way to define, document and execute GraphQL queries and mutations. Playground
provides intellisense, error handling and word hints. It also shows GraphQL schema and all queries and mutation available for a given task. Screenshot of Playground
is depicted in Figure 4 above.
These are examples of queries and mutation for our solution. You may see their description in Playground
DOCS pane.
The following Persons
query returns all persons.
query Persons {
personQuery {
persons {
id
givenName
surname
affiliations {
organization {
name
parent {
name
}
}
role {
name
}
}
relations {
p2 {
givenName
surname
}
kind
notes
}
}
}
}
Query PersonById
returns a single person by its unique id
parameter. In the following example, id
is set to 1
.
query PersonById {
personByIdQuery {
personById(id: 1) {
id
givenName
surname
relations {
p2 {
id
givenName
surname
}
kind
}
affiliations {
organization {
name
}
role {
name
}
}
}
}
}
Mutation PersonMutation
allows user to either create new persons or update existing ones.
mutation PersonMutation {
personMutation {
createPersons(
personsInput: [
{
givenName: "Vasya"
surname: "Pupkin"
born: 1990
phone: "111-222-333"
email: "vpupkin@ua.com"
address: "21, Torn Street"
affiliations: [{ since: 2000, organizationId: -4, roleId: -1 }]
relations: [{ since: 2017, kind: "friend", notes: "*!", p2Id: -1 }]
}
{
givenName: "Antony"
surname: "Fields"
born: 1995
phone: "123-122-331"
email: "afields@ua.com"
address: "30, Torn Street"
affiliations: [{ since: 2015, organizationId: -3, roleId: -1 }]
relations: [
{ since: 2017, kind: "friend", notes: "*!", p2Id: -2 }
{ since: 2017, kind: "friend", notes: "*!", p2Id: 1 }
]
}
]
) {
status
message
}
}
}
Testing
Integration tests are placed in project ServicesTest
(directory .\Tests). In-memory service is used for integration tests. This approach considerably reduces efforts to develop integration tests. Tests may be run out-of-the-box since they create and initially fill database.
Conclusion
This work discusses usage of GraphQL technology for CRUD operations with transactional data repository and presents appropriate service developed in .NET 5 C#. It also implements such useful features as JWT authentication, OpenApi, configurable log and integration tests using in-memory service.
History
- 28th February, 2021: Initial version