File: ValidationsGenerator\ValidationsGenerator.IValidatableObject.cs
Web Access
Project: src\src\Http\Http.Extensions\test\Microsoft.AspNetCore.Http.Extensions.Tests.csproj (Microsoft.AspNetCore.Http.Extensions.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests;
 
public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
{
    [Fact]
    public async Task CanValidateIValidatableObject()
    {
        var source = """
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
 
var builder = WebApplication.CreateBuilder();
builder.Services.AddSingleton<IRangeService, RangeService>();
builder.Services.AddValidation();
 
var app = builder.Build();
 
app.MapPost("/validatable-object", (ComplexValidatableType model) => Results.Ok());
 
app.Run();
 
public class ComplexValidatableType: IValidatableObject
{
    [Display(Name = "Value 1")]
    public int Value1 { get; set; }
 
    [EmailAddress]
    [Required]
    public required string Value2 { get; set; } = "test@example.com";
 
    public ValidatableSubType SubType { get; set; } = new ValidatableSubType();
 
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var rangeService = (IRangeService?)validationContext.GetService(typeof(IRangeService));
        var minimum = rangeService?.GetMinimum();
        var maximum = rangeService?.GetMaximum();
        if (Value1 < minimum || Value1 > maximum)
        {
            yield return new ValidationResult($"The field {nameof(Value1)} must be between {minimum} and {maximum}.", [nameof(Value1)]);
        }
    }
}
 
public class SubType
{
    [Required]
    public string RequiredProperty { get; set; } = "some-value";
 
    [StringLength(10)]
    public string? StringWithLength { get; set; }
}
 
public class ValidatableSubType : SubType, IValidatableObject
{
    public string Value3 { get; set; } = "some-value";
 
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Value3 != "some-value")
        {
            yield return new ValidationResult($"The field {validationContext.DisplayName} must be 'some-value'.", [nameof(Value3)]);
        }
    }
}
 
public interface IRangeService
{
    int GetMinimum();
    int GetMaximum();
}
 
public class RangeService : IRangeService
{
    public int GetMinimum() => 10;
    public int GetMaximum() => 100;
}
""";
 
        await Verify(source, out var compilation);
        await VerifyEndpoint(compilation, "/validatable-object", async (endpoint, serviceProvider) =>
        {
            await ValidateMethodCalledIfPropertyValidationsFail();
            await ValidateForSubtypeInvokedFirst();
            await ValidateForTopLevelInvoked();
 
            async Task ValidateMethodCalledIfPropertyValidationsFail()
            {
                var httpContext = CreateHttpContextWithPayload("""
                {
                    "Value1": 5,
                    "Value2": "",
                    "SubType": {
                        "Value3": "foo",
                        "RequiredProperty": "",
                        "StringWithLength": ""
                    }
                }
                """{
                    "Value1": 5,
                    "Value2": "",
                    "SubType": {
                        "Value3": "foo",
                        "RequiredProperty": "",
                        "StringWithLength": ""
                    }
                }
                """, serviceProvider);
 
                await endpoint.RequestDelegate(httpContext);
 
                var problemDetails = await AssertBadRequest(httpContext);
                Assert.Collection(problemDetails.Errors,
                    error =>
                    {
                        Assert.Equal("Value2", error.Key);
                        Assert.Collection(error.Value,
                            msg => Assert.Equal("The Value2 field is required.", msg));
                    },
                    error =>
                    {
                        Assert.Equal("SubType.RequiredProperty", error.Key);
                        Assert.Equal("The RequiredProperty field is required.", error.Value.Single());
                    },
                    error =>
                    {
                        Assert.Equal("SubType.Value3", error.Key);
                        Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single());
                    },
                    error =>
                    {
                        Assert.Equal("Value1", error.Key);
                        Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single());
                    });
            }
 
            async Task ValidateForSubtypeInvokedFirst()
            {
                var httpContext = CreateHttpContextWithPayload("""
                {
                    "Value1": 5,
                    "Value2": "test@test.com",
                    "SubType": {
                        "Value3": "foo",
                        "RequiredProperty": "some-value-2",
                        "StringWithLength": "element"
                    }
                }
                """{
                    "Value1": 5,
                    "Value2": "test@test.com",
                    "SubType": {
                        "Value3": "foo",
                        "RequiredProperty": "some-value-2",
                        "StringWithLength": "element"
                    }
                }
                """, serviceProvider);
 
                await endpoint.RequestDelegate(httpContext);
 
                var problemDetails = await AssertBadRequest(httpContext);
                Assert.Collection(problemDetails.Errors,
                    error =>
                    {
                        Assert.Equal("SubType.Value3", error.Key);
                        Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single());
                    },
                    error =>
                    {
                        Assert.Equal("Value1", error.Key);
                        Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single());
                    });
            }
 
            async Task ValidateForTopLevelInvoked()
            {
                var httpContext = CreateHttpContextWithPayload("""
                {
                    "Value1": 5,
                    "Value2": "test@test.com",
                    "SubType": {
                        "Value3": "some-value",
                        "RequiredProperty": "some-value-2",
                        "StringWithLength": "element"
                    }
                }
                """{
                    "Value1": 5,
                    "Value2": "test@test.com",
                    "SubType": {
                        "Value3": "some-value",
                        "RequiredProperty": "some-value-2",
                        "StringWithLength": "element"
                    }
                }
                """, serviceProvider);
 
                await endpoint.RequestDelegate(httpContext);
 
                var problemDetails = await AssertBadRequest(httpContext);
                Assert.Collection(problemDetails.Errors,
                    error =>
                    {
                        Assert.Equal("Value1", error.Key);
                        Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single());
                    });
            }
        });
    }
}