|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Http;
namespace Microsoft.Extensions.Validation.GeneratorTests;
public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
{
[Fact]
public async Task CanValidateComplexTypesWithJsonIgnore()
{
// Arrange
var source = """
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder();
builder.Services.AddValidation();
var app = builder.Build();
app.MapPost("/complex-type-with-json-ignore", (ComplexTypeWithJsonIgnore complexType) => Results.Ok("Passed"!));
app.MapPost("/record-type-with-json-ignore", (RecordTypeWithJsonIgnore recordType) => Results.Ok("Passed"!));
app.Run();
public class ComplexTypeWithJsonIgnore
{
[Range(10, 100)]
public int ValidatedProperty { get; set; } = 10;
[JsonIgnore]
[Required] // This should be ignored because of [JsonIgnore]
public string IgnoredProperty { get; set; } = null!;
[JsonIgnore]
public CircularReferenceType? CircularReference { get; set; }
}
public class CircularReferenceType
{
[JsonIgnore]
public ComplexTypeWithJsonIgnore? Parent { get; set; }
public string Name { get; set; } = "test";
}
public record RecordTypeWithJsonIgnore
{
[Range(10, 100)]
public int ValidatedProperty { get; set; } = 10;
[JsonIgnore]
[Required] // This should be ignored because of [JsonIgnore]
public string IgnoredProperty { get; set; } = null!;
[JsonIgnore]
public CircularReferenceRecord? CircularReference { get; set; }
}
public record CircularReferenceRecord
{
[JsonIgnore]
public RecordTypeWithJsonIgnore? Parent { get; set; }
public string Name { get; set; } = "test";
}
""";
await Verify(source, out var compilation);
await VerifyEndpoint(compilation, "/complex-type-with-json-ignore", async (endpoint, serviceProvider) =>
{
await ValidInputWithJsonIgnoreProducesNoWarnings(endpoint);
await InvalidValidatedPropertyProducesError(endpoint);
async Task ValidInputWithJsonIgnoreProducesNoWarnings(Endpoint endpoint)
{
var payload = """
{
"ValidatedProperty": 50
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
Assert.Equal(200, context.Response.StatusCode);
}
async Task InvalidValidatedPropertyProducesError(Endpoint endpoint)
{
var payload = """
{
"ValidatedProperty": 5
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("ValidatedProperty", kvp.Key);
Assert.Equal("The field ValidatedProperty must be between 10 and 100.", kvp.Value.Single());
});
}
});
await VerifyEndpoint(compilation, "/record-type-with-json-ignore", async (endpoint, serviceProvider) =>
{
await ValidInputWithJsonIgnoreProducesNoWarningsForRecord(endpoint);
await InvalidValidatedPropertyProducesErrorForRecord(endpoint);
async Task ValidInputWithJsonIgnoreProducesNoWarningsForRecord(Endpoint endpoint)
{
var payload = """
{
"ValidatedProperty": 50
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
Assert.Equal(200, context.Response.StatusCode);
}
async Task InvalidValidatedPropertyProducesErrorForRecord(Endpoint endpoint)
{
var payload = """
{
"ValidatedProperty": 5
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("ValidatedProperty", kvp.Key);
Assert.Equal("The field ValidatedProperty must be between 10 and 100.", kvp.Value.Single());
});
}
});
}
[Fact]
public async Task CanValidateComplexTypes()
{
// Arrange
var source = """
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder();
builder.Services.AddValidation();
builder.Services.AddSingleton<TestService>();
var app = builder.Build();
app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed"!));
app.Run();
public class ComplexType
{
[Range(10, 100)]
public int IntegerWithRange { get; set; } = 10;
[Range(10, 100), Display(Name = "Valid identifier")]
public int IntegerWithRangeAndDisplayName { get; set; } = 50;
[Required]
public SubType PropertyWithMemberAttributes { get; set; } = new SubType("some-value", default);
public SubType PropertyWithoutMemberAttributes { get; set; } = new SubType("some-value", default);
public SubTypeWithInheritance PropertyWithInheritance { get; set; } = new SubTypeWithInheritance("some-value", default);
// Nullable to validate https://github.com/dotnet/aspnetcore/issues/61737
public List<SubType>? ListOfSubTypes { get; set; } = [];
[DerivedValidation(ErrorMessage = "Value must be an even number")]
public int IntegerWithDerivedValidationAttribute { get; set; }
[CustomValidation(typeof(CustomValidators), nameof(CustomValidators.Validate))]
public int IntegerWithCustomValidation { get; set; } = 0;
[DerivedValidation, Range(10, 100)]
public int PropertyWithMultipleAttributes { get; set; } = 10;
[FromServices]
[Required] // This should be ignored because of [FromServices]
public TestService ServiceProperty { get; set; } = null!;
}
public class DerivedValidationAttribute : ValidationAttribute
{
public override bool IsValid(object? value) => value is int number && number % 2 == 0;
}
public class SubType(string? requiredProperty, string? stringWithLength)
{
[Required]
public string RequiredProperty { get; } = requiredProperty;
[StringLength(10)]
public string? StringWithLength { get; } = stringWithLength;
}
public class SubTypeWithInheritance(string? requiredProperty, string? stringWithLength) : SubType(requiredProperty, stringWithLength)
{
[EmailAddress]
public string? EmailString { get; set; }
}
public static class CustomValidators
{
public static ValidationResult Validate(int number, ValidationContext validationContext)
{
var parent = (ComplexType)validationContext.ObjectInstance;
if (parent.IntegerWithRange == number)
{
return new ValidationResult(
"Can't use the same number value in two properties on the same class.",
new[] { validationContext.MemberName });
}
return ValidationResult.Success;
}
}
public class TestService
{
[Range(10, 100)]
public int Value { get; set; } = 4;
}
""";
await Verify(source, out var compilation);
await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) =>
{
await InvalidIntegerWithRangeProducesError(endpoint);
await InvalidIntegerWithRangeAndDisplayNameProducesError(endpoint);
await MissingRequiredSubtypePropertyProducesError(endpoint);
await InvalidRequiredSubtypePropertyProducesError(endpoint);
await InvalidSubTypeWithInheritancePropertyProducesError(endpoint);
await InvalidListOfSubTypesProducesError(endpoint);
await InvalidPropertyWithDerivedValidationAttributeProducesError(endpoint);
await InvalidPropertyWithMultipleAttributesProducesError(endpoint);
await InvalidPropertyWithCustomValidationProducesError(endpoint);
await ValidInputProducesNoWarnings(endpoint);
async Task InvalidIntegerWithRangeProducesError(Endpoint endpoint)
{
var payload = """
{
"IntegerWithRange": 5
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("IntegerWithRange", kvp.Key);
Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single());
});
}
async Task InvalidIntegerWithRangeAndDisplayNameProducesError(Endpoint endpoint)
{
var payload = """
{
"IntegerWithRangeAndDisplayName": 5
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key);
Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single());
});
}
async Task MissingRequiredSubtypePropertyProducesError(Endpoint endpoint)
{
var payload = """
{
"PropertyWithMemberAttributes": null
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("PropertyWithMemberAttributes", kvp.Key);
Assert.Equal("The PropertyWithMemberAttributes field is required.", kvp.Value.Single());
});
}
async Task InvalidRequiredSubtypePropertyProducesError(Endpoint endpoint)
{
var payload = """
{
"PropertyWithMemberAttributes": {
"RequiredProperty": "",
"StringWithLength": "way-too-long"
}
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors,
kvp =>
{
Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key);
Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
},
kvp =>
{
Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key);
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
});
}
async Task InvalidSubTypeWithInheritancePropertyProducesError(Endpoint endpoint)
{
var payload = """
{
"PropertyWithInheritance": {
"RequiredProperty": "",
"StringWithLength": "way-too-long",
"EmailString": "not-an-email"
}
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors,
kvp =>
{
Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key);
Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single());
},
kvp =>
{
Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key);
Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
},
kvp =>
{
Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key);
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
});
}
async Task InvalidListOfSubTypesProducesError(Endpoint endpoint)
{
var payload = """
{
"ListOfSubTypes": [
{
"RequiredProperty": "",
"StringWithLength": "way-too-long"
},
{
"RequiredProperty": "valid",
"StringWithLength": "way-too-long"
},
{
"RequiredProperty": "valid",
"StringWithLength": "valid"
}
]
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors,
kvp =>
{
Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key);
Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
},
kvp =>
{
Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key);
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
},
kvp =>
{
Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key);
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
});
}
async Task InvalidPropertyWithDerivedValidationAttributeProducesError(Endpoint endpoint)
{
var payload = """
{
"IntegerWithDerivedValidationAttribute": 5
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("IntegerWithDerivedValidationAttribute", kvp.Key);
Assert.Equal("Value must be an even number", kvp.Value.Single());
});
}
async Task InvalidPropertyWithMultipleAttributesProducesError(Endpoint endpoint)
{
var payload = """
{
"PropertyWithMultipleAttributes": 5
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("PropertyWithMultipleAttributes", kvp.Key);
Assert.Collection(kvp.Value,
error =>
{
Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error);
},
error =>
{
Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error);
});
});
}
async Task InvalidPropertyWithCustomValidationProducesError(Endpoint endpoint)
{
var payload = """
{
"IntegerWithRange": 42,
"IntegerWithCustomValidation": 42
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("IntegerWithCustomValidation", kvp.Key);
var error = Assert.Single(kvp.Value);
Assert.Equal("Can't use the same number value in two properties on the same class.", error);
});
}
async Task ValidInputProducesNoWarnings(Endpoint endpoint)
{
var payload = """
{
"IntegerWithRange": 50,
"IntegerWithRangeAndDisplayName": 50,
"PropertyWithMemberAttributes": {
"RequiredProperty": "valid",
"StringWithLength": "valid"
},
"PropertyWithoutMemberAttributes": {
"RequiredProperty": "valid",
"StringWithLength": "valid"
},
"PropertyWithInheritance": {
"RequiredProperty": "valid",
"StringWithLength": "valid",
"EmailString": "test@example.com"
},
"ListOfSubTypes": [],
"IntegerWithDerivedValidationAttribute": 2,
"IntegerWithCustomValidation": 0,
"PropertyWithMultipleAttributes": 12
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
Assert.Equal(200, context.Response.StatusCode);
}
});
}
[Fact]
public async Task SkipsClassesWithNonAccessibleTypes()
{
// Arrange
var source = """
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder();
builder.Services.AddValidation();
var app = builder.Build();
app.MapPost("/accessibility-test", (AccessibilityTestType accessibilityTest) => Results.Ok("Passed"!));
app.Run();
public class AccessibilityTestType
{
[Required]
public string PublicProperty { get; set; } = "";
[Required]
private string PrivateProperty { get; set; } = "";
[Required]
protected string ProtectedProperty { get; set; } = "";
[Required]
private PrivateNestedType PrivateNestedProperty { get; set; } = new();
[Required]
protected ProtectedNestedType ProtectedNestedProperty { get; set; } = new();
[Required]
internal InternalNestedType InternalNestedProperty { get; set; } = new();
private class PrivateNestedType
{
[Required]
public string RequiredProperty { get; set; } = "";
}
protected class ProtectedNestedType
{
[Required]
public string RequiredProperty { get; set; } = "";
}
internal class InternalNestedType
{
[Required]
public string RequiredProperty { get; set; } = "";
}
}
""";
await Verify(source, out var compilation);
await VerifyEndpoint(compilation, "/accessibility-test", async (endpoint, serviceProvider) =>
{
await ValidPublicPropertyStillValidated(endpoint);
async Task ValidPublicPropertyStillValidated(Endpoint endpoint)
{
var payload = """
{
"PublicProperty": ""
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
await endpoint.RequestDelegate(context);
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
Assert.Equal("PublicProperty", kvp.Key);
Assert.Equal("The PublicProperty field is required.", kvp.Value.Single());
});
}
});
}
}
|