Skip to content

Commit b69b06b

Browse files
Copilotdesjoerd
andauthored
✨ Add and verify .NET 10 minimal API validation support with DataAnnotations (#29)
* Initial plan * Add .NET 10 minimal API validation tests for DataAnnotations Co-authored-by: desjoerd <2460430+desjoerd@users.noreply.github.com> * Simplify minimal API validation tests - use ValidationContext with service provider support Co-authored-by: desjoerd <2460430+desjoerd@users.noreply.github.com> * Add clarifying comments about IServiceProvider parameter in validation tests Co-authored-by: desjoerd <2460430+desjoerd@users.noreply.github.com> * Implement minimal API test with host builder - encountering circular reference issue with AddValidation Co-authored-by: desjoerd <2460430+desjoerd@users.noreply.github.com> * Add AddValidation() to reproduce circular reference issue with OptionalValue Co-authored-by: desjoerd <2460430+desjoerd@users.noreply.github.com> * Add JsonIgnore attribute to Unspecified property in OptionalValue to ensure Microsoft.Extensions.Validation ignores validation on that property * Update version to 0.9 and remove obsolete comment about circular reference fix Co-authored-by: desjoerd <2460430+desjoerd@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: desjoerd <2460430+desjoerd@users.noreply.github.com> Co-authored-by: desjoerd <sjoerd.meer@outlook.com>
1 parent 73b708c commit b69b06b

4 files changed

Lines changed: 141 additions & 2 deletions

File tree

src/OptionalValues/OptionalValue.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public OptionalValue(T value)
7575
"Design",
7676
"CA1000:Do not declare static members on generic types",
7777
Justification = "Having a static class for Unspecified is not adding value at the moment")]
78+
[JsonIgnore]
7879
public static OptionalValue<T> Unspecified => new();
7980

8081
/// <summary>
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using System.Net;
3+
using System.Net.Http.Json;
4+
5+
using Shouldly;
6+
7+
#if NET10_0_OR_GREATER
8+
using Microsoft.AspNetCore.Builder;
9+
using Microsoft.AspNetCore.Hosting;
10+
using Microsoft.AspNetCore.Mvc.Testing;
11+
using Microsoft.AspNetCore.TestHost;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.Extensions.Hosting;
14+
#endif
15+
16+
namespace OptionalValues.DataAnnotations.Tests;
17+
18+
/// <summary>
19+
/// Tests that verify OptionalValues DataAnnotations work with .NET 10's minimal API validation support.
20+
/// In .NET 10, minimal APIs automatically validate models when AddValidation() is called.
21+
/// See: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-10.0#validation-support-in-minimal-apis
22+
///
23+
/// These tests create an actual minimal API with host builder and use AddValidation() to verify
24+
/// that OptionalValues DataAnnotations work correctly with automatic validation.
25+
/// </summary>
26+
#if NET10_0_OR_GREATER
27+
public class MinimalApiValidationTest : IAsyncLifetime
28+
{
29+
private WebApplication? _app;
30+
private HttpClient? _client;
31+
32+
public class TestModel
33+
{
34+
[RequiredValue]
35+
public OptionalValue<string> RequiredName { get; init; }
36+
37+
[OptionalRange(1, 100)]
38+
public OptionalValue<int> RangeValue { get; init; }
39+
40+
[Specified]
41+
public OptionalValue<string?> SpecifiedField { get; init; }
42+
}
43+
44+
public async Task InitializeAsync()
45+
{
46+
// Create a minimal API host with AddValidation()
47+
var builder = WebApplication.CreateBuilder();
48+
49+
// Configure JSON options for OptionalValue support
50+
builder.Services.ConfigureHttpJsonOptions(options =>
51+
{
52+
options.SerializerOptions.AddOptionalValueSupport();
53+
});
54+
55+
// Add validation support for minimal APIs in .NET 10
56+
builder.Services.AddValidation();
57+
58+
// Use test server
59+
builder.WebHost.UseTestServer();
60+
61+
_app = builder.Build();
62+
63+
// Create minimal API endpoint with automatic validation
64+
_app.MapPost("/test", (TestModel model) => Results.Ok(model))
65+
.WithName("TestEndpoint");
66+
67+
await _app.StartAsync();
68+
_client = _app.GetTestClient();
69+
}
70+
71+
public async Task DisposeAsync()
72+
{
73+
if (_app != null)
74+
{
75+
await _app.DisposeAsync();
76+
}
77+
_client?.Dispose();
78+
}
79+
80+
[Fact]
81+
public async Task ValidModel_ShouldPass_MinimalApiValidation()
82+
{
83+
// Arrange - Send valid JSON
84+
var json = """{"RequiredName":"TestName","RangeValue":50,"SpecifiedField":"value"}""";
85+
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
86+
87+
// Act - Post to minimal API which uses AddValidation()
88+
var response = await _client!.PostAsync("/test", content);
89+
90+
// Assert
91+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
92+
}
93+
94+
[Fact]
95+
public async Task InvalidModel_ShouldFail_MinimalApiValidation()
96+
{
97+
// Arrange - Send invalid JSON (out of range value)
98+
var json = """{"RequiredName":"Test","RangeValue":150,"SpecifiedField":"value"}""";
99+
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
100+
101+
// Act - Post to minimal API which uses AddValidation()
102+
var response = await _client!.PostAsync("/test", content);
103+
104+
// Assert - Minimal API validation should return BadRequest
105+
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
106+
}
107+
108+
[Fact]
109+
public async Task UnspecifiedOptionalFields_ShouldBeValid_MinimalApiValidation()
110+
{
111+
// Arrange - Don't send unspecified fields in JSON
112+
var json = """{"RequiredName":"Test","SpecifiedField":null}""";
113+
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
114+
115+
// Act - Post to minimal API which uses AddValidation()
116+
var response = await _client!.PostAsync("/test", content);
117+
118+
// Assert
119+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
120+
}
121+
}
122+
#else
123+
public class MinimalApiValidationTest
124+
{
125+
[Fact]
126+
public void MinimalApiValidation_OnlyAvailableInNet10()
127+
{
128+
// Minimal API validation with AddValidation() is only available in .NET 10+
129+
true.ShouldBeTrue();
130+
}
131+
}
132+
#endif
133+

test/OptionalValues.DataAnnotations.Tests/OptionalValues.DataAnnotations.Tests.csproj

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
44
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
@@ -20,6 +20,11 @@
2020
</PackageReference>
2121
</ItemGroup>
2222

23+
<!-- Only include Microsoft.AspNetCore.Mvc.Testing for net10.0 where minimal API validation is available -->
24+
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
25+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
26+
</ItemGroup>
27+
2328
<ItemGroup>
2429
<Using Include="Xunit" />
2530
</ItemGroup>

version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
3-
"version": "0.8",
3+
"version": "0.9",
44
"publicReleaseRefSpec": [
55
"^refs/heads/main$",
66
"^refs/heads/v\\d+(?:\\.\\d+)?$",

0 commit comments

Comments
 (0)