Использование кастомного MetadataProvider и DataAnnotations в ASP.NET MVC

вторник, 6 сентября 2011, Александр Краковецкий

Стандартный способ валидации данных в 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);
            }
        }
    }
}

Расписывать код подробно не буду. Кому надо - разберется :)


Ищите нас в интернетах!

Комментарии

Свежие вакансии