diff --git a/Wolverine.Http.DataAnnotationsValidation.Tests/Usings.cs b/Wolverine.Http.DataAnnotationsValidation.Tests/Usings.cs
deleted file mode 100644
index bd8299f6f..000000000
--- a/Wolverine.Http.DataAnnotationsValidation.Tests/Usings.cs
+++ /dev/null
@@ -1,2 +0,0 @@
-global using Xunit;
-global using Shouldly;
\ No newline at end of file
diff --git a/Wolverine.Http.DataAnnotationsValidation.Tests/Wolverine.Http.DataAnnotationsValidation.Tests.csproj b/Wolverine.Http.DataAnnotationsValidation.Tests/Wolverine.Http.DataAnnotationsValidation.Tests.csproj
deleted file mode 100644
index d62d1454e..000000000
--- a/Wolverine.Http.DataAnnotationsValidation.Tests/Wolverine.Http.DataAnnotationsValidation.Tests.csproj
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
- false
- net9.0
-
-
-
-
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
-
-
-
-
-
diff --git a/build/build.cs b/build/build.cs
index 059ab0bfa..372c1f7cc 100644
--- a/build/build.cs
+++ b/build/build.cs
@@ -145,7 +145,7 @@ class Build : NukeBuild
});
Target HttpTests => _ => _
- .DependsOn(CoreHttpTests, DataAnnotationsValidationHttpTests);
+ .DependsOn(CoreHttpTests);
Target CoreHttpTests => _ => _
.DependsOn(Compile, DockerUp)
@@ -160,20 +160,6 @@ class Build : NukeBuild
.SetFramework(Framework));
});
- Target DataAnnotationsValidationHttpTests => _ => _
- .DependsOn(Compile)
- .ProceedAfterFailure()
- .Executes(() =>
- {
- DotNetTest(c => c
- .SetProjectFile(Solution.Http.Wolverine_Http_DataAnnotationsValidation_Tests)
- .SetConfiguration(Configuration)
- .EnableNoBuild()
- .EnableNoRestore()
- .SetFramework(Framework));
- });
-
-
Target Commands => _ => _
.DependsOn(HelpCommand, DescribeCommand, CodegenPreviewCommand);
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index f5147bd11..d2784c22e 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -223,8 +223,8 @@ const config: UserConfig = {
{text: 'Uploading Files', link: '/guide/http/files'},
{text: 'Integration with Sagas', link: '/guide/http/sagas'},
{text: 'Integration with Marten', link: '/guide/http/marten'},
+ {text: 'Validation', link: '/guide/http/validation'},
{text: 'Fluent Validation', link: '/guide/http/fluentvalidation'},
- {text: 'DataAnnotations Validation', link: '/guide/http/dataannotationsvalidation'},
{text: 'Problem Details', link: '/guide/http/problemdetails'},
{text: 'Caching', link: '/guide/http/caching'}
]
diff --git a/docs/guide/http/dataannotationsvalidation.md b/docs/guide/http/dataannotationsvalidation.md
deleted file mode 100644
index eeeaa8fe5..000000000
--- a/docs/guide/http/dataannotationsvalidation.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# DataAnnotations Validation Middleware for HTTP
-
-::: tip
-The Http package for DataAnnotations Validation is completely separate from the [non-HTTP](/guide/handlers/dataannotations-validation)
-package. If you have a hybrid application supporting both http-endpoint and other message handlers,
-you will need to install both packages.
-:::
-
-::: warning
-While it is possible to access the IoC Services via `ValidationContext`, we recommend instead using a
-more explicit `Validate` or `ValidateAsync()` method directly in your message handler class for the data input.
-:::
-
-Wolverine.Http has a separate package called `WolverineFx.Http.DataAnnotationsValidation` that provides a simple middleware
-to use [Data Annotation Attributes](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations?view=net-10.0)
-in your endpoints.
-
-To get started, install the Nuget reference:
-
-```bash
-dotnet add package WolverineFx.Http.DataAnnotationsValidation
-```
-
-Next, add this one single line of code to your Wolverine.Http bootstrapping:
-
-```csharp
-opts.UseFluentValidationProblemDetailMiddleware();
-```
-
-Using the validators is pretty much the same as the regular DataAnnotations package
-
-
\ No newline at end of file
diff --git a/docs/guide/http/fluentvalidation.md b/docs/guide/http/fluentvalidation.md
index b44874e3f..5ca6005c5 100644
--- a/docs/guide/http/fluentvalidation.md
+++ b/docs/guide/http/fluentvalidation.md
@@ -1,133 +1,3 @@
# Fluent Validation Middleware for HTTP
-::: warning
-If you need to use IoC services in a Fluent Validation `IValidator` that might force Wolverine to use a service locator
-pattern in the generated code (basically from `AddScoped(s => build it at runtime)`), we recommend instead using a
-more explicit `Validate` or `ValidateAsync()` method directly in your HTTP endpoint class for the data input.
-:::
-
-Wolverine.Http has a separate package called `WolverineFx.Http.FluentValidation` that provides a simple middleware
-for using [Fluent Validation](https://docs.fluentvalidation.net/en/latest/) in your HTTP endpoints.
-
-To get started, install that Nuget reference:
-
-```bash
-dotnet add package WolverineFx.Http.FluentValidation
-```
-
-Next, let's assume that you have some Fluent Validation validators registered in your application container for the
-request types of your HTTP endpoints -- and the [UseFluentValidation](/guide/handlers/fluent-validation) method from the
-`WolverineFx.FluentValidation` package will help find these validators and register them in a way that optimizes this
-middleware usage.
-
-Next, add this one single line of code to your Wolverine.Http bootstrapping:
-
-```csharp
-opts.UseFluentValidationProblemDetailMiddleware();
-```
-
-as shown in context below in an application shown below:
-
-
-
-```cs
-app.MapWolverineEndpoints(opts =>
-{
- // This is strictly to test the endpoint policy
-
- opts.ConfigureEndpoints(httpChain =>
- {
- // The HttpChain model is a configuration time
- // model of how the HTTP endpoint handles requests
-
- // This adds metadata for OpenAPI
- httpChain.WithMetadata(new CustomMetadata());
- });
-
- // more configuration for HTTP...
-
- // Opting into the Fluent Validation middleware from
- // Wolverine.Http.FluentValidation
- opts.UseFluentValidationProblemDetailMiddleware();
-```
-snippet source | anchor
-
-
-## AsParameters Binding
-
-The Fluent Validation middleware can also be used against the `[AsParameters]` input
-of an HTTP endpoint:
-
-
-
-```cs
-public static class ValidatedAsParametersEndpoint
-{
- [WolverineGet("/asparameters/validated")]
- public static string Get([AsParameters] ValidatedQuery query)
- {
- return $"{query.Name} is {query.Age}";
- }
-}
-
-public class ValidatedQuery
-{
- [FromQuery]
- public string? Name { get; set; }
-
- public int Age { get; set; }
-
- public class ValidatedQueryValidator : AbstractValidator
- {
- public ValidatedQueryValidator()
- {
- RuleFor(x => x.Name).NotNull();
- }
- }
-}
-```
-snippet source | anchor
-
-
-## QueryString Binding
-
-Wolverine.HTTP can apply the Fluent Validation middleware to complex types that are bound by the `[FromQuery]` behavior:
-
-
-
-```cs
-public record CreateCustomer
-(
- string FirstName,
- string LastName,
- string PostalCode
-)
-{
- public class CreateCustomerValidator : AbstractValidator
- {
- public CreateCustomerValidator()
- {
- RuleFor(x => x.FirstName).NotNull();
- RuleFor(x => x.LastName).NotNull();
- RuleFor(x => x.PostalCode).NotNull();
- }
- }
-}
-
-public static class CreateCustomerEndpoint
-{
- [WolverinePost("/validate/customer")]
- public static string Post(CreateCustomer customer)
- {
- return "Got a new customer";
- }
-
- [WolverinePost("/validate/customer2")]
- public static string Post2([FromQuery] CreateCustomer customer)
- {
- return "Got a new customer";
- }
-}
-```
-snippet source | anchor
-
+See the [Validation Page](./validation).
\ No newline at end of file
diff --git a/docs/guide/http/validation.md b/docs/guide/http/validation.md
new file mode 100644
index 000000000..555db6275
--- /dev/null
+++ b/docs/guide/http/validation.md
@@ -0,0 +1,401 @@
+# Validation within Wolverine.HTTP
+
+::: info
+You can of course use completely custom Wolverine middleware for validation, and once again, returning the `ProblemDetails`
+object or `WolverineContinue.NoProblems` to communicate validation errors is our main recommendation in that case.
+:::
+
+Wolverine.HTTP has direct support for utilizing validation within HTTP endpoint that all revolve around the
+[ProblemDetails](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails?view=aspnetcore-7.0) specification.
+
+1. Using one off `Validate()` or `ValidateAsync()` methods embedded directly in your endpoint types that return `ProblemDetails`. This is our recommendation for any
+ validation logic like data lookups that would require you to utilize IoC services or database calls.
+2. Fluent Validation middleware through the separate `WolverineFx.Http.FluentValidation` Nuget
+3. [Data Annotations](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations?view=net-10.0) middleware that is an option you have to explicitly configure within Wolverine.HTTP application
+
+::: tip
+We **very strongly** recommend using the one off `ValidateAsync()` method for any validation that requires you to use an IoC'
+service rather than trying to use the Fluent Validation `IValidator` interface. Especially if that validation logic
+is specific to that HTTP endpoint.
+:::
+
+## Using ProblemDetails
+
+Wolverine has some first class support for the [ProblemDetails](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails?view=aspnetcore-7.0) specification in its [HTTP middleware model](./middleware).
+Wolverine also has a [Fluent Validation middleware package](./fluentvalidation) for HTTP endpoints, but it's frequently valuable to write one
+off, explicit validation for certain endpoints.
+
+Consider this contrived sample endpoint with explicit validation being done in a "Before" middleware method:
+
+
+
+```cs
+public class ProblemDetailsUsageEndpoint
+{
+ public ProblemDetails Before(NumberMessage message)
+ {
+ // If the number is greater than 5, fail with a
+ // validation message
+ if (message.Number > 5)
+ return new ProblemDetails
+ {
+ Detail = "Number is bigger than 5",
+ Status = 400
+ };
+
+ // All good, keep on going!
+ return WolverineContinue.NoProblems;
+ }
+
+ [WolverinePost("/problems")]
+ public static string Post(NumberMessage message)
+ {
+ return "Ok";
+ }
+}
+
+public record NumberMessage(int Number);
+```
+snippet source | anchor
+
+
+Wolverine.Http now (as of 1.2.0) has a convention that sees a return value of `ProblemDetails` and looks at that as a
+"continuation" to tell the http handler code what to do next. One of two things will happen:
+
+1. If the `ProblemDetails` return value is the same instance as `WolverineContinue.NoProblems`, just keep going
+2. Otherwise, write the `ProblemDetails` out to the HTTP response and exit the HTTP request handling
+
+To make that clearer, here's the generated code:
+
+```csharp
+ public class POST_problems : Wolverine.Http.HttpHandler
+ {
+ private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
+
+ public POST_problems(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions) : base(wolverineHttpOptions)
+ {
+ _wolverineHttpOptions = wolverineHttpOptions;
+ }
+
+ public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
+ {
+ var problemDetailsUsageEndpoint = new WolverineWebApi.ProblemDetailsUsageEndpoint();
+ var (message, jsonContinue) = await ReadJsonAsync(httpContext);
+ if (jsonContinue == Wolverine.HandlerContinuation.Stop) return;
+
+ var problemDetails = problemDetailsUsageEndpoint.Before(message);
+ if (!(ReferenceEquals(problemDetails, Wolverine.Http.WolverineContinue.NoProblems)))
+ {
+ await Microsoft.AspNetCore.Http.Results.Problem(problemDetails).ExecuteAsync(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ var result_of_Post = WolverineWebApi.ProblemDetailsUsageEndpoint.Post(message);
+ await WriteString(httpContext, result_of_Post);
+ }
+
+ }
+```
+
+And for more context, here's the matching "happy path" and "sad path" tests for the endpoint above:
+
+
+
+```cs
+[Fact]
+public async Task continue_happy_path()
+{
+ // Should be good
+ await Scenario(x =>
+ {
+ x.Post.Json(new NumberMessage(3)).ToUrl("/problems");
+ });
+}
+
+[Fact]
+public async Task stop_with_problems_if_middleware_trips_off()
+{
+ // This is the "sad path" that should spawn a ProblemDetails
+ // object
+ var result = await Scenario(x =>
+ {
+ x.Post.Json(new NumberMessage(10)).ToUrl("/problems");
+ x.StatusCodeShouldBe(400);
+ x.ContentTypeShouldBe("application/problem+json");
+ });
+}
+```
+snippet source | anchor
+
+
+Lastly, if Wolverine sees the existence of a `ProblemDetails` return value in any middleware, Wolverine will fill in OpenAPI
+metadata for the "application/problem+json" content type and a status code of 400. This behavior can be easily overridden
+with your own metadata if you need to use a different status code like this:
+
+```csharp
+ // Use 418 as the status code instead
+ [ProducesResponseType(typeof(ProblemDetails), 418)]
+```
+
+### Using ProblemDetails with Marten aggregates
+
+Of course, if you are using [Marten's aggregates within your Wolverine http handlers](./marten), you also want to be able to validation using the aggregate's details in your middleware and this is perfectly possible like this:
+
+
+
+```cs
+[AggregateHandler]
+public static ProblemDetails Before(IShipOrder command, Order order)
+{
+ if (order.IsShipped())
+ {
+ return new ProblemDetails
+ {
+ Detail = "Order already shipped",
+ Status = 428
+ };
+ }
+ return WolverineContinue.NoProblems;
+}
+```
+snippet source | anchor
+
+
+## ProblemDetails Within Message Handlers
+
+`ProblemDetails` can be used within message handlers as well with similar rules. See this example
+from the tests:
+
+
+
+```cs
+public static class NumberMessageHandler
+{
+ public static ProblemDetails Validate(NumberMessage message)
+ {
+ if (message.Number > 5)
+ {
+ return new ProblemDetails
+ {
+ Detail = "Number is bigger than 5",
+ Status = 400
+ };
+ }
+
+ // All good, keep on going!
+ return WolverineContinue.NoProblems;
+ }
+
+ // This "Before" method would only be utilized as
+ // an HTTP endpoint
+ [WolverineBefore(MiddlewareScoping.HttpEndpoints)]
+ public static void BeforeButOnlyOnHttp(HttpContext context)
+ {
+ Debug.WriteLine("Got an HTTP request for " + context.TraceIdentifier);
+ CalledBeforeOnlyOnHttpEndpoints = true;
+ }
+
+ // This "Before" method would only be utilized as
+ // a message handler
+ [WolverineBefore(MiddlewareScoping.MessageHandlers)]
+ public static void BeforeButOnlyOnMessageHandlers()
+ {
+ CalledBeforeOnlyOnMessageHandlers = true;
+ }
+
+ // Look at this! You can use this as an HTTP endpoint too!
+ [WolverinePost("/problems2")]
+ public static void Handle(NumberMessage message)
+ {
+ Debug.WriteLine("Handled " + message);
+ Handled = true;
+ }
+
+ // These properties are just a cheap trick in Wolverine internal tests
+ public static bool Handled { get; set; }
+ public static bool CalledBeforeOnlyOnMessageHandlers { get; set; }
+ public static bool CalledBeforeOnlyOnHttpEndpoints { get; set; }
+}
+```
+snippet source | anchor
+
+
+This functionality was added so that some handlers could be both an endpoint and message handler
+without having to duplicate code or delegate to the handler through an endpoint.
+
+
+## Data Annotations
+
+::: warning
+While it is possible to access the IoC Services via `ValidationContext`, we recommend instead using a
+more explicit `Validate` or `ValidateAsync()` method directly in your message handler class for the data input.
+:::
+
+Wolverine.Http has a separate package called `WolverineFx.Http.DataAnnotationsValidation` that provides a simple middleware
+to use [Data Annotation Attributes](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations?view=net-10.0)
+in your endpoints.
+
+To get started, add this one line of code to your Wolverine.HTTP configuration:
+
+```csharp
+app.MapWolverineEndpoints(opts =>
+{
+ // Use Data Annotations that are built
+ // into the Wolverine.HTTP library
+ opts.UseDataAnnotationsValidationProblemDetailMiddleware();
+
+});
+```
+
+This middleware will kick in for any HTTP endpoint where the request type has any property
+decorated with a [`ValidationAttribute`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute?view=net-10.0)
+or which implements the `IValidatableObject` interface.
+
+Any validation errors detected will cause the HTTP request to fail with a `ProblemDetails` response.
+
+For an example, consider this input model that will be a request type in your application:
+
+snippet: sample_validated_CreateAccount
+
+As long as the Data Annotations middleware is active, the `CreateAccount` model would be validated if used
+as the request body like this:
+
+snippet: sample_posting_CreateAccount
+
+or even like this:
+
+snippet: sample_posting_create_account_as_query_string
+
+## Fluent Validation Middleware
+
+::: warning
+If you need to use IoC services in a Fluent Validation `IValidator` that might force Wolverine to use a service locator
+pattern in the generated code (basically from `AddScoped(s => build it at runtime)`), we recommend instead using a
+more explicit `Validate` or `ValidateAsync()` method directly in your HTTP endpoint class for the data input.
+:::
+
+Wolverine.Http has a separate package called `WolverineFx.Http.FluentValidation` that provides a simple middleware
+for using [Fluent Validation](https://docs.fluentvalidation.net/en/latest/) in your HTTP endpoints.
+
+To get started, install that Nuget reference:
+
+```bash
+dotnet add package WolverineFx.Http.FluentValidation
+```
+
+Next, let's assume that you have some Fluent Validation validators registered in your application container for the
+request types of your HTTP endpoints -- and the [UseFluentValidation](/guide/handlers/fluent-validation) method from the
+`WolverineFx.FluentValidation` package will help find these validators and register them in a way that optimizes this
+middleware usage.
+
+Next, add this one single line of code to your Wolverine.Http bootstrapping:
+
+```csharp
+opts.UseFluentValidationProblemDetailMiddleware();
+```
+
+as shown in context below in an application shown below:
+
+
+
+```cs
+app.MapWolverineEndpoints(opts =>
+{
+ // This is strictly to test the endpoint policy
+
+ opts.ConfigureEndpoints(httpChain =>
+ {
+ // The HttpChain model is a configuration time
+ // model of how the HTTP endpoint handles requests
+
+ // This adds metadata for OpenAPI
+ httpChain.WithMetadata(new CustomMetadata());
+ });
+
+ // more configuration for HTTP...
+
+ // Opting into the Fluent Validation middleware from
+ // Wolverine.Http.FluentValidation
+ opts.UseFluentValidationProblemDetailMiddleware();
+```
+snippet source | anchor
+
+
+## AsParameters Binding
+
+The Fluent Validation middleware can also be used against the `[AsParameters]` input
+of an HTTP endpoint:
+
+
+
+```cs
+public static class ValidatedAsParametersEndpoint
+{
+ [WolverineGet("/asparameters/validated")]
+ public static string Get([AsParameters] ValidatedQuery query)
+ {
+ return $"{query.Name} is {query.Age}";
+ }
+}
+
+public class ValidatedQuery
+{
+ [FromQuery]
+ public string? Name { get; set; }
+
+ public int Age { get; set; }
+
+ public class ValidatedQueryValidator : AbstractValidator
+ {
+ public ValidatedQueryValidator()
+ {
+ RuleFor(x => x.Name).NotNull();
+ }
+ }
+}
+```
+snippet source | anchor
+
+
+## QueryString Binding
+
+Wolverine.HTTP can apply the Fluent Validation middleware to complex types that are bound by the `[FromQuery]` behavior:
+
+
+
+```cs
+public record CreateCustomer
+(
+ string FirstName,
+ string LastName,
+ string PostalCode
+)
+{
+ public class CreateCustomerValidator : AbstractValidator
+ {
+ public CreateCustomerValidator()
+ {
+ RuleFor(x => x.FirstName).NotNull();
+ RuleFor(x => x.LastName).NotNull();
+ RuleFor(x => x.PostalCode).NotNull();
+ }
+ }
+}
+
+public static class CreateCustomerEndpoint
+{
+ [WolverinePost("/validate/customer")]
+ public static string Post(CreateCustomer customer)
+ {
+ return "Got a new customer";
+ }
+
+ [WolverinePost("/validate/customer2")]
+ public static string Post2([FromQuery] CreateCustomer customer)
+ {
+ return "Got a new customer";
+ }
+}
+```
+snippet source | anchor
+
diff --git a/src/Http/Wolverine.Http.DataAnnotationsValidation/DataAnnotationsValidationExtensions.cs b/src/Http/Wolverine.Http.DataAnnotationsValidation/DataAnnotationsValidationExtensions.cs
deleted file mode 100644
index 45fd7ea25..000000000
--- a/src/Http/Wolverine.Http.DataAnnotationsValidation/DataAnnotationsValidationExtensions.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Wolverine.Attributes;
-using Wolverine.Http.DataAnnotationsValidation;
-using Wolverine.Http.DataAnnotationsValidation.Internals;
-
-[assembly: WolverineModule]
-
-namespace Wolverine.Http.DataAnnotationsValidation;
-
-
-public class DataAnnotationsValidationExtension : IWolverineExtension
-{
- public void Configure(WolverineOptions options)
- {
- options.Services.AddSingleton(typeof(IProblemDetailSource<>), typeof(ProblemDetailSource<>));
- }
-}
\ No newline at end of file
diff --git a/src/Http/Wolverine.Http.DataAnnotationsValidation/Wolverine.Http.DataAnnotationsValidation.csproj b/src/Http/Wolverine.Http.DataAnnotationsValidation/Wolverine.Http.DataAnnotationsValidation.csproj
deleted file mode 100644
index 95d992062..000000000
--- a/src/Http/Wolverine.Http.DataAnnotationsValidation/Wolverine.Http.DataAnnotationsValidation.csproj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
- DataAnnotations Validation middleware for Wolverine HTTP Endpoints
- WolverineFx.Http.DataAnnotationsValidation
- false
- false
- false
- false
- false
-
-
-
-
-
-
-
diff --git a/src/Http/Wolverine.Http.DataAnnotationsValidation/WolverineHttpOptionsExtensions.cs b/src/Http/Wolverine.Http.DataAnnotationsValidation/WolverineHttpOptionsExtensions.cs
deleted file mode 100644
index dd0f55960..000000000
--- a/src/Http/Wolverine.Http.DataAnnotationsValidation/WolverineHttpOptionsExtensions.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Wolverine.Http.DataAnnotationsValidation.Internals;
-
-namespace Wolverine.Http.DataAnnotationsValidation;
-
-public static class WolverineHttpOptionsExtensions
-{
- #region sample_usage_of_http_add_policy
-
- ///
- /// Apply DataAnnotations Validation middleware to all Wolverine HTTP endpoints
- ///
- ///
- public static void UseDataAnnotationsValidationProblemDetailMiddleware(this WolverineHttpOptions httpOptions)
- {
- httpOptions.AddPolicy();
- }
-
- #endregion
-}
\ No newline at end of file
diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_todo_create.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_todo_create.cs
index 9fdae9fa5..8fbdbdf8c 100644
--- a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_todo_create.cs
+++ b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_todo_create.cs
@@ -40,11 +40,11 @@ public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Ht
return;
}
+ var tenantIdentifier = new JasperFx.MultiTenancy.TenantId(tenantId);
var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime);
messageContext.TenantId = tenantId;
// Building the Marten session using the detected tenant id
await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext, tenantId);
- var tenantIdentifier = new JasperFx.MultiTenancy.TenantId(tenantId);
// Reading the request body via JSON deserialization
var (command, jsonContinue) = await ReadJsonAsync(httpContext);
if (jsonContinue == Wolverine.HandlerContinuation.Stop) return;
diff --git a/Wolverine.Http.DataAnnotationsValidation.Tests/dataannotations_validation_middleware.cs b/src/Http/Wolverine.Http.Tests/dataannotations_validation_middleware.cs
similarity index 73%
rename from Wolverine.Http.DataAnnotationsValidation.Tests/dataannotations_validation_middleware.cs
rename to src/Http/Wolverine.Http.Tests/dataannotations_validation_middleware.cs
index b30fa6fed..d878bd9e5 100644
--- a/Wolverine.Http.DataAnnotationsValidation.Tests/dataannotations_validation_middleware.cs
+++ b/src/Http/Wolverine.Http.Tests/dataannotations_validation_middleware.cs
@@ -4,17 +4,22 @@
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using WolverineWebApi;
+using WolverineWebApi.Validation;
-namespace Wolverine.Http.DataAnnotationsValidation.Tests;
+namespace Wolverine.Http.Tests;
-public class dataannotations_validation_middleware : IAsyncLifetime
+public class dataannotations_validation_middleware : IntegrationContext
{
- protected IAlbaHost theHost;
+ public dataannotations_validation_middleware(AppFixture fixture) : base(fixture)
+ {
+ }
[Fact]
public void adds_problem_validation_to_open_api_metadata()
{
- var endpoints = theHost.Services.GetServices().SelectMany(x => x.Endpoints).OfType()
+ var endpoints = Host.Services.GetServices().SelectMany(x => x.Endpoints).OfType()
.ToList();
var endpoint = endpoints.Single(x => x.RoutePattern.RawText == "/validate/account");
@@ -30,7 +35,7 @@ public async Task one_validator_happy_path()
var createCustomer = new CreateAccount("accountName", "12345678");
// Succeeds w/ a 200
- var result = await theHost.Scenario(x =>
+ var result = await Host.Scenario(x =>
{
x.Post.Json(createCustomer).ToUrl("/validate/account");
x.ContentTypeShouldBe("text/plain");
@@ -42,7 +47,7 @@ public async Task one_validator_sad_path()
{
var createCustomer = new CreateAccount(null, "123");
- var results = await theHost.Scenario(x =>
+ var results = await Host.Scenario(x =>
{
x.Post.Json(createCustomer).ToUrl("/validate/account");
x.ContentTypeShouldBe("application/problem+json");
@@ -58,7 +63,7 @@ public async Task one_validator_sad_path()
public async Task one_validator_happy_path_on_complex_query_string_argument()
{
// Succeeds w/ a 200
- var result = await theHost.Scenario(x =>
+ var result = await Host.Scenario(x =>
{
x.Post.Url("/validate/account2")
.QueryString(nameof(CreateAccount.AccountName), "name")
@@ -70,7 +75,7 @@ public async Task one_validator_happy_path_on_complex_query_string_argument()
[Fact]
public async Task one_validator_sad_path_on_complex_query_string_argument()
{
- var results = await theHost.Scenario(x =>
+ var results = await Host.Scenario(x =>
{
x.Post.Url("/validate/account2")
.QueryString(nameof(CreateAccount.Reference), "11111");
@@ -86,7 +91,7 @@ public async Task one_validator_sad_path_on_complex_query_string_argument()
[Fact]
public async Task when_using_compound_handler_validation_is_called_before_load()
{
- var results = await theHost.Scenario(x =>
+ var results = await Host.Scenario(x =>
{
x.Post.Url("/validate/account-compound");
x.ContentTypeShouldBe("application/problem+json");
@@ -96,24 +101,4 @@ public async Task when_using_compound_handler_validation_is_called_before_load()
var problems = results.ReadAsJson();
}
- public async Task InitializeAsync()
- {
- var builder = WebApplication.CreateBuilder([]);
-
- builder.Host.UseWolverine();
- builder.Services.AddWolverineHttp();
-
- theHost = await AlbaHost.For(builder, app =>
- {
- app.MapWolverineEndpoints(opts =>
- {
- opts.UseDataAnnotationsValidationProblemDetailMiddleware();
- });
- });
- }
-
- public Task DisposeAsync()
- {
- return theHost.StopAsync();
- }
}
\ No newline at end of file
diff --git a/src/Http/Wolverine.Http.DataAnnotationsValidation/IProblemDetailSource.cs b/src/Http/Wolverine.Http/Validation/IProblemDetailSource.cs
similarity index 90%
rename from src/Http/Wolverine.Http.DataAnnotationsValidation/IProblemDetailSource.cs
rename to src/Http/Wolverine.Http/Validation/IProblemDetailSource.cs
index 282b5aab0..f1c40983b 100644
--- a/src/Http/Wolverine.Http.DataAnnotationsValidation/IProblemDetailSource.cs
+++ b/src/Http/Wolverine.Http/Validation/IProblemDetailSource.cs
@@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
-namespace Wolverine.Http.DataAnnotationsValidation;
+namespace Wolverine.Http.Validation;
///
/// What do you do with a validation failure? Generally assumed to throw an
diff --git a/src/Http/Wolverine.Http.DataAnnotationsValidation/Internals/DataAnnotationsHttpValidationExecutor.cs b/src/Http/Wolverine.Http/Validation/Internals/DataAnnotationsHttpValidationExecutor.cs
similarity index 83%
rename from src/Http/Wolverine.Http.DataAnnotationsValidation/Internals/DataAnnotationsHttpValidationExecutor.cs
rename to src/Http/Wolverine.Http/Validation/Internals/DataAnnotationsHttpValidationExecutor.cs
index 53b59de65..a1ae3a659 100644
--- a/src/Http/Wolverine.Http.DataAnnotationsValidation/Internals/DataAnnotationsHttpValidationExecutor.cs
+++ b/src/Http/Wolverine.Http/Validation/Internals/DataAnnotationsHttpValidationExecutor.cs
@@ -1,8 +1,8 @@
-using Microsoft.AspNetCore.Http;
-using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;
+using Microsoft.AspNetCore.Http;
-namespace Wolverine.Http.DataAnnotationsValidation.Internals;
+namespace Wolverine.Http.Validation.Internals;
public class DataAnnotationsHttpValidationExecutor
{
diff --git a/src/Http/Wolverine.Http.DataAnnotationsValidation/Internals/HttpChainDataAnnotationsValidationPolicy.cs b/src/Http/Wolverine.Http/Validation/Internals/HttpChainDataAnnotationsValidationPolicy.cs
similarity index 76%
rename from src/Http/Wolverine.Http.DataAnnotationsValidation/Internals/HttpChainDataAnnotationsValidationPolicy.cs
rename to src/Http/Wolverine.Http/Validation/Internals/HttpChainDataAnnotationsValidationPolicy.cs
index 10cac9c14..c98d90abf 100644
--- a/src/Http/Wolverine.Http.DataAnnotationsValidation/Internals/HttpChainDataAnnotationsValidationPolicy.cs
+++ b/src/Http/Wolverine.Http/Validation/Internals/HttpChainDataAnnotationsValidationPolicy.cs
@@ -1,10 +1,12 @@
+using System.ComponentModel.DataAnnotations;
using JasperFx;
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
+using JasperFx.Core.Reflection;
using Microsoft.AspNetCore.Http;
using Wolverine.Http.CodeGen;
-namespace Wolverine.Http.DataAnnotationsValidation.Internals;
+namespace Wolverine.Http.Validation.Internals;
internal class HttpChainDataAnnotationsValidationPolicy : IHttpPolicy
{
@@ -21,6 +23,12 @@ public void Apply(HttpChain chain, IServiceContainer container)
var validatedType = chain.HasRequestType ? chain.RequestType : chain.ComplexQueryStringType;
if (validatedType == null) return;
+ // ONLY apply if there are ValidationAttributes
+ if (!validatedType.GetProperties().Any(x => x.GetAllAttributes().Any()) && !validatedType.CanBeCastTo())
+ {
+ return;
+ }
+
chain.Metadata.ProducesValidationProblem();
var method =
diff --git a/src/Http/Wolverine.Http.DataAnnotationsValidation/Internals/ProblemDetailSource.cs b/src/Http/Wolverine.Http/Validation/Internals/ProblemDetailSource.cs
similarity index 91%
rename from src/Http/Wolverine.Http.DataAnnotationsValidation/Internals/ProblemDetailSource.cs
rename to src/Http/Wolverine.Http/Validation/Internals/ProblemDetailSource.cs
index fbe56cca3..487138782 100644
--- a/src/Http/Wolverine.Http.DataAnnotationsValidation/Internals/ProblemDetailSource.cs
+++ b/src/Http/Wolverine.Http/Validation/Internals/ProblemDetailSource.cs
@@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
-namespace Wolverine.Http.DataAnnotationsValidation.Internals;
+namespace Wolverine.Http.Validation.Internals;
public class ProblemDetailSource : IProblemDetailSource
{
diff --git a/src/Http/Wolverine.Http/WolverineHttpEndpointRouteBuilderExtensions.cs b/src/Http/Wolverine.Http/WolverineHttpEndpointRouteBuilderExtensions.cs
index 133bb290d..93d4c12a8 100644
--- a/src/Http/Wolverine.Http/WolverineHttpEndpointRouteBuilderExtensions.cs
+++ b/src/Http/Wolverine.Http/WolverineHttpEndpointRouteBuilderExtensions.cs
@@ -12,6 +12,8 @@
using Wolverine.Configuration;
using Wolverine.Http.CodeGen;
using Wolverine.Http.Transport;
+using Wolverine.Http.Validation;
+using Wolverine.Http.Validation.Internals;
using Wolverine.Middleware;
using Wolverine.Runtime;
using Wolverine.Runtime.Routing;
@@ -158,6 +160,8 @@ public static IServiceCollection AddWolverineHttp(this IServiceCollection servic
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+
+ services.AddSingleton(typeof(IProblemDetailSource<>), typeof(ProblemDetailSource<>));
services.ConfigureWolverine(opts =>
{
diff --git a/src/Http/Wolverine.Http/WolverineHttpOptions.cs b/src/Http/Wolverine.Http/WolverineHttpOptions.cs
index 5fcbb6dad..15a4152b5 100644
--- a/src/Http/Wolverine.Http/WolverineHttpOptions.cs
+++ b/src/Http/Wolverine.Http/WolverineHttpOptions.cs
@@ -11,6 +11,7 @@
using Wolverine.Http.Resources;
using Wolverine.Http.Runtime;
using Wolverine.Http.Runtime.MultiTenancy;
+using Wolverine.Http.Validation.Internals;
using Wolverine.Middleware;
namespace Wolverine.Http;
@@ -139,6 +140,15 @@ public WolverineHttpOptions()
///
public ServiceProviderSource ServiceProviderSource { get; set; } = ServiceProviderSource.IsolatedAndScoped;
+ ///
+ /// Apply DataAnnotations Validation middleware to all Wolverine HTTP endpoints
+ ///
+ ///
+ public void UseDataAnnotationsValidationProblemDetailMiddleware()
+ {
+ AddPolicy();
+ }
+
public async ValueTask TryDetectTenantId(HttpContext httpContext)
{
foreach (var strategy in TenantIdDetection.Strategies)
diff --git a/src/Http/WolverineWebApi/ProblemDetailsUsage.cs b/src/Http/WolverineWebApi/ProblemDetailsUsage.cs
index cae661d8c..b0f6a3704 100644
--- a/src/Http/WolverineWebApi/ProblemDetailsUsage.cs
+++ b/src/Http/WolverineWebApi/ProblemDetailsUsage.cs
@@ -9,7 +9,7 @@ namespace WolverineWebApi;
public class ProblemDetailsUsageEndpoint
{
- public ProblemDetails Before(NumberMessage message)
+ public ProblemDetails Validate(NumberMessage message)
{
// If the number is greater than 5, fail with a
// validation message
diff --git a/src/Http/WolverineWebApi/Program.cs b/src/Http/WolverineWebApi/Program.cs
index 2ea74c15b..440c7e9e2 100644
--- a/src/Http/WolverineWebApi/Program.cs
+++ b/src/Http/WolverineWebApi/Program.cs
@@ -231,6 +231,10 @@
// Opting into the Fluent Validation middleware from
// Wolverine.Http.FluentValidation
opts.UseFluentValidationProblemDetailMiddleware();
+
+ // Or instead, you could use Data Annotations that are built
+ // into the Wolverine.HTTP library
+ opts.UseDataAnnotationsValidationProblemDetailMiddleware();
#endregion
diff --git a/Wolverine.Http.DataAnnotationsValidation.Tests/DataAnnotationsValidationEndpoints.cs b/src/Http/WolverineWebApi/Validation/DataAnnotationsValidationEndpoints.cs
similarity index 82%
rename from Wolverine.Http.DataAnnotationsValidation.Tests/DataAnnotationsValidationEndpoints.cs
rename to src/Http/WolverineWebApi/Validation/DataAnnotationsValidationEndpoints.cs
index 2c86eda71..5cb39276f 100644
--- a/Wolverine.Http.DataAnnotationsValidation.Tests/DataAnnotationsValidationEndpoints.cs
+++ b/src/Http/WolverineWebApi/Validation/DataAnnotationsValidationEndpoints.cs
@@ -1,7 +1,8 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
+using Wolverine.Http;
-namespace Wolverine.Http.DataAnnotationsValidation.Tests;
+namespace WolverineWebApi.Validation;
#region sample_endpoint_with_dataannotations_validation
@@ -14,6 +15,8 @@ public override bool IsValid(object? value)
}
}
+#region sample_validated_CreateAccount
+
public record CreateAccount(
// don't forget the property prefix on records
[property: Required] string AccountName,
@@ -29,19 +32,33 @@ public IEnumerable Validate(ValidationContext validationContex
}
}
+#endregion
+
public static class CreateAccountEndpoint
{
+ #region sample_posting_CreateAccount
+
[WolverinePost("/validate/account")]
- public static string Post(CreateAccount account)
+ public static string Post(
+
+ // In this case CreateAccount is being posted
+ // as JSON
+ CreateAccount account)
{
return "Got a new account";
}
+ #endregion
+
+ #region sample_posting_create_account_as_query_string
+
[WolverinePost("/validate/account2")]
public static string Post2([FromQuery] CreateAccount customer)
{
return "Got a new account";
}
+
+ #endregion
}
#endregion
diff --git a/wolverine.sln b/wolverine.sln
index c98ac8bb0..55f3b6044 100644
--- a/wolverine.sln
+++ b/wolverine.sln
@@ -311,10 +311,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.DataAnnotationsVa
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.DataAnnotationsValidation.Tests", "src\Extensions\Wolverine.DataAnnotationsValidation.Tests\Wolverine.DataAnnotationsValidation.Tests.csproj", "{0FD02607-BF12-4201-90F9-3FA88BFCDFBC}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.Http.DataAnnotationsValidation", "src\Http\Wolverine.Http.DataAnnotationsValidation\Wolverine.Http.DataAnnotationsValidation.csproj", "{5860A8F5-018D-4993-9849-EFCABF4BDB16}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.Http.DataAnnotationsValidation.Tests", "Wolverine.Http.DataAnnotationsValidation.Tests\Wolverine.Http.DataAnnotationsValidation.Tests.csproj", "{AE07303F-C6CD-44CB-987D-3B64568DE2E0}"
-EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1753,30 +1749,6 @@ Global
{0FD02607-BF12-4201-90F9-3FA88BFCDFBC}.Release|x64.Build.0 = Release|Any CPU
{0FD02607-BF12-4201-90F9-3FA88BFCDFBC}.Release|x86.ActiveCfg = Release|Any CPU
{0FD02607-BF12-4201-90F9-3FA88BFCDFBC}.Release|x86.Build.0 = Release|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Debug|x64.ActiveCfg = Debug|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Debug|x64.Build.0 = Debug|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Debug|x86.ActiveCfg = Debug|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Debug|x86.Build.0 = Debug|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Release|Any CPU.Build.0 = Release|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Release|x64.ActiveCfg = Release|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Release|x64.Build.0 = Release|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Release|x86.ActiveCfg = Release|Any CPU
- {5860A8F5-018D-4993-9849-EFCABF4BDB16}.Release|x86.Build.0 = Release|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Debug|x64.ActiveCfg = Debug|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Debug|x64.Build.0 = Debug|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Debug|x86.ActiveCfg = Debug|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Debug|x86.Build.0 = Debug|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Release|Any CPU.Build.0 = Release|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Release|x64.ActiveCfg = Release|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Release|x64.Build.0 = Release|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Release|x86.ActiveCfg = Release|Any CPU
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1921,8 +1893,6 @@ Global
{CA5FB523-D71D-4EF3-97B8-00CCDC05C00D} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210}
{38F1B1E4-87B9-401C-9401-7C9318DFAF55} = {F429686D-BB41-4E1C-A84E-518F8A289AEF}
{0FD02607-BF12-4201-90F9-3FA88BFCDFBC} = {F429686D-BB41-4E1C-A84E-518F8A289AEF}
- {5860A8F5-018D-4993-9849-EFCABF4BDB16} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C}
- {AE07303F-C6CD-44CB-987D-3B64568DE2E0} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {30422362-0D90-4DBE-8C97-DD2B5B962768}