asp.net mvc 3 - net - Técnica para transportar metadatos para ver modelos con AutoMapper
razor mvc (3)
Uso AutoMapper para asignar mis objetos de dominio a mis modelos de vista. Tengo metadatos en mi capa de dominio, que me gustaría transferir a la capa de vista y a ModelMetadata. (Estos metadatos no son lógicos de UI, pero brindan la información necesaria para mis puntos de vista).
En este momento, mi solución es usar un MetadataProvider por separado (independientemente de ASP.NET MVC) y usar convenciones para aplicar los metadatos relevantes al objeto ModelMetadata a través de un AssociatedMetadataProvider. El problema con este enfoque es que tengo que probar las mismas convenciones al vincular ModelMetadata del dominio como lo hago con mi AutoMapping, y parece que debería haber una manera de hacerlo más ortogonal. ¿Alguien puede recomendar una mejor manera de lograr esto?
La solución de Betty es excelente para "heredar" anotaciones de datos. He extendido esta idea para incluir también la validación proporcionada por 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);
}
}
}
}
Luego en Global.asax.cs:
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new MappedModelValidatorProvider(Mapper.Instance));
Utilizo el siguiente enfoque para copiar automáticamente las anotaciones de datos de mis entidades a mi modelo de vista. Esto garantiza que cosas como StringLength y los valores requeridos sean siempre los mismos para la entidad / viewmodel.
Funciona con la configuración de Automapper, por lo que funciona si las propiedades tienen un nombre diferente en el modelo de vista, siempre que AutoMapper esté configurado correctamente.
Debe crear un ModelValidatorProvider personalizado y un ModelMetadataProvider personalizado para que esto funcione. Mi memoria sobre por qué es un poco confuso, pero creo que es tanto el trabajo de validación del lado del servidor y del cliente, como cualquier otro formato que hagas basado en los metadatos (por ejemplo, un asterisco al lado de los campos obligatorios).
Nota: simplifiqué mi código ligeramente como lo agregué a continuación, por lo que puede haber algunos pequeños problemas.
Proveedor de metadatos
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);
}
}
Validador Proviviente
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);
}
}
Método de ayuda mencionado en las 2 clases anteriores
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;
}
}
}
Otras notas
- Si usa la inyección de dependencia, asegúrese de que su contenedor no esté reemplazando el proveedor de metadatos o validador incorporado. En mi caso, yo estaba usando el paquete Ninject.MVC3 que ataba a uno de ellos después de crear el núcleo, luego tuve que volver a vincularlo para que mi clase realmente se usara. Recibía excepciones sobre que solo se permitía agregar Obligatorio una vez, me llevaba casi todo un día rastrearlo.
si sus metadatos se proporcionan con atributos, defina los atributos en MetaDataTypes , luego aplique el mismo MetaDataType tanto a su clase de dominio como a sus viewmodels. Puede definir todos los tipos de metadatos en un archivo DLL separado que es referencia para ambas capas. Hay algunos problemas con este enfoque si sus clases de ViewModel no tienen algunas propiedades que se usan en MetaDataType, pero esto se puede solucionar con un proveedor personalizado (tengo el código si le gusta este enfoque).