Skip to content

Commit 9747391

Browse files
committed
Fix more failures
- Pass type info to AzureAdOptionsTests - Propagate template lookup errors - Add custom validation to short-circuit when template lookup fails
1 parent e94a372 commit 9747391

File tree

10 files changed

+552
-51
lines changed

10 files changed

+552
-51
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http.Validation;
5+
using System;
6+
using System.Collections.Concurrent;
7+
using System.Collections.Generic;
8+
using System.ComponentModel.DataAnnotations;
9+
using System.Diagnostics;
10+
using System.Diagnostics.CodeAnalysis;
11+
using System.Linq;
12+
using System.Reflection;
13+
using System.Threading.Tasks;
14+
using System.Threading;
15+
16+
#nullable enable
17+
18+
namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options
19+
{
20+
public class CustomValidatableInfoResolver : IValidatableInfoResolver
21+
{
22+
public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo)
23+
{
24+
if (type == typeof(CollectionRuleOptions))
25+
{
26+
validatableInfo = CreateCollectionRuleOptions();
27+
return true;
28+
}
29+
30+
validatableInfo = null;
31+
return false;
32+
}
33+
34+
public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo)
35+
{
36+
validatableInfo = null;
37+
return false;
38+
}
39+
40+
private static ValidatableTypeInfo CreateCollectionRuleOptions()
41+
{
42+
return new ShortCircuitingValidatableTypeInfo(
43+
type: typeof(CollectionRuleOptions),
44+
members: [
45+
new CustomValidatablePropertyInfo(
46+
containingType: typeof(CollectionRuleOptions),
47+
propertyType: typeof(CollectionRuleTriggerOptions),
48+
name: "Trigger",
49+
displayName: "Trigger"
50+
),
51+
new CustomValidatablePropertyInfo(
52+
containingType: typeof(CollectionRuleOptions),
53+
propertyType: typeof(List<CollectionRuleActionOptions>),
54+
name: "Actions",
55+
displayName: "Actions"
56+
),
57+
new CustomValidatablePropertyInfo(
58+
containingType: typeof(CollectionRuleOptions),
59+
propertyType: typeof(CollectionRuleLimitsOptions),
60+
name: "Limits",
61+
displayName: "Limits"
62+
),
63+
]
64+
);
65+
}
66+
67+
sealed class ShortCircuitingValidatableTypeInfo : ValidatableTypeInfo
68+
{
69+
public ShortCircuitingValidatableTypeInfo(
70+
[param: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
71+
Type type,
72+
ValidatablePropertyInfo[] members) : base(type, members) {
73+
Type = type;
74+
Members = members;
75+
_membersCount = members.Length;
76+
_subTypes = type.GetAllImplementedTypes();
77+
}
78+
79+
private readonly int _membersCount;
80+
private readonly List<Type> _subTypes;
81+
82+
internal Type Type { get; }
83+
internal IReadOnlyList<ValidatablePropertyInfo> Members { get; }
84+
85+
public override async Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken)
86+
{
87+
Debug.Assert(context.ValidationContext is not null);
88+
if (value == null)
89+
{
90+
return;
91+
}
92+
93+
// Check if we've exceeded the maximum depth
94+
if (context.CurrentDepth >= context.ValidationOptions.MaxDepth)
95+
{
96+
throw new InvalidOperationException(
97+
$"Maximum validation depth of {context.ValidationOptions.MaxDepth} exceeded at '{context.CurrentValidationPath}' in '{Type.Name}'. " +
98+
"This is likely caused by a circular reference in the object graph. " +
99+
"Consider increasing the MaxDepth in ValidationOptions if deeper validation is required.");
100+
}
101+
102+
var originalPrefix = context.CurrentValidationPath;
103+
104+
try
105+
{
106+
// Finally validate IValidatableObject if implemented
107+
if (Type.ImplementsInterface(typeof(IValidatableObject)) && value is IValidatableObject validatable)
108+
{
109+
// Important: Set the DisplayName to the type name for top-level validations
110+
// and restore the original validation context properties
111+
var originalDisplayName = context.ValidationContext.DisplayName;
112+
var originalMemberName = context.ValidationContext.MemberName;
113+
114+
// Set the display name to the class name for IValidatableObject validation
115+
context.ValidationContext.DisplayName = Type.Name;
116+
context.ValidationContext.MemberName = null;
117+
118+
var validationResults = validatable.Validate(context.ValidationContext);
119+
bool hasErrors = false;
120+
foreach (var validationResult in validationResults)
121+
{
122+
if (validationResult != ValidationResult.Success && validationResult.ErrorMessage is not null)
123+
{
124+
var memberName = validationResult.MemberNames.First();
125+
var key = string.IsNullOrEmpty(originalPrefix) ?
126+
memberName :
127+
$"{originalPrefix}.{memberName}";
128+
129+
context.AddOrExtendValidationError(key, validationResult.ErrorMessage);
130+
hasErrors = true;
131+
}
132+
}
133+
134+
// Restore the original validation context properties
135+
context.ValidationContext.DisplayName = originalDisplayName;
136+
context.ValidationContext.MemberName = originalMemberName;
137+
if (hasErrors)
138+
{
139+
return;
140+
}
141+
}
142+
143+
var actualType = value.GetType();
144+
145+
// First validate members
146+
for (var i = 0; i < _membersCount; i++)
147+
{
148+
await Members[i].ValidateAsync(value, context, cancellationToken);
149+
context.CurrentValidationPath = originalPrefix;
150+
}
151+
152+
// Then validate sub-types if any
153+
foreach (var subType in _subTypes)
154+
{
155+
// Check if the actual type is assignable to the sub-type
156+
// and validate it if it is
157+
if (subType.IsAssignableFrom(actualType))
158+
{
159+
if (context.ValidationOptions.TryGetValidatableTypeInfo(subType, out var subTypeInfo))
160+
{
161+
await subTypeInfo.ValidateAsync(value, context, cancellationToken);
162+
context.CurrentValidationPath = originalPrefix;
163+
}
164+
}
165+
}
166+
}
167+
finally
168+
{
169+
context.CurrentValidationPath = originalPrefix;
170+
}
171+
}
172+
}
173+
174+
175+
sealed class CustomValidatableTypeInfo : ValidatableTypeInfo
176+
{
177+
public CustomValidatableTypeInfo(
178+
[param: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
179+
Type type,
180+
ValidatablePropertyInfo[] members) : base(type, members) { }
181+
}
182+
183+
sealed class CustomValidatablePropertyInfo : ValidatablePropertyInfo
184+
{
185+
public CustomValidatablePropertyInfo(
186+
[param: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
187+
Type containingType,
188+
Type propertyType,
189+
string name,
190+
string displayName) : base(containingType, propertyType, name, displayName)
191+
{
192+
ContainingType = containingType;
193+
Name = name;
194+
}
195+
196+
internal Type ContainingType { get; }
197+
internal string Name { get; }
198+
199+
protected override ValidationAttribute[] GetValidationAttributes()
200+
=> ValidationAttributeCache.GetValidationAttributes(ContainingType, Name);
201+
}
202+
203+
static class ValidationAttributeCache
204+
{
205+
private sealed record CacheKey(Type ContainingType, string PropertyName);
206+
private static readonly ConcurrentDictionary<CacheKey, ValidationAttribute[]> _cache = new();
207+
208+
public static ValidationAttribute[] GetValidationAttributes(
209+
Type containingType,
210+
string propertyName)
211+
{
212+
var key = new CacheKey(containingType, propertyName);
213+
return _cache.GetOrAdd(key, static k =>
214+
{
215+
var results = new List<ValidationAttribute>();
216+
217+
// Get attributes from the property
218+
var property = k.ContainingType.GetProperty(k.PropertyName);
219+
if (property != null)
220+
{
221+
var propertyAttributes = CustomAttributeExtensions.GetCustomAttributes<ValidationAttribute>(property, inherit: true);
222+
223+
results.AddRange(propertyAttributes);
224+
}
225+
226+
// Check constructors for parameters that match the property name
227+
// to handle record scenarios
228+
foreach (var constructor in k.ContainingType.GetConstructors())
229+
{
230+
// Look for parameter with matching name (case insensitive)
231+
var parameter = Enumerable.FirstOrDefault(
232+
constructor.GetParameters(),
233+
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
234+
235+
if (parameter != null)
236+
{
237+
var paramAttributes = CustomAttributeExtensions.GetCustomAttributes<ValidationAttribute>(parameter, inherit: true);
238+
239+
results.AddRange(paramAttributes);
240+
241+
break;
242+
}
243+
}
244+
245+
return results.ToArray();
246+
});
247+
}
248+
}
249+
}
250+
251+
internal static class TypeExtensions
252+
{
253+
public static List<Type> GetAllImplementedTypes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type)
254+
{
255+
ArgumentNullException.ThrowIfNull(type);
256+
257+
var implementedTypes = new List<Type>();
258+
259+
// Yield all interfaces directly and indirectly implemented by this type
260+
foreach (var interfaceType in type.GetInterfaces())
261+
{
262+
implementedTypes.Add(interfaceType);
263+
}
264+
265+
// Finally, walk up the inheritance chain
266+
var baseType = type.BaseType;
267+
while (baseType != null && baseType != typeof(object))
268+
{
269+
implementedTypes.Add(baseType);
270+
baseType = baseType.BaseType;
271+
}
272+
273+
return implementedTypes;
274+
}
275+
276+
public static bool ImplementsInterface(this Type type, Type interfaceType)
277+
{
278+
ArgumentNullException.ThrowIfNull(type);
279+
ArgumentNullException.ThrowIfNull(interfaceType);
280+
281+
// Check if interfaceType is actually an interface
282+
if (!interfaceType.IsInterface)
283+
{
284+
throw new ArgumentException($"Type {interfaceType.FullName} is not an interface.", nameof(interfaceType));
285+
}
286+
287+
return interfaceType.IsAssignableFrom(type);
288+
}
289+
}
290+
291+
internal static class ValidateContextExtensions
292+
{
293+
internal static void AddOrExtendValidationError(this ValidateContext context, string key, string error)
294+
{
295+
context.ValidationErrors ??= [];
296+
297+
if (context.ValidationErrors.TryGetValue(key, out var existingErrors) && !existingErrors.Contains(error))
298+
{
299+
context.ValidationErrors[key] = [.. existingErrors, error];
300+
}
301+
else
302+
{
303+
context.ValidationErrors[key] = [error];
304+
}
305+
}
306+
}
307+
}

src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTestCommon/TestValidatableType.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.AspNetCore.Http.Validation.Generated;
66
using Microsoft.Extensions.DependencyInjection;
77
using Microsoft.Diagnostics.Tools.Monitor;
8+
using Microsoft.Diagnostics.Monitoring.WebApi.Models;
89
using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options;
910
using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions;
1011
using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Triggers.EventCounterShortcuts;
@@ -61,6 +62,8 @@ internal sealed class TestValidatableTypes
6162
public required ThreadpoolQueueLengthOptions ThreadpoolQueueLengthOptions { get; init; }
6263
public required EventMeterOptions EventMeterOptions { get; init; }
6364

65+
// Nested member
66+
public required EventPipeProvider EventPipeProvider { get; init; }
6467

6568
// TODO: only one resolver per project? Generate this for tests, for now. Maybe want to separate this one out
6669
// by test later.

0 commit comments

Comments
 (0)