Skip to content

Commit 602fcd2

Browse files
committed
Skip non-public properties in validations generator
1 parent 5d0e0d0 commit 602fcd2

File tree

4 files changed

+273
-18
lines changed

4 files changed

+273
-18
lines changed

src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnow
6565
return false;
6666
}
6767

68+
// Skip types that are not accessible from generated code
69+
if (typeSymbol.DeclaredAccessibility is not Accessibility.Public)
70+
{
71+
return false;
72+
}
73+
6874
visitedTypes.Add(typeSymbol);
6975

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

157+
// Skip properties that are not accessible from generated code
158+
if (correspondingProperty.DeclaredAccessibility is not Accessibility.Public)
159+
{
160+
continue;
161+
}
162+
151163
// Check if the property's type is validatable, this resolves
152164
// validatable types in the inheritance hierarchy
153165
var hasValidatableType = TryExtractValidatableType(
@@ -186,6 +198,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
186198
continue;
187199
}
188200

201+
// Skip properties that are not accessible from generated code
202+
if (member.DeclaredAccessibility is not Accessibility.Public)
203+
{
204+
continue;
205+
}
206+
189207
var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
190208
var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired);
191209

src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,4 +385,95 @@ async Task ValidInputProducesNoWarnings(Endpoint endpoint)
385385
}
386386
});
387387
}
388+
389+
[Fact]
390+
public async Task SkipsClassesWithNonAccessibleTypes()
391+
{
392+
// Arrange
393+
var source = """
394+
using System;
395+
using System.ComponentModel.DataAnnotations;
396+
using System.Collections.Generic;
397+
using System.Threading.Tasks;
398+
using Microsoft.AspNetCore.Builder;
399+
using Microsoft.AspNetCore.Http;
400+
using Microsoft.Extensions.Validation;
401+
using Microsoft.AspNetCore.Routing;
402+
using Microsoft.Extensions.DependencyInjection;
403+
using Microsoft.AspNetCore.Mvc;
404+
405+
var builder = WebApplication.CreateBuilder();
406+
407+
builder.Services.AddValidation();
408+
409+
var app = builder.Build();
410+
411+
app.MapPost("/accessibility-test", (AccessibilityTestType accessibilityTest) => Results.Ok("Passed"!));
412+
413+
app.Run();
414+
415+
public class AccessibilityTestType
416+
{
417+
[Required]
418+
public string PublicProperty { get; set; } = "";
419+
420+
[Required]
421+
private string PrivateProperty { get; set; } = "";
422+
423+
[Required]
424+
protected string ProtectedProperty { get; set; } = "";
425+
426+
[Required]
427+
private PrivateNestedType PrivateNestedProperty { get; set; } = new();
428+
429+
[Required]
430+
protected ProtectedNestedType ProtectedNestedProperty { get; set; } = new();
431+
432+
[Required]
433+
internal InternalNestedType InternalNestedProperty { get; set; } = new();
434+
435+
private class PrivateNestedType
436+
{
437+
[Required]
438+
public string RequiredProperty { get; set; } = "";
439+
}
440+
441+
protected class ProtectedNestedType
442+
{
443+
[Required]
444+
public string RequiredProperty { get; set; } = "";
445+
}
446+
447+
internal class InternalNestedType
448+
{
449+
[Required]
450+
public string RequiredProperty { get; set; } = "";
451+
}
452+
}
453+
""";
454+
await Verify(source, out var compilation);
455+
await VerifyEndpoint(compilation, "/accessibility-test", async (endpoint, serviceProvider) =>
456+
{
457+
await ValidPublicPropertyStillValidated(endpoint);
458+
459+
async Task ValidPublicPropertyStillValidated(Endpoint endpoint)
460+
{
461+
var payload = """
462+
{
463+
"PublicProperty": ""
464+
}
465+
""";
466+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
467+
468+
await endpoint.RequestDelegate(context);
469+
470+
var problemDetails = await AssertBadRequest(context);
471+
Assert.Collection(problemDetails.Errors, kvp =>
472+
{
473+
Assert.Equal("PublicProperty", kvp.Key);
474+
Assert.Equal("The PublicProperty field is required.", kvp.Value.Single());
475+
});
476+
}
477+
});
478+
}
388479
}

src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -82,30 +82,12 @@ public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.
8282
validatableInfo = new GeneratedValidatableTypeInfo(
8383
type: typeof(global::System.Collections.Generic.Dictionary<string, global::TestService>),
8484
members: [
85-
new GeneratedValidatablePropertyInfo(
86-
containingType: typeof(global::System.Collections.Generic.Dictionary<string, global::TestService>),
87-
propertyType: typeof(global::System.Collections.Generic.ICollection<global::TestService>),
88-
name: "System.Collections.Generic.IDictionary<TKey,TValue>.Values",
89-
displayName: "System.Collections.Generic.IDictionary<TKey,TValue>.Values"
90-
),
91-
new GeneratedValidatablePropertyInfo(
92-
containingType: typeof(global::System.Collections.Generic.Dictionary<string, global::TestService>),
93-
propertyType: typeof(global::System.Collections.Generic.IEnumerable<global::TestService>),
94-
name: "System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>.Values",
95-
displayName: "System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>.Values"
96-
),
9785
new GeneratedValidatablePropertyInfo(
9886
containingType: typeof(global::System.Collections.Generic.Dictionary<string, global::TestService>),
9987
propertyType: typeof(global::TestService),
10088
name: "this[]",
10189
displayName: "this[]"
10290
),
103-
new GeneratedValidatablePropertyInfo(
104-
containingType: typeof(global::System.Collections.Generic.Dictionary<string, global::TestService>),
105-
propertyType: typeof(global::System.Collections.ICollection),
106-
name: "System.Collections.IDictionary.Values",
107-
displayName: "System.Collections.IDictionary.Values"
108-
),
10991
]
11092
);
11193
return true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//HintName: ValidatableInfoResolver.g.cs
2+
#nullable enable annotations
3+
//------------------------------------------------------------------------------
4+
// <auto-generated>
5+
// This code was generated by a tool.
6+
//
7+
// Changes to this file may cause incorrect behavior and will be lost if
8+
// the code is regenerated.
9+
// </auto-generated>
10+
//------------------------------------------------------------------------------
11+
#nullable enable
12+
#pragma warning disable ASP0029
13+
14+
namespace System.Runtime.CompilerServices
15+
{
16+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
17+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
18+
file sealed class InterceptsLocationAttribute : System.Attribute
19+
{
20+
public InterceptsLocationAttribute(int version, string data)
21+
{
22+
}
23+
}
24+
}
25+
26+
namespace Microsoft.Extensions.Validation.Generated
27+
{
28+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
29+
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo
30+
{
31+
public GeneratedValidatablePropertyInfo(
32+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
33+
global::System.Type containingType,
34+
global::System.Type propertyType,
35+
string name,
36+
string displayName) : base(containingType, propertyType, name, displayName)
37+
{
38+
ContainingType = containingType;
39+
Name = name;
40+
}
41+
42+
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
43+
internal global::System.Type ContainingType { get; }
44+
internal string Name { get; }
45+
46+
protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes()
47+
=> ValidationAttributeCache.GetValidationAttributes(ContainingType, Name);
48+
}
49+
50+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
51+
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo
52+
{
53+
public GeneratedValidatableTypeInfo(
54+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
55+
global::System.Type type,
56+
ValidatablePropertyInfo[] members) : base(type, members) { }
57+
}
58+
59+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
60+
file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver
61+
{
62+
public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
63+
{
64+
validatableInfo = null;
65+
if (type == typeof(global::AccessibilityTestType))
66+
{
67+
validatableInfo = new GeneratedValidatableTypeInfo(
68+
type: typeof(global::AccessibilityTestType),
69+
members: [
70+
new GeneratedValidatablePropertyInfo(
71+
containingType: typeof(global::AccessibilityTestType),
72+
propertyType: typeof(string),
73+
name: "PublicProperty",
74+
displayName: "PublicProperty"
75+
),
76+
]
77+
);
78+
return true;
79+
}
80+
81+
return false;
82+
}
83+
84+
// No-ops, rely on runtime code for ParameterInfo-based resolution
85+
public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
86+
{
87+
validatableInfo = null;
88+
return false;
89+
}
90+
}
91+
92+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
93+
file static class GeneratedServiceCollectionExtensions
94+
{
95+
[InterceptsLocation]
96+
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)
97+
{
98+
// Use non-extension method to avoid infinite recursion.
99+
return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options =>
100+
{
101+
options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver());
102+
if (configureOptions is not null)
103+
{
104+
configureOptions(options);
105+
}
106+
});
107+
}
108+
}
109+
110+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
111+
file static class ValidationAttributeCache
112+
{
113+
private sealed record CacheKey(
114+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
115+
[property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
116+
global::System.Type ContainingType,
117+
string PropertyName);
118+
private static readonly global::System.Collections.Concurrent.ConcurrentDictionary<CacheKey, global::System.ComponentModel.DataAnnotations.ValidationAttribute[]> _cache = new();
119+
120+
public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes(
121+
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
122+
global::System.Type containingType,
123+
string propertyName)
124+
{
125+
var key = new CacheKey(containingType, propertyName);
126+
return _cache.GetOrAdd(key, static k =>
127+
{
128+
var results = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>();
129+
130+
// Get attributes from the property
131+
var property = k.ContainingType.GetProperty(k.PropertyName);
132+
if (property != null)
133+
{
134+
var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
135+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(property, inherit: true);
136+
137+
results.AddRange(propertyAttributes);
138+
}
139+
140+
// Check constructors for parameters that match the property name
141+
// to handle record scenarios
142+
foreach (var constructor in k.ContainingType.GetConstructors())
143+
{
144+
// Look for parameter with matching name (case insensitive)
145+
var parameter = global::System.Linq.Enumerable.FirstOrDefault(
146+
constructor.GetParameters(),
147+
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
148+
149+
if (parameter != null)
150+
{
151+
var paramAttributes = global::System.Reflection.CustomAttributeExtensions
152+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(parameter, inherit: true);
153+
154+
results.AddRange(paramAttributes);
155+
156+
break;
157+
}
158+
}
159+
160+
return results.ToArray();
161+
});
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)