Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 102 additions & 4 deletions docs/guide/durability/marten/event-sourcing.md
Original file line number Diff line number Diff line change
Expand Up @@ -914,16 +914,114 @@ own rules about value type identifiers](https://martendb.io/documents/identity.h

For a message handler, let's start with this example identifier type and aggregate from the Wolverine tests:

snippet: sample_strong_typed_identifier_with_aggregate
<!-- snippet: sample_strong_typed_identifier_with_aggregate -->
<a id='snippet-sample_strong_typed_identifier_with_aggregate'></a>
```cs
[StronglyTypedId(Template.Guid)]
public readonly partial struct LetterId;

public class StrongLetterAggregate
{
public StrongLetterAggregate()
{
}

public LetterId Id { get; set; }

public int ACount { get; set; }
public int BCount { get; set; }
public int CCount { get; set; }
public int DCount { get; set; }

public void Apply(AEvent _) => ACount++;
public void Apply(BEvent _) => BCount++;
public void Apply(CEvent _) => CCount++;
public void Apply(DEvent _) => DCount++;
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/MartenTests/AggregateHandlerWorkflow/strong_named_identifiers.cs#L185-L209' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_strong_typed_identifier_with_aggregate' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

And now let's use that identifier type in message handlers:

snippet: sample_using_strong_typed_identifier_with_aggregate_handler_workflow
<!-- snippet: sample_using_strong_typed_identifier_with_aggregate_handler_workflow -->
<a id='snippet-sample_using_strong_typed_identifier_with_aggregate_handler_workflow'></a>
```cs
public record IncrementStrongA(LetterId Id);

public record AddFrom(LetterId Id1, LetterId Id2);

public record IncrementBOnBoth(LetterId Id1, LetterId Id2);

public record FetchCounts(LetterId Id);

public static class StrongLetterHandler
{
public static StrongLetterAggregate Handle(FetchCounts counts,
[ReadAggregate] StrongLetterAggregate aggregate) => aggregate;

public static AEvent Handle(IncrementStrongA command, [WriteAggregate] StrongLetterAggregate aggregate)
{
return new();
}

public static void Handle(
IncrementBOnBoth command,
[WriteAggregate(nameof(IncrementBOnBoth.Id1))] IEventStream<StrongLetterAggregate> stream1,
[WriteAggregate(nameof(IncrementBOnBoth.Id2))] IEventStream<StrongLetterAggregate> stream2
)
{
stream1.AppendOne(new BEvent());
stream2.AppendOne(new BEvent());
}

public static IEnumerable<object> Handle(
AddFrom command,
[WriteAggregate(nameof(AddFrom.Id1))] StrongLetterAggregate _,
[ReadAggregate(nameof(AddFrom.Id2))] StrongLetterAggregate readOnly)
{
for (int i = 0; i < readOnly.ACount; i++)
{
yield return new AEvent();
}

for (int i = 0; i < readOnly.BCount; i++)
{
yield return new BEvent();
}

for (int i = 0; i < readOnly.CCount; i++)
{
yield return new CEvent();
}

for (int i = 0; i < readOnly.DCount; i++)
{
yield return new DEvent();
}
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/MartenTests/AggregateHandlerWorkflow/strong_named_identifiers.cs#L124-L181' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_strong_typed_identifier_with_aggregate_handler_workflow' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

And also in some of the equivalent Wolverine.HTTP endpoints:

snippet: sample_using_strong_typed_id_as_route_argument
<!-- snippet: sample_using_strong_typed_id_as_route_argument -->
<a id='snippet-sample_using_strong_typed_id_as_route_argument'></a>
```cs
[WolverineGet("/sti/aggregate/longhand/{id}")]
public static ValueTask<StrongLetterAggregate> Handle2(LetterId id, IDocumentSession session) =>
session.Events.FetchLatest<StrongLetterAggregate>(id.Value);

// This is an equivalent to the endpoint above
[WolverineGet("/sti/aggregate/{id}")]
public static StrongLetterAggregate Handle(
[ReadAggregate] StrongLetterAggregate aggregate) => aggregate;
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/StrongTypedIdentifiers.cs#L11-L22' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_strong_typed_id_as_route_argument' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


tools do this for you, and value types generated by these tools are
legal route argument variables for Wolverine.HTTP now.
legal route argument variables for Wolverine.HTTP now.
43 changes: 43 additions & 0 deletions docs/guide/http/fluentvalidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,46 @@ public class ValidatedQuery
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/FormEndpoints.cs#L201-L228' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_fluent_validation_with_asparameters' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## QueryString Binding <Badge type="tip" text="5.0" />

Wolverine.HTTP can apply the Fluent Validation middleware to complex types that are bound by the `[FromQuery]` behavior:

<!-- snippet: sample_CreateCustomer_endpoint_with_validation -->
<a id='snippet-sample_createcustomer_endpoint_with_validation'></a>
```cs
public record CreateCustomer
(
string FirstName,
string LastName,
string PostalCode
)
{
public class CreateCustomerValidator : AbstractValidator<CreateCustomer>
{
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";
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Validation/CreateCustomerEndpoint.cs#L8-L43' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_createcustomer_endpoint_with_validation' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
8 changes: 7 additions & 1 deletion docs/introduction/from-mediatr.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,15 @@ public static class CreateCustomerEndpoint
{
return "Got a new customer";
}

[WolverinePost("/validate/customer2")]
public static string Post2([FromQuery] CreateCustomer customer)
{
return "Got a new customer";
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Validation/CreateCustomerEndpoint.cs#L7-L36' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_createcustomer_endpoint_with_validation' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Validation/CreateCustomerEndpoint.cs#L8-L43' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_createcustomer_endpoint_with_validation' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

In the application bootstrapping, I've added this option:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ internal class HttpChainFluentValidationPolicy : IHttpPolicy
{
public void Apply(IReadOnlyList<HttpChain> chains, GenerationRules rules, IServiceContainer container)
{
foreach (var chain in chains.Where(x => x.HasRequestType))
foreach (var chain in chains)
{
Apply(chain, container);
}
}

public void Apply(HttpChain chain, IServiceContainer container)
{
var validatorInterface = typeof(IValidator<>).MakeGenericType(chain.RequestType!);
var validatedType = chain.HasRequestType ? chain.RequestType : chain.ComplexQueryStringType;
if (validatedType == null) return;

var validatorInterface = typeof(IValidator<>).MakeGenericType(validatedType);

var registered = container.RegistrationsFor(validatorInterface);

Expand All @@ -30,27 +33,27 @@ public void Apply(HttpChain chain, IServiceContainer container)

var method =
typeof(FluentValidationHttpExecutor).GetMethod(nameof(FluentValidationHttpExecutor.ExecuteOne))!
.MakeGenericMethod(chain.RequestType);
.MakeGenericMethod(validatedType);

var methodCall = new MethodCall(typeof(FluentValidationHttpExecutor), method)
{
CommentText = "Execute FluentValidation validators"
};

var maybeResult = new MaybeEndWithResultFrame(methodCall.ReturnVariable!);
chain.Middleware.InsertRange(0, new Frame[]{methodCall,maybeResult});
chain.Middleware.InsertRange(0, [methodCall, maybeResult]);
}
else if (registered.Count() > 1)
{
chain.Metadata.ProducesValidationProblem(400);

var method =
typeof(FluentValidationHttpExecutor).GetMethod(nameof(FluentValidationHttpExecutor.ExecuteMany))!
.MakeGenericMethod(chain.RequestType);
.MakeGenericMethod(validatedType);

var methodCall = new MethodCall(typeof(FluentValidationHttpExecutor), method);
var maybeResult = new MaybeEndWithResultFrame(methodCall.ReturnVariable!);
chain.Middleware.InsertRange(0, new Frame[]{methodCall,maybeResult});
chain.Middleware.InsertRange(0, [methodCall, maybeResult]);
}
}
}
35 changes: 35 additions & 0 deletions src/Http/Wolverine.Http.Tests/fluent_validation_middleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,41 @@ public async Task one_validator_sad_path()
// in the request
var problems = results.ReadAsJson<HttpValidationProblemDetails>();
}

[Fact]
public async Task one_validator_happy_path_on_complex_query_string_argument()
{
// Succeeds w/ a 200
var result = await Scenario(x =>
{
x.Post.Url("/validate/customer2")
.QueryString(nameof(CreateCustomer.FirstName), "Creed")
.QueryString(nameof(CreateCustomer.LastName), "Humphrey")
.QueryString(nameof(CreateCustomer.PostalCode), "11111") ;
x.ContentTypeShouldBe("text/plain");
});
}

[Fact]
public async Task one_validator_sad_path_on_complex_query_string_argument()
{
var createCustomer = new CreateCustomer(null, "Humphrey", "11111");

var results = await Scenario(x =>
{
x.Post.Url("/validate/customer2")
.QueryString(nameof(CreateCustomer.FirstName), "Creed")
//.QueryString(nameof(CreateCustomer.LastName), "Humphrey")
.QueryString(nameof(CreateCustomer.PostalCode), "11111") ;
x.ContentTypeShouldBe("application/problem+json");
x.StatusCodeShouldBe(400);
});

// Just proving that we have HttpValidationProblemDetails content
// in the request
var problems = results.ReadAsJson<HttpValidationProblemDetails>();
}

[Fact]
public async Task one_validator_sad_path_in_different_assembly()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ namespace Internal.Generated.WolverineHandlers
public sealed class DELETE_validate_user_compound : Wolverine.Http.HttpHandler
{
private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
private readonly Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.BlockUser> _problemDetailSource;
private readonly FluentValidation.IValidator<WolverineWebApi.Validation.BlockUser> _validator;
private readonly Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.BlockUser> _problemDetailSource;

public DELETE_validate_user_compound(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.BlockUser> problemDetailSource, FluentValidation.IValidator<WolverineWebApi.Validation.BlockUser> validator) : base(wolverineHttpOptions)
public DELETE_validate_user_compound(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, FluentValidation.IValidator<WolverineWebApi.Validation.BlockUser> validator, Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.BlockUser> problemDetailSource) : base(wolverineHttpOptions)
{
_wolverineHttpOptions = wolverineHttpOptions;
_problemDetailSource = problemDetailSource;
_validator = validator;
_problemDetailSource = problemDetailSource;
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ namespace Internal.Generated.WolverineHandlers
public sealed class POST_validate_customer : Wolverine.Http.HttpHandler
{
private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
private readonly Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.CreateCustomer> _problemDetailSource;
private readonly FluentValidation.IValidator<WolverineWebApi.Validation.CreateCustomer> _validator;
private readonly Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.CreateCustomer> _problemDetailSource;

public POST_validate_customer(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.CreateCustomer> problemDetailSource, FluentValidation.IValidator<WolverineWebApi.Validation.CreateCustomer> validator) : base(wolverineHttpOptions)
public POST_validate_customer(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, FluentValidation.IValidator<WolverineWebApi.Validation.CreateCustomer> validator, Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.CreateCustomer> problemDetailSource) : base(wolverineHttpOptions)
{
_wolverineHttpOptions = wolverineHttpOptions;
_problemDetailSource = problemDetailSource;
_validator = validator;
_problemDetailSource = problemDetailSource;
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ namespace Internal.Generated.WolverineHandlers
public sealed class POST_validate_user : Wolverine.Http.HttpHandler
{
private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
private readonly System.Collections.Generic.IEnumerable<FluentValidation.IValidator<WolverineWebApi.Validation.CreateUser>> _validatorIEnumerable;
private readonly Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.CreateUser> _problemDetailSource;
private readonly System.Collections.Generic.IEnumerable<FluentValidation.IValidator<WolverineWebApi.Validation.CreateUser>> _validatorIEnumerable;

public POST_validate_user(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, System.Collections.Generic.IEnumerable<FluentValidation.IValidator<WolverineWebApi.Validation.CreateUser>> validatorIEnumerable, Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.CreateUser> problemDetailSource) : base(wolverineHttpOptions)
public POST_validate_user(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.CreateUser> problemDetailSource, System.Collections.Generic.IEnumerable<FluentValidation.IValidator<WolverineWebApi.Validation.CreateUser>> validatorIEnumerable) : base(wolverineHttpOptions)
{
_wolverineHttpOptions = wolverineHttpOptions;
_validatorIEnumerable = validatorIEnumerable;
_problemDetailSource = problemDetailSource;
_validatorIEnumerable = validatorIEnumerable;
}


Expand Down
7 changes: 7 additions & 0 deletions src/Http/WolverineWebApi/Validation/CreateCustomerEndpoint.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Wolverine.Http;

namespace WolverineWebApi.Validation;
Expand Down Expand Up @@ -31,6 +32,12 @@ 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";
}
}

#endregion
Expand Down
Loading