File: Emitters\ValidationsGenerator.Emitter.cs
Web Access
Project: src\aspnetcore\src\Validation\gen\Microsoft.Extensions.Validation.ValidationsGenerator.csproj (Microsoft.Extensions.Validation.ValidationsGenerator)
// 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";
    }
}