Git Product home page Git Product logo

graphql.validation's Introduction

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

graphql.validation's People

Contributors

actions-user avatar benmccallum avatar danielcweber avatar dependabot-preview[bot] avatar dependabot-support avatar dependabot[bot] avatar faketogosamogo avatar mbcrawfo avatar simoncropp avatar sungam3r avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

graphql.validation's Issues

Make ArgumentValidation public

Hi,
it would be great if ArgumentValidation would be public: that would allow to use the library in a broader context - for example, with the widely used GraphQL.Conventions-library.

Support DI in validators

Is the feature request related to a problem

Validators are creating using Activator.CreateInstance and as such don't support DI (via ctor injection) of dependencies.

Describe the solution

Extend the ValidatorTypeCache to support DI scenarios. Rough notes, but I'd need to try if it'd work.

  • Add an overload for AddValidatorsFromAssembly that takes a factory method to resolve a validator
    ** Instead of instantiating the types it finds upfront and storing them in the type cache, it'd store (still against each type to be validated) the type of the validator that was found, to be resolved instead via DI later.
    ** Stores the factory method for use later via TryGetValidators
  • Update the TryGetValidators method to use that factory method if it's not null, else does normal behaviour to other type cache.

It's likely the factory method would need to have an argument of the graph context, so most of us could abuse the UserContext for resolving that in the right scope. That would probably mean a new overload for TryGetValidators that accepts the context. Haven't traced that back yet, but hopefully that'd be possible.

Describe alternatives considered

Potentially we could have a completely different ValidatorTypeCache implementation for DI that is swapped in?

Additional context

I need this to support a validation scenario where I need to pull additional info async in the validator. I'm sure this will come up for others too eventually, e.g. validate email isn't used on createUser kinda mutation or something.

Validation on non-object argument types

Is the feature request related to a problem

I imagine there will be scenarios where we will want to validate an argument (or multiple arguments) at the root level of a query field. This library is awesome, but obviously has the limitation that everything needs to be behind an InputGraphType, aligned with a fluent IValidator, that can then be fired when the arg is requested in the resolver.

Describe the solution

Similar to how the dotnet-graphql/server has an Authorization project and each field can be annotated with an .AuthorizeWith(...), could we have something like that as an extension method off argument declarations in fields?

It would I guess add the validator behind a cache (like the existing type validator cache) for retrieval when needed, perhaps keyed by type:argumentName and then could be retrieved and fired in the context.GetValidatedArgument<T>(...) you've already implemented.

Describe alternatives considered

It could be that basic scalar validation by GraphQL-dotnet given it's typed to some degree is enough. I'm trying to think of concrete examples where I'd need this in a query and not a mutation. Perhaps something like a query that looks up users by email, and you'd want to ensure that the string arg of the email is actually even an email before you go deep into resolving and down into your app layers.

Obviously I could wrap all my query args in an input object, but I'm not sure I wanna do that. I use GitHub's api as a bit of a best practise, and they don't do that for all their queries, all the args at root-level on the fields.

Unable to resolve the Validator

Greetings,

I just cloned the code and when running it, I get the following error:

System.InvalidOperationException: 'Cannot resolve scoped service 'MyInputValidator' from root provider.'

image

I'm on the latest SDK (6.0.301) as specified in global.json

Extension methods off IResolveFieldContext now

Is the feature request related to a problem

A recent GraphQL PR that was merged now means that resolvers are given the interface IResolveFieldContext or IResolveFieldContext<T> rather than the concrete type.

Describe the solution

We'll need to update the extension methods in this project to extend the interface instead now.
In fact, since IResolveFieldContext<T> inherits IResolveFieldContext<T>, we potentially can get rid of the duplication of the extensions.

Describe alternatives considered

Temporary workaround (in my own code) is to have wrapping extension methods that just internally cast to the concrete and then use the existing methods in here off that. I'm not using any different context types, so it doesn't bother me atm.

Additional context

N/A.

Make ArgumentValidation public for manual calling

Is the feature request related to a problem

Inability to fire the validation manually in other situations. Specifically, to unravel input args with properties that themselves are complex objects, a solution has been proposed here that utilises JSON deserialization rather than GetArgument<T>(). I'd like to use that technique, but also be able to call fluent validation after I've got my object.

Describe the solution

I essentially just need to be able to call the following:
ArgumentValidation.Validate(context.GetCache(), typeof(TArgument), argument, context.UserContext);, but ArgumentValidation is private. Can you make it public?

Describe alternatives considered

In the meantime I tried to pull in some of these types into my solution, but got to a point where that wasn't gonna work. So not sure of any alternatives.

Additional context

N/A.

Validation not firing

Is the feature request related to a problem

I'm trying to hook up the validation as per docs but validation isn't rejecting the input and getting to my resolver. I'm not sure whether the validation is supposed to fire as part of the middleware and never get to my resolver or whether the call to GetValidatedArgument is the only thing that triggers the validation.

Describe the solution

Update readme to make this clearer. In the meantime I'll take a look at the src for the middleware.

Cheers, and thanks for a great library I'm sure we'll get a lot of use out of.

Dont work validation for different names.

I have model:
class Test{
public int TestId{ get; set; }
}

in my input type field TestId named by test_id:

Field<NonNullGraphType>("test_id");

in validator:
RuleFor(b => b.TestId).GreaterThan(-1);

and validation for this field not working.

Clarify the term "Validation"

Hi. I have long wanted to review this project. Finally, I found the time for this. I mainly support graphql-dotnet and server projects. Nevertheless, I need to understand and know the ecosystem as a whole around these projects to understand the needs of developers. Thank you for creating and supporting this project to integrate graphql-dotnet with FluentValidation.

I read the readme and source code. Now I have a suggestion to explicitly say in the readme that 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. Here validation occurs at the execution stage. It is very important. The reader may have the wrong impression of the functionality (as it was with me). At first I thought that I would find validation rules here that would work before request execution stage.

Fix GraphQL version to match NuGet versions

Is the feature request related to a problem

Currently this library requires GraphQL preview >= 1630 which is a version that is only available through the MyGet feed. Lowering this requirement will allow people to use this without adding new nuget sources. Adding new nuget sources is often times difficult with build pipelines or even disallowed in certain organizations.

Describe the solution

Lower minimum library version dependency.

Describe alternatives considered

N/A

Additional context

N/A

How i can change exceptions.

Hellow.
I need the field in which the exception occurred to be written in the Path of the exception.
How can i do this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.