SimonCropp / GraphQL.Validation

Add FluentValidation support to GraphQL.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

GraphQL.Validation

Build status NuGet Status

Add FluentValidation support to GraphQL.net

See Milestones for release notes.

NuGet package

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

Usage

Define validators

Given the following input:

public class MyInput
{
    public string Content { get; set; } = null!;
}

snippet source | anchor

And graph:

public class MyInputGraph :
    InputObjectGraphType
{
    public MyInputGraph() =>
        Field<StringGraphType>("content");
}

snippet source | anchor

A custom validator can be defined as follows:

public class MyInputValidator :
    AbstractValidator<MyInput>
{
    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.

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

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<string, object>. There are two approaches to achieving this:

1. Have the user context class implement IDictionary

Given a user context class of the following form:

public class MyUserContext(string myProperty) :
    Dictionary<string, object?>
{
    public string MyProperty { get; } = myProperty;
}

snippet source | anchor

The ExecutionOptions.UserContext can then be set as follows:

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

var options = new ExecutionOptions
{
    Schema = schema,
    Query = queryString,
    Variables = inputs,
    UserContext = new Dictionary<string, object?>
    {
        {
            "MyUserContext",
            new MyUserContext
            (
                myProperty: "the value"
            )
        }
    }
};
options.UseFluentValidation(validatorCache);

snippet source | anchor

No UserContext

If no instance is passed to ExecutionOptions.UserContext:

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<string, object>.

Trigger validation

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

public class Query :
    ObjectGraphType
{
    public Query() =>
        Field<ResultGraph>("inputQuery")
            .Argument<MyInputGraph>("input")
            .Resolve(context =>
                {
                    var input = context.GetValidatedArgument<MyInput>("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. GraphQL.NET has a concept of validation rules 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:

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<Startup>();
        return new(builder);
    }
}

snippet source | anchor

Unit

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

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<string, ArgumentValue>
            {
                {
                    "input", new ArgumentValue(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 value = new Dictionary<string, object>();
        var fieldContext = new ResolveFieldContext
        {
            Arguments = new Dictionary<string, ArgumentValue>
            {
                {
                    "input", new ArgumentValue(value, ArgumentSource.Variable)
                }
            },
            UserContext = userContext
        };
        var exception = Assert.Throws<ValidationException>(
            () => field.Resolver!.ResolveAsync(fieldContext));
        return Verify(exception.Message);
    }
}

snippet source | anchor

Icon

Shield designed by Maxim Kulikov from The Noun Project

About

Add FluentValidation support to GraphQL.net

License:MIT License


Languages

Language:C# 100.0%