Техника для переноса метаданных для просмотра моделей с помощью AutoMapper

Я использую AutoMapper для сопоставления моих доменных объектов с моими моделями представления. У меня есть метаданные в моем доменном слое, которые я хотел бы перенести на слой представления и в ModelMetadata. (Эти метаданные не являются логикой пользовательского интерфейса, но предоставляют необходимую информацию для моих представлений).

Прямо сейчас мое решение состоит в том, чтобы использовать отдельный MetadataProvider (независимо от ASP.NET MVC) и использовать соглашения для применения соответствующих метаданных к объекту ModelMetadata через AssociatedMetadataProvider. Проблема с этим подходом заключается в том, что при связывании ModelMetadata из домена мне приходится тестировать те же соглашения, что и при использовании автоматического сопоставления, и, похоже, должен быть способ сделать это более ортогональным. Кто-нибудь может порекомендовать лучший способ сделать это?

 rboarman05 апр. 2012 г., 19:30
Покажите некоторый код, чтобы мы могли видеть, что вы делаете.

Ответы на вопрос(3)

MetaDataTypesзатем примените один и тот же MetaDataType к классу вашего домена и к вашим моделям представления. Вы можете определить все MetaDataTypes в отдельной DLL, которая является ссылкой на оба слоя. Есть некоторые проблемы с этим подходом, если ваши классы ViewModel не имеют некоторых свойств, которые используются в MetaDataType, но это можно исправить с помощью специального провайдера (у меня есть код, если вам нравится этот подход).

 Francesco Abbruzzese06 апр. 2012 г., 11:28
Посмотрите на автомата ITypeConvertergithub.com/AutoMapper/AutoMapper/wiki/Custom-type-converters Это позволяет настроить отображение. Таким образом, вы можете добавить логику для извлечения метаданных из исходного типа и поместить их в словарь, индексированный парой (type / propertyName или PropertyInfo). Затем пользовательский поставщик метаданных получает доступ к этому словарю для получения метаданных ... это нетривиальная работа ... но она кажется мне выполнимой
 smartcaveman05 апр. 2012 г., 21:23
Метаданные поступают из разных мест, и в настоящее время у меня нет выделенных классов «Метаданные» (и я не хочу их). Я думаю, что это может быть хорошим местом для меня, чтобы искать точку расширяемости, хотя. Спасибо
 Francesco Abbruzzese05 апр. 2012 г., 22:04
Вы можете написать своего рода «брокерский» поставщик метаданных, который просто перераспределяет поиск метаданных из одного типа в другой. Затем вы можете «перенаправить» класс ViewModel на тип домена.
 smartcaveman05 апр. 2012 г., 23:34
Это в основном то, что я сейчас делаю. Проблема в том, что «брокер» оценивает объект ModelMetadata, чтобы получить ссылку на метаданные для сопоставленного доменного объекта. И логика, необходимая для того, чтобы знать, что представляет собой проекция, в значительной степени та же логика, которую я использую для AutoMapping. Я хочу избавиться от избыточности.

расширил эту идею, чтобы включить проверку, предоставляемую IValidatableObject.

public class MappedModelValidatorProvider : DataAnnotationsModelValidatorProvider
{
    private readonly IMapper _mapper;

    public MappedModelValidatorProvider(IMapper mapper)
    {
        _mapper = mapper;
    }

    protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
    {
        var mappedAttributes = _mapper.ConfigurationProvider.GetMappedAttributes(metadata.ContainerType, metadata.PropertyName, attributes);
        foreach (var validator in base.GetValidators(metadata, context, mappedAttributes))
        {
            yield return validator;
        }
        foreach (var typeMap in _mapper.ConfigurationProvider.GetAllTypeMaps().Where(i => i.SourceType == metadata.ModelType))
        {
            if (typeof(IValidatableObject).IsAssignableFrom(typeMap.DestinationType))
            {
                var model = _mapper.Map(metadata.Model, typeMap.SourceType, typeMap.DestinationType);
                var modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeMap.DestinationType);
                yield return new ValidatableObjectAdapter(modelMetadata, context);
            }
        }
    }
}

Затем в Global.asax.cs:

ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new MappedModelValidatorProvider(Mapper.Instance));
Решение Вопроса

чтобы автоматически копировать аннотации данных из моих сущностей в мою модель представления. Это гарантирует, что такие вещи, как StringLength и Required, всегда будут одинаковыми для entity / viewmodel.

Он работает с использованием конфигурации Automapper, поэтому работает, если свойства именуются в модели представления, если AutoMapper настроен правильно.

Вам нужно создать пользовательский ModelValidatorProvider и пользовательский ModelMetadataProvider, чтобы это работало. Моя память о том, почему это немного туманно, но я считаю, что это так и на стороне сервера, и на стороне клиента, а также любое другое форматирование, которое вы выполняете на основе метаданных (например, звездочка рядом с обязательными полями).

Примечание. Я немного упростил свой код, добавив его ниже, поэтому может возникнуть несколько небольших проблем.

Поставщик метаданных

public class MetadataProvider : DataAnnotationsModelMetadataProvider
{        
    private IConfigurationProvider _mapper;

    public MetadataProvider(IConfigurationProvider mapper)
    {           
        _mapper = mapper;
    }

    protected override System.Web.Mvc.ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {           
        //Grab attributes from the entity columns and copy them to the view model
        var mappedAttributes = _mapper.GetMappedAttributes(containerType, propertyName, attributes);

        return base.CreateMetadata(mappedAttributes, containerType, modelAccessor, modelType, propertyName);

}
}

Валидатор Провиддер

public class ValidatorProvider : DataAnnotationsModelValidatorProvider
{
    private IConfigurationProvider _mapper;

    public ValidatorProvider(IConfigurationProvider mapper) 
    {
        _mapper = mapper;
    }

    protected override System.Collections.Generic.IEnumerable<ModelValidator> GetValidators(System.Web.Mvc.ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
    {   
        var mappedAttributes = _mapper.GetMappedAttributes(metadata.ContainerType, metadata.PropertyName, attributes);
        return base.GetValidators(metadata, context, mappedAttributes);
    }
}

Вспомогательный метод, указанный в 2 классах

public static IEnumerable<Attribute> GetMappedAttributes(this IConfigurationProvider mapper, Type sourceType, string propertyName, IEnumerable<Attribute> existingAttributes)
{
    if (sourceType != null)
    {
        foreach (var typeMap in mapper.GetAllTypeMaps().Where(i => i.SourceType == sourceType))
        {
            foreach (var propertyMap in typeMap.GetPropertyMaps())
            {
                if (propertyMap.IsIgnored() || propertyMap.SourceMember == null)
                    continue;

                if (propertyMap.SourceMember.Name == propertyName)
                {
                    foreach (ValidationAttribute attribute in propertyMap.DestinationProperty.GetCustomAttributes(typeof(ValidationAttribute), true))
                    {
                        if (!existingAttributes.Any(i => i.GetType() == attribute.GetType()))
                            yield return attribute;
                    }
                }
            }
        }
    }

    if (existingAttributes != null)
    {
        foreach (var attribute in existingAttributes)
        {
            yield return attribute;
        }
    }

}

Другие заметки

Если вы используете внедрение зависимостей, убедитесь, что ваш контейнер еще не заменяет встроенный поставщик метаданных или поставщик валидатора. В моем случае я использовал пакет Ninject.MVC3, который связал один из них после создания ядра, и мне пришлось потом перепривязывать его, чтобы мой класс фактически использовался. Я получал исключения из-за того, что Required разрешено добавлять только один раз, и большую часть дня я занимался его поиском.
 smartcaveman11 апр. 2012 г., 15:56
это полезно У меня есть похожий подход, который я использую прямо сейчас, но с моим собственным источником метаданных (не AutoMapper's). Это может быть расширено, чтобы сделать то же самое, что и ваш. Помоги мне понять кое-что: ты проходишь мимоmetadata.ContainerType как тип источника, но кажется, что он будет искать тип вашего бизнес-объекта. Это заставляет меня думать, что вы (а) получаете ModelMetadata для вашего бизнес-объекта и копируете атрибуты модели представления, или (б) сопоставляете ваши модели представления с вашими бизнес-объектами с помощью AutoMapper (мой вариант использования противоположен). Вы можете это прояснить?
 Betty11 апр. 2012 г., 22:30
Я сопоставляю оба направления с AutoMapper. Этот код предназначен для применения метаданных из бизнес-объектов к моим моделям представления, однако я использую сопоставления, которые идут в другом направлении, чтобы найти метаданные. Никакой конкретной причины, о которой я знаю, и теперь, когда вы упомянули это, это кажется немного странным.
 smartcaveman11 апр. 2012 г., 16:09
Похоже,IConfigurationProvider это место для работы. Глядя наисточникПохоже, что лучший подход для моего сценария будет проводка доevent EventHandler<TypeMapCreatedEventArgs> TypeMapCreated; в моем IoC. Вы пробовали такой подход?Похоже, он срабатывает каждый раз, когда создается типтак что я могу подключить это к моему существующему поставщику метаданных
 Mr. T14 апр. 2014 г., 17:57
Думаю, я нашел путь вперед -DestinationProperty теперь имеетMemberInfo свойство, которое являетсяICustomAttributeProvider поэтому код меняется наpropertyMap.DestinationProperty.MemberInfo.GetCustomAttributes(typeof (ValidationAttribute), true))
 Betty11 апр. 2012 г., 22:36
Я на самом деле не видел это событие раньше. Я мог бы использовать его для создания и кэширования сопоставлений метаданных, однако все еще нужны оба поставщика MVC для его использования. Вместо этого я мог бы просто добавить кеширование в свой метод расширения, чтобы не видеть преимущества использования этого события. Если вы в конечном итоге используете его, я хотел бы услышать об этом.

Ваш ответ на вопрос