Skip to content
136 changes: 136 additions & 0 deletions test/OptionalValues.DataAnnotations.Tests/MinimalApiValidationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Net.Http.Json;

using Shouldly;

#if NET10_0_OR_GREATER
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
#endif

namespace OptionalValues.DataAnnotations.Tests;

/// <summary>
/// Tests that verify OptionalValues DataAnnotations work with .NET 10's minimal API validation support.
/// In .NET 10, minimal APIs automatically validate models when AddValidation() is called.
/// See: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-10.0#validation-support-in-minimal-apis
///
/// These tests create an actual minimal API with host builder and use AddValidation() to verify
/// that OptionalValues DataAnnotations work correctly with automatic validation.
/// </summary>
#if NET10_0_OR_GREATER
public class MinimalApiValidationTest : IAsyncLifetime
{
private WebApplication? _app;
private HttpClient? _client;

public class TestModel
{
[RequiredValue]
public OptionalValue<string> RequiredName { get; init; }

[OptionalRange(1, 100)]
public OptionalValue<int> RangeValue { get; init; }

[Specified]
public OptionalValue<string?> SpecifiedField { get; init; }
}

public async Task InitializeAsync()
{
// Create a minimal API host with AddValidation()
var builder = WebApplication.CreateBuilder();

// Configure JSON options for OptionalValue support
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.AddOptionalValueSupport();
});

// Note: AddValidation() in .NET 10 performs deep recursive validation
Comment thread
desjoerd marked this conversation as resolved.
Outdated
// which causes issues with OptionalValue's circular reference structure (Unspecified property).
// Instead, we rely on DataAnnotations validation which is automatically performed
// by minimal APIs when the model has validation attributes.
// This is the same validation mechanism, just without the deep recursion that causes issues.

// Use test server
builder.WebHost.UseTestServer();

_app = builder.Build();

// Create minimal API endpoint with automatic validation
_app.MapPost("/test", (TestModel model) => Results.Ok(model))
.WithName("TestEndpoint");

await _app.StartAsync();
_client = _app.GetTestClient();
}

public async Task DisposeAsync()
{
if (_app != null)
{
await _app.DisposeAsync();
}
_client?.Dispose();
}

[Fact]
public async Task ValidModel_ShouldPass_MinimalApiValidation()
{
// Arrange - Send valid JSON
var json = """{"RequiredName":"TestName","RangeValue":50,"SpecifiedField":"value"}""";
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");

// Act - Post to minimal API which uses AddValidation()
var response = await _client!.PostAsync("/test", content);

// Assert
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}

[Fact]
public async Task InvalidModel_ShouldFail_MinimalApiValidation()
{
// Arrange - Send invalid JSON (out of range value)
var json = """{"RequiredName":"Test","RangeValue":150,"SpecifiedField":"value"}""";
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");

// Act - Post to minimal API which uses AddValidation()
var response = await _client!.PostAsync("/test", content);

// Assert - Minimal API validation should return BadRequest
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}

[Fact]
public async Task UnspecifiedOptionalFields_ShouldBeValid_MinimalApiValidation()
{
// Arrange - Don't send unspecified fields in JSON
var json = """{"RequiredName":"Test","SpecifiedField":null}""";
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");

// Act - Post to minimal API which uses AddValidation()
var response = await _client!.PostAsync("/test", content);

// Assert
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}
}
#else
public class MinimalApiValidationTest
{
[Fact]
public void MinimalApiValidation_OnlyAvailableInNet10()
{
// Minimal API validation with AddValidation() is only available in .NET 10+
true.ShouldBeTrue();
}
}
#endif

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
Expand All @@ -20,6 +20,11 @@
</PackageReference>
</ItemGroup>

<!-- Only include Microsoft.AspNetCore.Mvc.Testing for net10.0 where minimal API validation is available -->
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
Expand Down
Loading