将 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。否则,代码会随着每个添加的服务变得越来越混乱,并且在某些时候您将无法跟踪导致您的字段更新、集合更新等的原因。

经常面临的具体问题:

  1. 需要非静态 classes 为您的实体做映射

    您可能需要使用 DbContext 来加载和引用实体,您可能还需要其他 classes - 一些将图像上传到文件存储的工具,一些非静态 class hashing/salt 用于密码等...您必须以某种方式将其传递给 automapper,在 AutoMapper 配置文件中注入或创建,这两种做法都很麻烦。

  2. 可能需要在同一 ViewModel(Dto) -> 实体对上进行多个映射

    对于相同的视图模型实体对,您可能需要不同的映射,具体取决于该实体是否是聚合,或者不是 + 取决于您是否需要引用该实体或引用和更新。总的来说这是可以解决的,但是会在代码中产生很多不需要的噪音,而且更难维护。

  3. 非常脏的代码,很难维护。

    这是关于基元(字符串、整数等)的自动映射和手动映射引用、转换后的值等。自动映射器的代码看起来真的很奇怪,您必须为属性定义映射(或者不,如果您更喜欢隐式自动映射器映射——与 ORM 配对时也具有破坏性)并使用 AfterMap、BeforeMap、Conventions、ConstructUsing 等来映射其他属性,这会使事情变得更加复杂。

  4. 复杂映射

    当您必须进行复杂的映射时,例如从 2 个以上的源 class 映射到 1 个目的地 class,您将不得不使事情变得更加复杂,可能调用如下代码:

    var target = new Target();
    Mapper.Map(source1, target);
    Mapper.Map(source2, target);
    //etc..
    

    该代码会导致错误,因为您不能将 source1 和 source2 映射在一起,并且映射可能取决于将源 classes 映射到目标的顺序。如果你忘记做 1 个映射或者如果你的映射在 1 个 属性 上有冲突的映射,我不是在说,互相覆盖。

这些问题可能看起来很小,但在我面临使用自动映射库将 ViewModel/Dto 映射到实体的几个项目中,它比从未使用它造成的痛苦要大得多。

这里有一些链接供您参考:

为此,我们编写了一个简单的映射器。它按名称映射并忽略虚拟属性(因此它适用于 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 比我想象的更擅长不讨人喜欢。尽管我不会以一种或另一种方式推荐它,但如果您确实使用它,请利用构造和配置验证等功能来帮助防止静默故障。