├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml ├── stale.yml └── workflows │ ├── merge-dependabot.yml │ └── on-push-do-docs.yml ├── .gitignore ├── code_of_conduct.md ├── license.txt ├── readme.md └── src ├── .editorconfig ├── .gitattributes ├── Directory.Build.props ├── Directory.Packages.props ├── GraphQL.FluentValidation ├── ArgumentTypeCacheBag.cs ├── ArgumentValidation.cs ├── FluentValidationExtensions.cs ├── FluentValidationExtensions_GetArgument.cs ├── FluentValidationExtensions_GetArgumentAsync.cs ├── FluentValidationExtensions_UserContext.cs ├── GlobalUsings.cs ├── GraphQL.FluentValidation.csproj ├── IValidatorCache.cs ├── InternalsVisibleTo.cs ├── ModuleInitializer.cs ├── TypeComparer.cs ├── ValidationMiddleware.cs ├── ValidatorCacheExtensions.cs ├── ValidatorInstanceCache.cs └── ValidatorServiceCache.cs ├── GraphQL.Validation.sln ├── GraphQL.Validation.sln.DotSettings ├── SampleWeb.Tests ├── GraphQLControllerTests.RunQuery.verified.txt ├── GraphQLControllerTests.cs ├── ModuleInitializer.cs ├── QueryTests.RunInputQuery.verified.txt ├── QueryTests.RunInvalidInputQuery.verified.txt ├── QueryTests.cs └── SampleWeb.Tests.csproj ├── SampleWeb ├── GraphQlUserContext.cs ├── Graphs │ ├── MyInput.cs │ ├── MyInputGraph.cs │ ├── MyInputValidator.cs │ ├── Result.cs │ └── ResultGraph.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Query.cs ├── SampleWeb.csproj ├── Schema.cs ├── Startup.cs └── ValidatorCacheBuilder.cs ├── Shared.sln.DotSettings ├── Tests ├── Arguments │ ├── AsyncComplexInput.cs │ ├── AsyncComplexInputGraph.cs │ ├── AsyncComplexInputValidator.cs │ ├── AsyncInput.cs │ ├── AsyncInputGraph.cs │ ├── AsyncInputValidator.cs │ ├── ComplexInput.cs │ ├── ComplexInputGraph.cs │ ├── ComplexInputInner.cs │ ├── ComplexInputInnerGraph.cs │ ├── ComplexInputInnerValidator.cs │ ├── ComplexInputListItem.cs │ ├── ComplexInputListItemGraph.cs │ ├── ComplexInputListItemValidator.cs │ ├── ComplexInputValidator.cs │ ├── DerivedComplexInput.cs │ ├── DerivedComplexInputGraph.cs │ ├── Input.cs │ ├── InputGraph.cs │ ├── InputValidator.cs │ ├── NoEmptyConstructorValidator.cs │ ├── NoValidatorInput.cs │ ├── NoValidatorInputGraph.cs │ └── NoValidatorInputValidator.cs ├── GlobalUsings.cs ├── IntegrationTests.AsyncComplexInvalid.verified.txt ├── IntegrationTests.AsyncComplexValid.verified.txt ├── IntegrationTests.AsyncInvalid.verified.txt ├── IntegrationTests.AsyncValid.verified.txt ├── IntegrationTests.ComplexInvalid.verified.txt ├── IntegrationTests.ComplexInvalid2.verified.txt ├── IntegrationTests.ComplexValid.verified.txt ├── IntegrationTests.DerivedComplexInvalid.verified.txt ├── IntegrationTests.GetCurrentValidators.verified.txt ├── IntegrationTests.Invalid.verified.txt ├── IntegrationTests.NoValidatorInvalid.verified.txt ├── IntegrationTests.NoValidatorValid.verified.txt ├── IntegrationTests.Valid.verified.txt ├── IntegrationTests.ValidNull.verified.txt ├── IntegrationTests.cs ├── ModuleInitializer.cs ├── Query.cs ├── QueryExecutor.cs ├── Result.cs ├── ResultGraph.cs ├── Schema.cs ├── Snippets │ └── QueryExecution.cs ├── TestConfig.cs └── Tests.csproj ├── appveyor.yml ├── global.json ├── icon.png ├── key.snk ├── mdsnippets.json └── nuget.config /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: SimonCropp -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug fix 3 | about: Create a bug fix to help us improve 4 | --- 5 | 6 | Note: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with "please read the issue template". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template. 7 | 8 | #### Preamble 9 | 10 | General questions may be better placed [StackOveflow](https://stackoverflow.com/). 11 | 12 | Where relevant, ensure you are using the current stable versions on your development stack. For example: 13 | 14 | * Visual Studio 15 | * [.NET SDK or .NET Core SDK](https://www.microsoft.com/net/download) 16 | * Any related NuGet packages 17 | 18 | Any code or stack traces must be properly formatted with [GitHub markdown](https://guides.github.com/features/mastering-markdown/). 19 | 20 | #### Describe the bug 21 | 22 | A clear and concise description of what the bug is. Include any relevant version information. 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | Add any other context about the problem here. 27 | 28 | #### Minimal Repro 29 | 30 | Ensure you have replicated the bug in a minimal solution with the fewest moving parts. Often this will help point to the true cause of the problem. Upload this repro as part of the issue, preferably a public GitHub repository or a downloadable zip. The repro will allow the maintainers of this project to smoke test the any fix. 31 | 32 | #### Submit a PR that fixes the bug 33 | 34 | Submit a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/) that fixes the bug. Include in this PR a test that verifies the fix. If you were not able to fix the bug, a PR that illustrates your partial progress will suffice. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: How to raise feature requests 4 | --- 5 | 6 | Note: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with "please read the issue template". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template. 7 | 8 | If you are certain the feature will be accepted, it is better to raise a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/). 9 | 10 | If you are uncertain if the feature will be accepted, outline the proposal below to confirm it is viable, prior to raising a PR that implements the feature. 11 | 12 | Note that even if the feature is a good idea and viable, it may not be accepted since the ongoing effort in maintaining the feature may outweigh the benefit it delivers. 13 | 14 | 15 | #### Is the feature request related to a problem 16 | 17 | A clear and concise description of what the problem is. 18 | 19 | #### Describe the solution 20 | 21 | A clear and concise proposal of how you intend to implement the feature. 22 | 23 | #### Describe alternatives considered 24 | 25 | A clear and concise description of any alternative solutions or features you've considered. 26 | 27 | #### Additional context 28 | 29 | Add any other context about the feature request here. 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/src" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 7 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Set to true to ignore issues in a milestone (defaults to false) 6 | exemptMilestones: true 7 | # Comment to post when marking an issue as stale. Set to `false` to disable 8 | markComment: > 9 | This issue has been automatically marked as stale because it has not had 10 | recent activity. It will be closed if no further activity occurs. Thank you 11 | for your contributions. 12 | # Comment to post when closing a stale issue. Set to `false` to disable 13 | closeComment: false 14 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 15 | pulls: 16 | daysUntilStale: 30 17 | exemptLabels: 18 | - Question 19 | - Bug 20 | - Feature 21 | - Improvement -------------------------------------------------------------------------------- /.github/workflows/merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: merge-dependabot 2 | on: 3 | pull_request: 4 | jobs: 5 | automerge: 6 | runs-on: ubuntu-latest 7 | if: github.actor == 'dependabot[bot]' 8 | steps: 9 | - name: Dependabot Auto Merge 10 | uses: ahmadnassri/action-dependabot-auto-merge@v2.6.6 11 | with: 12 | target: minor 13 | github-token: ${{ secrets.dependabot }} 14 | command: squash and merge -------------------------------------------------------------------------------- /.github/workflows/on-push-do-docs.yml: -------------------------------------------------------------------------------- 1 | name: on-push-do-docs 2 | on: 3 | push: 4 | jobs: 5 | docs: 6 | runs-on: windows-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Run MarkdownSnippets 10 | run: | 11 | dotnet tool install --global MarkdownSnippets.Tool 12 | mdsnippets ${GITHUB_WORKSPACE} 13 | shell: bash 14 | - name: Push changes 15 | run: | 16 | git config --local user.email "action@github.com" 17 | git config --local user.name "GitHub Action" 18 | git commit -m "Docs changes" -a || echo "nothing to commit" 19 | remote="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git" 20 | branch="${GITHUB_REF:11}" 21 | git push "${remote}" ${branch} || echo "nothing to push" 22 | shell: bash -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.user 3 | bin/ 4 | obj/ 5 | .vs/ 6 | *.DotSettings.user 7 | .idea/ 8 | *.received.* 9 | nugets/ -------------------------------------------------------------------------------- /code_of_conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at simon.cropp@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Simon Cropp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # GraphQL.Validation 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/wvk8wm3n227b2b3q/branch/main?svg=true)](https://ci.appveyor.com/project/SimonCropp/graphql-validation) 4 | [![NuGet Status](https://img.shields.io/nuget/v/GraphQL.FluentValidation.svg)](https://www.nuget.org/packages/GraphQL.FluentValidation/) 5 | 6 | Add [FluentValidation](https://fluentvalidation.net/) support to [GraphQL.net](https://github.com/graphql-dotnet/graphql-dotnet) 7 | 8 | **See [Milestones](../../milestones?state=closed) for release notes.** 9 | 10 | 11 | ### Powered by 12 | 13 | [![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSourceSupport) 14 | 15 | 16 | ## NuGet package 17 | 18 | https://nuget.org/packages/GraphQL.FluentValidation/ 19 | 20 | 21 | ## Usage 22 | 23 | 24 | ### Define validators 25 | 26 | Given the following input: 27 | 28 | 29 | 30 | ```cs 31 | public class MyInput 32 | { 33 | public string Content { get; set; } = null!; 34 | } 35 | ``` 36 | snippet source | anchor 37 | 38 | 39 | And graph: 40 | 41 | 42 | 43 | ```cs 44 | public class MyInputGraph : 45 | InputObjectGraphType 46 | { 47 | public MyInputGraph() => 48 | Field("content"); 49 | } 50 | ``` 51 | snippet source | anchor 52 | 53 | 54 | A custom validator can be defined as follows: 55 | 56 | 57 | 58 | ```cs 59 | public class MyInputValidator : 60 | AbstractValidator 61 | { 62 | public MyInputValidator() => 63 | RuleFor(_ => _.Content) 64 | .NotEmpty(); 65 | } 66 | ``` 67 | snippet source | anchor 68 | 69 | 70 | 71 | ### Setup Validators 72 | 73 | Validators need to be added to the `ValidatorTypeCache`. This should be done once at application startup. 74 | 75 | 76 | 77 | ```cs 78 | var validatorCache = new ValidatorInstanceCache(); 79 | validatorCache.AddValidatorsFromAssembly(assemblyContainingValidators); 80 | var schema = new Schema(); 81 | schema.UseFluentValidation(); 82 | var executer = new DocumentExecuter(); 83 | ``` 84 | snippet source | anchor 85 | 86 | 87 | Generally `ValidatorTypeCache` is scoped per app and can be collocated with `Schema`, `DocumentExecuter` initialization. 88 | 89 | Dependency Injection can be used for validators. Create a `ValidatorTypeCache` with the 90 | `useDependencyInjection: true` parameter and call one of the `AddValidatorsFrom*` methods from 91 | [FluentValidation.DependencyInjectionExtensions](https://www.nuget.org/packages/FluentValidation.DependencyInjectionExtensions/) 92 | package in the `Startup`. By default, validators are added to the DI container with a transient lifetime. 93 | 94 | 95 | ### Add to ExecutionOptions 96 | 97 | Validation needs to be added to any instance of `ExecutionOptions`. 98 | 99 | 100 | 101 | ```cs 102 | var options = new ExecutionOptions 103 | { 104 | Schema = schema, 105 | Query = queryString, 106 | Variables = inputs 107 | }; 108 | options.UseFluentValidation(validatorCache); 109 | 110 | var executionResult = await executer.ExecuteAsync(options); 111 | ``` 112 | snippet source | anchor 113 | 114 | 115 | 116 | ### UserContext must be a dictionary 117 | 118 | 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: 119 | 120 | 121 | #### 1. Have the user context class implement IDictionary 122 | 123 | Given a user context class of the following form: 124 | 125 | 126 | 127 | ```cs 128 | public class MyUserContext(string myProperty) : 129 | Dictionary 130 | { 131 | public string MyProperty { get; } = myProperty; 132 | } 133 | ``` 134 | snippet source | anchor 135 | 136 | 137 | The `ExecutionOptions.UserContext` can then be set as follows: 138 | 139 | 140 | 141 | ```cs 142 | var options = new ExecutionOptions 143 | { 144 | Schema = schema, 145 | Query = queryString, 146 | Variables = inputs, 147 | UserContext = new MyUserContext 148 | ( 149 | myProperty: "the value" 150 | ) 151 | }; 152 | options.UseFluentValidation(validatorCache); 153 | ``` 154 | snippet source | anchor 155 | 156 | 157 | 158 | #### 2. Have the user context class exist inside a IDictionary 159 | 160 | 161 | 162 | ```cs 163 | var options = new ExecutionOptions 164 | { 165 | Schema = schema, 166 | Query = queryString, 167 | Variables = inputs, 168 | UserContext = new Dictionary 169 | { 170 | { 171 | "MyUserContext", 172 | new MyUserContext 173 | ( 174 | myProperty: "the value" 175 | ) 176 | } 177 | } 178 | }; 179 | options.UseFluentValidation(validatorCache); 180 | ``` 181 | snippet source | anchor 182 | 183 | 184 | 185 | #### No UserContext 186 | 187 | If no instance is passed to `ExecutionOptions.UserContext`: 188 | 189 | 190 | 191 | ```cs 192 | var options = new ExecutionOptions 193 | { 194 | Schema = schema, 195 | Query = queryString, 196 | Variables = inputs 197 | }; 198 | options.UseFluentValidation(validatorCache); 199 | ``` 200 | snippet source | anchor 201 | 202 | 203 | Then the `UseFluentValidation` method will instantiate it to a new `Dictionary`. 204 | 205 | 206 | ### Trigger validation 207 | 208 | To trigger the validation, when reading arguments use `GetValidatedArgument` instead of `GetArgument`: 209 | 210 | 211 | 212 | ```cs 213 | public class Query : 214 | ObjectGraphType 215 | { 216 | public Query() => 217 | Field("inputQuery") 218 | .Argument("input") 219 | .Resolve(context => 220 | { 221 | var input = context.GetValidatedArgument("input"); 222 | return new Result 223 | { 224 | Data = input.Content 225 | }; 226 | } 227 | ); 228 | } 229 | ``` 230 | snippet source | anchor 231 | 232 | 233 | 234 | ### Difference from IValidationRule 235 | 236 | The validation implemented in this project has nothing to do with the validation of the incoming GraphQL 237 | request, which is described in the [official specification](http://spec.graphql.org/June2018/#sec-Validation). 238 | [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) 239 | that would work **before** request execution stage. In this project validation occurs for input arguments 240 | **at the request execution stage**. This additional validation complements but does not replace the standard 241 | set of validation rules. 242 | 243 | 244 | ## Testing 245 | 246 | ### Integration 247 | 248 | A full end-to-en test can be run against the GraphQL controller: 249 | 250 | 251 | 252 | ```cs 253 | public class GraphQLControllerTests 254 | { 255 | [Fact] 256 | public async Task RunQuery() 257 | { 258 | using var server = GetTestServer(); 259 | using var client = server.CreateClient(); 260 | var query = """ 261 | { 262 | inputQuery(input: {content: "TheContent"}) { 263 | data 264 | } 265 | } 266 | """; 267 | var body = new 268 | { 269 | query 270 | }; 271 | var serialized = JsonConvert.SerializeObject(body); 272 | using var content = new StringContent( 273 | serialized, 274 | Encoding.UTF8, 275 | "application/json"); 276 | using var request = new HttpRequestMessage(HttpMethod.Post, "graphql") 277 | { 278 | Content = content 279 | }; 280 | using var response = await client.SendAsync(request); 281 | await Verify(response); 282 | } 283 | 284 | static TestServer GetTestServer() 285 | { 286 | var builder = new WebHostBuilder(); 287 | builder.UseStartup(); 288 | return new(builder); 289 | } 290 | } 291 | ``` 292 | snippet source | anchor 293 | 294 | 295 | 296 | ### Unit 297 | 298 | Unit tests can be run a specific field of a query: 299 | 300 | 301 | 302 | ```cs 303 | public class QueryTests 304 | { 305 | [Fact] 306 | public async Task RunInputQuery() 307 | { 308 | var field = new Query().GetField("inputQuery")!; 309 | 310 | var userContext = new GraphQLUserContext(); 311 | FluentValidationExtensions.AddCacheToContext( 312 | userContext, 313 | ValidatorCacheBuilder.Instance); 314 | 315 | var input = new MyInput 316 | { 317 | Content = "TheContent" 318 | }; 319 | var fieldContext = new ResolveFieldContext 320 | { 321 | Arguments = new Dictionary 322 | { 323 | { 324 | "input", new(input, ArgumentSource.Variable) 325 | } 326 | }, 327 | UserContext = userContext 328 | }; 329 | var result = await field.Resolver!.ResolveAsync(fieldContext); 330 | await Verify(result); 331 | } 332 | 333 | [Fact] 334 | public Task RunInvalidInputQuery() 335 | { 336 | Thread.CurrentThread.CurrentUICulture = new("en-US"); 337 | var field = new Query().GetField("inputQuery")!; 338 | 339 | var userContext = new GraphQLUserContext(); 340 | FluentValidationExtensions.AddCacheToContext( 341 | userContext, 342 | ValidatorCacheBuilder.Instance); 343 | 344 | var input = new MyInput 345 | { 346 | Content = null! 347 | }; 348 | var fieldContext = new ResolveFieldContext 349 | { 350 | Arguments = new Dictionary 351 | { 352 | { 353 | "input", new(input, ArgumentSource.Variable) 354 | } 355 | }, 356 | UserContext = userContext 357 | }; 358 | var exception = Assert.Throws( 359 | () => field.Resolver!.ResolveAsync(fieldContext)); 360 | return Verify(exception.Message); 361 | } 362 | } 363 | ``` 364 | snippet source | anchor 365 | 366 | 367 | 368 | ## Icon 369 | 370 | [Shield](https://thenounproject.com/term/shield/1893182/) designed by [Maxim Kulikov](https://thenounproject.com/maxim221/) from [The Noun Project](https://thenounproject.com) 371 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | 6 | [*.cs] 7 | indent_size = 4 8 | charset = utf-8 9 | 10 | # Redundant accessor body 11 | resharper_redundant_accessor_body_highlighting = error 12 | 13 | # Replace with field keyword 14 | resharper_replace_with_field_keyword_highlighting = error 15 | 16 | # Replace with single call to Single(..) 17 | resharper_replace_with_single_call_to_single_highlighting = error 18 | 19 | # Replace with single call to SingleOrDefault(..) 20 | resharper_replace_with_single_call_to_single_or_default_highlighting = error 21 | 22 | # Replace with single call to LastOrDefault(..) 23 | resharper_replace_with_single_call_to_last_or_default_highlighting = error 24 | 25 | # Replace with single call to Last(..) 26 | resharper_replace_with_single_call_to_last_highlighting = error 27 | 28 | # Replace with single call to First(..) 29 | resharper_replace_with_single_call_to_first_highlighting = error 30 | 31 | # Replace with single call to FirstOrDefault(..) 32 | resharper_replace_with_single_call_to_first_or_default_highlighting = error 33 | 34 | # Replace with single call to Any(..) 35 | resharper_replace_with_single_call_to_any_highlighting = error 36 | 37 | # Replace with single call to Count(..) 38 | resharper_replace_with_single_call_to_count_highlighting = error 39 | 40 | # Declare types in namespaces 41 | dotnet_diagnostic.CA1050.severity = none 42 | 43 | # Use Literals Where Appropriate 44 | dotnet_diagnostic.CA1802.severity = error 45 | 46 | # Template should be a static expression 47 | dotnet_diagnostic.CA2254.severity = error 48 | 49 | # Potentially misleading parameter name in lambda or local function 50 | resharper_all_underscore_local_parameter_name_highlighting = none 51 | 52 | # Redundant explicit collection creation in argument of 'params' parameter 53 | resharper_redundant_explicit_params_array_creation_highlighting = error 54 | 55 | # Do not initialize unnecessarily 56 | dotnet_diagnostic.CA1805.severity = error 57 | 58 | # Avoid unsealed attributes 59 | dotnet_diagnostic.CA1813.severity = error 60 | 61 | # Test for empty strings using string length 62 | dotnet_diagnostic.CA1820.severity = none 63 | 64 | # Remove empty finalizers 65 | dotnet_diagnostic.CA1821.severity = error 66 | 67 | # Mark members as static 68 | dotnet_diagnostic.CA1822.severity = error 69 | 70 | # Avoid unused private fields 71 | dotnet_diagnostic.CA1823.severity = error 72 | 73 | # Avoid zero-length array allocations 74 | dotnet_diagnostic.CA1825.severity = error 75 | 76 | # Use property instead of Linq Enumerable method 77 | dotnet_diagnostic.CA1826.severity = error 78 | 79 | # Do not use Count()/LongCount() when Any() can be used 80 | dotnet_diagnostic.CA1827.severity = error 81 | dotnet_diagnostic.CA1828.severity = error 82 | 83 | # Use Length/Count property instead of Enumerable.Count method 84 | dotnet_diagnostic.CA1829.severity = error 85 | 86 | # Prefer strongly-typed Append and Insert method overloads on StringBuilder 87 | dotnet_diagnostic.CA1830.severity = error 88 | 89 | # Use AsSpan instead of Range-based indexers for string when appropriate 90 | dotnet_diagnostic.CA1831.severity = error 91 | 92 | # Use AsSpan instead of Range-based indexers for string when appropriate 93 | dotnet_diagnostic.CA1831.severity = error 94 | dotnet_diagnostic.CA1832.severity = error 95 | dotnet_diagnostic.CA1833.severity = error 96 | 97 | # Use StringBuilder.Append(char) for single character strings 98 | dotnet_diagnostic.CA1834.severity = error 99 | 100 | # Prefer IsEmpty over Count when available 101 | dotnet_diagnostic.CA1836.severity = error 102 | 103 | # Prefer IsEmpty over Count when available 104 | dotnet_diagnostic.CA1836.severity = error 105 | 106 | # Use Environment.ProcessId instead of Process.GetCurrentProcess().Id 107 | dotnet_diagnostic.CA1837.severity = error 108 | 109 | # Use Environment.ProcessPath instead of Process.GetCurrentProcess().MainModule.FileName 110 | dotnet_diagnostic.CA1839.severity = error 111 | 112 | # Use Environment.CurrentManagedThreadId instead of Thread.CurrentThread.ManagedThreadId 113 | dotnet_diagnostic.CA1840.severity = error 114 | 115 | # Prefer Dictionary Contains methods 116 | dotnet_diagnostic.CA1841.severity = error 117 | 118 | # Do not use WhenAll with a single task 119 | dotnet_diagnostic.CA1842.severity = error 120 | 121 | # Do not use WhenAll/WaitAll with a single task 122 | dotnet_diagnostic.CA1842.severity = error 123 | dotnet_diagnostic.CA1843.severity = error 124 | 125 | # Use span-based 'string.Concat' 126 | dotnet_diagnostic.CA1845.severity = error 127 | 128 | # Prefer AsSpan over Substring 129 | dotnet_diagnostic.CA1846.severity = error 130 | 131 | # Use string.Contains(char) instead of string.Contains(string) with single characters 132 | dotnet_diagnostic.CA1847.severity = error 133 | 134 | # Prefer static HashData method over ComputeHash 135 | dotnet_diagnostic.CA1850.severity = error 136 | 137 | # Possible multiple enumerations of IEnumerable collection 138 | dotnet_diagnostic.CA1851.severity = error 139 | 140 | # Unnecessary call to Dictionary.ContainsKey(key) 141 | dotnet_diagnostic.CA1853.severity = error 142 | 143 | # Prefer the IDictionary.TryGetValue(TKey, out TValue) method 144 | dotnet_diagnostic.CA1854.severity = error 145 | 146 | # Use Span.Clear() instead of Span.Fill() 147 | dotnet_diagnostic.CA1855.severity = error 148 | 149 | # Incorrect usage of ConstantExpected attribute 150 | dotnet_diagnostic.CA1856.severity = error 151 | 152 | # The parameter expects a constant for optimal performance 153 | dotnet_diagnostic.CA1857.severity = error 154 | 155 | # Use StartsWith instead of IndexOf 156 | dotnet_diagnostic.CA1858.severity = error 157 | 158 | # Avoid using Enumerable.Any() extension method 159 | dotnet_diagnostic.CA1860.severity = error 160 | 161 | # Avoid constant arrays as arguments 162 | dotnet_diagnostic.CA1861.severity = error 163 | 164 | # Use the StringComparison method overloads to perform case-insensitive string comparisons 165 | dotnet_diagnostic.CA1862.severity = error 166 | 167 | # Prefer the IDictionary.TryAdd(TKey, TValue) method 168 | dotnet_diagnostic.CA1864.severity = error 169 | 170 | # Use string.Method(char) instead of string.Method(string) for string with single char 171 | dotnet_diagnostic.CA1865.severity = error 172 | dotnet_diagnostic.CA1866.severity = error 173 | dotnet_diagnostic.CA1867.severity = error 174 | 175 | # Unnecessary call to 'Contains' for sets 176 | dotnet_diagnostic.CA1868.severity = error 177 | 178 | # Cache and reuse 'JsonSerializerOptions' instances 179 | dotnet_diagnostic.CA1869.severity = error 180 | 181 | # Use a cached 'SearchValues' instance 182 | dotnet_diagnostic.CA1870.severity = error 183 | 184 | # Microsoft .NET properties 185 | trim_trailing_whitespace = true 186 | csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion 187 | resharper_namespace_body = file_scoped 188 | dotnet_naming_rule.private_constants_rule.severity = warning 189 | dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style 190 | dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols 191 | dotnet_naming_rule.private_instance_fields_rule.severity = warning 192 | dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style 193 | dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols 194 | dotnet_naming_rule.private_static_fields_rule.severity = warning 195 | dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style 196 | dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols 197 | dotnet_naming_rule.private_static_readonly_rule.severity = warning 198 | dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style 199 | dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols 200 | dotnet_naming_style.lower_camel_case_style.capitalization = camel_case 201 | dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case 202 | dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private 203 | dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field 204 | dotnet_naming_symbols.private_constants_symbols.required_modifiers = const 205 | dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private 206 | dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field 207 | dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private 208 | dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field 209 | dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static 210 | dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private 211 | dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field 212 | dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly 213 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none 214 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none 215 | dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none 216 | 217 | # ReSharper properties 218 | resharper_object_creation_when_type_not_evident = target_typed 219 | 220 | # ReSharper inspection severities 221 | resharper_arrange_object_creation_when_type_evident_highlighting = error 222 | resharper_arrange_object_creation_when_type_not_evident_highlighting = error 223 | resharper_arrange_redundant_parentheses_highlighting = error 224 | resharper_arrange_static_member_qualifier_highlighting = error 225 | resharper_arrange_this_qualifier_highlighting = error 226 | resharper_arrange_type_member_modifiers_highlighting = none 227 | resharper_built_in_type_reference_style_for_member_access_highlighting = hint 228 | resharper_built_in_type_reference_style_highlighting = hint 229 | resharper_check_namespace_highlighting = none 230 | resharper_convert_to_using_declaration_highlighting = error 231 | resharper_css_not_resolved_highlighting = warning 232 | resharper_field_can_be_made_read_only_local_highlighting = none 233 | resharper_merge_into_logical_pattern_highlighting = warning 234 | resharper_merge_into_pattern_highlighting = error 235 | resharper_method_has_async_overload_highlighting = warning 236 | # because stop rider giving errors before source generators have run 237 | resharper_partial_type_with_single_part_highlighting = warning 238 | resharper_redundant_base_qualifier_highlighting = warning 239 | resharper_redundant_cast_highlighting = error 240 | resharper_redundant_empty_object_creation_argument_list_highlighting = error 241 | resharper_redundant_empty_object_or_collection_initializer_highlighting = error 242 | resharper_redundant_name_qualifier_highlighting = error 243 | resharper_redundant_suppress_nullable_warning_expression_highlighting = error 244 | resharper_redundant_using_directive_highlighting = error 245 | resharper_redundant_verbatim_string_prefix_highlighting = error 246 | resharper_redundant_lambda_signature_parentheses_highlighting = error 247 | resharper_replace_substring_with_range_indexer_highlighting = warning 248 | resharper_suggest_var_or_type_built_in_types_highlighting = error 249 | resharper_suggest_var_or_type_elsewhere_highlighting = error 250 | resharper_suggest_var_or_type_simple_types_highlighting = error 251 | resharper_unnecessary_whitespace_highlighting = error 252 | resharper_use_await_using_highlighting = warning 253 | resharper_use_deconstruction_highlighting = warning 254 | 255 | # Sort using and Import directives with System.* appearing first 256 | dotnet_sort_system_directives_first = true 257 | 258 | # Avoid "this." and "Me." if not necessary 259 | dotnet_style_qualification_for_field = false:error 260 | dotnet_style_qualification_for_property = false:error 261 | dotnet_style_qualification_for_method = false:error 262 | dotnet_style_qualification_for_event = false:error 263 | 264 | # Use language keywords instead of framework type names for type references 265 | dotnet_style_predefined_type_for_locals_parameters_members = true:error 266 | dotnet_style_predefined_type_for_member_access = true:error 267 | 268 | # Suggest more modern language features when available 269 | dotnet_style_object_initializer = true:error 270 | dotnet_style_collection_initializer = true:error 271 | dotnet_style_coalesce_expression = false:error 272 | dotnet_style_null_propagation = true:error 273 | dotnet_style_explicit_tuple_names = true:error 274 | 275 | # Prefer "var" everywhere 276 | csharp_style_var_for_built_in_types = true:error 277 | csharp_style_var_when_type_is_apparent = true:error 278 | csharp_style_var_elsewhere = true:error 279 | 280 | # Prefer method-like constructs to have a block body 281 | csharp_style_expression_bodied_methods = true:error 282 | csharp_style_expression_bodied_local_functions = true:error 283 | csharp_style_expression_bodied_constructors = true:error 284 | csharp_style_expression_bodied_operators = true:error 285 | resharper_place_expr_method_on_single_line = false 286 | 287 | # Prefer property-like constructs to have an expression-body 288 | csharp_style_expression_bodied_properties = true:error 289 | csharp_style_expression_bodied_indexers = true:error 290 | csharp_style_expression_bodied_accessors = true:error 291 | 292 | # Suggest more modern language features when available 293 | csharp_style_pattern_matching_over_is_with_cast_check = true:error 294 | csharp_style_pattern_matching_over_as_with_null_check = true:error 295 | csharp_style_inlined_variable_declaration = true:suggestion 296 | csharp_style_throw_expression = true:suggestion 297 | csharp_style_conditional_delegate_call = true:suggestion 298 | 299 | # Newline settings 300 | #csharp_new_line_before_open_brace = all:error 301 | resharper_max_array_initializer_elements_on_line = 1 302 | csharp_new_line_before_else = true 303 | csharp_new_line_before_catch = true 304 | csharp_new_line_before_finally = true 305 | csharp_new_line_before_members_in_object_initializers = true 306 | csharp_new_line_before_members_in_anonymous_types = true 307 | resharper_wrap_before_first_type_parameter_constraint = true 308 | resharper_wrap_extends_list_style = chop_always 309 | resharper_wrap_after_dot_in_method_calls = false 310 | resharper_wrap_before_binary_pattern_op = false 311 | resharper_wrap_object_and_collection_initializer_style = chop_always 312 | resharper_place_simple_initializer_on_single_line = false 313 | 314 | # space 315 | resharper_space_around_lambda_arrow = true 316 | 317 | dotnet_style_require_accessibility_modifiers = never:error 318 | resharper_place_type_constraints_on_same_line = false 319 | resharper_blank_lines_inside_namespace = 0 320 | resharper_blank_lines_after_file_scoped_namespace_directive = 1 321 | resharper_blank_lines_inside_type = 0 322 | 323 | insert_final_newline = false 324 | resharper_place_attribute_on_same_line = false 325 | resharper_space_around_lambda_arrow = true 326 | resharper_place_constructor_initializer_on_same_line = false 327 | 328 | #braces https://www.jetbrains.com/help/resharper/EditorConfig_CSHARP_CSharpCodeStylePageImplSchema.html#Braces 329 | resharper_braces_for_ifelse = required 330 | resharper_braces_for_foreach = required 331 | resharper_braces_for_while = required 332 | resharper_braces_for_dowhile = required 333 | resharper_braces_for_lock = required 334 | resharper_braces_for_fixed = required 335 | resharper_braces_for_for = required 336 | 337 | resharper_return_value_of_pure_method_is_not_used_highlighting = error 338 | 339 | resharper_all_underscore_local_parameter_name_highlighting = none 340 | 341 | resharper_misleading_body_like_statement_highlighting = error 342 | 343 | resharper_redundant_record_class_keyword_highlighting = error 344 | 345 | resharper_redundant_extends_list_entry_highlighting = error 346 | 347 | # Xml files 348 | [*.{xml,config,nuspec,resx,vsixmanifest,csproj,targets,props,fsproj}] 349 | indent_size = 2 350 | # https://www.jetbrains.com/help/resharper/EditorConfig_XML_XmlCodeStylePageSchema.html#resharper_xml_blank_line_after_pi 351 | resharper_blank_line_after_pi = false 352 | resharper_space_before_self_closing = true 353 | ij_xml_space_inside_empty_tag = true 354 | 355 | [*.json] 356 | indent_size = 2 357 | 358 | # Verify settings 359 | [*.{received,verified}.{txt,xml,json,md,sql,csv,html,htm,md}] 360 | charset = utf-8-bom 361 | end_of_line = lf 362 | indent_size = unset 363 | indent_style = unset 364 | insert_final_newline = false 365 | tab_width = unset 366 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/.gitattributes: -------------------------------------------------------------------------------- 1 | * text 2 | *.snk binary 3 | *.png binary 4 | 5 | *.verified.txt text eol=lf working-tree-encoding=UTF-8 6 | *.verified.xml text eol=lf working-tree-encoding=UTF-8 7 | *.verified.json text eol=lf working-tree-encoding=UTF-8 8 | 9 | .editorconfig text eol=lf working-tree-encoding=UTF-8 10 | Shared.sln.DotSettings text eol=lf working-tree-encoding=UTF-8 -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10.0.0 4 | 1.0.0 5 | preview 6 | GraphQL, Validation, FluentValidation 7 | Add FluentValidation (https://fluentvalidation.net/) support to GraphQL.net (https://github.com/graphql-dotnet/graphql-dotnet) 8 | CS1591;NU1608;NU1109 9 | true 10 | true 11 | true 12 | true 13 | 14 | -------------------------------------------------------------------------------- /src/Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/ArgumentTypeCacheBag.cs: -------------------------------------------------------------------------------- 1 | static class ArgumentTypeCacheBag 2 | { 3 | const string key = "GraphQL.FluentValidation.ValidatorTypeCache"; 4 | 5 | public static void SetCache(this ExecutionOptions options, IValidatorCache cache) 6 | { 7 | // ReSharper disable once ConditionIsAlwaysTrueOrFalse 8 | // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract 9 | if (options.UserContext == null) 10 | { 11 | options.UserContext = new Dictionary 12 | { 13 | { key, cache } 14 | }; 15 | return; 16 | } 17 | 18 | AddValidatorCache(options.UserContext, cache); 19 | } 20 | 21 | internal static void AddValidatorCache(this IDictionary dictionary, IValidatorCache cache) => 22 | dictionary.Add(key, cache); 23 | 24 | public static IValidatorCache GetCache(this IResolveFieldContext context) 25 | { 26 | var userContext = context.UserContext; 27 | if (userContext == null) 28 | { 29 | throw new("Expected UserContext to be of type IDictionary."); 30 | } 31 | 32 | if (!userContext.TryGetValue(key, out var result)) 33 | { 34 | throw new($"Could not extract {nameof(IValidatorCache)} from {nameof(IResolveFieldContext)}.{nameof(IResolveFieldContext.UserContext)}. It is possible {nameof(FluentValidationExtensions)}.{nameof(FluentValidationExtensions.UseFluentValidation)} was not used."); 35 | } 36 | 37 | return (IValidatorCache)result!; 38 | } 39 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/ArgumentValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.Results; 2 | 3 | namespace GraphQL.FluentValidation; 4 | 5 | /// 6 | /// Low level validation API for extensibility scenarios. 7 | /// 8 | public static class ArgumentValidation 9 | { 10 | /// 11 | /// Validate an instance 12 | /// 13 | public static Task ValidateAsync(IValidatorCache cache, Type type, TArgument instance, IDictionary userContext) 14 | => ValidateAsync(cache, type, instance, userContext, null); 15 | 16 | /// 17 | /// Validate an instance 18 | /// 19 | public static async Task ValidateAsync(IValidatorCache cache, Type type, TArgument instance, IDictionary userContext, IServiceProvider? provider, Cancel cancel = default) 20 | { 21 | var currentType = (Type?)type; 22 | var validationContext = default(ValidationContext); 23 | 24 | while (currentType != null) 25 | { 26 | if (cache.TryGetValidators(currentType, provider, out var buildAll)) 27 | { 28 | validationContext ??= BuildValidationContext(instance, userContext); 29 | 30 | var tasks = buildAll.Select(_ => _.ValidateAsync(validationContext, cancel)); 31 | var validationResults = await Task.WhenAll(tasks); 32 | 33 | var results = validationResults 34 | .SelectMany(result => result.Errors); 35 | 36 | ThrowIfResults(results); 37 | } 38 | 39 | currentType = currentType.BaseType; 40 | } 41 | } 42 | 43 | /// 44 | /// Validate an instance 45 | /// 46 | public static void Validate(IValidatorCache cache, Type type, TArgument instance, IDictionary userContext) 47 | => Validate(cache, type, instance, userContext, null); 48 | 49 | /// 50 | /// Validate an instance 51 | /// 52 | public static void Validate(IValidatorCache cache, Type type, TArgument instance, IDictionary userContext, IServiceProvider? provider) 53 | { 54 | if (instance == null) 55 | { 56 | return; 57 | } 58 | 59 | var currentType = (Type?)type; 60 | var validationContext = default(ValidationContext); 61 | 62 | while (currentType != null) 63 | { 64 | if (cache.TryGetValidators(currentType, provider, out var buildAll)) 65 | { 66 | validationContext ??= BuildValidationContext(instance, userContext); 67 | var results = buildAll 68 | .SelectMany(validator => validator.Validate(validationContext).Errors); 69 | 70 | ThrowIfResults(results); 71 | } 72 | 73 | currentType = currentType.BaseType; 74 | } 75 | } 76 | 77 | static void ThrowIfResults(IEnumerable results) 78 | { 79 | var list = results.ToList(); 80 | if (list.Count != 0) 81 | { 82 | throw new ValidationException(list); 83 | } 84 | } 85 | 86 | static ValidationContext BuildValidationContext(TArgument instance, IDictionary userContext) 87 | { 88 | ValidationContext validationContext = new(instance); 89 | validationContext.RootContextData.Add("UserContext", userContext); 90 | return validationContext; 91 | } 92 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/FluentValidationExtensions.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Instrumentation; 2 | using GraphQL.Types; 3 | 4 | namespace GraphQL; 5 | 6 | /// 7 | /// Extensions to GraphQL to enable FluentValidation. 8 | /// 9 | public static partial class FluentValidationExtensions 10 | { 11 | /// 12 | /// Adds a FieldMiddleware to the GraphQL pipeline that converts a to s./> 13 | /// 14 | public static ExecutionOptions UseFluentValidation(this ExecutionOptions executionOptions, IValidatorCache validatorTypeCache) 15 | { 16 | validatorTypeCache.Freeze(); 17 | executionOptions.SetCache(validatorTypeCache); 18 | return executionOptions; 19 | } 20 | 21 | /// 22 | /// Adds a FieldMiddleware to the GraphQL pipeline that converts a to s./> 23 | /// 24 | public static void UseFluentValidation(this ISchema schema) => 25 | schema.FieldMiddleware.Use(new ValidationMiddleware()); 26 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/FluentValidationExtensions_GetArgument.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL; 2 | 3 | public static partial class FluentValidationExtensions 4 | { 5 | /// 6 | /// Wraps to validate the resulting argument instance. 7 | /// Uses to perform validation. 8 | /// If a occurs it will be converted to s by a field middleware. 9 | /// 10 | public static TArgument GetValidatedArgument(this IResolveFieldContext context, string name) => 11 | GetValidatedArgument(context, name, default!); 12 | 13 | /// 14 | /// Wraps to validate the resulting argument instance. 15 | /// Uses to perform validation. 16 | /// If a occurs it will be converted to s by a field middleware. 17 | /// 18 | public static TArgument GetValidatedArgument(this IResolveFieldContext context, string name, TArgument defaultValue) 19 | { 20 | var argument = context.GetArgument(name, defaultValue); 21 | var validatorCache = context.GetCache(); 22 | ArgumentValidation.Validate(validatorCache, typeof(TArgument), argument, context.UserContext, context.RequestServices); 23 | return argument!; 24 | } 25 | 26 | /// 27 | /// Wraps to validate the resulting argument instance. 28 | /// Uses to perform validation. 29 | /// If a occurs it will be converted to s by a field middleware. 30 | /// 31 | public static object GetValidatedArgument(this IResolveFieldContext context, Type argumentType, string name) => 32 | GetValidatedArgument(context, argumentType, name, null!); 33 | 34 | /// 35 | /// Wraps to validate the resulting argument instance. 36 | /// Uses to perform validation. 37 | /// If a occurs it will be converted to s by a field middleware. 38 | /// 39 | public static object GetValidatedArgument(this IResolveFieldContext context, Type argumentType, string name, object defaultValue) 40 | { 41 | var argument = context.GetArgument(argumentType, name, defaultValue); 42 | var validatorCache = context.GetCache(); 43 | ArgumentValidation.Validate(validatorCache, argumentType, argument, context.UserContext, context.RequestServices); 44 | return argument!; 45 | } 46 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/FluentValidationExtensions_GetArgumentAsync.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL; 2 | 3 | public static partial class FluentValidationExtensions 4 | { 5 | /// 6 | /// Wraps to validate the resulting argument instance. 7 | /// Uses to perform validation. 8 | /// If a occurs it will be converted to s by a field middleware. 9 | /// 10 | public static Task GetValidatedArgumentAsync(this IResolveFieldContext context, string name) => 11 | GetValidatedArgumentAsync(context, name, default!); 12 | 13 | /// 14 | /// Wraps to validate the resulting argument instance. 15 | /// Uses to perform validation. 16 | /// If a occurs it will be converted to s by a field middleware. 17 | /// 18 | public static async Task GetValidatedArgumentAsync(this IResolveFieldContext context, string name, TArgument defaultValue) 19 | { 20 | var argument = context.GetArgument(name, defaultValue); 21 | var validatorCache = context.GetCache(); 22 | await ArgumentValidation.ValidateAsync(validatorCache, typeof(TArgument), argument, context.UserContext, context.RequestServices); 23 | return argument!; 24 | } 25 | 26 | /// 27 | /// Wraps to validate the resulting argument instance. 28 | /// Uses to perform validation. 29 | /// If a occurs it will be converted to s by a field middleware. 30 | /// 31 | public static Task GetValidatedArgumentAsync(this IResolveFieldContext context, Type argumentType, string name) => 32 | GetValidatedArgumentAsync(context, argumentType, name, default!); 33 | 34 | /// 35 | /// Wraps to validate the resulting argument instance. 36 | /// Uses to perform validation. 37 | /// If a occurs it will be converted to s by a field middleware. 38 | /// 39 | public static async Task GetValidatedArgumentAsync(this IResolveFieldContext context, Type argumentType, string name, object defaultValue) 40 | { 41 | var argument = context.GetArgument(argumentType, name, defaultValue); 42 | var validatorCache = context.GetCache(); 43 | await ArgumentValidation.ValidateAsync(validatorCache, argumentType, argument, context.UserContext, context.RequestServices); 44 | return argument!; 45 | } 46 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/FluentValidationExtensions_UserContext.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL; 2 | 3 | public static partial class FluentValidationExtensions 4 | { 5 | /// 6 | /// When performing validation the instance 7 | /// will be added to with an key of "UserContext". 8 | /// During validation this instance can be retrieved from using this method. 9 | /// 10 | public static T UserContext(this IValidationContext validationContext) => 11 | (T)validationContext.RootContextData["UserContext"]; 12 | 13 | /// 14 | /// Injects a instance into a user context for testing purposes. 15 | /// 16 | public static void AddCacheToContext(T userContext, IValidatorCache cache) 17 | where T : Dictionary => 18 | userContext.AddValidatorCache(cache); 19 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Diagnostics.CodeAnalysis; 2 | global using FluentValidation; 3 | global using GraphQL; 4 | global using GraphQL.FluentValidation; 5 | global using Microsoft.Extensions.DependencyInjection; -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/GraphQL.FluentValidation.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/IValidatorCache.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.FluentValidation; 2 | 3 | public interface IValidatorCache 4 | { 5 | bool IsFrozen { get; } 6 | void Freeze(); 7 | bool TryGetValidators(Type argumentType, IServiceProvider? provider, [NotNullWhen(true)] out IEnumerable? validators); 8 | void AddResult(AssemblyScanner.AssemblyScanResult result); 9 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/InternalsVisibleTo.cs: -------------------------------------------------------------------------------- 1 | [assembly:InternalsVisibleTo("SampleWeb.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100638A43140E8A1271C1453DF1379E64B40B67A1F333864C1AEF5AC318A0FA2008545C3D35A82EF005EDF0DE1AD1E1EA155722FE289DF0E462F78C40A668CBC96D7BE1D487FAEF5714A54BB4E57909C86B3924C2DB6D55CCF59939B99EB0CAB6E8A91429BA0CE630C08A319B323BDDCBBD509F1AFE4AE77A6CBB8B447F588FEBC3")] 2 | [assembly:InternalsVisibleTo("GraphQL.FluentValidation.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100638A43140E8A1271C1453DF1379E64B40B67A1F333864C1AEF5AC318A0FA2008545C3D35A82EF005EDF0DE1AD1E1EA155722FE289DF0E462F78C40A668CBC96D7BE1D487FAEF5714A54BB4E57909C86B3924C2DB6D55CCF59939B99EB0CAB6E8A91429BA0CE630C08A319B323BDDCBBD509F1AFE4AE77A6CBB8B447F588FEBC3")] -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/ModuleInitializer.cs: -------------------------------------------------------------------------------- 1 | static class ModuleInitializer 2 | { 3 | [ModuleInitializer] 4 | public static void Initialize() => 5 | ValidatorOptions.Global.DisplayNameResolver = ValidatorOptions.Global.PropertyNameResolver; 6 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/TypeComparer.cs: -------------------------------------------------------------------------------- 1 | class TypeComparer : 2 | IComparer 3 | { 4 | public int Compare(Type? x, Type? y) => 5 | x?.FullName?.CompareTo(y?.FullName) ?? 0; 6 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/ValidationMiddleware.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.Results; 2 | using GraphQL.Instrumentation; 3 | 4 | class ValidationMiddleware : 5 | IFieldMiddleware 6 | { 7 | static ExecutionError ToExecutionError(ValidationFailure failure) => 8 | new($"{failure.PropertyName}: {failure.ErrorMessage}") 9 | { 10 | Path = [failure.PropertyName], 11 | Code = failure.ErrorCode 12 | }; 13 | 14 | public async ValueTask ResolveAsync(IResolveFieldContext context, FieldMiddlewareDelegate next) 15 | { 16 | try 17 | { 18 | return await next(context); 19 | } 20 | catch (ValidationException validationException) 21 | { 22 | context.Errors.AddRange(validationException.Errors.Select(ToExecutionError)); 23 | 24 | return null; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/ValidatorCacheExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.FluentValidation; 2 | 3 | public static class ValidatorCacheExtensions 4 | { 5 | /// 6 | /// Add all s from the assembly that contains . 7 | /// 8 | public static IValidatorCache AddValidatorsFromAssemblyContaining(this IValidatorCache cache, bool throwIfNoneFound = true) => 9 | AddValidatorsFromAssemblyContaining(cache, typeof(T), throwIfNoneFound); 10 | 11 | /// 12 | /// Add all s from the assembly that contains . 13 | /// 14 | public static IValidatorCache AddValidatorsFromAssemblyContaining(this IValidatorCache cache, Type type, bool throwIfNoneFound = true) => 15 | AddValidatorsFromAssembly(cache, type.Assembly, throwIfNoneFound); 16 | 17 | /// 18 | /// Add all s in . 19 | /// 20 | public static IValidatorCache AddValidatorsFromAssembly(this IValidatorCache cache, Assembly assembly, bool throwIfNoneFound = true) 21 | { 22 | if (cache.IsFrozen) 23 | { 24 | throw new InvalidOperationException($"{nameof(IValidatorCache)} cannot be changed once it has been used. Use a new instance instead."); 25 | } 26 | 27 | var results = AssemblyScanner.FindValidatorsInAssembly(assembly).ToList(); 28 | if (results.Count == 0) 29 | { 30 | if (throwIfNoneFound) 31 | { 32 | throw new($"No validators were found in {assembly.GetName().Name}."); 33 | } 34 | 35 | return cache; 36 | } 37 | 38 | foreach (var result in results) 39 | { 40 | var validatorType = result.ValidatorType; 41 | if (validatorType.IsAbstract) 42 | { 43 | continue; 44 | } 45 | 46 | cache.AddResult(result); 47 | } 48 | 49 | return cache; 50 | } 51 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/ValidatorInstanceCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace GraphQL.FluentValidation; 4 | 5 | /// 6 | /// Cache for all instances. 7 | /// Should only be configured once at startup time. 8 | /// Uses to create s. 9 | /// 10 | public class ValidatorInstanceCache(Func? fallback = null) : 11 | IValidatorCache 12 | { 13 | ConcurrentDictionary> cache = []; 14 | 15 | static TypeComparer typeComparer = new(); 16 | 17 | public ImmutableSortedDictionary> GetCurrentValidators() 18 | { 19 | var dictionary = new SortedDictionary>(typeComparer); 20 | foreach (var item in cache) 21 | { 22 | dictionary.Add(item.Key, item.Value); 23 | } 24 | 25 | return dictionary.ToImmutableSortedDictionary(typeComparer); 26 | } 27 | 28 | public bool IsFrozen { get; private set; } 29 | 30 | public void Freeze() => 31 | IsFrozen = true; 32 | 33 | public bool TryGetValidators(Type argumentType, IServiceProvider? provider, [NotNullWhen(true)] out IEnumerable? validators) 34 | { 35 | var list = cache.GetOrAdd( 36 | argumentType, 37 | type => 38 | { 39 | var validator = fallback?.Invoke(type); 40 | if (validator == null) 41 | { 42 | return []; 43 | } 44 | 45 | return [validator]; 46 | }); 47 | 48 | validators = list; 49 | return list.Count != 0; 50 | } 51 | 52 | public void AddResult(AssemblyScanner.AssemblyScanResult result) 53 | { 54 | if (result.ValidatorType.GetConstructor([]) == null) 55 | { 56 | Trace.WriteLine($"Ignoring ''{result.ValidatorType.FullName}'' since it does not have a public parameterless constructor."); 57 | return; 58 | } 59 | 60 | var single = result.InterfaceType.GenericTypeArguments.Single(); 61 | if (!cache.TryGetValue(single, out var list)) 62 | { 63 | cache[single] = list = []; 64 | } 65 | 66 | list.Add((IValidator)Activator.CreateInstance(result.ValidatorType, true)!); 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/GraphQL.FluentValidation/ValidatorServiceCache.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.FluentValidation; 2 | 3 | /// 4 | /// Cache for all . 5 | /// Should only be configured once at startup time. 6 | /// Uses for resolving s. 7 | /// 8 | public class ValidatorServiceCache : IValidatorCache 9 | { 10 | Dictionary> cache = []; 11 | 12 | public bool IsFrozen { get; private set; } 13 | 14 | public void Freeze() => 15 | IsFrozen = true; 16 | 17 | public bool TryGetValidators(Type argumentType, IServiceProvider? provider, [NotNullWhen(true)] out IEnumerable? validators) 18 | { 19 | if (cache.TryGetValue(argumentType, out var validatorInfo)) 20 | { 21 | validators = validatorInfo.Select(_ => (IValidator)provider!.GetRequiredService(_)); 22 | return true; 23 | } 24 | 25 | validators = null; 26 | return false; 27 | } 28 | 29 | public void AddResult(AssemblyScanner.AssemblyScanResult result) 30 | { 31 | var single = result.InterfaceType.GenericTypeArguments.Single(); 32 | if (!cache.TryGetValue(single, out var list)) 33 | { 34 | cache[single] = list = []; 35 | } 36 | 37 | list.Add(result.ValidatorType); 38 | } 39 | } -------------------------------------------------------------------------------- /src/GraphQL.Validation.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 16 3 | VisualStudioVersion = 16.0.29201.188 4 | MinimumVisualStudioVersion = 16.0.29201.188 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.FluentValidation", "GraphQL.FluentValidation\GraphQL.FluentValidation.csproj", "{8D87D482-BC30-49F6-946C-73DD62FC4EC0}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{49CBE4DA-DFE0-4320-A82A-8BC11FCBA14D}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C1E7C253-4815-48DA-8871-B0ADB1A46A7D}" 10 | ProjectSection(SolutionItems) = preProject 11 | .editorconfig = .editorconfig 12 | .gitattributes = .gitattributes 13 | ..\.gitignore = ..\.gitignore 14 | appveyor.yml = appveyor.yml 15 | Directory.Build.props = Directory.Build.props 16 | Directory.Packages.props = Directory.Packages.props 17 | icon.png = icon.png 18 | ..\license.txt = ..\license.txt 19 | mdsnippets.json = mdsnippets.json 20 | ..\readme.md = ..\readme.md 21 | EndProjectSection 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleWeb", "SampleWeb\SampleWeb.csproj", "{AA587D92-8312-4B26-88AB-0EF4FA49828B}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleWeb.Tests", "SampleWeb.Tests\SampleWeb.Tests.csproj", "{B7CE2386-EA76-463B-912B-9C6941A82F3A}" 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {8D87D482-BC30-49F6-946C-73DD62FC4EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {8D87D482-BC30-49F6-946C-73DD62FC4EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {8D87D482-BC30-49F6-946C-73DD62FC4EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {8D87D482-BC30-49F6-946C-73DD62FC4EC0}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {49CBE4DA-DFE0-4320-A82A-8BC11FCBA14D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {49CBE4DA-DFE0-4320-A82A-8BC11FCBA14D}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {49CBE4DA-DFE0-4320-A82A-8BC11FCBA14D}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {49CBE4DA-DFE0-4320-A82A-8BC11FCBA14D}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {AA587D92-8312-4B26-88AB-0EF4FA49828B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {AA587D92-8312-4B26-88AB-0EF4FA49828B}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {AA587D92-8312-4B26-88AB-0EF4FA49828B}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {AA587D92-8312-4B26-88AB-0EF4FA49828B}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {B7CE2386-EA76-463B-912B-9C6941A82F3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {B7CE2386-EA76-463B-912B-9C6941A82F3A}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {B7CE2386-EA76-463B-912B-9C6941A82F3A}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {B7CE2386-EA76-463B-912B-9C6941A82F3A}.Release|Any CPU.Build.0 = Release|Any CPU 49 | EndGlobalSection 50 | GlobalSection(SolutionProperties) = preSolution 51 | HideSolutionNode = FALSE 52 | EndGlobalSection 53 | GlobalSection(ExtensibilityGlobals) = postSolution 54 | SolutionGuid = {2810F44A-7EA7-41FC-90E6-B95D30A66EFA} 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /src/GraphQL.Validation.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | ..\Shared.sln.DotSettings 3 | True 4 | True 5 | 1 6 | -------------------------------------------------------------------------------- /src/SampleWeb.Tests/GraphQLControllerTests.RunQuery.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Status: 200 OK, 3 | Content: { 4 | Headers: { 5 | Content-Type: application/graphql-response+json; charset=utf-8 6 | }, 7 | Value: { 8 | data: { 9 | inputQuery: { 10 | data: TheContent 11 | } 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/SampleWeb.Tests/GraphQLControllerTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.TestHost; 3 | using Newtonsoft.Json; 4 | 5 | #region GraphQLControllerTests 6 | 7 | public class GraphQLControllerTests 8 | { 9 | [Fact] 10 | public async Task RunQuery() 11 | { 12 | using var server = GetTestServer(); 13 | using var client = server.CreateClient(); 14 | var query = """ 15 | { 16 | inputQuery(input: {content: "TheContent"}) { 17 | data 18 | } 19 | } 20 | """; 21 | var body = new 22 | { 23 | query 24 | }; 25 | var serialized = JsonConvert.SerializeObject(body); 26 | using var content = new StringContent( 27 | serialized, 28 | Encoding.UTF8, 29 | "application/json"); 30 | using var request = new HttpRequestMessage(HttpMethod.Post, "graphql") 31 | { 32 | Content = content 33 | }; 34 | using var response = await client.SendAsync(request); 35 | await Verify(response); 36 | } 37 | 38 | static TestServer GetTestServer() 39 | { 40 | var builder = new WebHostBuilder(); 41 | builder.UseStartup(); 42 | return new(builder); 43 | } 44 | } 45 | 46 | #endregion -------------------------------------------------------------------------------- /src/SampleWeb.Tests/ModuleInitializer.cs: -------------------------------------------------------------------------------- 1 | public static class ModuleInitializer 2 | { 3 | [ModuleInitializer] 4 | public static void Initialize() 5 | { 6 | VerifierSettings.InitializePlugins(); 7 | VerifierSettings.IgnoreMembers("Content-Length"); 8 | } 9 | } -------------------------------------------------------------------------------- /src/SampleWeb.Tests/QueryTests.RunInputQuery.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Data: TheContent 3 | } -------------------------------------------------------------------------------- /src/SampleWeb.Tests/QueryTests.RunInvalidInputQuery.verified.txt: -------------------------------------------------------------------------------- 1 | Validation failed: 2 | -- Content: 'Content' must not be empty. Severity: Error -------------------------------------------------------------------------------- /src/SampleWeb.Tests/QueryTests.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using GraphQL; 3 | using GraphQL.Execution; 4 | 5 | #region QueryTests 6 | 7 | public class QueryTests 8 | { 9 | [Fact] 10 | public async Task RunInputQuery() 11 | { 12 | var field = new Query().GetField("inputQuery")!; 13 | 14 | var userContext = new GraphQLUserContext(); 15 | FluentValidationExtensions.AddCacheToContext( 16 | userContext, 17 | ValidatorCacheBuilder.Instance); 18 | 19 | var input = new MyInput 20 | { 21 | Content = "TheContent" 22 | }; 23 | var fieldContext = new ResolveFieldContext 24 | { 25 | Arguments = new Dictionary 26 | { 27 | { 28 | "input", new(input, ArgumentSource.Variable) 29 | } 30 | }, 31 | UserContext = userContext 32 | }; 33 | var result = await field.Resolver!.ResolveAsync(fieldContext); 34 | await Verify(result); 35 | } 36 | 37 | [Fact] 38 | public Task RunInvalidInputQuery() 39 | { 40 | Thread.CurrentThread.CurrentUICulture = new("en-US"); 41 | var field = new Query().GetField("inputQuery")!; 42 | 43 | var userContext = new GraphQLUserContext(); 44 | FluentValidationExtensions.AddCacheToContext( 45 | userContext, 46 | ValidatorCacheBuilder.Instance); 47 | 48 | var input = new MyInput 49 | { 50 | Content = null! 51 | }; 52 | var fieldContext = new ResolveFieldContext 53 | { 54 | Arguments = new Dictionary 55 | { 56 | { 57 | "input", new(input, ArgumentSource.Variable) 58 | } 59 | }, 60 | UserContext = userContext 61 | }; 62 | var exception = Assert.Throws( 63 | () => field.Resolver!.ResolveAsync(fieldContext)); 64 | return Verify(exception.Message); 65 | } 66 | } 67 | 68 | #endregion -------------------------------------------------------------------------------- /src/SampleWeb.Tests/SampleWeb.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | Exe 5 | $(NoWarn);xUnit1051 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/SampleWeb/GraphQlUserContext.cs: -------------------------------------------------------------------------------- 1 | public class GraphQLUserContext : 2 | Dictionary; -------------------------------------------------------------------------------- /src/SampleWeb/Graphs/MyInput.cs: -------------------------------------------------------------------------------- 1 | #region input 2 | 3 | public class MyInput 4 | { 5 | public string Content { get; set; } = null!; 6 | } 7 | 8 | #endregion -------------------------------------------------------------------------------- /src/SampleWeb/Graphs/MyInputGraph.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | #region Graph 4 | public class MyInputGraph : 5 | InputObjectGraphType 6 | { 7 | public MyInputGraph() => 8 | Field("content"); 9 | } 10 | #endregion -------------------------------------------------------------------------------- /src/SampleWeb/Graphs/MyInputValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | #region validator 4 | public class MyInputValidator : 5 | AbstractValidator 6 | { 7 | public MyInputValidator() => 8 | RuleFor(_ => _.Content) 9 | .NotEmpty(); 10 | } 11 | #endregion -------------------------------------------------------------------------------- /src/SampleWeb/Graphs/Result.cs: -------------------------------------------------------------------------------- 1 | public class Result 2 | { 3 | public string? Data { get; set; } 4 | } -------------------------------------------------------------------------------- /src/SampleWeb/Graphs/ResultGraph.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | public class ResultGraph : 4 | ObjectGraphType 5 | { 6 | public ResultGraph() => 7 | Field(h => h.Data); 8 | } -------------------------------------------------------------------------------- /src/SampleWeb/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | 3 | WebHost.CreateDefaultBuilder() 4 | .UseStartup() 5 | .Build().Run(); -------------------------------------------------------------------------------- /src/SampleWeb/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5000", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "SampleWeb": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "launchUrl": "graphiql", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | }, 18 | "applicationUrl": "http://localhost:5000" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/SampleWeb/Query.cs: -------------------------------------------------------------------------------- 1 | using GraphQL; 2 | using GraphQL.Types; 3 | 4 | #region GetValidatedArgument 5 | 6 | public class Query : 7 | ObjectGraphType 8 | { 9 | public Query() => 10 | Field("inputQuery") 11 | .Argument("input") 12 | .Resolve(context => 13 | { 14 | var input = context.GetValidatedArgument("input"); 15 | return new Result 16 | { 17 | Data = input.Content 18 | }; 19 | } 20 | ); 21 | } 22 | 23 | #endregion -------------------------------------------------------------------------------- /src/SampleWeb/SampleWeb.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/SampleWeb/Schema.cs: -------------------------------------------------------------------------------- 1 | public class Schema : GraphQL.Types.Schema 2 | { 3 | public Schema(IServiceProvider serviceProvider, Query query) : 4 | base(serviceProvider) 5 | { 6 | RegisterTypeMapping(typeof(MyInput), typeof(MyInputGraph)); 7 | RegisterTypeMapping(typeof(Result), typeof(ResultGraph)); 8 | Query = query; 9 | } 10 | } -------------------------------------------------------------------------------- /src/SampleWeb/Startup.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using GraphQL; 3 | using GraphQL.Types; 4 | 5 | public class Startup 6 | { 7 | public void ConfigureServices(IServiceCollection services) 8 | { 9 | foreach (var type in GetGraphQLTypes()) 10 | { 11 | services.AddSingleton(type); 12 | } 13 | 14 | services.AddValidatorsFromAssemblyContaining(); 15 | services.AddGraphQL(builder => builder 16 | .AddSchema() 17 | .ConfigureExecutionOptions(options => 18 | { 19 | options.ThrowOnUnhandledException = true; 20 | options.UseFluentValidation(ValidatorCacheBuilder.InstanceDI); 21 | }) 22 | .AddSystemTextJson() 23 | .AddGraphTypes(typeof(Schema).Assembly)); 24 | } 25 | 26 | static IEnumerable GetGraphQLTypes() => 27 | typeof(Startup).Assembly 28 | .GetTypes() 29 | .Where(_ => !_.IsAbstract && 30 | (_.IsAssignableTo(typeof(IObjectGraphType)) || 31 | _.IsAssignableTo(typeof(IInputObjectGraphType)))); 32 | 33 | public void Configure(IApplicationBuilder builder) 34 | { 35 | builder.UseGraphQL(); 36 | builder.UseGraphQLGraphiQL(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/SampleWeb/ValidatorCacheBuilder.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.FluentValidation; 2 | 3 | public static class ValidatorCacheBuilder 4 | { 5 | public static ValidatorInstanceCache Instance; 6 | 7 | public static ValidatorServiceCache InstanceDI; 8 | 9 | static ValidatorCacheBuilder() 10 | { 11 | Instance = new(); 12 | Instance.AddValidatorsFromAssembly(typeof(Startup).Assembly); 13 | 14 | InstanceDI = new(); 15 | InstanceDI.AddValidatorsFromAssembly(typeof(Startup).Assembly); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Shared.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | False 3 | Quiet 4 | True 5 | True 6 | True 7 | DO_NOT_SHOW 8 | ERROR 9 | ERROR 10 | ERROR 11 | WARNING 12 | ERROR 13 | ERROR 14 | ERROR 15 | ERROR 16 | ERROR 17 | ERROR 18 | ERROR 19 | ERROR 20 | ERROR 21 | ERROR 22 | ERROR 23 | ERROR 24 | ERROR 25 | ERROR 26 | ERROR 27 | ERROR 28 | ERROR 29 | ERROR 30 | ERROR 31 | ERROR 32 | ERROR 33 | ERROR 34 | ERROR 35 | ERROR 36 | ERROR 37 | ERROR 38 | ERROR 39 | ERROR 40 | ERROR 41 | ERROR 42 | ERROR 43 | DO_NOT_SHOW 44 | DO_NOT_SHOW 45 | ERROR 46 | ERROR 47 | ERROR 48 | ERROR 49 | ERROR 50 | ERROR 51 | ERROR 52 | ERROR 53 | ERROR 54 | ERROR 55 | ERROR 56 | ERROR 57 | C90+,E79+,S14+ 58 | ERROR 59 | ERROR 60 | ERROR 61 | ERROR 62 | ERROR 63 | ERROR 64 | ERROR 65 | ERROR 66 | ERROR 67 | ERROR 68 | ERROR 69 | ERROR 70 | ERROR 71 | ERROR 72 | ERROR 73 | ERROR 74 | ERROR 75 | ERROR 76 | ERROR 77 | ERROR 78 | ERROR 79 | ERROR 80 | ERROR 81 | ERROR 82 | ERROR 83 | ERROR 84 | ERROR 85 | ERROR 86 | ERROR 87 | ERROR 88 | ERROR 89 | ERROR 90 | ERROR 91 | ERROR 92 | ERROR 93 | ERROR 94 | ERROR 95 | ERROR 96 | ERROR 97 | ERROR 98 | ERROR 99 | ERROR 100 | ERROR 101 | ERROR 102 | ERROR 103 | ERROR 104 | ERROR 105 | ERROR 106 | ERROR 107 | ERROR 108 | ERROR 109 | ERROR 110 | ERROR 111 | ERROR 112 | ERROR 113 | ERROR 114 | ERROR 115 | ERROR 116 | ERROR 117 | ERROR 118 | ERROR 119 | ERROR 120 | ERROR 121 | ERROR 122 | DO_NOT_SHOW 123 | *.received.* 124 | *.verified.* 125 | ERROR 126 | ERROR 127 | DO_NOT_SHOW 128 | ECMAScript 2016 129 | <?xml version="1.0" encoding="utf-16"?><Profile name="c# Cleanup"><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><CSCodeStyleAttributes ArrangeVarStyle="True" ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeCodeBodyStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" /><CssAlphabetizeProperties>True</CssAlphabetizeProperties><JSStringLiteralQuotesDescriptor>True</JSStringLiteralQuotesDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsInsertSemicolon>True</JsInsertSemicolon><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><HtmlReformatCode>True</HtmlReformatCode><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><IDEA_SETTINGS>&lt;profile version="1.0"&gt; 130 | &lt;option name="myName" value="c# Cleanup" /&gt; 131 | &lt;/profile&gt;</IDEA_SETTINGS><RIDER_SETTINGS>&lt;profile&gt; 132 | &lt;Language id="EditorConfig"&gt; 133 | &lt;Reformat&gt;false&lt;/Reformat&gt; 134 | &lt;/Language&gt; 135 | &lt;Language id="HTML"&gt; 136 | &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; 137 | &lt;Reformat&gt;false&lt;/Reformat&gt; 138 | &lt;Rearrange&gt;false&lt;/Rearrange&gt; 139 | &lt;/Language&gt; 140 | &lt;Language id="JSON"&gt; 141 | &lt;Reformat&gt;false&lt;/Reformat&gt; 142 | &lt;/Language&gt; 143 | &lt;Language id="RELAX-NG"&gt; 144 | &lt;Reformat&gt;false&lt;/Reformat&gt; 145 | &lt;/Language&gt; 146 | &lt;Language id="XML"&gt; 147 | &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; 148 | &lt;Reformat&gt;false&lt;/Reformat&gt; 149 | &lt;Rearrange&gt;false&lt;/Rearrange&gt; 150 | &lt;/Language&gt; 151 | &lt;/profile&gt;</RIDER_SETTINGS></Profile> 152 | ExpressionBody 153 | ExpressionBody 154 | ExpressionBody 155 | False 156 | NEVER 157 | NEVER 158 | False 159 | False 160 | False 161 | True 162 | False 163 | CHOP_ALWAYS 164 | False 165 | False 166 | RemoveIndent 167 | RemoveIndent 168 | False 169 | True 170 | True 171 | True 172 | True 173 | True 174 | ERROR 175 | DoNothing 176 | -------------------------------------------------------------------------------- /src/Tests/Arguments/AsyncComplexInput.cs: -------------------------------------------------------------------------------- 1 | public class AsyncComplexInput 2 | { 3 | public ComplexInputInner? Inner { get; set; } 4 | 5 | public List? Items { get; set; } 6 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/AsyncComplexInputGraph.cs: -------------------------------------------------------------------------------- 1 | public class AsyncComplexInputGraph : 2 | InputObjectGraphType 3 | { 4 | public AsyncComplexInputGraph() 5 | { 6 | Field("inner"); 7 | 8 | Field>>("items"); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/AsyncComplexInputValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | public class AsyncComplexInputValidator : 4 | AbstractValidator 5 | { 6 | public AsyncComplexInputValidator() => 7 | RuleFor(_ => _.Inner!) 8 | .NotEmpty() 9 | .MustAsync((o, _) => Task.FromResult(o != null && !string.IsNullOrWhiteSpace(o.Content))) 10 | .WithMessage("Inner async test failed msg.") 11 | .SetValidator(new ComplexInputInnerValidator()); 12 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/AsyncInput.cs: -------------------------------------------------------------------------------- 1 | public class AsyncInput 2 | { 3 | public string? Content { get; set; } 4 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/AsyncInputGraph.cs: -------------------------------------------------------------------------------- 1 | public class AsyncInputGraph : 2 | InputObjectGraphType 3 | { 4 | public AsyncInputGraph() => 5 | Field("content"); 6 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/AsyncInputValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | public class AsyncInputValidator : 4 | AbstractValidator 5 | { 6 | public AsyncInputValidator() => 7 | RuleFor(_ => _.Content) 8 | .MustAsync((s, _) => Task.FromResult(!string.IsNullOrWhiteSpace(s))); 9 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/ComplexInput.cs: -------------------------------------------------------------------------------- 1 | public class ComplexInput 2 | { 3 | public ComplexInputInner? Inner { get; set; } 4 | 5 | public List? Items { get; set; } 6 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/ComplexInputGraph.cs: -------------------------------------------------------------------------------- 1 | public class ComplexInputGraph : 2 | InputObjectGraphType 3 | { 4 | public ComplexInputGraph() 5 | { 6 | Field("inner"); 7 | 8 | Field>>("items"); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/ComplexInputInner.cs: -------------------------------------------------------------------------------- 1 | public class ComplexInputInner 2 | { 3 | public string? Content { get; set; } 4 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/ComplexInputInnerGraph.cs: -------------------------------------------------------------------------------- 1 | public class ComplexInputInnerGraph : 2 | InputObjectGraphType 3 | { 4 | public ComplexInputInnerGraph() => 5 | Field("content"); 6 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/ComplexInputInnerValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | public class ComplexInputInnerValidator : 4 | AbstractValidator 5 | { 6 | public ComplexInputInnerValidator() => 7 | RuleFor(_ => _.Content!) 8 | .NotEmpty(); 9 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/ComplexInputListItem.cs: -------------------------------------------------------------------------------- 1 | public class ComplexInputListItem 2 | { 3 | public int Id { get; set; } 4 | public string? Content { get; set; } 5 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/ComplexInputListItemGraph.cs: -------------------------------------------------------------------------------- 1 | public class ComplexInputListItemGraph : 2 | InputObjectGraphType 3 | { 4 | public ComplexInputListItemGraph() 5 | { 6 | Field>("id"); 7 | Field("content"); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/ComplexInputListItemValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | public class ComplexInputListItemValidator : 4 | AbstractValidator 5 | { 6 | public ComplexInputListItemValidator() 7 | { 8 | RuleFor(_ => _.Id) 9 | .NotEmpty(); 10 | 11 | RuleFor(_ => _.Content) 12 | .NotEmpty(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/ComplexInputValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | public class ComplexInputValidator : 4 | AbstractValidator 5 | { 6 | public ComplexInputValidator() 7 | { 8 | RuleFor(_ => _.Inner!) 9 | .NotEmpty() 10 | .SetValidator(new ComplexInputInnerValidator()); 11 | 12 | RuleFor(_ => _.Items) 13 | .NotEmpty() 14 | .ForEach(_ => _.SetValidator(new ComplexInputListItemValidator())); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/DerivedComplexInput.cs: -------------------------------------------------------------------------------- 1 | public class DerivedComplexInput : ComplexInput 2 | { 3 | public string? SomeProperty { get; set; } 4 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/DerivedComplexInputGraph.cs: -------------------------------------------------------------------------------- 1 | public class DerivedComplexInputGraph : 2 | InputObjectGraphType 3 | { 4 | public DerivedComplexInputGraph() 5 | { 6 | Field("inner"); 7 | 8 | Field>>("items"); 9 | 10 | Field("someProperty"); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/Input.cs: -------------------------------------------------------------------------------- 1 | public class Input 2 | { 3 | public string? Content { get; set; } 4 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/InputGraph.cs: -------------------------------------------------------------------------------- 1 | public class InputGraph : 2 | InputObjectGraphType 3 | { 4 | public InputGraph() => 5 | Field("content"); 6 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/InputValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | public class InputValidator : 4 | AbstractValidator 5 | { 6 | public InputValidator() => 7 | RuleFor(_ => _.Content) 8 | .NotEmpty(); 9 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/NoEmptyConstructorValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | // ReSharper disable UnusedParameter.Local 3 | 4 | public class NoEmptyConstructorValidator : 5 | AbstractValidator 6 | { 7 | public NoEmptyConstructorValidator(string foo) 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/NoValidatorInput.cs: -------------------------------------------------------------------------------- 1 | public class NoValidatorInput 2 | { 3 | public string? Content { get; set; } 4 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/NoValidatorInputGraph.cs: -------------------------------------------------------------------------------- 1 | public class NoValidatorInputGraph : 2 | InputObjectGraphType 3 | { 4 | public NoValidatorInputGraph() => 5 | Field("content"); 6 | } -------------------------------------------------------------------------------- /src/Tests/Arguments/NoValidatorInputValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | // ReSharper disable once UnusedTypeParameter 4 | public class NoValidatorInputValidator : 5 | AbstractValidator 6 | { 7 | public NoValidatorInputValidator() => 8 | RuleFor(_ => _.Content) 9 | .NotEmpty(); 10 | } -------------------------------------------------------------------------------- /src/Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using GraphQL.Types; -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.AsyncComplexInvalid.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | errors: [ 3 | { 4 | message: Inner: Inner async test failed msg., 5 | path: [ 6 | Inner 7 | ], 8 | extensions: { 9 | code: AsyncPredicateValidator, 10 | codes: [ 11 | AsyncPredicateValidator 12 | ] 13 | } 14 | }, 15 | { 16 | message: Inner.Content: 'Content' must not be empty., 17 | path: [ 18 | Inner.Content 19 | ], 20 | extensions: { 21 | code: NotEmptyValidator, 22 | codes: [ 23 | NotEmptyValidator 24 | ] 25 | } 26 | } 27 | ], 28 | data: { 29 | asyncComplexInputQuery: null 30 | } 31 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.AsyncComplexValid.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | data: { 3 | asyncComplexInputQuery: { 4 | data: TheContent 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.AsyncInvalid.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | errors: [ 3 | { 4 | message: Content: The specified condition was not met for 'Content'., 5 | path: [ 6 | Content 7 | ], 8 | extensions: { 9 | code: AsyncPredicateValidator, 10 | codes: [ 11 | AsyncPredicateValidator 12 | ] 13 | } 14 | } 15 | ], 16 | data: { 17 | asyncQuery: null 18 | } 19 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.AsyncValid.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | data: { 3 | asyncQuery: { 4 | data: TheContent 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.ComplexInvalid.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | errors: [ 3 | { 4 | message: Inner.Content: 'Content' must not be empty., 5 | path: [ 6 | Inner.Content 7 | ], 8 | extensions: { 9 | code: NotEmptyValidator, 10 | codes: [ 11 | NotEmptyValidator 12 | ] 13 | } 14 | }, 15 | { 16 | message: Items: 'Items' must not be empty., 17 | path: [ 18 | Items 19 | ], 20 | extensions: { 21 | code: NotEmptyValidator, 22 | codes: [ 23 | NotEmptyValidator 24 | ] 25 | } 26 | } 27 | ], 28 | data: { 29 | complexInputQuery: null 30 | } 31 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.ComplexInvalid2.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | errors: [ 3 | { 4 | message: Inner: 'Inner' must not be empty., 5 | path: [ 6 | Inner 7 | ], 8 | extensions: { 9 | code: NotEmptyValidator, 10 | codes: [ 11 | NotEmptyValidator 12 | ] 13 | } 14 | }, 15 | { 16 | message: Items: 'Items' must not be empty., 17 | path: [ 18 | Items 19 | ], 20 | extensions: { 21 | code: NotEmptyValidator, 22 | codes: [ 23 | NotEmptyValidator 24 | ] 25 | } 26 | } 27 | ], 28 | data: { 29 | complexInputQuery: null 30 | } 31 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.ComplexValid.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | data: { 3 | complexInputQuery: { 4 | data: {"Inner":{"Content":"TheContent"},"Items":[{"Id":1,"Content":"Some content 1"},{"Id":2,"Content":"Some content 2"}]} 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.DerivedComplexInvalid.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | errors: [ 3 | { 4 | message: Inner.Content: 'Content' must not be empty., 5 | path: [ 6 | Inner.Content 7 | ], 8 | extensions: { 9 | code: NotEmptyValidator, 10 | codes: [ 11 | NotEmptyValidator 12 | ] 13 | } 14 | }, 15 | { 16 | message: Items: 'Items' must not be empty., 17 | path: [ 18 | Items 19 | ], 20 | extensions: { 21 | code: NotEmptyValidator, 22 | codes: [ 23 | NotEmptyValidator 24 | ] 25 | } 26 | } 27 | ], 28 | data: { 29 | derivedComplexInputQuery: null 30 | } 31 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.GetCurrentValidators.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | Key: AsyncComplexInput, 4 | Value: [ 5 | [ 6 | { 7 | Components: [ 8 | { 9 | HasCondition: false, 10 | HasAsyncCondition: false, 11 | Validator: { 12 | Name: NotEmptyValidator 13 | } 14 | }, 15 | { 16 | HasCondition: false, 17 | HasAsyncCondition: false, 18 | Validator: { 19 | Name: AsyncPredicateValidator 20 | } 21 | }, 22 | { 23 | HasCondition: false, 24 | HasAsyncCondition: false, 25 | Validator: { 26 | Name: ChildValidatorAdaptor, 27 | ValidatorType: ComplexInputInnerValidator 28 | } 29 | } 30 | ], 31 | Member: AsyncComplexInput.Inner, 32 | PropertyFunc: { 33 | Type: Func, 34 | Target: PropertyRule.<>c__DisplayClass1_0, 35 | Method: ComplexInputInner Create(AsyncComplexInput) 36 | }, 37 | Expression: _ => _.Inner, 38 | Current: { 39 | HasCondition: false, 40 | HasAsyncCondition: false, 41 | Validator: { 42 | Name: ChildValidatorAdaptor, 43 | ValidatorType: ComplexInputInnerValidator 44 | } 45 | }, 46 | TypeToValidate: ComplexInputInner, 47 | HasCondition: false, 48 | HasAsyncCondition: false, 49 | PropertyName: Inner 50 | } 51 | ] 52 | ] 53 | }, 54 | { 55 | Key: AsyncInput, 56 | Value: [ 57 | [ 58 | { 59 | Components: [ 60 | { 61 | HasCondition: false, 62 | HasAsyncCondition: false, 63 | Validator: { 64 | Name: AsyncPredicateValidator 65 | } 66 | } 67 | ], 68 | Member: AsyncInput.Content, 69 | PropertyFunc: { 70 | Type: Func, 71 | Target: PropertyRule.<>c__DisplayClass1_0, 72 | Method: System.String Create(AsyncInput) 73 | }, 74 | Expression: _ => _.Content, 75 | Current: { 76 | HasCondition: false, 77 | HasAsyncCondition: false, 78 | Validator: { 79 | Name: AsyncPredicateValidator 80 | } 81 | }, 82 | TypeToValidate: string, 83 | HasCondition: false, 84 | HasAsyncCondition: false, 85 | PropertyName: Content 86 | } 87 | ] 88 | ] 89 | }, 90 | { 91 | Key: ComplexInput, 92 | Value: [ 93 | [ 94 | { 95 | Components: [ 96 | { 97 | HasCondition: false, 98 | HasAsyncCondition: false, 99 | Validator: { 100 | Name: NotEmptyValidator 101 | } 102 | }, 103 | { 104 | HasCondition: false, 105 | HasAsyncCondition: false, 106 | Validator: { 107 | Name: ChildValidatorAdaptor, 108 | ValidatorType: ComplexInputInnerValidator 109 | } 110 | } 111 | ], 112 | Member: ComplexInput.Inner, 113 | PropertyFunc: { 114 | Type: Func, 115 | Target: PropertyRule.<>c__DisplayClass1_0, 116 | Method: ComplexInputInner Create(ComplexInput) 117 | }, 118 | Expression: _ => _.Inner, 119 | Current: { 120 | HasCondition: false, 121 | HasAsyncCondition: false, 122 | Validator: { 123 | Name: ChildValidatorAdaptor, 124 | ValidatorType: ComplexInputInnerValidator 125 | } 126 | }, 127 | TypeToValidate: ComplexInputInner, 128 | HasCondition: false, 129 | HasAsyncCondition: false, 130 | PropertyName: Inner 131 | }, 132 | { 133 | Components: [ 134 | { 135 | HasCondition: false, 136 | HasAsyncCondition: false, 137 | Validator: { 138 | Name: NotEmptyValidator 139 | } 140 | }, 141 | { 142 | HasCondition: false, 143 | HasAsyncCondition: false, 144 | Validator: { 145 | Name: ChildValidatorAdaptor, 146 | ValidatorType: InlineValidator> 147 | } 148 | } 149 | ], 150 | Member: ComplexInput.Items, 151 | PropertyFunc: { 152 | Type: Func>, 153 | Target: PropertyRule>.<>c__DisplayClass1_0, 154 | Method: System.Collections.Generic.List`1[ComplexInputListItem] Create(ComplexInput) 155 | }, 156 | Expression: _ => _.Items, 157 | Current: { 158 | HasCondition: false, 159 | HasAsyncCondition: false, 160 | Validator: { 161 | Name: ChildValidatorAdaptor, 162 | ValidatorType: InlineValidator> 163 | } 164 | }, 165 | TypeToValidate: List, 166 | HasCondition: false, 167 | HasAsyncCondition: false, 168 | PropertyName: Items 169 | } 170 | ] 171 | ] 172 | }, 173 | { 174 | Key: ComplexInputInner, 175 | Value: [ 176 | [ 177 | { 178 | Components: [ 179 | { 180 | HasCondition: false, 181 | HasAsyncCondition: false, 182 | Validator: { 183 | Name: NotEmptyValidator 184 | } 185 | } 186 | ], 187 | Member: ComplexInputInner.Content, 188 | PropertyFunc: { 189 | Type: Func, 190 | Target: PropertyRule.<>c__DisplayClass1_0, 191 | Method: System.String Create(ComplexInputInner) 192 | }, 193 | Expression: _ => _.Content, 194 | Current: { 195 | HasCondition: false, 196 | HasAsyncCondition: false, 197 | Validator: { 198 | Name: NotEmptyValidator 199 | } 200 | }, 201 | TypeToValidate: string, 202 | HasCondition: false, 203 | HasAsyncCondition: false, 204 | PropertyName: Content 205 | } 206 | ] 207 | ] 208 | }, 209 | { 210 | Key: ComplexInputListItem, 211 | Value: [ 212 | [ 213 | { 214 | Components: [ 215 | { 216 | HasCondition: false, 217 | HasAsyncCondition: false, 218 | Validator: { 219 | Name: NotEmptyValidator 220 | } 221 | } 222 | ], 223 | Member: ComplexInputListItem.Id, 224 | PropertyFunc: { 225 | Type: Func, 226 | Target: PropertyRule.<>c__DisplayClass1_0, 227 | Method: Int32 Create(ComplexInputListItem) 228 | }, 229 | Expression: _ => _.Id, 230 | Current: { 231 | HasCondition: false, 232 | HasAsyncCondition: false, 233 | Validator: { 234 | Name: NotEmptyValidator 235 | } 236 | }, 237 | TypeToValidate: int, 238 | HasCondition: false, 239 | HasAsyncCondition: false, 240 | PropertyName: Id 241 | }, 242 | { 243 | Components: [ 244 | { 245 | HasCondition: false, 246 | HasAsyncCondition: false, 247 | Validator: { 248 | Name: NotEmptyValidator 249 | } 250 | } 251 | ], 252 | Member: ComplexInputListItem.Content, 253 | PropertyFunc: { 254 | Type: Func, 255 | Target: PropertyRule.<>c__DisplayClass1_0, 256 | Method: System.String Create(ComplexInputListItem) 257 | }, 258 | Expression: _ => _.Content, 259 | Current: { 260 | HasCondition: false, 261 | HasAsyncCondition: false, 262 | Validator: { 263 | Name: NotEmptyValidator 264 | } 265 | }, 266 | TypeToValidate: string, 267 | HasCondition: false, 268 | HasAsyncCondition: false, 269 | PropertyName: Content 270 | } 271 | ] 272 | ] 273 | }, 274 | { 275 | Key: Input, 276 | Value: [ 277 | [ 278 | { 279 | Components: [ 280 | { 281 | HasCondition: false, 282 | HasAsyncCondition: false, 283 | Validator: { 284 | Name: NotEmptyValidator 285 | } 286 | } 287 | ], 288 | Member: Input.Content, 289 | PropertyFunc: { 290 | Type: Func, 291 | Target: PropertyRule.<>c__DisplayClass1_0, 292 | Method: System.String Create(Input) 293 | }, 294 | Expression: _ => _.Content, 295 | Current: { 296 | HasCondition: false, 297 | HasAsyncCondition: false, 298 | Validator: { 299 | Name: NotEmptyValidator 300 | } 301 | }, 302 | TypeToValidate: string, 303 | HasCondition: false, 304 | HasAsyncCondition: false, 305 | PropertyName: Content 306 | } 307 | ] 308 | ] 309 | } 310 | ] -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.Invalid.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | errors: [ 3 | { 4 | message: Content: 'Content' must not be empty., 5 | path: [ 6 | Content 7 | ], 8 | extensions: { 9 | code: NotEmptyValidator, 10 | codes: [ 11 | NotEmptyValidator 12 | ] 13 | } 14 | } 15 | ], 16 | data: { 17 | inputQuery: null 18 | } 19 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.NoValidatorInvalid.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | errors: [ 3 | { 4 | message: Content: 'Content' must not be empty., 5 | path: [ 6 | Content 7 | ], 8 | extensions: { 9 | code: NotEmptyValidator, 10 | codes: [ 11 | NotEmptyValidator 12 | ] 13 | } 14 | } 15 | ], 16 | data: { 17 | noValidatorQuery: null 18 | } 19 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.NoValidatorValid.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | data: { 3 | noValidatorQuery: { 4 | data: TheContent 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.Valid.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | data: { 3 | inputQuery: { 4 | data: TheContent 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.ValidNull.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | data: { 3 | inputQuery: { 4 | data: it was null 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.FluentValidation; 2 | 3 | public class IntegrationTests 4 | { 5 | static ValidatorInstanceCache cache; 6 | 7 | static IntegrationTests() 8 | { 9 | cache = new( 10 | type => 11 | { 12 | if (type == typeof(NoValidatorInput)) 13 | { 14 | return new NoValidatorInputValidator(); 15 | } 16 | 17 | return null; 18 | }); 19 | cache.AddValidatorsFromAssemblyContaining(); 20 | } 21 | 22 | [Fact] 23 | public Task GetCurrentValidators() 24 | { 25 | var items = cache.GetCurrentValidators(); 26 | return Verify(items.Where(_ => _.Key != typeof(NoValidatorInput) && _.Value.Count > 0)); 27 | } 28 | 29 | [Fact] 30 | public async Task AsyncValid() 31 | { 32 | var query = """ 33 | { 34 | asyncQuery 35 | ( 36 | input: { 37 | content: "TheContent" 38 | } 39 | ) 40 | { 41 | data 42 | } 43 | } 44 | """; 45 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 46 | await VerifyJson(result); 47 | } 48 | 49 | [Fact] 50 | public async Task AsyncInvalid() 51 | { 52 | var query = """ 53 | { 54 | asyncQuery 55 | ( 56 | input: { 57 | content: "" 58 | } 59 | ) 60 | { 61 | data 62 | } 63 | } 64 | """; 65 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 66 | await VerifyJson(result); 67 | } 68 | 69 | [Fact] 70 | public async Task ValidNull() 71 | { 72 | var query = """ 73 | { 74 | inputQuery 75 | { 76 | data 77 | } 78 | } 79 | """; 80 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 81 | await VerifyJson(result); 82 | } 83 | 84 | [Fact] 85 | public async Task NoValidatorValid() 86 | { 87 | var query = """ 88 | { 89 | noValidatorQuery 90 | ( 91 | input: { 92 | content: "TheContent" 93 | } 94 | ) 95 | { 96 | data 97 | } 98 | } 99 | """; 100 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 101 | await VerifyJson(result); 102 | } 103 | 104 | [Fact] 105 | public async Task NoValidatorInvalid() 106 | { 107 | var query = """ 108 | { 109 | noValidatorQuery 110 | ( 111 | input: { 112 | content: "" 113 | } 114 | ) 115 | { 116 | data 117 | } 118 | } 119 | """; 120 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 121 | await VerifyJson(result); 122 | } 123 | 124 | [Fact] 125 | public async Task Valid() 126 | { 127 | var query = """ 128 | { 129 | inputQuery 130 | ( 131 | input: { 132 | content: "TheContent" 133 | } 134 | ) 135 | { 136 | data 137 | } 138 | } 139 | """; 140 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 141 | await VerifyJson(result); 142 | } 143 | 144 | [Fact] 145 | public async Task Invalid() 146 | { 147 | var query = """ 148 | { 149 | inputQuery 150 | ( 151 | input: { 152 | content: "" 153 | } 154 | ) 155 | { 156 | data 157 | } 158 | } 159 | """; 160 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 161 | await VerifyJson(result); 162 | } 163 | 164 | [Fact] 165 | public async Task ComplexValid() 166 | { 167 | var query = """ 168 | { 169 | complexInputQuery 170 | ( 171 | input: { 172 | inner: { 173 | content: "TheContent" 174 | }, 175 | items: [ 176 | { id: 1, content: "Some content 1" }, 177 | { id: 2, content: "Some content 2" } 178 | ] 179 | } 180 | ) 181 | { 182 | data 183 | } 184 | } 185 | """; 186 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 187 | await VerifyJson(result); 188 | } 189 | 190 | [Fact] 191 | public async Task ComplexInvalid() 192 | { 193 | var query = """ 194 | { 195 | complexInputQuery 196 | ( 197 | input: { 198 | inner: { 199 | content: "" 200 | }, 201 | items: [] 202 | } 203 | ) 204 | { 205 | data 206 | } 207 | } 208 | """; 209 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 210 | await VerifyJson(result); 211 | } 212 | 213 | [Fact] 214 | public async Task DerivedComplexInvalid() 215 | { 216 | var query = """ 217 | { 218 | derivedComplexInputQuery 219 | ( 220 | input: { 221 | inner: { 222 | content: "" 223 | }, 224 | items: [] 225 | } 226 | ) 227 | { 228 | data 229 | } 230 | } 231 | """; 232 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 233 | await VerifyJson(result); 234 | } 235 | 236 | [Fact] 237 | public async Task ComplexInvalid2() 238 | { 239 | var query = """ 240 | { 241 | complexInputQuery 242 | ( 243 | input: { 244 | inner: null, 245 | items: null 246 | } 247 | ) 248 | { 249 | data 250 | } 251 | } 252 | """; 253 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 254 | await VerifyJson(result); 255 | } 256 | 257 | [Fact] 258 | public async Task AsyncComplexValid() 259 | { 260 | var query = """ 261 | { 262 | asyncComplexInputQuery 263 | ( 264 | input: { 265 | inner: { 266 | content: "TheContent" 267 | }, 268 | items: [ 269 | { id: 1, content: "Some content 1" }, 270 | { id: 2, content: "Some content 2" } 271 | ] 272 | } 273 | ) 274 | { 275 | data 276 | } 277 | } 278 | """; 279 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 280 | await VerifyJson(result); 281 | } 282 | 283 | [Fact] 284 | public async Task AsyncComplexInvalid() 285 | { 286 | var query = """ 287 | { 288 | asyncComplexInputQuery 289 | ( 290 | input: { 291 | inner: { 292 | content: "" 293 | }, 294 | items: null 295 | } 296 | ) 297 | { 298 | data 299 | } 300 | } 301 | """; 302 | var result = await QueryExecutor.ExecuteQuery(query, null, cache); 303 | await VerifyJson(result); 304 | } 305 | } -------------------------------------------------------------------------------- /src/Tests/ModuleInitializer.cs: -------------------------------------------------------------------------------- 1 | public static class ModuleInitializer 2 | { 3 | [ModuleInitializer] 4 | public static void Initialize() => 5 | VerifyDiffPlex.Initialize(); 6 | } -------------------------------------------------------------------------------- /src/Tests/Query.cs: -------------------------------------------------------------------------------- 1 | using GraphQL; 2 | using Newtonsoft.Json; 3 | 4 | public class Query : 5 | ObjectGraphType 6 | { 7 | public Query() 8 | { 9 | Field("noValidatorQuery") 10 | .Argument("input") 11 | .Resolve(context => 12 | { 13 | var input = context.GetValidatedArgument("input"); 14 | return new Result 15 | { 16 | Data = input?.Content ?? "it was null" 17 | }; 18 | } 19 | ); 20 | Field("inputQuery") 21 | .Argument("input") 22 | .Resolve(context => 23 | { 24 | var input = context.GetValidatedArgument("input"); 25 | return new Result 26 | { 27 | Data = input?.Content ?? "it was null" 28 | }; 29 | } 30 | ); 31 | 32 | Field("complexInputQuery") 33 | .Argument("input") 34 | .Resolve(context => 35 | { 36 | var input = context.GetValidatedArgument("input"); 37 | return new Result 38 | { 39 | Data = JsonConvert.SerializeObject(input) 40 | }; 41 | } 42 | ); 43 | 44 | Field("derivedComplexInputQuery") 45 | .Argument("input") 46 | .Resolve(context => 47 | { 48 | var input = context.GetValidatedArgument("input"); 49 | return new Result 50 | { 51 | Data = JsonConvert.SerializeObject(input) 52 | }; 53 | } 54 | ); 55 | 56 | Field("asyncQuery") 57 | .Argument("input") 58 | .ResolveAsync(async context => 59 | { 60 | var input = await context.GetValidatedArgumentAsync("input"); 61 | return new Result 62 | { 63 | Data = input.Content 64 | }; 65 | } 66 | ); 67 | 68 | Field("asyncComplexInputQuery") 69 | .Argument("input") 70 | .ResolveAsync(async context => 71 | { 72 | var input = await context.GetValidatedArgumentAsync("input"); 73 | return new Result 74 | { 75 | Data = input.Inner!.Content 76 | }; 77 | } 78 | ); 79 | } 80 | } -------------------------------------------------------------------------------- /src/Tests/QueryExecutor.cs: -------------------------------------------------------------------------------- 1 | using GraphQL; 2 | using GraphQL.FluentValidation; 3 | using GraphQL.SystemTextJson; 4 | 5 | static class QueryExecutor 6 | { 7 | static GraphQLSerializer graphQlSerializer = new(indent: true); 8 | 9 | public static async Task ExecuteQuery(string queryString, Inputs? inputs, IValidatorCache cache) 10 | { 11 | Thread.CurrentThread.CurrentUICulture = new("en-US"); 12 | 13 | queryString = queryString.Replace('\'', '"'); 14 | using var schema = new Schema(); 15 | schema.UseFluentValidation(); 16 | var documentExecuter = new DocumentExecuter(); 17 | var executionOptions = new ExecutionOptions 18 | { 19 | Schema = schema, 20 | Query = queryString, 21 | Variables = inputs 22 | }; 23 | executionOptions.UseFluentValidation(cache); 24 | 25 | var result = await documentExecuter.ExecuteAsync(executionOptions); 26 | var stream = new MemoryStream(); 27 | await graphQlSerializer.WriteAsync(stream, result); 28 | stream.Position = 0; 29 | var reader = new StreamReader(stream); 30 | return reader.ReadToEnd(); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Tests/Result.cs: -------------------------------------------------------------------------------- 1 | public class Result 2 | { 3 | public string? Data { get; set; } 4 | } -------------------------------------------------------------------------------- /src/Tests/ResultGraph.cs: -------------------------------------------------------------------------------- 1 | public class ResultGraph : 2 | ObjectGraphType 3 | { 4 | public ResultGraph() => 5 | Field(h => h.Data); 6 | } -------------------------------------------------------------------------------- /src/Tests/Schema.cs: -------------------------------------------------------------------------------- 1 | public class Schema : 2 | GraphQL.Types.Schema 3 | { 4 | public Schema() => 5 | Query = new Query(); 6 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/QueryExecution.cs: -------------------------------------------------------------------------------- 1 | using GraphQL; 2 | using GraphQL.FluentValidation; 3 | // ReSharper disable UnusedVariable 4 | #pragma warning disable 649 5 | 6 | class QueryExecution 7 | { 8 | string queryString = null!; 9 | Inputs inputs = null!; 10 | Schema schema = null!; 11 | ValidatorInstanceCache validatorCache = null!; 12 | DocumentExecuter executer = null!; 13 | 14 | static void ExecuteQuery(Assembly assemblyContainingValidators) 15 | { 16 | #region StartConfig 17 | 18 | var validatorCache = new ValidatorInstanceCache(); 19 | validatorCache.AddValidatorsFromAssembly(assemblyContainingValidators); 20 | var schema = new Schema(); 21 | schema.UseFluentValidation(); 22 | var executer = new DocumentExecuter(); 23 | 24 | #endregion 25 | } 26 | 27 | async Task ExecuteQuery() 28 | { 29 | #region UseFluentValidation 30 | 31 | var options = new ExecutionOptions 32 | { 33 | Schema = schema, 34 | Query = queryString, 35 | Variables = inputs 36 | }; 37 | options.UseFluentValidation(validatorCache); 38 | 39 | var executionResult = await executer.ExecuteAsync(options); 40 | 41 | #endregion 42 | } 43 | 44 | #region ContextImplementingDictionary 45 | 46 | public class MyUserContext(string myProperty) : 47 | Dictionary 48 | { 49 | public string MyProperty { get; } = myProperty; 50 | } 51 | 52 | #endregion 53 | 54 | void ExecuteQueryWithContextImplementingDictionary() 55 | { 56 | #region ExecuteQueryWithContextImplementingDictionary 57 | 58 | var options = new ExecutionOptions 59 | { 60 | Schema = schema, 61 | Query = queryString, 62 | Variables = inputs, 63 | UserContext = new MyUserContext 64 | ( 65 | myProperty: "the value" 66 | ) 67 | }; 68 | options.UseFluentValidation(validatorCache); 69 | 70 | #endregion 71 | } 72 | 73 | void ExecuteQueryWithContextInsideDictionary() 74 | { 75 | #region ExecuteQueryWithContextInsideDictionary 76 | 77 | var options = new ExecutionOptions 78 | { 79 | Schema = schema, 80 | Query = queryString, 81 | Variables = inputs, 82 | UserContext = new Dictionary 83 | { 84 | { 85 | "MyUserContext", 86 | new MyUserContext 87 | ( 88 | myProperty: "the value" 89 | ) 90 | } 91 | } 92 | }; 93 | options.UseFluentValidation(validatorCache); 94 | 95 | #endregion 96 | } 97 | 98 | void NoContext() 99 | { 100 | #region NoContext 101 | 102 | var options = new ExecutionOptions 103 | { 104 | Schema = schema, 105 | Query = queryString, 106 | Variables = inputs 107 | }; 108 | options.UseFluentValidation(validatorCache); 109 | 110 | #endregion 111 | } 112 | } -------------------------------------------------------------------------------- /src/Tests/TestConfig.cs: -------------------------------------------------------------------------------- 1 | [assembly: CollectionBehavior(DisableTestParallelization = true, MaxParallelThreads = 1)] -------------------------------------------------------------------------------- /src/Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | Exe 5 | $(NoWarn);xUnit1051 6 | testing 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2022 2 | environment: 3 | DOTNET_NOLOGO: true 4 | DOTNET_CLI_TELEMETRY_OPTOUT: true 5 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 6 | build_script: 7 | - pwsh: | 8 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile "./dotnet-install.ps1" 9 | ./dotnet-install.ps1 -JSonFile src/global.json -Architecture x64 -InstallDir 'C:\Program Files\dotnet' 10 | - dotnet build src --configuration Release 11 | - dotnet test src --configuration Release --no-build --no-restore 12 | test: off 13 | artifacts: 14 | - path: nugets\*.nupkg 15 | on_failure: 16 | - ps: Get-ChildItem *.received.* -recurse | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } -------------------------------------------------------------------------------- /src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "msbuild-sdks": { 3 | "MSBuild.Sdk.Extras": "3.0.44" 4 | }, 5 | "sdk": { 6 | "version": "9.0.300", 7 | "allowPrerelease": true, 8 | "rollForward": "latestFeature" 9 | } 10 | } -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonCropp/GraphQL.Validation/0b65c5ce91e243a091b01a05b0cafbdeaf0e1c4a/src/icon.png -------------------------------------------------------------------------------- /src/key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonCropp/GraphQL.Validation/0b65c5ce91e243a091b01a05b0cafbdeaf0e1c4a/src/key.snk -------------------------------------------------------------------------------- /src/mdsnippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json", 3 | "TocExcludes": [ "NuGet package", "Release Notes", "Icon" ], 4 | "MaxWidth": 80, 5 | "ValidateContent": true, 6 | "Convention": "InPlaceOverwrite" 7 | } -------------------------------------------------------------------------------- /src/nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 20 | 21 | 24 | 25 | 26 | --------------------------------------------------------------------------------