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
1 change: 1 addition & 0 deletions src/OptionalValues/OptionalValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public OptionalValue(T value)
"Design",
"CA1000:Do not declare static members on generic types",
Justification = "Having a static class for Unspecified is not adding value at the moment")]
[JsonIgnore]
public static OptionalValue<T> Unspecified => new();

/// <summary>
Expand Down
133 changes: 133 additions & 0 deletions test/OptionalValues.DataAnnotations.Tests/MinimalApiValidationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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();
});

// Add validation support for minimal APIs in .NET 10
builder.Services.AddValidation();

// 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
2 changes: 1 addition & 1 deletion version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "0.8",
"version": "0.9",
"publicReleaseRefSpec": [
"^refs/heads/main$",
"^refs/heads/v\\d+(?:\\.\\d+)?$",
Expand Down