Использование кастомного MetadataProvider и DataAnnotations в ASP.NET MVC
Стандартный способ валидации данных в ASP.NET MVC - это использование System.ComponentModel.DataAnnotations. Напомню, что с помощью специальных атрибутов можно указать правила валидации для конкретных свойств бизнес объектов. Но мы можем столкнуться с ситуацией, когда правила валидации не известны заранее и хранятся, например, в базе данных или других источниках. В таком случае использование DataAnnotations выглядит не реальным. В статье рассмотрим, как все таки этого добиться.
Пусть у нас есть некий бизнес объект Product:
public class Product { public int Id { get; set; } public string Name { get; set; } }
В контроллере (или другом удобном месте) в методе Create я хочу добавить некий код, который будет "на лету" добавлять правила валидации этого объекта, например так:
using System.ComponentModel.DataAnnotations; public ActionResult Create() { InMemoryMetadataManager.AddColumnAttributes<Product>(p => p.Name, new RequiredAttribute()); return View(); }
Теперь серверная валидация (Model.IsValid) должна возвращать false в случае если пользователь не введет название продукта.
Рассмотрим классы, которые необходимы нам для того, чтобы такой подход заработал.
InMemoryMetadataTypeDescriptor.cs:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Reflection; namespace InMemoryMetadataProvider { public class InMemoryMetadataTypeDescriptor : CustomTypeDescriptor { private Type Type { get; set; } public InMemoryMetadataTypeDescriptor(ICustomTypeDescriptor parent, Type type) : base(parent) { Type = type; } // Returns the collection of attributes for a given type (table) public override AttributeCollection GetAttributes() { AttributeCollection baseAttributes = base.GetAttributes(); List<Attribute> extraAttributes = InMemoryMetadataManager.GetTableAttributes(Type); if (extraAttributes.Count != 0) { // only create a new collection if it is necessary return AttributeCollection.FromExisting(baseAttributes, extraAttributes.ToArray()); } else { return baseAttributes; } } // Returns a collection of properties (columns) for the type, each with attributes // for that table public override PropertyDescriptorCollection GetProperties() { PropertyDescriptorCollection originalCollection = base.GetProperties(); bool customDescriptorsCreated = false; List<PropertyDescriptor> tempPropertyDescriptors = new List<PropertyDescriptor>(); foreach (PropertyDescriptor propDescriptor in originalCollection) { PropertyInfo propInfo = Type.GetProperty(propDescriptor.Name, propDescriptor.PropertyType); List<Attribute> newMetadata = InMemoryMetadataManager.GetColumnAttributes(propInfo); if (newMetadata.Count > 0) { tempPropertyDescriptors.Add(new PropertyDescriptorWrapper(propDescriptor, newMetadata.ToArray())); customDescriptorsCreated = true; } else { tempPropertyDescriptors.Add(propDescriptor); } } if (customDescriptorsCreated) { // only create a new collection if it is necessary return new PropertyDescriptorCollection(tempPropertyDescriptors.ToArray(), true); } else { return originalCollection; } } // PropertyDescriptor does not have a straightforward extensibility model that would // allow for easy addition of Attributes, so a derived class wrapping an another // instance has to be used. private class PropertyDescriptorWrapper : PropertyDescriptor { private PropertyDescriptor _wrappedPropertyDescriptor; public PropertyDescriptorWrapper(PropertyDescriptor wrappedPropertyDescriptor, Attribute[] newAttributes) : base(wrappedPropertyDescriptor, newAttributes) { _wrappedPropertyDescriptor = wrappedPropertyDescriptor; } public override bool CanResetValue(object component) { return _wrappedPropertyDescriptor.CanResetValue(component); } public override Type ComponentType { get { return _wrappedPropertyDescriptor.ComponentType; } } public override object GetValue(object component) { return _wrappedPropertyDescriptor.GetValue(component); } public override bool IsReadOnly { get { return _wrappedPropertyDescriptor.IsReadOnly; } } public override Type PropertyType { get { return _wrappedPropertyDescriptor.PropertyType; } } public override void ResetValue(object component) { _wrappedPropertyDescriptor.ResetValue(component); } public override void SetValue(object component, object value) { _wrappedPropertyDescriptor.SetValue(component, value); } public override bool ShouldSerializeValue(object component) { return _wrappedPropertyDescriptor.ShouldSerializeValue(component); } } } }
InMemoryMetadataTypeDescriptionProvider.cs:
using System; using System.ComponentModel; namespace InMemoryMetadataProvider { public class InMemoryMetadataTypeDescriptionProvider : TypeDescriptionProvider { private Type Type { get; set; } // Creates an instance for the given type. The InMemoryMetadataTypeDescriptionProvider will fall back // to default reflection-based behavior when retrieving attributes. public InMemoryMetadataTypeDescriptionProvider(Type type) : this(type, TypeDescriptor.GetProvider(type)) { Type = type; } // Creates an instance for the given type. The InMemoryMetadataTypeDescriptionProvider will use the given // parent provider for chaining public InMemoryMetadataTypeDescriptionProvider(Type type, TypeDescriptionProvider parentProvider) : base(parentProvider) { Type = type; } public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance) { return new InMemoryMetadataTypeDescriptor(base.GetTypeDescriptor(objectType, instance), Type); } } }
InMemoryMetadataManager.cs:
using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; namespace InMemoryMetadataProvider { public static class InMemoryMetadataManager { private static Dictionary<Type, List<Attribute>> s_tableAttributes = new Dictionary<Type, List<Attribute>>(); private static Dictionary<PropertyInfo, List<Attribute>> s_columnAttributes = new Dictionary<PropertyInfo, List<Attribute>>(); public static void AddTableAttributes<T>(params Attribute[] attributes) where T : class { AddTableAttributes(typeof(T), attributes); } public static void AddTableAttributes(Type table, params Attribute[] attributes) { if (attributes == null) throw new ArgumentNullException("attribute"); List<Attribute> propAttributes ; if (!s_tableAttributes.TryGetValue(table, out propAttributes)) { propAttributes = new List<Attribute>(); s_tableAttributes[table] = propAttributes; } propAttributes.AddRange(attributes); } internal static List<Attribute> GetTableAttributes(Type type) { List<Attribute> attributes; return s_tableAttributes.TryGetValue(type, out attributes) ? attributes : new List<Attribute>(); } // Allows for strongly type property references: // AddColumnAttribute<Product>( p => p.ProductName, ...) public static void AddColumnAttributes<T>(Expression<Func<T, object>> propertyAccessor, params Attribute[] attributes) where T : class { AddColumnAttributes(GetProperty<T>(propertyAccessor), attributes); } public static void AddColumnAttributes(PropertyInfo prop, params Attribute[] attributes) { if (attributes == null) throw new ArgumentNullException("attribute"); List<Attribute> attributeCollection; if (!s_columnAttributes.TryGetValue(prop, out attributeCollection)) { attributeCollection = new List<Attribute>(); s_columnAttributes[prop] = attributeCollection; } attributeCollection.AddRange(attributes); } internal static List<Attribute> GetColumnAttributes(PropertyInfo property) { List<Attribute> attributes; return s_columnAttributes.TryGetValue(property, out attributes) ? attributes : new List<Attribute>(); } private static PropertyInfo GetProperty<T>(Expression<Func<T, object>> propertyAccessor) { if (propertyAccessor == null) throw new ArgumentNullException("propertyAccessor"); try { // o => o.Property LambdaExpression lambda = (LambdaExpression)propertyAccessor; MemberExpression member; if (lambda.Body is UnaryExpression) { // If the property is not an Object, then the member access expression will be wrapped in a conversion expression // (object)o.Property UnaryExpression convert = (UnaryExpression)lambda.Body; // o.Property member = (MemberExpression)convert.Operand; } else { // o.Property member = (MemberExpression)lambda.Body; } // Property PropertyInfo property = (PropertyInfo)member.Member; return property; } catch(Exception e) { throw new ArgumentException("The property accessor expression is not in the expected format 'o => o.Property'.", e); } } } }
Расписывать код подробно не буду. Кому надо - разберется :)