Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNull
var validationAttributes = parameterInfo
.GetCustomAttributes<ValidationAttribute>()
.ToArray();

// If there are no validation attributes and this type is not a complex type
// we don't need to validate it. Complex types without attributes are still
// validatable because we want to run the validations on the properties.
if (validationAttributes.Length == 0 && !IsClass(parameterInfo.ParameterType))
{
validatableInfo = null;
return false;
}
validatableInfo = new RuntimeValidatableParameterInfo(
parameterType: parameterInfo.ParameterType,
name: parameterInfo.Name,
Expand All @@ -47,7 +56,7 @@ private static string GetDisplayName(ParameterInfo parameterInfo)
return parameterInfo.Name!;
}

private sealed class RuntimeValidatableParameterInfo(
internal sealed class RuntimeValidatableParameterInfo(
Type parameterType,
string name,
string displayName,
Expand All @@ -58,4 +67,31 @@ private sealed class RuntimeValidatableParameterInfo(

private readonly ValidationAttribute[] _validationAttributes = validationAttributes;
}

private static bool IsClass(Type type)
{
// Skip primitives, enums, and common built-in types that don't need validation
// if they don't have attributes
if (type.IsPrimitive ||
type.IsEnum ||
type == typeof(string) ||
type == typeof(decimal) ||
type == typeof(DateTime) ||
type == typeof(DateTimeOffset) ||
type == typeof(TimeOnly) ||
type == typeof(DateOnly) ||
type == typeof(TimeSpan) ||
type == typeof(Guid))
{
return false;
}

// Check if the underlying type in a nullable is valid
if (Nullable.GetUnderlyingType(type) is { } nullableType)
{
return IsClass(nullableType);
}

return type.IsClass;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.DataAnnotations;
using System.Reflection;

namespace Microsoft.AspNetCore.Http.Validation.Tests;

public class RuntimeValidatableParameterInfoResolverTests
{
private readonly RuntimeValidatableParameterInfoResolver _resolver = new();

[Fact]
public void TryGetValidatableTypeInfo_AlwaysReturnsFalse()
{
var result = _resolver.TryGetValidatableTypeInfo(typeof(string), out var validatableInfo);

Assert.False(result);
Assert.Null(validatableInfo);
}

[Fact]
public void TryGetValidatableParameterInfo_WithNullName_ThrowsInvalidOperationException()
{
var parameterInfo = new NullNameParameterInfo();

var exception = Assert.Throws<InvalidOperationException>(() =>
_resolver.TryGetValidatableParameterInfo(parameterInfo, out _));

Assert.Contains("without a name", exception.Message);
}

[Theory]
[InlineData(typeof(string))]
[InlineData(typeof(int))]
[InlineData(typeof(bool))]
[InlineData(typeof(DateTime))]
[InlineData(typeof(Guid))]
[InlineData(typeof(decimal))]
[InlineData(typeof(DayOfWeek))] // Enum
public void TryGetValidatableParameterInfo_WithSimpleTypesAndNoAttributes_ReturnsFalse(Type parameterType)
{
var parameterInfo = GetParameter(parameterType);

var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);

Assert.False(result);
Assert.Null(validatableInfo);
}

[Fact]
public void TryGetValidatableParameterInfo_WithClassTypeAndNoAttributes_ReturnsTrue()
{
var parameterInfo = GetParameter(typeof(TestClass));

var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);

Assert.True(result);
Assert.NotNull(validatableInfo);
var parameterValidatableInfo = Assert.IsType<RuntimeValidatableParameterInfoResolver.RuntimeValidatableParameterInfo>(validatableInfo);
Assert.Equal("testParam", parameterValidatableInfo.Name);
Assert.Equal("testParam", parameterValidatableInfo.DisplayName);
}

[Fact]
public void TryGetValidatableParameterInfo_WithSimpleTypeAndAttributes_ReturnsTrue()
{
var parameterInfo = typeof(TestController)
.GetMethod(nameof(TestController.MethodWithAttributedParam))!
.GetParameters()[0];

var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);

Assert.True(result);
Assert.NotNull(validatableInfo);
var parameterValidatableInfo = Assert.IsType<RuntimeValidatableParameterInfoResolver.RuntimeValidatableParameterInfo>(validatableInfo);
Assert.Equal("value", parameterValidatableInfo.Name);
Assert.Equal("value", parameterValidatableInfo.DisplayName);
}

[Fact]
public void TryGetValidatableParameterInfo_WithDisplayAttribute_UsesDisplayNameFromAttribute()
{
var parameterInfo = typeof(TestController)
.GetMethod(nameof(TestController.MethodWithDisplayAttribute))!
.GetParameters()[0];

var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);

Assert.True(result);
Assert.NotNull(validatableInfo);
var parameterValidatableInfo = Assert.IsType<RuntimeValidatableParameterInfoResolver.RuntimeValidatableParameterInfo>(validatableInfo);
Assert.Equal("value", parameterValidatableInfo.Name);
Assert.Equal("Custom Display Name", parameterValidatableInfo.DisplayName);
}

[Fact]
public void TryGetValidatableParameterInfo_WithDisplayAttributeWithNullName_UsesParameterName()
{
var parameterInfo = typeof(TestController)
.GetMethod(nameof(TestController.MethodWithNullDisplayName))!
.GetParameters()[0];

var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);

Assert.True(result);
Assert.NotNull(validatableInfo);
var parameterValidatableInfo = Assert.IsType<RuntimeValidatableParameterInfoResolver.RuntimeValidatableParameterInfo>(validatableInfo);
Assert.Equal("value", parameterValidatableInfo.Name);
Assert.Equal("value", parameterValidatableInfo.DisplayName);
}

[Fact]
public void TryGetValidatableParameterInfo_WithNullableValueType_ReturnsFalse()
{
var parameterInfo = GetParameter(typeof(int?));

var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);

Assert.False(result);
Assert.Null(validatableInfo);
}

[Fact]
public void TryGetValidatableParameterInfo_WithNullableReferenceType_ReturnsTrue()
{
var parameterInfo = GetNullableParameter(typeof(TestClass));

var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);

Assert.True(result);
Assert.NotNull(validatableInfo);
var parameterValidatableInfo = Assert.IsType<RuntimeValidatableParameterInfoResolver.RuntimeValidatableParameterInfo>(validatableInfo);
Assert.Equal("testParam", parameterValidatableInfo.Name);
Assert.Equal("testParam", parameterValidatableInfo.DisplayName);
}

private static ParameterInfo GetParameter(Type parameterType)
{
return typeof(TestParameterHolder)
.GetMethod(nameof(TestParameterHolder.Method))!
.MakeGenericMethod(parameterType)
.GetParameters()[0];
}

private static ParameterInfo GetNullableParameter(Type parameterType)
{
return typeof(TestParameterHolder)
.GetMethod(nameof(TestParameterHolder.MethodWithNullable))!
.MakeGenericMethod(parameterType)
.GetParameters()[0];
}

private class TestClass { }

private class TestParameterHolder
{
public void Method<T>(T testParam) { }
public void MethodWithNullable<T>(T? testParam) { }
}

private class TestController
{
public void MethodWithAttributedParam([Required] string value) { }

public void MethodWithDisplayAttribute([Display(Name = "Custom Display Name")][Required] string value) { }

public void MethodWithNullDisplayName([Display(Name = null)][Required] string value) { }
}

private class NullNameParameterInfo : ParameterInfo
{
public override string? Name => null;
public override Type ParameterType => typeof(string);
}
}
15 changes: 1 addition & 14 deletions src/Http/Routing/src/ValidationEndpointFilterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
Expand All @@ -11,8 +10,6 @@ namespace Microsoft.AspNetCore.Http.Validation;

internal static class ValidationEndpointFilterFactory
{
private const string ValidationContextJustification = "The DisplayName property is always statically initialized in the ValidationContext through this codepath.";

public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context, EndpointFilterDelegate next)
{
var parameters = context.MethodInfo.GetParameters();
Expand Down Expand Up @@ -60,7 +57,7 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context
// initialize an explicit DisplayName. We can suppress the warning here.
// Eventually, this can be removed when the code is updated to
// use https://github.com/dotnet/runtime/issues/113134.
var validationContext = CreateValidationContext(argument, displayName, context.HttpContext.RequestServices);
var validationContext = new ValidationContext(argument, displayName, context.HttpContext.RequestServices, null);
validatableContext.ValidationContext = validationContext;
await validatableParameter.ValidateAsync(argument, validatableContext, context.HttpContext.RequestAborted);
}
Expand All @@ -76,16 +73,6 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context
};
}

/// <remarks>
/// ValidationContext is not trim-friendly in codepaths that don't
/// initialize an explicit DisplayName. We can suppress the warning here.
/// Eventually, this can be removed when the code is updated to
/// use https://github.com/dotnet/runtime/issues/113134.
/// </remarks>
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = ValidationContextJustification)]
private static ValidationContext CreateValidationContext(object argument, string displayName, IServiceProvider serviceProvider)
=> new(argument, serviceProvider, items: null) { DisplayName = displayName };

private static string GetDisplayName(ParameterInfo parameterInfo)
{
var displayAttribute = parameterInfo.GetCustomAttribute<DisplayAttribute>();
Expand Down
Loading