File: DataAnnotationsMetadataProvider.cs
Web Access
Project: src\src\Mvc\Mvc.DataAnnotations\src\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj (Microsoft.AspNetCore.Mvc.DataAnnotations)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Mvc.DataAnnotations;
 
/// <summary>
/// An implementation of <see cref="IBindingMetadataProvider"/> and <see cref="IDisplayMetadataProvider"/> for
/// the System.ComponentModel.DataAnnotations attribute classes.
/// </summary>
internal sealed class DataAnnotationsMetadataProvider :
    IBindingMetadataProvider,
    IDisplayMetadataProvider,
    IValidationMetadataProvider
{
    private readonly IStringLocalizerFactory? _stringLocalizerFactory;
    private readonly MvcOptions _options;
    private readonly MvcDataAnnotationsLocalizationOptions _localizationOptions;
 
    public DataAnnotationsMetadataProvider(
        MvcOptions options,
        IOptions<MvcDataAnnotationsLocalizationOptions> localizationOptions,
        IStringLocalizerFactory? stringLocalizerFactory)
    {
        ArgumentNullException.ThrowIfNull(options);
        ArgumentNullException.ThrowIfNull(localizationOptions);
 
        _options = options;
        _localizationOptions = localizationOptions.Value;
        _stringLocalizerFactory = stringLocalizerFactory;
    }
 
    /// <inheritdoc />
    public void CreateBindingMetadata(BindingMetadataProviderContext context)
    {
        ArgumentNullException.ThrowIfNull(context);
 
        var editableAttribute = context.Attributes.OfType<EditableAttribute>().FirstOrDefault();
        if (editableAttribute != null)
        {
            context.BindingMetadata.IsReadOnly = !editableAttribute.AllowEdit;
        }
    }
 
    /// <inheritdoc />
    public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
    {
        ArgumentNullException.ThrowIfNull(context);
 
        var attributes = context.Attributes;
        var dataTypeAttribute = attributes.OfType<DataTypeAttribute>().FirstOrDefault();
        var displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault();
        var displayColumnAttribute = attributes.OfType<DisplayColumnAttribute>().FirstOrDefault();
        var displayFormatAttribute = attributes.OfType<DisplayFormatAttribute>().FirstOrDefault();
        var displayNameAttribute = attributes.OfType<DisplayNameAttribute>().FirstOrDefault();
        var hiddenInputAttribute = attributes.OfType<HiddenInputAttribute>().FirstOrDefault();
        var scaffoldColumnAttribute = attributes.OfType<ScaffoldColumnAttribute>().FirstOrDefault();
        var uiHintAttribute = attributes.OfType<UIHintAttribute>().FirstOrDefault();
 
        // Special case the [DisplayFormat] attribute hanging off an applied [DataType] attribute. This property is
        // non-null for DataType.Currency, DataType.Date, DataType.Time, and potentially custom [DataType]
        // subclasses. The DataType.Currency, DataType.Date, and DataType.Time [DisplayFormat] attributes have a
        // non-null DataFormatString and the DataType.Date and DataType.Time [DisplayFormat] attributes have
        // ApplyFormatInEditMode==true.
        if (displayFormatAttribute == null && dataTypeAttribute != null)
        {
            displayFormatAttribute = dataTypeAttribute.DisplayFormat;
        }
 
        var displayMetadata = context.DisplayMetadata;
 
        // ConvertEmptyStringToNull
        if (displayFormatAttribute != null)
        {
            displayMetadata.ConvertEmptyStringToNull = displayFormatAttribute.ConvertEmptyStringToNull;
        }
 
        // DataTypeName
        if (dataTypeAttribute != null)
        {
            displayMetadata.DataTypeName = dataTypeAttribute.GetDataTypeName();
        }
        else if (displayFormatAttribute != null && !displayFormatAttribute.HtmlEncode)
        {
            displayMetadata.DataTypeName = nameof(DataType.Html);
        }
 
        var containerType = context.Key.ContainerType ?? context.Key.ModelType;
        IStringLocalizer? localizer = null;
        if (_stringLocalizerFactory != null && _localizationOptions.DataAnnotationLocalizerProvider != null)
        {
            localizer = _localizationOptions.DataAnnotationLocalizerProvider(containerType, _stringLocalizerFactory);
        }
 
        // Description
        if (displayAttribute != null)
        {
            if (localizer != null &&
                !string.IsNullOrEmpty(displayAttribute.Description) &&
                displayAttribute.ResourceType == null)
            {
                displayMetadata.Description = () => localizer[displayAttribute.Description];
            }
            else
            {
                displayMetadata.Description = displayAttribute.GetDescription;
            }
        }
 
        // DisplayFormatString
        if (displayFormatAttribute != null)
        {
            displayMetadata.DisplayFormatString = displayFormatAttribute.DataFormatString;
        }
 
        // DisplayName
        // DisplayAttribute has precedence over DisplayNameAttribute.
        if (displayAttribute?.GetName() != null)
        {
            if (localizer != null &&
                !string.IsNullOrEmpty(displayAttribute.Name) &&
                displayAttribute.ResourceType == null)
            {
                displayMetadata.DisplayName = () => localizer[displayAttribute.Name];
            }
            else
            {
                displayMetadata.DisplayName = displayAttribute.GetName;
            }
        }
        else if (displayNameAttribute != null)
        {
            if (localizer != null &&
                !string.IsNullOrEmpty(displayNameAttribute.DisplayName))
            {
                displayMetadata.DisplayName = () => localizer[displayNameAttribute.DisplayName];
            }
            else
            {
                displayMetadata.DisplayName = () => displayNameAttribute.DisplayName;
            }
        }
 
        // EditFormatString
        if (displayFormatAttribute != null && displayFormatAttribute.ApplyFormatInEditMode)
        {
            displayMetadata.EditFormatString = displayFormatAttribute.DataFormatString;
        }
 
        // IsEnum et cetera
        var underlyingType = Nullable.GetUnderlyingType(context.Key.ModelType) ?? context.Key.ModelType;
 
        if (underlyingType.IsEnum)
        {
            // IsEnum
            displayMetadata.IsEnum = true;
 
            // IsFlagsEnum
            displayMetadata.IsFlagsEnum = underlyingType.IsDefined(typeof(FlagsAttribute), inherit: false);
 
            // EnumDisplayNamesAndValues and EnumNamesAndValues
            //
            // Order EnumDisplayNamesAndValues by DisplayAttribute.Order, then by the order of Enum.GetNames().
            // That method orders by absolute value, then its behavior is undefined (but hopefully stable).
            // Add to EnumNamesAndValues in same order but Dictionary does not guarantee order will be preserved.
 
            var groupedDisplayNamesAndValues = new List<KeyValuePair<EnumGroupAndName, string>>();
            var namesAndValues = new Dictionary<string, string>();
 
            IStringLocalizer? enumLocalizer = null;
            if (_stringLocalizerFactory != null && _localizationOptions.DataAnnotationLocalizerProvider != null)
            {
                enumLocalizer = _localizationOptions.DataAnnotationLocalizerProvider(underlyingType, _stringLocalizerFactory);
            }
 
            var enumFields = Enum.GetNames(underlyingType)
                .Select(name => underlyingType.GetField(name)!)
                .OrderBy(field => field.GetCustomAttribute<DisplayAttribute>(inherit: false)?.GetOrder() ?? 1000);
 
            foreach (var field in enumFields)
            {
                var groupName = GetDisplayGroup(field);
                var value = ((Enum)field.GetValue(obj: null)!).ToString("d");
 
                groupedDisplayNamesAndValues.Add(new KeyValuePair<EnumGroupAndName, string>(
                    new EnumGroupAndName(
                        groupName,
                        () => GetDisplayName(field, enumLocalizer)),
                    value));
                namesAndValues.Add(field.Name, value);
            }
 
            displayMetadata.EnumGroupedDisplayNamesAndValues = groupedDisplayNamesAndValues;
            displayMetadata.EnumNamesAndValues = namesAndValues;
        }
 
        // HasNonDefaultEditFormat
        if (!string.IsNullOrEmpty(displayFormatAttribute?.DataFormatString) &&
            displayFormatAttribute?.ApplyFormatInEditMode == true)
        {
            // Have a non-empty EditFormatString based on [DisplayFormat] from our cache.
            if (dataTypeAttribute == null)
            {
                // Attributes include no [DataType]; [DisplayFormat] was applied directly.
                displayMetadata.HasNonDefaultEditFormat = true;
            }
            else if (dataTypeAttribute.DisplayFormat != displayFormatAttribute)
            {
                // Attributes include separate [DataType] and [DisplayFormat]; [DisplayFormat] provided override.
                displayMetadata.HasNonDefaultEditFormat = true;
            }
            else if (dataTypeAttribute.GetType() != typeof(DataTypeAttribute))
            {
                // Attributes include [DisplayFormat] copied from [DataType] and [DataType] was of a subclass.
                // Assume the [DataType] constructor used the protected DisplayFormat setter to override its
                // default.  That is derived [DataType] provided override.
                displayMetadata.HasNonDefaultEditFormat = true;
            }
        }
 
        // HideSurroundingHtml
        if (hiddenInputAttribute != null)
        {
            displayMetadata.HideSurroundingHtml = !hiddenInputAttribute.DisplayValue;
        }
 
        // HtmlEncode
        if (displayFormatAttribute != null)
        {
            displayMetadata.HtmlEncode = displayFormatAttribute.HtmlEncode;
        }
 
        // NullDisplayText
        if (displayFormatAttribute != null)
        {
            displayMetadata.NullDisplayText = displayFormatAttribute.NullDisplayText;
        }
 
        // Order
        if (displayAttribute?.GetOrder() is int order)
        {
            displayMetadata.Order = order;
        }
 
        // Placeholder
        if (displayAttribute != null)
        {
            if (localizer != null &&
                !string.IsNullOrEmpty(displayAttribute.Prompt) &&
                displayAttribute.ResourceType == null)
            {
                displayMetadata.Placeholder = () => localizer[displayAttribute.Prompt];
            }
            else
            {
                displayMetadata.Placeholder = displayAttribute.GetPrompt;
            }
        }
 
        // ShowForDisplay
        if (scaffoldColumnAttribute != null)
        {
            displayMetadata.ShowForDisplay = scaffoldColumnAttribute.Scaffold;
        }
 
        // ShowForEdit
        if (scaffoldColumnAttribute != null)
        {
            displayMetadata.ShowForEdit = scaffoldColumnAttribute.Scaffold;
        }
 
        // SimpleDisplayProperty
        if (displayColumnAttribute != null)
        {
            displayMetadata.SimpleDisplayProperty = displayColumnAttribute.DisplayColumn;
        }
 
        // TemplateHint
        if (uiHintAttribute != null)
        {
            displayMetadata.TemplateHint = uiHintAttribute.UIHint;
        }
        else if (hiddenInputAttribute != null)
        {
            displayMetadata.TemplateHint = "HiddenInput";
        }
    }
 
    /// <inheritdoc />
    public void CreateValidationMetadata(ValidationMetadataProviderContext context)
    {
        ArgumentNullException.ThrowIfNull(context);
 
        // Read interface .Count once rather than per iteration
        var contextAttributes = context.Attributes;
        var contextAttributesCount = contextAttributes.Count;
        var attributes = new List<object>(contextAttributesCount);
 
        for (var i = 0; i < contextAttributesCount; i++)
        {
            var attribute = contextAttributes[i];
            if (attribute is ValidationProviderAttribute validationProviderAttribute)
            {
                attributes.AddRange(validationProviderAttribute.GetValidationAttributes());
            }
            else
            {
                attributes.Add(attribute);
            }
        }
 
        // RequiredAttribute marks a property as required by validation - this means that it
        // must have a non-null value on the model during validation.
        var requiredAttribute = attributes.OfType<RequiredAttribute>().FirstOrDefault();
 
        // For non-nullable reference types, treat them as-if they had an implicit [Required].
        // This allows the developer to specify [Required] to customize the error message, so
        // if they already have [Required] then there's no need for us to do this check.
        if (!_options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes &&
            requiredAttribute == null &&
            !context.Key.ModelType.IsValueType &&
            context.Key.MetadataKind != ModelMetadataKind.Type)
        {
            var addInferredRequiredAttribute = false;
            if (context.Key.MetadataKind == ModelMetadataKind.Type)
            {
                // Do nothing.
            }
            else if (context.Key.MetadataKind == ModelMetadataKind.Property)
            {
                var property = context.Key.PropertyInfo;
                if (property is not null)
                {
                    addInferredRequiredAttribute = IsRequired(context);
                }
            }
            else if (context.Key.MetadataKind == ModelMetadataKind.Parameter)
            {
                // If the default value is assigned we don't need to check the nullabilty
                // since the parameter will be optional.
                if (!context.Key.ParameterInfo!.HasDefaultValue)
                {
                    addInferredRequiredAttribute = IsRequired(context);
                }
            }
            else
            {
                throw new InvalidOperationException("Unsupported ModelMetadataKind: " + context.Key.MetadataKind);
            }
 
            if (addInferredRequiredAttribute)
            {
                // Since this behavior specifically relates to non-null-ness, we will use the non-default
                // option to tolerate empty/whitespace strings. empty/whitespace INPUT will still result in
                // a validation error by default because we convert empty/whitespace strings to null
                // unless you say otherwise.
                requiredAttribute = new RequiredAttribute()
                {
                    AllowEmptyStrings = true,
                };
                attributes.Add(requiredAttribute);
            }
        }
 
        if (requiredAttribute != null)
        {
            context.ValidationMetadata.IsRequired = true;
        }
 
        foreach (var attribute in attributes.OfType<ValidationAttribute>())
        {
            // If another provider has already added this attribute, do not repeat it.
            // This will prevent attributes like RemoteAttribute (which implement ValidationAttribute and
            // IClientModelValidator) to be added to the ValidationMetadata twice.
            // This is to ensure we do not end up with duplication validation rules on the client side.
            if (!context.ValidationMetadata.ValidatorMetadata.Contains(attribute))
            {
                context.ValidationMetadata.ValidatorMetadata.Add(attribute);
            }
        }
    }
 
    private static string GetDisplayName(FieldInfo field, IStringLocalizer? stringLocalizer)
    {
        var display = field.GetCustomAttribute<DisplayAttribute>(inherit: false);
        if (display != null)
        {
            // Note [Display(Name = "")] is allowed but we will not attempt to localize the empty name.
            var name = display.GetName();
            if (stringLocalizer != null && !string.IsNullOrEmpty(name) && display.ResourceType == null)
            {
                name = stringLocalizer[name];
            }
 
            return name ?? field.Name;
        }
 
        return field.Name;
    }
 
    // Return non-empty group specified in a [Display] attribute for a field, if any; string.Empty otherwise.
    private static string GetDisplayGroup(FieldInfo field)
    {
        var display = field.GetCustomAttribute<DisplayAttribute>(inherit: false);
        if (display != null)
        {
            // Note [Display(Group = "")] is allowed.
            var group = display.GetGroupName();
            if (group != null)
            {
                return group;
            }
        }
 
        return string.Empty;
    }
 
    internal static bool IsRequired(ValidationMetadataProviderContext context)
    {
        var nullabilityContext = new NullabilityInfoContext();
        var nullability = context.Key.MetadataKind switch
        {
            ModelMetadataKind.Parameter => nullabilityContext.Create(context.Key.ParameterInfo!),
            ModelMetadataKind.Property => nullabilityContext.Create(context.Key.PropertyInfo!),
            _ => null
        };
        var isOptional = nullability != null && nullability.ReadState != NullabilityState.NotNull;
        return !isOptional;
    }
}