// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
using Microsoft.CodeAnalysis.CSharp;
using System.IO;
namespace Microsoft.Extensions.Validation;
public sealed partial class ValidationsGenerator : IIncrementalGenerator
{
public static string GeneratedCodeConstructor => $@"global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(ValidationsGenerator).Assembly.FullName}"", ""{typeof(ValidationsGenerator).Assembly.GetName().Version}"")";
public static string GeneratedCodeAttribute => $"[{GeneratedCodeConstructor}]";
internal static void Emit(SourceProductionContext context, (ImmutableArray<InterceptableLocation?> AddValidationLocations, ImmutableArray<ValidatableType> ValidatableTypes) emitInputs)
{
var locations = emitInputs.AddValidationLocations.OfType<InterceptableLocation>().ToImmutableArray();
if (locations.IsEmpty)
{
// Avoid generating code if no AddValidation call was found.
return;
}
var source = Emit(locations, emitInputs.ValidatableTypes);
context.AddSource("ValidatableInfoResolver.g.cs", SourceText.From(source, Encoding.UTF8));
}
private static string Emit(ImmutableArray<InterceptableLocation> addValidationLocations, ImmutableArray<ValidatableType> validatableTypes) => $$"""
#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
{
{{GeneratedCodeAttribute}}
[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute : global::System.Attribute
{
public InterceptsLocationAttribute(int version, string data)
{
}
}
}
namespace Microsoft.Extensions.Validation.Generated
{
{{GeneratedCodeAttribute}}
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,
global::Microsoft.Extensions.Validation.DisplayNameInfo? displayNameInfo = null) : base(containingType, propertyType, name, displayNameInfo)
{
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.GetPropertyValidationAttributes(ContainingType, Name);
}
{{GeneratedCodeAttribute}}
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,
global::Microsoft.Extensions.Validation.DisplayNameInfo? displayNameInfo = null) : base(type, members, displayNameInfo)
{
Type = type;
}
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
internal global::System.Type Type { get; }
protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes()
=> ValidationAttributeCache.GetTypeValidationAttributes(Type);
}
{{GeneratedCodeAttribute}}
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.IValidatableTypeInfo? validatableTypeInfo)
{
validatableTypeInfo = null;
{{EmitTypeChecks(validatableTypes)}}
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.IValidatableParameterInfo? validatableParameterInfo)
{
validatableParameterInfo = null;
return false;
}
}
{{GeneratedCodeAttribute}}
file static class GeneratedServiceCollectionExtensions
{
{{EmitAddValidationInterceptorAttributes(addValidationLocations)}}
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 global::Microsoft.Extensions.Validation.Generated.GeneratedValidatableInfoResolver());
if (configureOptions is not null)
{
configureOptions(options);
}
});
}
}
{{GeneratedCodeAttribute}}
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[]> _propertyCache = new();
private static readonly global::System.Lazy<global::System.Collections.Concurrent.ConcurrentDictionary<global::System.Type, global::System.ComponentModel.DataAnnotations.ValidationAttribute[]>> _lazyTypeCache = new (() => new ());
private static global::System.Collections.Concurrent.ConcurrentDictionary<global::System.Type, global::System.ComponentModel.DataAnnotations.ValidationAttribute[]> TypeCache => _lazyTypeCache.Value;
public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes(
[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 _propertyCache.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,
global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.DeclaredOnly);
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();
});
}
public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes(
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
global::System.Type type
)
{
return TypeCache.GetOrAdd(type, static t =>
{
var typeAttributes = global::System.Reflection.CustomAttributeExtensions
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(t, inherit: true);
return global::System.Linq.Enumerable.ToArray(typeAttributes);
});
}
}
{{GeneratedCodeAttribute}}
file sealed class LiteralDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo
{
private readonly string _literal;
public LiteralDisplayName(string literal)
{
_literal = literal;
}
public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? type)
{
var localizer = context.ValidationOptions.Localizer;
if (localizer is null)
{
return _literal;
}
return localizer.ResolveDisplayName(new global::Microsoft.Extensions.Validation.DisplayNameLocalizationContext
{
Type = type,
DisplayName = _literal,
MemberName = memberName,
}) ?? _literal;
}
}
{{GeneratedCodeAttribute}}
file sealed class PropertyResourceDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo
{
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
private readonly global::System.Type _containingType;
private readonly string _propertyName;
public PropertyResourceDisplayName(
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
global::System.Type containingType,
string propertyName)
{
_containingType = containingType;
_propertyName = propertyName;
}
public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? type)
=> DisplayAttributeCache.GetPropertyDisplayAttribute(_containingType, _propertyName)?.GetName();
}
{{GeneratedCodeAttribute}}
file sealed class TypeResourceDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo
{
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
private readonly global::System.Type _type;
public TypeResourceDisplayName(
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
global::System.Type type)
{
_type = type;
}
public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? type)
=> DisplayAttributeCache.GetTypeDisplayAttribute(_type)?.GetName();
}
{{GeneratedCodeAttribute}}
file static class DisplayAttributeCache
{
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.DisplayAttribute?> _propertyCache = new();
private static readonly global::System.Collections.Concurrent.ConcurrentDictionary<global::System.Type, global::System.ComponentModel.DataAnnotations.DisplayAttribute?> _typeCache = new();
public static global::System.ComponentModel.DataAnnotations.DisplayAttribute? GetPropertyDisplayAttribute(
[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 _propertyCache.GetOrAdd(key, static k =>
{
// Check primary-constructor parameters first to handle record scenarios where
// [Display(ResourceType = ..., Name = ...)] is on the parameter rather than the property.
foreach (var constructor in k.ContainingType.GetConstructors())
{
var parameter = global::System.Linq.Enumerable.FirstOrDefault(
constructor.GetParameters(),
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.Ordinal));
if (parameter != null)
{
var paramDisplayAttr = global::System.Reflection.CustomAttributeExtensions
.GetCustomAttribute<global::System.ComponentModel.DataAnnotations.DisplayAttribute>(parameter);
if (paramDisplayAttr is not null)
{
return paramDisplayAttr;
}
break;
}
}
var property = k.ContainingType.GetProperty(
k.PropertyName,
global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.DeclaredOnly);
return property is null
? null
: global::System.Reflection.CustomAttributeExtensions
.GetCustomAttribute<global::System.ComponentModel.DataAnnotations.DisplayAttribute>(property, inherit: true);
});
}
public static global::System.ComponentModel.DataAnnotations.DisplayAttribute? GetTypeDisplayAttribute(
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
global::System.Type type)
{
return _typeCache.GetOrAdd(type, static t =>
global::System.Reflection.CustomAttributeExtensions
.GetCustomAttribute<global::System.ComponentModel.DataAnnotations.DisplayAttribute>(t, inherit: true));
}
}
}
""";
private static string EmitAddValidationInterceptorAttributes(ImmutableArray<InterceptableLocation> addValidations)
{
// We filtered early for empty locations, so we are sure we have at least one.
var firstAttribute = addValidations[0].GetInterceptsLocationAttributeSyntax();
if (addValidations.Length == 1)
{
// Common case
return firstAttribute;
}
// This is the less common case.
var sb = new StringBuilder(firstAttribute);
for (var i = 1; i < addValidations.Length; i++)
{
sb.AppendLine().Append(" ").Append(addValidations[i].GetInterceptsLocationAttributeSyntax());
}
return sb.ToString();
}
private static string EmitTypeChecks(ImmutableArray<ValidatableType> validatableTypes)
{
var sw = new StringWriter();
var cw = new CodeWriter(sw, baseIndent: 3);
foreach (var validatableType in validatableTypes)
{
var typeName = validatableType.TypeFQN;
cw.WriteLine($"if (type == typeof({typeName}))");
cw.StartBlock();
cw.WriteLine($"validatableTypeInfo = new global::Microsoft.Extensions.Validation.Generated.GeneratedValidatableTypeInfo(");
cw.Indent++;
cw.WriteLine($"type: typeof({typeName}),");
if (validatableType.Members.IsDefaultOrEmpty)
{
cw.WriteLine("members: [],");
}
else
{
cw.WriteLine("members: [");
cw.Indent++;
foreach (var member in validatableType.Members)
{
EmitValidatableMemberForCreate(member, cw);
}
cw.Indent--;
cw.WriteLine("],");
}
cw.WriteLine($"displayNameInfo: {FormatTypeDisplayNameInfo(validatableType)}");
cw.Indent--;
cw.WriteLine(");");
cw.WriteLine("return true;");
cw.EndBlock();
}
return sw.ToString();
}
private static void EmitValidatableMemberForCreate(ValidatableProperty member, CodeWriter cw)
{
cw.WriteLine("new global::Microsoft.Extensions.Validation.Generated.GeneratedValidatablePropertyInfo(");
cw.Indent++;
cw.WriteLine($"containingType: typeof({member.ContainingTypeFQN}),");
cw.WriteLine($"propertyType: typeof({member.TypeFQN}),");
cw.WriteLine($"name: \"{member.Name}\",");
cw.WriteLine($"displayNameInfo: {FormatPropertyDisplayNameInfo(member)}");
cw.Indent--;
cw.WriteLine("),");
}
private static string FormatPropertyDisplayNameInfo(ValidatableProperty member)
{
if (member.HasResourceDisplayAttribute)
{
return $"new global::Microsoft.Extensions.Validation.Generated.PropertyResourceDisplayName(typeof({member.ContainingTypeFQN}), \"{member.Name}\")";
}
if (member.DisplayName is not null)
{
return $"new global::Microsoft.Extensions.Validation.Generated.LiteralDisplayName({SymbolDisplay.FormatLiteral(member.DisplayName, quote: true)})";
}
return "null";
}
private static string FormatTypeDisplayNameInfo(ValidatableType validatableType)
{
if (validatableType.HasResourceDisplayAttribute)
{
return $"new global::Microsoft.Extensions.Validation.Generated.TypeResourceDisplayName(typeof({validatableType.TypeFQN}))";
}
if (validatableType.DisplayName is not null)
{
return $"new global::Microsoft.Extensions.Validation.Generated.LiteralDisplayName({SymbolDisplay.FormatLiteral(validatableType.DisplayName, quote: true)})";
}
return "null";
}
}