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
18 changes: 18 additions & 0 deletions src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnow
return false;
}

// Skip types that are not accessible from generated code
Copy link

Copilot AI Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a comment explaining why non-public types are skipped, as this is a significant behavioral change that affects which types get validation generation.

Suggested change
// Skip types that are not accessible from generated code
// Skip types that are not accessible from generated code
// Only public types are included for validation generation because generated code can only access public types.
// This is a significant behavioral decision: non-public types will not have validation generated.

Copilot uses AI. Check for mistakes.

if (typeSymbol.DeclaredAccessibility is not Accessibility.Public)
{
return false;
}

visitedTypes.Add(typeSymbol);

// Extract validatable types discovered in base types of this type and add them to the top-level list.
Expand Down Expand Up @@ -148,6 +154,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
continue;
}

// Skip properties that are not accessible from generated code
Copy link

Copilot AI Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a comment explaining why non-public properties are skipped, similar to the type-level check, to maintain consistency in documentation.

Suggested change
// Skip properties that are not accessible from generated code
// Skip properties that are not accessible from generated code
// Only public properties can be accessed by generated code, so non-public properties are skipped.

Copilot uses AI. Check for mistakes.

if (correspondingProperty.DeclaredAccessibility is not Accessibility.Public)
{
continue;
}

// Check if the property's type is validatable, this resolves
// validatable types in the inheritance hierarchy
var hasValidatableType = TryExtractValidatableType(
Expand Down Expand Up @@ -186,6 +198,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
continue;
}

// Skip properties that are not accessible from generated code
Copy link

Copilot AI Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a comment explaining why non-public properties are skipped, to maintain consistency with the other accessibility checks in this file.

Suggested change
// Skip properties that are not accessible from generated code
// Skip properties that are not accessible from generated code
// Non-public properties are skipped because they cannot be accessed by the generated code.

Copilot uses AI. Check for mistakes.

if (member.DeclaredAccessibility is not Accessibility.Public)
{
continue;
}

var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,95 @@ async Task ValidInputProducesNoWarnings(Endpoint endpoint)
}
});
}

[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());
});
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,30 +82,12 @@ public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.
validatableInfo = new GeneratedValidatableTypeInfo(
type: typeof(global::System.Collections.Generic.Dictionary<string, global::TestService>),
members: [
new GeneratedValidatablePropertyInfo(
containingType: typeof(global::System.Collections.Generic.Dictionary<string, global::TestService>),
propertyType: typeof(global::System.Collections.Generic.ICollection<global::TestService>),
name: "System.Collections.Generic.IDictionary<TKey,TValue>.Values",
displayName: "System.Collections.Generic.IDictionary<TKey,TValue>.Values"
),
new GeneratedValidatablePropertyInfo(
containingType: typeof(global::System.Collections.Generic.Dictionary<string, global::TestService>),
propertyType: typeof(global::System.Collections.Generic.IEnumerable<global::TestService>),
name: "System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>.Values",
displayName: "System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>.Values"
),
new GeneratedValidatablePropertyInfo(
containingType: typeof(global::System.Collections.Generic.Dictionary<string, global::TestService>),
propertyType: typeof(global::TestService),
name: "this[]",
displayName: "this[]"
),
new GeneratedValidatablePropertyInfo(
containingType: typeof(global::System.Collections.Generic.Dictionary<string, global::TestService>),
propertyType: typeof(global::System.Collections.ICollection),
name: "System.Collections.IDictionary.Values",
displayName: "System.Collections.IDictionary.Values"
),
]
);
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//HintName: ValidatableInfoResolver.g.cs
#nullable enable annotations
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable
#pragma warning disable ASP0029

namespace System.Runtime.CompilerServices
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute : System.Attribute
{
public InterceptsLocationAttribute(int version, string data)
{
}
}
}

namespace Microsoft.Extensions.Validation.Generated
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo
{
public GeneratedValidatablePropertyInfo(
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
global::System.Type containingType,
global::System.Type propertyType,
string name,
string displayName) : base(containingType, propertyType, name, displayName)
{
ContainingType = containingType;
Name = name;
}

[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
internal global::System.Type ContainingType { get; }
internal string Name { get; }

protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes()
=> ValidationAttributeCache.GetValidationAttributes(ContainingType, Name);
}

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo
{
public GeneratedValidatableTypeInfo(
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
global::System.Type type,
ValidatablePropertyInfo[] members) : base(type, members) { }
}

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver
{
public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
{
validatableInfo = null;
if (type == typeof(global::AccessibilityTestType))
{
validatableInfo = new GeneratedValidatableTypeInfo(
type: typeof(global::AccessibilityTestType),
members: [
new GeneratedValidatablePropertyInfo(
containingType: typeof(global::AccessibilityTestType),
propertyType: typeof(string),
name: "PublicProperty",
displayName: "PublicProperty"
),
]
);
return true;
}

return false;
}

// No-ops, rely on runtime code for ParameterInfo-based resolution
public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
{
validatableInfo = null;
return false;
}
}

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
file static class GeneratedServiceCollectionExtensions
{
[InterceptsLocation]
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action<global::Microsoft.Extensions.Validation.ValidationOptions>? configureOptions = null)
{
// Use non-extension method to avoid infinite recursion.
return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options =>
{
options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver());
if (configureOptions is not null)
{
configureOptions(options);
}
});
}
}

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
file static class ValidationAttributeCache
{
private sealed record CacheKey(
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
[property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
global::System.Type ContainingType,
string PropertyName);
private static readonly global::System.Collections.Concurrent.ConcurrentDictionary<CacheKey, global::System.ComponentModel.DataAnnotations.ValidationAttribute[]> _cache = new();

public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes(
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
global::System.Type containingType,
string propertyName)
{
var key = new CacheKey(containingType, propertyName);
return _cache.GetOrAdd(key, static k =>
{
var results = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>();

// Get attributes from the property
var property = k.ContainingType.GetProperty(k.PropertyName);
if (property != null)
{
var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(property, inherit: true);

results.AddRange(propertyAttributes);
}

// Check constructors for parameters that match the property name
// to handle record scenarios
foreach (var constructor in k.ContainingType.GetConstructors())
{
// Look for parameter with matching name (case insensitive)
var parameter = global::System.Linq.Enumerable.FirstOrDefault(
constructor.GetParameters(),
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));

if (parameter != null)
{
var paramAttributes = global::System.Reflection.CustomAttributeExtensions
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(parameter, inherit: true);

results.AddRange(paramAttributes);

break;
}
}

return results.ToArray();
});
}
}
}
Loading