ASP.NET MVC 模型和数据对象映射实践
在使用 MVC 开发项目的过程中遇到了个问题,就是模型和数据实体之间的如何快捷的转换?是不是可以像 Entity Framework 的那样 EntityTypeConfiguration,或者只需要少量的代码就可以把数据实体对象转换成一个 Model 对象(当时还不知道有 AutoMapper 这种东西),所以自己尝试写了一个简单的实现。
1、初步尝试
EntityTypeConverter 类主要用把数据实体转换成 Model
/// <summary> /// 提供一组实体对象模型转换的方法。 /// </summary> /// <typeparam name="TEntityInfo">指定的实体信息对象类型。</typeparam> public static class EntityTypeConverter<TEntityInfo> { /// <summary> /// 将指定的实体对象转换成 <see cref="BaseModel"/> 类型的对象模型。 /// </summary> /// <typeparam name="BaseModel">指定类型的模型对象。</typeparam> /// <param name="tEntityInfo">指定的实体信息。</param> /// <returns><see cref="BaseModel"/> 类型的对象模型。</returns> public static BaseModel ConverterToModel<BaseModel>(TEntityInfo tEntityInfo) where BaseModel : new() { if (tEntityInfo == null) throw new ArgumentNullException("tEntityInfo"); BaseModeltEntity = new BaseModel(); foreach (var prop in typeof(BaseModel).GetProperties()) { if (!prop.CanRead || !prop.CanWrite) continue; if (!CommonHelper.GetCustomTypeConverter(prop.PropertyType).CanConvertFrom(typeof(string))) continue; string fieldName = String.Empty;
// 验证是否有别名 var customAttribute = prop.CustomAttributes.Where(att => att.AttributeType == typeof(ModelFieldAliasAttribute)).FirstOrDefault(); if (customAttribute != null) { var constructorArgument = customAttribute.ConstructorArguments[0]; fieldName = constructorArgument.Value.ToString(); } else { fieldName = prop.Name; } PropertyInfo property = typeof(TEntityInfo).GetProperty(fieldName); if (property != null) { dynamic value = property.GetValue(tEntityInfo, null); prop.SetValue(tEntity, value, null); } } return tEntity; } /// <summary> /// 将指定的实体对象转换成 <see cref="BaseModel"/> 类型的对象模型列表。 /// </summary> /// <typeparam name="BaseModel">指定类型的模型对象。</typeparam> /// <param name="tEntityList">指定的实体信息列表。</param> /// <returns><see cref="BaseModel"/> 类型的对象模型列表。</returns> public static List<BaseModel> ConverterToList<BaseModel>(IList<TEntityInfo> tEntityList) where BaseModel: new() { List<BaseModel> modelList = new List<BaseModel>(); if (tEntityList != null && tEntityList.Count > 0) { foreach (var item in tEntityList) { modelList.Add(ConverterToModel<BaseModel>(item)); } } return modelList; } }
辅助类基于 TypeConverter 实现的方法,用于泛型字段的转换
public class GenericListTypeConverter<T> : TypeConverter { protected readonly TypeConverter typeConverter; public GenericListTypeConverter() { typeConverter = TypeDescriptor.GetConverter(typeof(T)); if (typeConverter == null) throw new InvalidOperationException("No type converter exists for type " + typeof(T).FullName); } protected virtual string[] GetStringArray(string input) { if (!String.IsNullOrEmpty(input)) { string[] result = input.Split(','); Array.ForEach(result, s => s.Trim()); return result; } else return new string[0]; } public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) { string[] items = GetStringArray(sourceType.ToString()); return items.Any(); } return base.CanConvertFrom(context, sourceType); } public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string) { string[] items = GetStringArray((string)value); var result = new List<T>(); Array.ForEach(items, s => { object item = typeConverter.ConvertFromInvariantString(s); if (item != null) { result.Add((T)item); } }); return result; } return base.ConvertFrom(context, culture, value); } public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { if (destinationType == typeof(string)) { string result = string.Empty; if (((IList<T>)value) != null) { //we don't use string.Join() because it doesn't support invariant culture for (int i = 0; i < ((IList<T>)value).Count; i++) { var str1 = Convert.ToString(((IList<T>)value)[i], CultureInfo.InvariantCulture); result += str1; //don't add comma after the last element if (i != ((IList<T>)value).Count - 1) result += ","; } } return result; } return base.ConvertTo(context, culture, value, destinationType); } }
自定义属性 ModelFieldAlias 类用于标识字段别名。
/// <summary> /// 表示一个模型转换对象字段类型的特性。 /// </summary> [AttributeUsage(ValidTargets, AllowMultiple = false, Inherited = false)] public class ModelFieldAliasAttribute : Attribute { /// <summary> /// 指定此属性可以应用特性的应用程序元素。 /// </summary> internal const AttributeTargets ValidTargets = AttributeTargets.Field | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Parameter; /// <summary> /// 模型的字段别名。 /// </summary> private string _fieldAlias; /// <summary> /// 初始化 <see cref="ModelFieldAliasAttribute"/> 类的新实例。 /// </summary> public ModelFieldAliasAttribute() { } /// <summary> /// 使用指定的筛选类型初始化 <see cref="ModelFieldAliasAttribute"/> 类的新实例。 /// </summary> /// <param name="fieldAlias">指定模型的字段别名。</param> public ModelFieldAliasAttribute(string fieldAlias) { _fieldAlias = fieldAlias; } /// <summary> /// 获取或设置模型的字段别名。 /// </summary> public string FieldAlias { get { return _fieldAlias; } set { _fieldAlias = value; } } }
使用方法:
// 集合对象 List<ChannelNavigationInfo> channelNavigationList = new List<ChannelNavigationInfo>(); List<ChannelNavigationModel> channelNavigationModelList = new List<ChannelNavigationModel>(); channelNavigationModelList = EntityTypeConverter<ChannelNavigationInfo>.ConverterToList<ChannelNavigationModel>(channelNavigationList); // 单独对象 ChannelNavigationInfo channelNavigation = new ChannelNavigationInfo(); ChannelNavigationModel channelNavigationModel = new ChannelNavigationModel(); channelNavigationModel = EntityTypeConverter<ChannelNavigationInfo>.ConverterToModel<ChannelNavigationModel>(channelNavigation);
由于系统中大多数情况下都是对数据查询的业务,很少有插入和更新,所以没有发现这种实现的弊端,后来开发管理系统的时候,需要大量的数据插入和更新操作,发现这种方法没法做反向映射和转换。
2、AutoMapper
AutoMapper 是用来解决对象之间映射转换的类库。对于我们开发人员来说,写对象之间互相转换的代码是一件极其浪费生命的事情,AutoMapper 能够帮助我们节省不少时间。
知道这个类库是在研究 nopCommerce 这个项目的时候看到的,使用 AutoMapper 创建映射非常简单。
Mapper.CreateMap<Order, OrderDto>();//创建映射关系Order –> OrderDto OrderDto dto = Mapper.Map<OrderDto>(order);//使用Map方法,直接将order对象装换成OrderDto对象
AutoMapper能够自动识别和匹配大部分对象属性:
如果源类和目标类的属性名称相同,直接匹配,目标类型的 CustomerName 可以匹配源类型的 Customer.Name,目标类型的 TotalRecords 可以匹配源类型的 GetTotalRecords() 方法。
AutoMapper 还支持自定义匹配规则:
Mapper.CreateMap<CalendarEvent, CalendarEventForm>() // 属性匹配,匹配源类中 WorkEvent.Date 到 EventDate .ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.WorkEvent.Date)) .ForMember(dest => dest.SomeValue, opt => opt.Ignore())//忽略目标类中的属性 .ForMember(dest => dest.TotalAmount, opt => opt.MapFrom(src => src.TotalAmount ?? 0))//复杂的匹配 .ForMember(dest => dest.OrderDate, opt => opt.UserValue<DateTime>(DateTime.Now)); //固定值匹配
为了方便使用,可以把扩展方法统一在一个类中实现。
/// <summary> /// 提供一组对象映射相关的扩展方法。 /// </summary> public static class MappingExtensions { #region City... /// <summary> /// 执行从 <see cref="City"/> 对象到 <see cref="CityModel"/> 对象的映射。 /// </summary> /// <param name="entity">指定的 <see cref="City"/> 对象。</param> /// <returns><see cref="CityModel"/> 对象。</returns> public static CityModel ToModel(this City entity) { return Mapper.Map<City, CityModel>(entity); } /// <summary> /// 执行从 <see cref="CityModel"/> 对象到 <see cref="City"/> 对象的映射。 /// </summary> /// <param name="model">指定的 <see cref="CityModel"/> 对象。</param> /// <returns><see cref="City"/> 对象。</returns> public static City ToEntity(this CityModel model) { return Mapper.Map<CityModel, City>(model); } /// <summary> /// 执行从 <see cref="CityModel"/> 对象到 <see cref="City"/> 对象的映射。 /// </summary> /// <param name="model">指定的 <see cref="CityModel"/> 模型对象。</param> /// <param name="destination">指定的 <see cref="City"/> 实体对象。</param> /// <returns><see cref="City"/> 对象。</returns> public static City ToEntity(this CityModel model, City destination) { return Mapper.Map(model, destination); } #endregion }
最后,在 Global.cs 文件中程序启动前,调用该方法。
AutoMapperConfiguration.Configuration();