An open API service indexing awesome lists of open source software.

https://github.com/simoncropp/graphql.validation

Add FluentValidation support to GraphQL.net
https://github.com/simoncropp/graphql.validation

Last synced: about 1 year ago
JSON representation

Add FluentValidation support to GraphQL.net

Awesome Lists containing this project

README

          

# GraphQL.Validation

[![Build status](https://ci.appveyor.com/api/projects/status/wvk8wm3n227b2b3q/branch/main?svg=true)](https://ci.appveyor.com/project/SimonCropp/graphql-validation)
[![NuGet Status](https://img.shields.io/nuget/v/GraphQL.FluentValidation.svg)](https://www.nuget.org/packages/GraphQL.FluentValidation/)

Add [FluentValidation](https://fluentvalidation.net/) support to [GraphQL.net](https://github.com/graphql-dotnet/graphql-dotnet)

**See [Milestones](../../milestones?state=closed) for release notes.**

### Powered by

[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSourceSupport)

## NuGet package

https://nuget.org/packages/GraphQL.FluentValidation/

## Usage

### Define validators

Given the following input:


```cs
public class MyInput
{
public string Content { get; set; } = null!;
}
```
snippet source | anchor

And graph:


```cs
public class MyInputGraph :
InputObjectGraphType
{
public MyInputGraph() =>
Field("content");
}
```
snippet source | anchor

A custom validator can be defined as follows:


```cs
public class MyInputValidator :
AbstractValidator
{
public MyInputValidator() =>
RuleFor(_ => _.Content)
.NotEmpty();
}
```
snippet source | anchor

### Setup Validators

Validators need to be added to the `ValidatorTypeCache`. This should be done once at application startup.


```cs
var validatorCache = new ValidatorInstanceCache();
validatorCache.AddValidatorsFromAssembly(assemblyContainingValidators);
var schema = new Schema();
schema.UseFluentValidation();
var executer = new DocumentExecuter();
```
snippet source | anchor

Generally `ValidatorTypeCache` is scoped per app and can be collocated with `Schema`, `DocumentExecuter` initialization.

Dependency Injection can be used for validators. Create a `ValidatorTypeCache` with the
`useDependencyInjection: true` parameter and call one of the `AddValidatorsFrom*` methods from
[FluentValidation.DependencyInjectionExtensions](https://www.nuget.org/packages/FluentValidation.DependencyInjectionExtensions/)
package in the `Startup`. By default, validators are added to the DI container with a transient lifetime.

### Add to ExecutionOptions

Validation needs to be added to any instance of `ExecutionOptions`.


```cs
var options = new ExecutionOptions
{
Schema = schema,
Query = queryString,
Variables = inputs
};
options.UseFluentValidation(validatorCache);

var executionResult = await executer.ExecuteAsync(options);
```
snippet source | anchor

### UserContext must be a dictionary

This library needs to be able to pass the list of validators, in the form of `ValidatorTypeCache`, through the graphql context. The only way of achieving this is to use the `ExecutionOptions.UserContext`. To facilitate this, the type passed to `ExecutionOptions.UserContext` has to implement `IDictionary`. There are two approaches to achieving this:

#### 1. Have the user context class implement IDictionary

Given a user context class of the following form:


```cs
public class MyUserContext(string myProperty) :
Dictionary
{
public string MyProperty { get; } = myProperty;
}
```
snippet source | anchor

The `ExecutionOptions.UserContext` can then be set as follows:


```cs
var options = new ExecutionOptions
{
Schema = schema,
Query = queryString,
Variables = inputs,
UserContext = new MyUserContext
(
myProperty: "the value"
)
};
options.UseFluentValidation(validatorCache);
```
snippet source | anchor

#### 2. Have the user context class exist inside a IDictionary


```cs
var options = new ExecutionOptions
{
Schema = schema,
Query = queryString,
Variables = inputs,
UserContext = new Dictionary
{
{
"MyUserContext",
new MyUserContext
(
myProperty: "the value"
)
}
}
};
options.UseFluentValidation(validatorCache);
```
snippet source | anchor

#### No UserContext

If no instance is passed to `ExecutionOptions.UserContext`:


```cs
var options = new ExecutionOptions
{
Schema = schema,
Query = queryString,
Variables = inputs
};
options.UseFluentValidation(validatorCache);
```
snippet source | anchor

Then the `UseFluentValidation` method will instantiate it to a new `Dictionary`.

### Trigger validation

To trigger the validation, when reading arguments use `GetValidatedArgument` instead of `GetArgument`:


```cs
public class Query :
ObjectGraphType
{
public Query() =>
Field("inputQuery")
.Argument("input")
.Resolve(context =>
{
var input = context.GetValidatedArgument("input");
return new Result
{
Data = input.Content
};
}
);
}
```
snippet source | anchor

### Difference from IValidationRule

The validation implemented in this project has nothing to do with the validation of the incoming GraphQL
request, which is described in the [official specification](http://spec.graphql.org/June2018/#sec-Validation).
[GraphQL.NET](https://github.com/graphql-dotnet/graphql-dotnet) has a concept of [validation rules](https://github.com/graphql-dotnet/graphql-dotnet/blob/master/src/GraphQL/Validation/IValidationRule.cs)
that would work **before** request execution stage. In this project validation occurs for input arguments
**at the request execution stage**. This additional validation complements but does not replace the standard
set of validation rules.

## Testing

### Integration

A full end-to-en test can be run against the GraphQL controller:


```cs
public class GraphQLControllerTests
{
[Fact]
public async Task RunQuery()
{
using var server = GetTestServer();
using var client = server.CreateClient();
var query = """
{
inputQuery(input: {content: "TheContent"}) {
data
}
}
""";
var body = new
{
query
};
var serialized = JsonConvert.SerializeObject(body);
using var content = new StringContent(
serialized,
Encoding.UTF8,
"application/json");
using var request = new HttpRequestMessage(HttpMethod.Post, "graphql")
{
Content = content
};
using var response = await client.SendAsync(request);
await Verify(response);
}

static TestServer GetTestServer()
{
var builder = new WebHostBuilder();
builder.UseStartup();
return new(builder);
}
}
```
snippet source | anchor

### Unit

Unit tests can be run a specific field of a query:


```cs
public class QueryTests
{
[Fact]
public async Task RunInputQuery()
{
var field = new Query().GetField("inputQuery")!;

var userContext = new GraphQLUserContext();
FluentValidationExtensions.AddCacheToContext(
userContext,
ValidatorCacheBuilder.Instance);

var input = new MyInput
{
Content = "TheContent"
};
var fieldContext = new ResolveFieldContext
{
Arguments = new Dictionary
{
{
"input", new(input, ArgumentSource.Variable)
}
},
UserContext = userContext
};
var result = await field.Resolver!.ResolveAsync(fieldContext);
await Verify(result);
}

[Fact]
public Task RunInvalidInputQuery()
{
Thread.CurrentThread.CurrentUICulture = new("en-US");
var field = new Query().GetField("inputQuery")!;

var userContext = new GraphQLUserContext();
FluentValidationExtensions.AddCacheToContext(
userContext,
ValidatorCacheBuilder.Instance);

var input = new MyInput
{
Content = null!
};
var fieldContext = new ResolveFieldContext
{
Arguments = new Dictionary
{
{
"input", new(input, ArgumentSource.Variable)
}
},
UserContext = userContext
};
var exception = Assert.Throws(
() => field.Resolver!.ResolveAsync(fieldContext));
return Verify(exception.Message);
}
}
```
snippet source | anchor

## Icon

[Shield](https://thenounproject.com/term/shield/1893182/) designed by [Maxim Kulikov](https://thenounproject.com/maxim221/) from [The Noun Project](https://thenounproject.com)