Skip to content

Commit e603da9

Browse files
authored
Dev (#28)
* - abstract fluentresult validation dependency in base endpoints into a IRequestValidation abstraction and provide default implementation using fluent validation. * - add options to di service registration methods - add option to disable default request validation via options - rename classes for clarity * - code cleanup * - add new benchamrk results * - update deps to latest * - fix doc error - bump version
1 parent 3a9bb1f commit e603da9

33 files changed

+550
-109
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@
1818
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1919
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
2020

21-
<Version>1.1.1</Version>
21+
<Version>1.2.0</Version>
2222
</PropertyGroup>
2323
</Project>

docs/RequestValidation.md

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Request Validation
22

3-
Every endpoint type in ModEndpoints packages supports request validation using FluentValidation. If a validator is registered for a request model, the request will be automatically validated before reaching the endpoint handler.
3+
Every endpoint type in ModEndpoints packages supports request validation. By default, request validation utilizes FluentValidation and if a validator is registered for a request model, the request will be automatically validated before reaching the endpoint handler.
44

55
Each endpoint type has its own default behavior when request validation fails:
6-
- `MinimalEndpoint` attempts to return an `IResult` with a 400 Bad Request response if validation fails and the response model is compatible with `IResult` for representing a bad request; otherwise, it throws a `ValidationException`.
6+
- `MinimalEndpoint` attempts to return an `IResult` with a 400 Bad Request response if validation fails and the response model is compatible with `IResult` for representing a bad request; otherwise, it throws a `RequestValidationException`.
77
- `WebResultEndpoint` will return an IResult with a 400 Bad Request response if validation fails.
88
- `BusinessResultEndpoint` and `ServiceEndpoint` will return a business result indicating failure with an invalid status if validation does not pass.
99

10-
You can customize this behavior for individual endpoints by overriding the `HandleInvalidValidationResultAsync` method. This method is invoked by the internals of the endpoint implementation when request validation fails and receives a `ValidationResult` (from `FluentValidation`) in an invalid state and the `HttpContext` as parameters. Response type varies depending on the endpoint type and/or response model.
10+
You can customize this behavior for individual endpoints by overriding the `HandleInvalidValidationResultAsync` method. This method is invoked by the internals of the endpoint implementation when request validation fails and receives a failed `RequestValidationResult` containing errors and the `HttpContext` as parameters. Response type varies depending on the endpoint type and/or response model.
1111

1212
>**Note**: If request validation fails, the endpoint handler method `HandleAsync` will not be called.
1313
@@ -36,7 +36,7 @@ internal class GetBookById(ServiceDbContext db)
3636
}
3737

3838
protected override ValueTask<IResult> HandleInvalidValidationResultAsync(
39-
ValidationResult validationResult,
39+
RequestValidationResult validationResult,
4040
HttpContext context,
4141
CancellationToken ct)
4242
{
@@ -52,4 +52,33 @@ internal class GetBookById(ServiceDbContext db)
5252
5353
}
5454
}
55-
```
55+
```
56+
57+
## Disabling Default Request Validation
58+
Default request validation can be disabled during dependency injection by setting UseDefaultRequestValidation to false. This can be useful if you want to implement your own request validation logic or if you want to use a different validation library.
59+
60+
If you are using `ModEndpoints.Core` package:
61+
```csharp
62+
var builder = WebApplication.CreateBuilder(args);
63+
64+
// Add services to the container.
65+
builder.Services.AddModEndpointsCoreFromAssemblyContaining<MyEndpoint>(conf =>
66+
{
67+
conf.UseDefaultRequestValidation = false;
68+
});
69+
70+
// ... add other services
71+
```
72+
73+
If you are using `ModEndpoints` package:
74+
```csharp
75+
var builder = WebApplication.CreateBuilder(args);
76+
77+
// Add services to the container.
78+
builder.Services.AddModEndpointsFromAssemblyContaining<MyEndpoint>(conf =>
79+
{
80+
conf.CoreOptions.UseDefaultRequestValidation = false;
81+
});
82+
83+
// ... add other services
84+
```
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
 k6  .\k6 run minimal_api_basic.js
2+
3+
/\ Grafana /‾‾/
4+
/\ / \ |\ __ / /
5+
/ \/ \ | |/ / / ‾‾\
6+
/ \ | ( | (‾) |
7+
/ __________ \ |_|\_\ \_____/
8+
9+
execution: local
10+
script: minimal_api_basic.js
11+
output: -
12+
13+
scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
14+
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)
15+
16+
17+
✓ status was 200
18+
19+
checks.........................: 100.00% 8608101 out of 8608101
20+
data_received..................: 1.5 GB 13 MB/s
21+
data_sent......................: 878 MB 8.0 MB/s
22+
http_req_blocked...............: avg=2.9µs min=0s med=0s max=34.96ms p(90)=0s p(95)=0s
23+
http_req_connecting............: avg=7ns min=0s med=0s max=1.52ms p(90)=0s p(95)=0s
24+
✓ http_req_duration..............: avg=895.55µs min=0s med=999.7µs max=60.58ms p(90)=1.82ms p(95)=2.01ms
25+
{ expected_response:true }...: avg=895.55µs min=0s med=999.7µs max=60.58ms p(90)=1.82ms p(95)=2.01ms
26+
http_req_failed................: 0.00% 0 out of 8608101
27+
http_req_receiving.............: avg=29.45µs min=0s med=0s max=52.89ms p(90)=0s p(95)=0s
28+
http_req_sending...............: avg=9.07µs min=0s med=0s max=49.85ms p(90)=0s p(95)=0s
29+
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
30+
http_req_waiting...............: avg=857.02µs min=0s med=999.5µs max=60.58ms p(90)=1.66ms p(95)=2.01ms
31+
http_reqs......................: 8608101 78255.150473/s
32+
iteration_duration.............: avg=976.94µs min=0s med=1ms max=70.23ms p(90)=1.99ms p(95)=2.03ms
33+
iterations.....................: 8608101 78255.150473/s
34+
vus............................: 1 min=1 max=100
35+
vus_max........................: 100 min=100 max=100
36+
37+
38+
running (1m50.0s), 000/100 VUs, 8608101 complete and 0 interrupted iterations
39+
default ✓ [======================================] 000/100 VUs 1m50s
40+
41+
42+
 k6  .\k6 run minimal_endpoint_basic.js
43+
44+
/\ Grafana /‾‾/
45+
/\ / \ |\ __ / /
46+
/ \/ \ | |/ / / ‾‾\
47+
/ \ | ( | (‾) |
48+
/ __________ \ |_|\_\ \_____/
49+
50+
execution: local
51+
script: minimal_endpoint_basic.js
52+
output: -
53+
54+
scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
55+
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)
56+
57+
58+
✓ status was 200
59+
60+
checks.........................: 100.00% 8705828 out of 8705828
61+
data_received..................: 1.5 GB 14 MB/s
62+
data_sent......................: 932 MB 8.5 MB/s
63+
http_req_blocked...............: avg=2.89µs min=0s med=0s max=42.15ms p(90)=0s p(95)=0s
64+
http_req_connecting............: avg=7ns min=0s med=0s max=3.84ms p(90)=0s p(95)=0s
65+
✓ http_req_duration..............: avg=877.57µs min=0s med=999.4µs max=100.48ms p(90)=1.71ms p(95)=2.01ms
66+
{ expected_response:true }...: avg=877.57µs min=0s med=999.4µs max=100.48ms p(90)=1.71ms p(95)=2.01ms
67+
http_req_failed................: 0.00% 0 out of 8705828
68+
http_req_receiving.............: avg=30.08µs min=0s med=0s max=88.46ms p(90)=0s p(95)=0s
69+
http_req_sending...............: avg=9.37µs min=0s med=0s max=51.46ms p(90)=0s p(95)=0s
70+
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
71+
http_req_waiting...............: avg=838.11µs min=0s med=999.1µs max=100.48ms p(90)=1.59ms p(95)=2.01ms
72+
http_reqs......................: 8705828 79143.69269/s
73+
iteration_duration.............: avg=962.91µs min=0s med=999.9µs max=103.71ms p(90)=1.99ms p(95)=2.04ms
74+
iterations.....................: 8705828 79143.69269/s
75+
vus............................: 1 min=1 max=100
76+
vus_max........................: 100 min=100 max=100
77+
78+
79+
running (1m50.0s), 000/100 VUs, 8705828 complete and 0 interrupted iterations
80+
default ✓ [======================================] 000/100 VUs 1m50s
81+
82+
83+
 k6  .\k6 run webresult_endpoint_basic.js
84+
85+
/\ Grafana /‾‾/
86+
/\ / \ |\ __ / /
87+
/ \/ \ | |/ / / ‾‾\
88+
/ \ | ( | (‾) |
89+
/ __________ \ |_|\_\ \_____/
90+
91+
execution: local
92+
script: webresult_endpoint_basic.js
93+
output: -
94+
95+
scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
96+
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)
97+
98+
99+
✓ status was 200
100+
101+
checks.........................: 100.00% 8558194 out of 8558194
102+
data_received..................: 1.5 GB 13 MB/s
103+
data_sent......................: 933 MB 8.5 MB/s
104+
http_req_blocked...............: avg=2.95µs min=0s med=0s max=32.42ms p(90)=0s p(95)=0s
105+
http_req_connecting............: avg=7ns min=0s med=0s max=1.52ms p(90)=0s p(95)=0s
106+
✓ http_req_duration..............: avg=891.78µs min=0s med=999.6µs max=92.66ms p(90)=1.84ms p(95)=2.01ms
107+
{ expected_response:true }...: avg=891.78µs min=0s med=999.6µs max=92.66ms p(90)=1.84ms p(95)=2.01ms
108+
http_req_failed................: 0.00% 0 out of 8558194
109+
http_req_receiving.............: avg=29.78µs min=0s med=0s max=54.24ms p(90)=0s p(95)=0s
110+
http_req_sending...............: avg=9.2µs min=0s med=0s max=38.77ms p(90)=0s p(95)=0s
111+
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
112+
http_req_waiting...............: avg=852.8µs min=0s med=999.3µs max=79.03ms p(90)=1.68ms p(95)=2.01ms
113+
http_reqs......................: 8558194 77801.723038/s
114+
iteration_duration.............: avg=979.59µs min=0s med=1ms max=93.54ms p(90)=1.99ms p(95)=2.06ms
115+
iterations.....................: 8558194 77801.723038/s
116+
vus............................: 1 min=1 max=100
117+
vus_max........................: 100 min=100 max=100
118+
119+
120+
running (1m50.0s), 000/100 VUs, 8558194 complete and 0 interrupted iterations
121+
default ✓ [======================================] 000/100 VUs 1m50s
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
 k6  .\k6 run minimal_api_inprocess.js
2+
3+
/\ Grafana /‾‾/
4+
/\ / \ |\ __ / /
5+
/ \/ \ | |/ / / ‾‾\
6+
/ \ | ( | (‾) |
7+
/ __________ \ |_|\_\ \_____/
8+
9+
execution: local
10+
script: minimal_api_inprocess.js
11+
output: -
12+
13+
scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
14+
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)
15+
16+
17+
✓ status was 200
18+
19+
checks.........................: 100.00% 6137937 out of 6137937
20+
data_received..................: 1.1 GB 10 MB/s
21+
data_sent......................: 1.5 GB 13 MB/s
22+
http_req_blocked...............: avg=3.95µs min=0s med=0s max=41.59ms p(90)=0s p(95)=0s
23+
http_req_connecting............: avg=12ns min=0s med=0s max=5.25ms p(90)=0s p(95)=0s
24+
✓ http_req_duration..............: avg=1.23ms min=0s med=1.01ms max=82.18ms p(90)=2.05ms p(95)=2.52ms
25+
{ expected_response:true }...: avg=1.23ms min=0s med=1.01ms max=82.18ms p(90)=2.05ms p(95)=2.52ms
26+
http_req_failed................: 0.00% 0 out of 6137937
27+
http_req_receiving.............: avg=43.81µs min=0s med=0s max=70.43ms p(90)=0s p(95)=0s
28+
http_req_sending...............: avg=17.99µs min=0s med=0s max=61.93ms p(90)=0s p(95)=0s
29+
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
30+
http_req_waiting...............: avg=1.17ms min=0s med=1.01ms max=80.69ms p(90)=2.03ms p(95)=2.51ms
31+
http_reqs......................: 6137937 55799.278999/s
32+
iteration_duration.............: avg=1.37ms min=0s med=1.06ms max=93.6ms p(90)=2.2ms p(95)=2.59ms
33+
iterations.....................: 6137937 55799.278999/s
34+
vus............................: 1 min=1 max=100
35+
vus_max........................: 100 min=100 max=100
36+
37+
38+
running (1m50.0s), 000/100 VUs, 6137937 complete and 0 interrupted iterations
39+
default ✓ [======================================] 000/100 VUs 1m50s
40+
41+
42+
 k6  .\k6 run minimal_endpoint_inprocess.js
43+
44+
/\ Grafana /‾‾/
45+
/\ / \ |\ __ / /
46+
/ \/ \ | |/ / / ‾‾\
47+
/ \ | ( | (‾) |
48+
/ __________ \ |_|\_\ \_____/
49+
50+
execution: local
51+
script: minimal_endpoint_inprocess.js
52+
output: -
53+
54+
scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
55+
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)
56+
57+
58+
✓ status was 200
59+
60+
checks.........................: 100.00% 6159786 out of 6159786
61+
data_received..................: 1.2 GB 11 MB/s
62+
data_sent......................: 1.5 GB 14 MB/s
63+
http_req_blocked...............: avg=3.7µs min=0s med=0s max=40.08ms p(90)=0s p(95)=0s
64+
http_req_connecting............: avg=12ns min=0s med=0s max=2.04ms p(90)=0s p(95)=0s
65+
✓ http_req_duration..............: avg=1.23ms min=0s med=1.01ms max=93.94ms p(90)=2.04ms p(95)=2.51ms
66+
{ expected_response:true }...: avg=1.23ms min=0s med=1.01ms max=93.94ms p(90)=2.04ms p(95)=2.51ms
67+
http_req_failed................: 0.00% 0 out of 6159786
68+
http_req_receiving.............: avg=40.19µs min=0s med=0s max=56.44ms p(90)=0s p(95)=0s
69+
http_req_sending...............: avg=16.76µs min=0s med=0s max=81.37ms p(90)=0s p(95)=0s
70+
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
71+
http_req_waiting...............: avg=1.17ms min=0s med=1.01ms max=93.94ms p(90)=2.02ms p(95)=2.49ms
72+
http_reqs......................: 6159786 55997.813908/s
73+
iteration_duration.............: avg=1.36ms min=0s med=1.07ms max=100.17ms p(90)=2.17ms p(95)=2.56ms
74+
iterations.....................: 6159786 55997.813908/s
75+
vus............................: 1 min=1 max=100
76+
vus_max........................: 100 min=100 max=100
77+
78+
79+
running (1m50.0s), 000/100 VUs, 6159786 complete and 0 interrupted iterations
80+
default ✓ [======================================] 000/100 VUs 1m50s
81+
82+
83+
 k6  .\k6 run webresult_endpoint_inprocess.js
84+
85+
/\ Grafana /‾‾/
86+
/\ / \ |\ __ / /
87+
/ \/ \ | |/ / / ‾‾\
88+
/ \ | ( | (‾) |
89+
/ __________ \ |_|\_\ \_____/
90+
91+
execution: local
92+
script: webresult_endpoint_inprocess.js
93+
output: -
94+
95+
scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
96+
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)
97+
98+
99+
✓ status was 200
100+
101+
checks.........................: 100.00% 6127045 out of 6127045
102+
data_received..................: 1.1 GB 10 MB/s
103+
data_sent......................: 1.5 GB 14 MB/s
104+
http_req_blocked...............: avg=3.71µs min=0s med=0s max=29.23ms p(90)=0s p(95)=0s
105+
http_req_connecting............: avg=15ns min=0s med=0s max=14.92ms p(90)=0s p(95)=0s
106+
✓ http_req_duration..............: avg=1.24ms min=0s med=1.01ms max=92.84ms p(90)=2.04ms p(95)=2.52ms
107+
{ expected_response:true }...: avg=1.24ms min=0s med=1.01ms max=92.84ms p(90)=2.04ms p(95)=2.52ms
108+
http_req_failed................: 0.00% 0 out of 6127045
109+
http_req_receiving.............: avg=40.83µs min=0s med=0s max=53.23ms p(90)=0s p(95)=0s
110+
http_req_sending...............: avg=16.93µs min=0s med=0s max=89.84ms p(90)=0s p(95)=0s
111+
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
112+
http_req_waiting...............: avg=1.18ms min=0s med=1.01ms max=92.84ms p(90)=2.01ms p(95)=2.5ms
113+
http_reqs......................: 6127045 55700.352834/s
114+
iteration_duration.............: avg=1.37ms min=0s med=1.08ms max=93.84ms p(90)=2.15ms p(95)=2.55ms
115+
iterations.....................: 6127045 55700.352834/s
116+
vus............................: 1 min=1 max=100
117+
vus_max........................: 100 min=100 max=100
118+
119+
120+
running (1m50.0s), 000/100 VUs, 6127045 complete and 0 interrupted iterations
121+
default ✓ [======================================] 000/100 VUs 1m50s
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using FluentValidation.Results;
2+
3+
namespace BenchmarkWebApi.Extensions;
4+
5+
public static class ValidationResultExtensions
6+
{
7+
public static IResult ToMinimalApiResult(this ValidationResult validationResult)
8+
{
9+
var errors = GetErrors(validationResult);
10+
return Results.ValidationProblem(errors);
11+
}
12+
13+
private static Dictionary<string, string[]> GetErrors(ValidationResult validationResult)
14+
{
15+
return validationResult.Errors
16+
.GroupBy(e => e.PropertyName)
17+
.Select(g => new { g.Key, Values = g.Select(e => e.ErrorMessage).ToArray() })
18+
.ToDictionary(pair => pair.Key, pair => pair.Values);
19+
}
20+
}

samples/BenchmarkWebApi/Features/MinimalApis/InProcessTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using BenchmarkWebApi.Services;
1+
using BenchmarkWebApi.Extensions;
2+
using BenchmarkWebApi.Services;
23
using FluentValidation;
34
using Microsoft.AspNetCore.Mvc;
45
using ModEndpoints.Core;

samples/ServiceEndpointClient/ServiceEndpointClient.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
10-
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.4" />
9+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
10+
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.5" />
1111
</ItemGroup>
1212

1313
<ItemGroup>

samples/ShowcaseWebApi/ShowcaseWebApi.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.4" />
8+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.5" />
99
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
1010
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
1111
<PackageReference Include="Asp.Versioning.Http" Version="8.1.0" />

samples/WeatherForecastWebApi/WeatherForecastWebApi.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
11-
<PackageReference Include="Scalar.AspNetCore" Version="2.2.7" />
10+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" />
11+
<PackageReference Include="Scalar.AspNetCore" Version="2.3.1" />
1212
</ItemGroup>
1313

1414
<ItemGroup>

0 commit comments

Comments
 (0)