GraphQL vs REST in .NET Core
Tags: graphql, rest, dotnet, performance
Published: 2021-12-16 15:20:44

## GraphQL

In 2012 Facebook started **GraphQL** as an internal project. An idea was to increase the flexibility of client-server communication, especially for data fetching issues in their mobile apps. In 2015 [**GraphQL**](https://graphql.org/) had became open source, which resulted in growing **GraphQL** popularity also in fortune 500 companies:

> Expedia Group has moved away from its RESTful API strategy, which was causing hardships for development teams, in favor of a GraphQL approach.

> In 2019, Airbnb software engineer Brie Bunge said about 5.8% of all Airbnb traffic involves GraphQL, and she expected that number to reach 10% by the end of last year. 

> LinkedIn has organized its “economic graph” of relationships between users, using a unified graph to hide complex systems behind a simple GraphQL API. They are leveraging the technology to enable a consistent access experience across online, nearline, and offline environments.

## GraphQL vs REST

But is **GraphQL** an alternative for **REST** or should be treated as an extension, what are the possible challenges that should be considered when choosing **GraphQL**?

### Design perspective

- **GraphQL** is transport agnostic **query language** while **REST** is _HTTP_ based  **architectural style** for _API_ design. Obviously, **GraphQL** typically served over _HTTP_, and  exposes one endpoint (that often designed to accept _POST_ requests, but it is not a must) while it is an "entrypoint" for **GraphQL** queries: server returns the data that's queried by the client.
- **REST** API, on the other hand, bases on HTTP and defines a suite of URLs each of which expose a single resource using various HTTP verbs and HTTP error codes in compliance with REST API design standards, for example [can be found them here](https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design). In other words, it is "bound" to HTTP.

- **API versioning**
From [**GraphQL** best practices](https://graphql.org/learn/best-practices/#versioning):
> While there's nothing that prevents a GraphQL service from being versioned just like any other REST API, GraphQL takes a strong opinion on avoiding versioning by providing the tools for the continuous evolution of a GraphQL schema.
When there's limited control over the data that's returned from an API endpoint, any change can be considered a breaking change, and breaking changes require a new version. If adding new features to an API requires a new version, then a tradeoff emerges between releasing often and having many incremental versions versus the understandability and maintainability of the API.

There is API backward compatibility issues "underneath" and a problems for **REST** and **GraphQL** are similar - while **REST** API during development should be kept backward compatible or should be versioned, so **GraphQL** schema should evolve in the same way: it also has to be backward compatible with a previous schema (Let's imagine, that during developing of new functionality few old fields from new schema have been removed - from a client perspective the issue is the same as when you will remove a fields from a **REST** API - in both cases client will receive an error in response).   
 
#### Before you start coding
 
* Do you expect search engines to index your URL (if so, than for these particular issue API **REST** has to be your choice: search engines do not index _POST_ requests which are often used for sending **GraphQL** queries).
* Do you plan to expose **GraphQL** endpoint as a data fetch from only one data storage source?  
* Do you plan design your **GraphQL** as a _Gateway_? 
* Will it be _gateway_ for **REST** API or _gateway_ for a number of **GraphQL** services (here you can expect n + 1 problem and it can be a challenge to design [DataLoaders](https://github.com/graphql/dataloader) on your layers)? You also should consider here how authorization rules, throttling rates or caching timeouts should be handled. Since **GraphQL** uses only one endpoint **REST** route-specific rules should be transferred into **GraphQL** schema rules: you may need to write authorization, throttling or caching logic in a separate layer.
* Is **GraphQL** API is a part of your _Mobile_ and _Web_ application or internal microservice?

### Coding

For **.NET** Web App you can add **GraphQL** to you app in a few ways: for example, via separate endpoint in your controller, adding your custom **GraphQL** [Middleware](https://github.com/graphql-dotnet/examples/blob/master/src/AspNetCoreCustom/Example/GraphQLMiddleware.cs) or you can use NUGET package [GraphQL.Server.Transports.AspNetCore](https://www.nuget.org/packages/GraphQL.Server.Transports.AspNetCore) and add [GraphQLHttpMiddleware](https://github.com/graphql-dotnet/server/blob/master/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs) or even [Web Sockets Middleware](https://github.com/graphql-dotnet/server/blob/master/src/Transports.Subscriptions.WebSockets/GraphQLWebSocketsMiddleware.cs).

Here I'll show an approach with adding **GraphQL** endpoint via standard Web API controller. First let's add such controller to Web API project:

```cs
[ApiController]
[Route("api")]
public class GraphQLController : ControllerBase
{
	private readonly IGraphQLProcessor _graphQLService;

	public GraphQLController(IGraphQLProcessor graphQLService)
	{
		_graphQLService = graphQLService;
	}

	[HttpPost("/graphql")]
	[ApiConventionMethod(typeof(GraphQLApiConventions), nameof(GraphQLApiConventions.Post))]
	public async Task<IActionResult> Post([FromBody] GraphQLRequest request)
	{
		var result = await _graphQLService.ProcessQuery(request);
		if (result.HasError)
		{
			return BadRequest(result);
		}

		if (result.Data is null)
		{
			return NotFound();
		}

		return Ok(result);
	}
}

```
`IGraphQLProcessor` is a service responsible for schema execution and exposes one method for processing **GraphQL** requests: `Task<GraphQLResponse> ProcessQuery(GraphQLRequest request)`:

```cs
public async Task<GraphQLResponse> ProcessQuery(GraphQLRequest request)
{
	var result = await _schema.ExecuteAsync(_documentWriter, o =>
	{
		o.Query = request.Query;
		o.Inputs = request.Variables.ToInputs();
		o.OperationName = request.OperationName;
		o.ValidationRules = DocumentValidator.CoreRules;
		o.EnableMetrics = false;
		o.ThrowOnUnhandledException = true;
	});

	var response = JsonSerializer.Deserialize<GraphQLResponse>(result, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
	return response;
}
```
Similar logic for schema execution you can implement in your own Middleware like in [this one](https://github.com/graphql-dotnet/examples/blob/master/src/AspNetCoreCustom/Example/GraphQLMiddleware.cs).

In the next step, we should define our **GraphQL** schema. In the case it is used for data fetching and data mutations:

```cs
[ExcludeFromCodeCoverage]
public class GraphQLSchema : Schema
{
	public GraphQLSchema(QueryGraphType query, MutationGraphType mutation, IServiceProvider serviceProvider) : base(serviceProvider)
	{
		Query = query;
		Mutation = mutation;
	}
}
```
`QueryGraphType` is a schema definition part responsible for fetching data:

```cs
[ExcludeFromCodeCoverage]
public class QueryGraphType : ObjectGraphType
{
	public QueryGraphType(INotesService notesService)
	{
		Name = $"{GetType().Name}";

		FieldAsync<NoteGraphType, Note>("note",
			"Gets a note by its id.",
			arguments: new QueryArguments(new QueryArgument<StringGraphType> { Name = "noteId" }),
			resolve: context =>
			{
				var noteId = context.GetArgument<string>("noteId");
				return notesService.GetNote(noteId);
			});

		FieldAsync<ListGraphType<NoteGraphType>, IEnumerable<Note>>("notes", resolve: context => notesService.GetNotes());
	}
}


[ExcludeFromCodeCoverage]
public class NoteGraphType : ObjectGraphType<Note>
{
	public NoteGraphType()
	{
		Name = nameof(Note);
		Field<StringGraphType>("Id");
		Field<LongGraphType>("DocumentId");
		Field<StringGraphType>("Title");
		Field<StringGraphType>("Description");
		Field<DateTimeGraphType>("Timestamp");
	}
}
```  
`INotesService` here is a business layer for data operations on `INotesRepository` wich uses **MongoDb** database. Obviously, that in real cloud native scenarios `INotesService` can aggregate and route a lot of data sources: REST, GraphQL, Database, etc.  

Similar, `MutationGraphType` defined for notes adding, update and delete as:

```cs
[ExcludeFromCodeCoverage]
public class MutationGraphType : ObjectGraphType
{
	public MutationGraphType(INotesService notesService)
	{
		Name = $"{GetType().Name}";
		Description = "Mutation for the entities in the service object graph.";
		this.AuthorizeWith("DefaultPolicy");

		FieldAsync<NoteGraphType, Note>(
			"addNote",
			"Add note to database.",
			new QueryArguments(new QueryArgument<NonNullGraphType<NoteInputGraphType>>{ Name = "note" }),
			context =>
			{
				var note = context.GetArgument<Note>("note");
				return notesService.AddNote(note);
			});

		FieldAsync<NoteGraphType, Note>(
			"updateNote",
			"Update note in database.",
			new QueryArguments(
				new QueryArgument<NonNullGraphType<NoteInputGraphType>> { Name = "note" },
				new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "noteId" }),
			context =>
			{
				var note = context.GetArgument<Note>("note");
				note.Id = context.GetArgument<string>("noteId");
				return notesService.UpdateNote(note);
			});

		Field<StringGraphType>(
			"deleteNote",
			"Delete note from database.",
			new QueryArguments(new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "noteId" }),
			context =>
			{
				var noteId = context.GetArgument<string>("noteId");
				notesService.DeleteNote(noteId);
				return $"The note with noteId: '{noteId}' has been successfully deleted from db.";
			}).AuthorizeWith("AdminPolicy");
	}       
}

[ExcludeFromCodeCoverage]
public sealed class NoteInputGraphType : InputObjectGraphType<Note>
{
	public NoteInputGraphType()
	{
		Name = "NoteInput";
		Field(r => r.Description);
		Field(r => r.Title);
		Field(r => r.DocumentId);
		Field(r => r.Timestamp);
	}
}
```
In fact, all defined graph types, `DocumentWriter`, `DocumentExecuter` should be registered in Web Application Dependency Injection container. I prefer to define `GraphQLServicesCollectionExtensions` class to have separate configuration for **GraphQL** stuff:

```cs
[ExcludeFromCodeCoverage]
public static class GraphQLServicesCollectionExtensions
{
	public static IServiceCollection ConfigureGraphQL(this IServiceCollection services)
	{
		services.AddTransient<IGraphQLProcessor, GraphQLProcessor>();               

		global::GraphQL.MicrosoftDI.GraphQLBuilderExtensions
			.AddGraphQL(services)
			.AddDocumentExecuter<DocumentExecuter>()
			.AddDocumentWriter<DocumentWriter>()
			.AddValidationRule<AuthorizationValidationRule>()
			.AddDataLoader()
			.AddSelfActivatingSchema<GraphQLSchema>()
			.AddSystemTextJson(options => options.PropertyNameCaseInsensitive = true);

		return services;
	}
}
```
 - `AddGraphQL().AddSelfActivatingSchema<GraphQLSchema>()` registers our schema with all graph types
 - `AddDataLoader()` registers [DataLoader](https://github.com/graphql/dataloader) for batch processing and caching requests for _n + 1_ issue
 - `services.AddTransient<IGraphQLProcessor, GraphQLProcessor>()` reqisters our `GraphQLProcessor`

### Consuming created API

Now we can consume created **GraphQL** API. In the [GitHub Repo](https://github.com/alekshura/Compentio.Notes.GraphQL) same functionality has been added with  **REST** approach and **GraphQL** endpoint. Also widely used [Swagger](https://docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger?view=aspnetcore-6.0) configured for Web API Endpoints as well as [AltairUI](https://altair.sirmuel.design/) added for **GraphQL** endpoint testing. Naturally, [AltairUI](https://altair.sirmuel.design/) it not a must for **GraphQL**, you can also use [Swagger](https://docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger?view=aspnetcore-6.0), [GraphiQL](https://graphql-dotnet.github.io/docs/getting-started/graphiql), or [GraphQL Playground](https://github.com/graphql/graphql-playground). 

In a case of [AltairUI](https://altair.sirmuel.design/) data fetching looks like:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u7t251ghzelshuzkcaav.png)
 
For mutation in a case of `updateNote`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e6a0ill5p17wuabtwc3q.png)
where we send two variables `note` and `noteId`
```json
{
  "note": {
  "documentId": 1234,
  "title": "Title for mutation modified",
  "description": "Description for mutation modified",
  "timestamp": "2021-12-13T10:35:14.830Z"
  },
  "noteId": "61b733a7434d2ea2b8a57d50"
}
```
For this case _HTTP_ request sent by AltairUI looks like:
```json
{
   "query":"mutation ($note: NoteInput!, $noteId: String!){\n  updateNote(note: $note, noteId: $noteId) {\n    title\n    description\n    documentId\n    timestamp\n  }\n}",
   "variables":{
      "note":{
         "documentId":1234,
         "title":"Title for mutation modified",
         "description":"Description for mutation modified",
         "timestamp":"2021-12-13T10:35:14.830Z"
      },
      "noteId":"61b733a7434d2ea2b8a57d50"
   },
   "operationName":null
}
``` 

### Validation
Naturally, data validation is a one of the base part of Web Application. In **GraphQL** [validation](https://graphql-dotnet.github.io/docs/getting-started/query-validation/) run when a query is executed. There is a predefined list of  _validation rules_ that are turned on by default. You can add your own _validation rules_ or clear out the existing ones by setting the `ValidationRules` property:

```cs
var result = await _schema.ExecuteAsync(_documentWriter, o =>
{
	o.Query = request.Query;
	o.Inputs = request.Variables.ToInputs();
	o.OperationName = request.OperationName;
	o.ValidationRules = DocumentValidator.CoreRules.Concat(new[] { new NoteValidationRule() });
	o.EnableMetrics = false;
	o.ThrowOnUnhandledException = true;
});
```
Let assume, that note's title should be less than 50 characters.  `ValidationRule` implementation can look like:

```cs
public class NoteValidationRule : IValidationRule
{
	public async Task<INodeVisitor> ValidateAsync(ValidationContext context)
	{
		return new NodeVisitors(
			new MatchingNodeVisitor<Argument>((arg, context) =>
			{
				ValidateAsync(arg, context, context.TypeInfo.GetArgument());
			})
		);
	}

	private void ValidateAsync(IHaveValue node, ValidationContext context, QueryArgument argument)
	{
		if (argument is null)
			return;

		if (!IsNoteArgument(argument.Name))
			return;

		var note = context.Inputs.FirstOrDefault(x => IsNoteArgument(x.Key)).Value as Dictionary<string, object>;
		var noteTitle = note["title"] as string;

		if (!string.IsNullOrEmpty(noteTitle) && noteTitle.Length > 50)
		{
			context.ReportError(new ValidationError(context.Document.OriginalQuery, "1.0", $"Field 'title' in argument '{argument.Name}' can not be longer than 50", node));
		}
	}
	private static bool IsNoteArgument(string argumentName)
	{
		return argumentName.Equals("note", StringComparison.InvariantCultureIgnoreCase);
	}
}
```

### Authorization

While in example **GraphQL** endpoint defined in API controller, it uses defined authentication for it. 
[Authorization](https://graphql-dotnet.github.io/docs/getting-started/authorization/) in GraphQL based on Validation approach with using of `AuthorizationValidationRule`.
To add it to the project we need to do:
- Install [GraphQL.Server.Authorization.AspNetCore](https://www.nuget.org/packages/GraphQL.Server.Authorization.AspNetCore/) package
- register your policies, register `HttpContextAccessor` and `IClaimsPrincipalAccessor`:
```cs
services.AddAuthorization(o => {
                o.AddPolicy("DefaultPolicy", policyBuilder => policyBuilder.RequireAuthenticatedUser());
                o.AddPolicy("AdminPolicy", policyBuilder => policyBuilder.RequireClaim("role", "Admin"));
            });

services.AddHttpContextAccessor().AddTransient<IClaimsPrincipalAccessor, DefaultClaimsPrincipalAccessor>();
```
- Register `AuthorizationValidationRule` in container by adding `AddValidationRule<AuthorizationValidationRule>()`:
```cs
global::GraphQL.MicrosoftDI.GraphQLBuilderExtensions
                .AddGraphQL(services)
                .AddDocumentExecuter<DocumentExecuter>()
                .AddDocumentWriter<DocumentWriter>()
                .AddValidationRule<AuthorizationValidationRule>()
                .AddDataLoader()
                .AddSelfActivatingSchema<GraphQLSchema>()
                .AddSystemTextJson(options => options.PropertyNameCaseInsensitive = true);
```
- In `GraphQLProcessor` inject `AuthorizationValidationRule` and add it to `ValidationRules` collection:
```cs
public class GraphQLProcessor : IGraphQLProcessor
{
	private readonly ISchema _schema;
	private readonly IDocumentWriter _documentWriter;
	private readonly AuthorizationValidationRule _authorizationRule;

	public GraphQLProcessor(IDocumentWriter documentWriter, ISchema schema,  AuthorizationValidationRule authorizationRule)
	{
		_documentWriter = documentWriter;
		_schema = schema;
		_authorizationRule = authorizationRule;
	}
	

	public async Task<GraphQLResponse> ProcessQuery(GraphQLRequest request)
	{
		var result = await _schema.ExecuteAsync(_documentWriter, o =>
		{
			o.Query = request.Query;
			o.Inputs = request.Variables.ToInputs();
			o.OperationName = request.OperationName;
			o.ValidationRules = DocumentValidator.CoreRules
				.Concat(new[] { new NoteValidationRule() })
				.Concat(new[] { _authorizationRule });
			o.EnableMetrics = false;
			o.ThrowOnUnhandledException = true;
		});

		var response = JsonSerializer.Deserialize<GraphQLResponse>(result, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
		return response;
	}        
}
```
Having it configured now we can add authorization for graph objects, for example only **Admin** has permissions to Note removal, what is achieved by adding `.AuthorizeWith("AdminPolicy")` to field definition:

```cs
Field<StringGraphType>(
	"deleteNote",
	"Delete note from database.",
	new QueryArguments(new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "noteId" }),
	context =>
	{
		var noteId = context.GetArgument<string>("noteId");
		notesService.DeleteNote(noteId);
		return $"The note with noteId: '{noteId}' has been successfully deleted from db.";
	}).AuthorizeWith("AdminPolicy");

```


### Performance comparison
Lets now compare operations for data fetching and mutating for **GraphQL** and **REST**.

For notes list and details data fetching **GraphQL** average time is about 23 _ms_ whereas for corresponding **REST** _GET_ is about 14 _ms_. For mutation operations the times for **REST** and **GraphQL** are similar. So if a performance is a must for you and data fetching times are about 10 _ms_ than **REST** should be a choice. 

### Summary
Obviously, **GraphQL** is powerful tool for designing API, especially for clients (mobile and web applications or microservices - it perfectly solves over-fetching and under-fetching issues), it has growing community, a number of solutions of known problems of data fetching and updating. However, it should be noted that it does not solve all the problems that arise when designing **REST** services: the complexity and number of endpoints is transferred to the complexity of the scheme. 
Also, the statement that **GraphQL** is client oriented is a minor abuse: after all, the server defines the schema, and the client can only request a subset of the data defined on the server.

### Code example

{% github alekshura/Compentio.Notes.GraphQL no-readme%}