Click here to Skip to main content
15,868,016 members
Articles / Programming Languages / C#

.NET 5 Services with GraphQL Data Access and JWT Authentication

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
28 Feb 2021CPOL10 min read 12.7K   407   17   12
Web services implementing GraphQL technology with repository access optimization, JSON Web Token (JWT) authentication and some other useful features.
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:

Flow

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:

Non-optimized data fetch

Figure 2. Non-optimized data fetch.

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

  1. Open solution GraphQL_DotNet.sln with VS2019 that supports .NET 5 and build the solution.

  2. 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.

  3. Configuration file appsetting.json of GraphQlService contains object FeatureToggles:
    JavaScript
    "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.

  4. 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.

    Playground Web application

    Figure 4. Playground Web application.

    You can use similar GraphiQL Web application instead of Playground: browse on https://localhost:5001/graphiql.

    GraphiQL Web application

    Figure 5. GraphiQL Web application.
  5. 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.

    GraphQL query with authentication

    Figure 6. GraphQL query with Postman.
  6. You may also use OpenApi (a. k. a. Swagger): browse to https://localhost:5001/swagger:

    OpenAPi (Swagger)

    Figure 7. OpenApi (Swagger).

    Activate POST /Gql in Swagger Web page.

    Then in Postman, press Code link in the upper-right corner:

    HTTP request from Postman

    Figure 8. HTTP request from Postman.

    Copy query to Swagger's Request body textbox and execute method.

    POST /Gql request

    Figure 9. POST /Gql request.

    POST /Gql response

    Figure 10. POST /Gql response.
  7. In all cases, you may use unsafe call to http://localhost:5000 allowed for illustration and debugging.

  8. 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.

    Image 11

    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 .

  9. 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.

JavaScript
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.

JavaScript
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.

JavaScript
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Israel Israel


  • Nov 2010: Code Project Contests - Windows Azure Apps - Winner
  • Feb 2011: Code Project Contests - Windows Azure Apps - Grand Prize Winner



Comments and Discussions

 
QuestionGreat job! Pin
Lorenzo16-Jan-23 2:35
Lorenzo16-Jan-23 2:35 
QuestionJWT: validation and refreshing Pin
Win32nipuh23-Jun-21 20:19
professionalWin32nipuh23-Jun-21 20:19 
AnswerRe: JWT: validation and refreshing Pin
Igor Ladnik24-Jun-21 3:03
professionalIgor Ladnik24-Jun-21 3:03 
PraiseGreat, but one question about c# client Pin
Win32nipuh6-Jun-21 22:03
professionalWin32nipuh6-Jun-21 22:03 
GeneralRe: Great, but one question about c# client Pin
Igor Ladnik7-Jun-21 0:33
professionalIgor Ladnik7-Jun-21 0:33 
GeneralRe: Great, but one question about c# client Pin
Win32nipuh8-Jun-21 21:48
professionalWin32nipuh8-Jun-21 21:48 
GeneralRe: Great, but one question about c# client Pin
Igor Ladnik9-Jun-21 1:32
professionalIgor Ladnik9-Jun-21 1:32 
GeneralRe: Great, but one question about c# client Pin
Win32nipuh11-Jun-21 1:59
professionalWin32nipuh11-Jun-21 1:59 
GeneralRe: Great, but one question about c# client Pin
Igor Ladnik11-Jun-21 8:29
professionalIgor Ladnik11-Jun-21 8:29 
GeneralRe: Great, but one question about c# client Pin
Win32nipuh23-Jun-21 20:14
professionalWin32nipuh23-Jun-21 20:14 
PraiseFine article Pin
Pirks128-Feb-21 3:57
Pirks128-Feb-21 3:57 
Very helpful article. An excellent example of a successful bundle of multiple frameworks.
GeneralRe: Fine article Pin
Igor Ladnik28-Feb-21 3:58
professionalIgor Ladnik28-Feb-21 3:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.