364 lines
15 KiB
C#
364 lines
15 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
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.Internal
|
|
{
|
|
/// <summary>
|
|
/// An implementation of <see cref="IBindingMetadataProvider"/> and <see cref="IDisplayMetadataProvider"/> for
|
|
/// the System.ComponentModel.DataAnnotations attribute classes.
|
|
/// </summary>
|
|
public class DataAnnotationsMetadataProvider :
|
|
IBindingMetadataProvider,
|
|
IDisplayMetadataProvider,
|
|
IValidationMetadataProvider
|
|
{
|
|
private readonly IStringLocalizerFactory _stringLocalizerFactory;
|
|
private readonly MvcDataAnnotationsLocalizationOptions _localizationOptions;
|
|
|
|
public DataAnnotationsMetadataProvider(
|
|
IOptions<MvcDataAnnotationsLocalizationOptions> options,
|
|
IStringLocalizerFactory stringLocalizerFactory)
|
|
{
|
|
if (options == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(options));
|
|
}
|
|
|
|
_localizationOptions = options.Value;
|
|
_stringLocalizerFactory = stringLocalizerFactory;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void CreateBindingMetadata(BindingMetadataProviderContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
var editableAttribute = context.Attributes.OfType<EditableAttribute>().FirstOrDefault();
|
|
if (editableAttribute != null)
|
|
{
|
|
context.BindingMetadata.IsReadOnly = !editableAttribute.AllowEdit;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(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 = DataType.Html.ToString();
|
|
}
|
|
|
|
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;
|
|
var underlyingTypeInfo = underlyingType.GetTypeInfo();
|
|
|
|
if (underlyingTypeInfo.IsEnum)
|
|
{
|
|
// IsEnum
|
|
displayMetadata.IsEnum = true;
|
|
|
|
// IsFlagsEnum
|
|
displayMetadata.IsFlagsEnum = underlyingTypeInfo.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>();
|
|
var enumLocalizer = _stringLocalizerFactory?.Create(underlyingType);
|
|
|
|
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() != null)
|
|
{
|
|
displayMetadata.Order = displayAttribute.GetOrder().Value;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
// 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 = context.Attributes.OfType<RequiredAttribute>().FirstOrDefault();
|
|
if (requiredAttribute != null)
|
|
{
|
|
context.ValidationMetadata.IsRequired = true;
|
|
}
|
|
|
|
foreach (var attribute in context.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;
|
|
}
|
|
}
|
|
} |