将 ViewModel 投影回模型的最佳方式
Best way to project ViewModel back into Model
考虑使用 ViewModel:
public class ViewModel
{
public int id { get; set; }
public int a { get; set; }
public int b { get; set; }
}
和这样的原始模型:
public class Model
{
public int id { get; set; }
public int a { get; set; }
public int b { get; set; }
public int c { get; set; }
public virtual Object d { get; set; }
}
每次获取视图模型时,我都必须将所有 ViewModel
属性一一放入 Model
。类似于:
var model = Db.Models.Find(viewModel.Id);
model.a = viewModel.a;
model.b = viewModel.b;
Db.SaveChanges();
这总是会引起很多问题。我什至有时会忘记提及某些属性,然后灾难就发生了!
我在寻找类似的东西:
Mapper.Map(model, viewModel);
顺便说一句:我只使用 AutoMapper 将 Model 转换为 ViewModel,但反之亦然,我总是会遇到错误。
总的来说,这可能不是您正在寻找的答案,但这里引用了 AutoMapper 作者的话:
I can’t for the life of me understand why I’d want to dump a DTO
straight back in to a model object.
我认为从 ViewModel 映射到 Entity 的最佳方法是不要为此使用 AutoMapper。 AutoMapper 是用于映射对象的好工具,无需使用除静态之外的任何其他 classes。否则,代码会随着每个添加的服务变得越来越混乱,并且在某些时候您将无法跟踪导致您的字段更新、集合更新等的原因。
经常面临的具体问题:
需要非静态 classes 为您的实体做映射
您可能需要使用 DbContext 来加载和引用实体,您可能还需要其他 classes - 一些将图像上传到文件存储的工具,一些非静态 class hashing/salt 用于密码等...您必须以某种方式将其传递给 automapper,在 AutoMapper 配置文件中注入或创建,这两种做法都很麻烦。
可能需要在同一 ViewModel(Dto) -> 实体对上进行多个映射
对于相同的视图模型实体对,您可能需要不同的映射,具体取决于该实体是否是聚合,或者不是 + 取决于您是否需要引用该实体或引用和更新。总的来说这是可以解决的,但是会在代码中产生很多不需要的噪音,而且更难维护。
非常脏的代码,很难维护。
这是关于基元(字符串、整数等)的自动映射和手动映射引用、转换后的值等。自动映射器的代码看起来真的很奇怪,您必须为属性定义映射(或者不,如果您更喜欢隐式自动映射器映射——与 ORM 配对时也具有破坏性)并使用 AfterMap、BeforeMap、Conventions、ConstructUsing 等来映射其他属性,这会使事情变得更加复杂。
复杂映射
当您必须进行复杂的映射时,例如从 2 个以上的源 class 映射到 1 个目的地 class,您将不得不使事情变得更加复杂,可能调用如下代码:
var target = new Target();
Mapper.Map(source1, target);
Mapper.Map(source2, target);
//etc..
该代码会导致错误,因为您不能将 source1 和 source2 映射在一起,并且映射可能取决于将源 classes 映射到目标的顺序。如果你忘记做 1 个映射或者如果你的映射在 1 个 属性 上有冲突的映射,我不是在说,互相覆盖。
这些问题可能看起来很小,但在我面临使用自动映射库将 ViewModel/Dto 映射到实体的几个项目中,它比从未使用它造成的痛苦要大得多。
这里有一些链接供您参考:
- Jimmy Bogard, author of AutoMapper about 2-way mapping for your entities
- A small article with comments about problems faced when mapping ViewModel->Entity with code examples
- SO 中的类似问题:Best Practices For Mapping DTO to Domain Object?
为此,我们编写了一个简单的映射器。它按名称映射并忽略虚拟属性(因此它适用于 entity framework)。如果您想忽略某些属性,请添加 PropertyCopyIgnoreAttribute。
用法:
PropertyCopy.Copy<ViewModel, Model>(vm, dbmodel);
PropertyCopy.Copy<Model, ViewModel>(dbmodel, vm);
代码:
public static class PropertyCopy
{
public static void Copy<TDest, TSource>(TDest destination, TSource source)
where TSource : class
where TDest : class
{
var destProperties = destination.GetType().GetProperties()
.Where(x => !x.CustomAttributes.Any(y => y.AttributeType.Name == PropertyCopyIgnoreAttribute.Name) && x.CanRead && x.CanWrite && !x.GetGetMethod().IsVirtual);
var sourceProperties = source.GetType().GetProperties()
.Where(x => !x.CustomAttributes.Any(y => y.AttributeType.Name == PropertyCopyIgnoreAttribute.Name) && x.CanRead && x.CanWrite && !x.GetGetMethod().IsVirtual);
var copyProperties = sourceProperties.Join(destProperties, x => x.Name, y => y.Name, (x, y) => x);
foreach (var sourceProperty in copyProperties)
{
var prop = destProperties.FirstOrDefault(x => x.Name == sourceProperty.Name);
prop.SetValue(destination, sourceProperty.GetValue(source));
}
}
}
我想解决您问题中关于 "forgetting some properties and disaster happens" 的具体一点。发生这种情况的原因是您的模型上没有构造函数,您只有可以从任何地方设置(或不设置)的设置器。这不是防御性编码的好方法。
我在所有模型上都使用构造函数,如下所示:
public User(Person person, string email, string username, string password, bool isActive)
{
Person = person;
Email = email;
Username = username;
Password = password;
IsActive = isActive;
}
public Person Person { get; }
public string Email { get; }
public string Username { get; }
public string Password { get; }
public bool IsActive { get; }
如您所见,我没有 setter,因此对象构造必须通过构造函数完成。如果您尝试创建一个没有所有必需参数的对象,编译器会报错。
通过这种方法,很明显,从 ViewModel 到 Model 时,像 AutoMapper 这样的工具没有意义,因为使用这种模式的模型构建不再是简单的映射,而是构建对象。
此外,随着您的模型变得更加复杂,您会发现它们与您的 ViewModel 有很大不同。 ViewModels 往往是扁平的,具有简单的属性,如 string、int、bool 等。另一方面,模型通常包含自定义对象。您会注意到在我的示例中有一个 Person 对象,但是 UserViewModel 会像这样使用原语:
public class UserViewModel
{
public int Id { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Email { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool IsActive { get; set;}
}
因此,从原始对象到复杂对象的映射限制了 AutoMapper 的实用性。
我的方法始终是手动构建 ViewModels 到 Model 方向。另一方面,从模型到 ViewModel,我经常使用混合方法,我会手动将 Person 映射到 FirstName、LastName,但我会为简单的属性使用映射器。
编辑: 根据下面的讨论,AutoMapper 比我想象的更擅长不讨人喜欢。尽管我不会以一种或另一种方式推荐它,但如果您确实使用它,请利用构造和配置验证等功能来帮助防止静默故障。
考虑使用 ViewModel:
public class ViewModel
{
public int id { get; set; }
public int a { get; set; }
public int b { get; set; }
}
和这样的原始模型:
public class Model
{
public int id { get; set; }
public int a { get; set; }
public int b { get; set; }
public int c { get; set; }
public virtual Object d { get; set; }
}
每次获取视图模型时,我都必须将所有 ViewModel
属性一一放入 Model
。类似于:
var model = Db.Models.Find(viewModel.Id);
model.a = viewModel.a;
model.b = viewModel.b;
Db.SaveChanges();
这总是会引起很多问题。我什至有时会忘记提及某些属性,然后灾难就发生了! 我在寻找类似的东西:
Mapper.Map(model, viewModel);
顺便说一句:我只使用 AutoMapper 将 Model 转换为 ViewModel,但反之亦然,我总是会遇到错误。
总的来说,这可能不是您正在寻找的答案,但这里引用了 AutoMapper 作者的话:
I can’t for the life of me understand why I’d want to dump a DTO straight back in to a model object.
我认为从 ViewModel 映射到 Entity 的最佳方法是不要为此使用 AutoMapper。 AutoMapper 是用于映射对象的好工具,无需使用除静态之外的任何其他 classes。否则,代码会随着每个添加的服务变得越来越混乱,并且在某些时候您将无法跟踪导致您的字段更新、集合更新等的原因。
经常面临的具体问题:
需要非静态 classes 为您的实体做映射
您可能需要使用 DbContext 来加载和引用实体,您可能还需要其他 classes - 一些将图像上传到文件存储的工具,一些非静态 class hashing/salt 用于密码等...您必须以某种方式将其传递给 automapper,在 AutoMapper 配置文件中注入或创建,这两种做法都很麻烦。
可能需要在同一 ViewModel(Dto) -> 实体对上进行多个映射
对于相同的视图模型实体对,您可能需要不同的映射,具体取决于该实体是否是聚合,或者不是 + 取决于您是否需要引用该实体或引用和更新。总的来说这是可以解决的,但是会在代码中产生很多不需要的噪音,而且更难维护。
非常脏的代码,很难维护。
这是关于基元(字符串、整数等)的自动映射和手动映射引用、转换后的值等。自动映射器的代码看起来真的很奇怪,您必须为属性定义映射(或者不,如果您更喜欢隐式自动映射器映射——与 ORM 配对时也具有破坏性)并使用 AfterMap、BeforeMap、Conventions、ConstructUsing 等来映射其他属性,这会使事情变得更加复杂。
复杂映射
当您必须进行复杂的映射时,例如从 2 个以上的源 class 映射到 1 个目的地 class,您将不得不使事情变得更加复杂,可能调用如下代码:
var target = new Target(); Mapper.Map(source1, target); Mapper.Map(source2, target); //etc..
该代码会导致错误,因为您不能将 source1 和 source2 映射在一起,并且映射可能取决于将源 classes 映射到目标的顺序。如果你忘记做 1 个映射或者如果你的映射在 1 个 属性 上有冲突的映射,我不是在说,互相覆盖。
这些问题可能看起来很小,但在我面临使用自动映射库将 ViewModel/Dto 映射到实体的几个项目中,它比从未使用它造成的痛苦要大得多。
这里有一些链接供您参考:
- Jimmy Bogard, author of AutoMapper about 2-way mapping for your entities
- A small article with comments about problems faced when mapping ViewModel->Entity with code examples
- SO 中的类似问题:Best Practices For Mapping DTO to Domain Object?
为此,我们编写了一个简单的映射器。它按名称映射并忽略虚拟属性(因此它适用于 entity framework)。如果您想忽略某些属性,请添加 PropertyCopyIgnoreAttribute。
用法:
PropertyCopy.Copy<ViewModel, Model>(vm, dbmodel);
PropertyCopy.Copy<Model, ViewModel>(dbmodel, vm);
代码:
public static class PropertyCopy
{
public static void Copy<TDest, TSource>(TDest destination, TSource source)
where TSource : class
where TDest : class
{
var destProperties = destination.GetType().GetProperties()
.Where(x => !x.CustomAttributes.Any(y => y.AttributeType.Name == PropertyCopyIgnoreAttribute.Name) && x.CanRead && x.CanWrite && !x.GetGetMethod().IsVirtual);
var sourceProperties = source.GetType().GetProperties()
.Where(x => !x.CustomAttributes.Any(y => y.AttributeType.Name == PropertyCopyIgnoreAttribute.Name) && x.CanRead && x.CanWrite && !x.GetGetMethod().IsVirtual);
var copyProperties = sourceProperties.Join(destProperties, x => x.Name, y => y.Name, (x, y) => x);
foreach (var sourceProperty in copyProperties)
{
var prop = destProperties.FirstOrDefault(x => x.Name == sourceProperty.Name);
prop.SetValue(destination, sourceProperty.GetValue(source));
}
}
}
我想解决您问题中关于 "forgetting some properties and disaster happens" 的具体一点。发生这种情况的原因是您的模型上没有构造函数,您只有可以从任何地方设置(或不设置)的设置器。这不是防御性编码的好方法。
我在所有模型上都使用构造函数,如下所示:
public User(Person person, string email, string username, string password, bool isActive)
{
Person = person;
Email = email;
Username = username;
Password = password;
IsActive = isActive;
}
public Person Person { get; }
public string Email { get; }
public string Username { get; }
public string Password { get; }
public bool IsActive { get; }
如您所见,我没有 setter,因此对象构造必须通过构造函数完成。如果您尝试创建一个没有所有必需参数的对象,编译器会报错。
通过这种方法,很明显,从 ViewModel 到 Model 时,像 AutoMapper 这样的工具没有意义,因为使用这种模式的模型构建不再是简单的映射,而是构建对象。
此外,随着您的模型变得更加复杂,您会发现它们与您的 ViewModel 有很大不同。 ViewModels 往往是扁平的,具有简单的属性,如 string、int、bool 等。另一方面,模型通常包含自定义对象。您会注意到在我的示例中有一个 Person 对象,但是 UserViewModel 会像这样使用原语:
public class UserViewModel
{
public int Id { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Email { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool IsActive { get; set;}
}
因此,从原始对象到复杂对象的映射限制了 AutoMapper 的实用性。
我的方法始终是手动构建 ViewModels 到 Model 方向。另一方面,从模型到 ViewModel,我经常使用混合方法,我会手动将 Person 映射到 FirstName、LastName,但我会为简单的属性使用映射器。
编辑: 根据下面的讨论,AutoMapper 比我想象的更擅长不讨人喜欢。尽管我不会以一种或另一种方式推荐它,但如果您确实使用它,请利用构造和配置验证等功能来帮助防止静默故障。