将ViewModel最好的投射回Model的方式

16

考虑使用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中。像这样:

每次获取视图模型后,我必须逐个将ViewModel的属性放进Model中。

var model = Db.Models.Find(viewModel.Id);
model.a = viewModel.a;
model.b = viewModel.b;
Db.SaveChanges();

这总是会引起很多问题。有时我甚至会忘记提及一些属性,导致灾难的发生!我正在寻找类似于以下内容的东西:

Mapper.Map(model, viewModel);

顺便说一下:我只使用AutoMapper将模型转换为视图模型,但是反过来总是会遇到错误。


2
如果您在使用Automapper时遇到了一些问题,请发布您用于将模型映射到视图模型的代码。 - Eugene Podskal
你的意思是我可以在相反的模式下使用 AutoMapper 吗? - ghazyy
我的Automapper经验有限,但我相当确定可以配置应该映射哪些属性以及以何种方式进行映射,因此您可以忽略多余的属性或计算缺失的属性。 - Eugene Podskal
@ghazyy:你可以使用它,但你需要创建从ViewModel到Model的映射,例如 Mapper.CreateMap<ViewModel, Model>() - Arturo Menchaca
3
AutoMapper应该仅用于将模型转换为视图模型或DTO,而不是反过来,因为它并不适用于这种类型的映射。这也是AutoMapper作者的官方立场。 - Tseng
@Tseng 谢谢,我没有考虑到这种映射可能会引起的问题。虽然可以使用automapper完成,但我同意这可能不应该这样做。 - Eugene Podskal
4个回答

14

总的来说,可能不是您正在寻找的答案,但以下是AutoMapper作者的一句话引用:

我无法想象为什么要将DTO直接倒回模型对象。

我认为从ViewModel映射到实体的最佳方式不是使用AutoMapper。AutoMapper是一个很好的工具,可用于映射对象而无需使用任何其他类,除了静态类。否则,随着每个添加的服务,代码会变得越来越混乱,并且在某个时刻,您将无法跟踪是什么导致了字段更新、集合更新等问题。

通常遇到以下特定问题:

  1. 需要非静态类来对实体进行映射

    您可能需要使用DbContext来加载和引用实体,您还可能需要其他类 - 一些工具来上传图像到文件存储,一些非静态类来为密码进行哈希/盐等... 您必须以某种方式将其传递给自动映射程序,注入或在AutoMapper配置文件中创建,并且这两种做法都很麻烦。

  2. 可能需要对同一ViewModel(Dto) -> Entity对进行多次映射

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

  3. 非常难以维护的肮脏代码。

    这个问题涉及基本类型(字符串、整数等)的自动映射和手动映射引用、转换值等。对于AutoMapper来说,代码看起来非常奇怪,您必须为属性定义映射(或者如果您喜欢隐式的自动映射,就不需要定义映射 - 这也与ORM配对时具有破坏性),并使用AfterMap、BeforeMap、Conventions、ConstructUsing等方式进行其他属性的映射,这使得事情变得更加复杂。

  4. 复杂的映射

    当您必须执行复杂的映射时,例如从2个或更多源类映射到1个目标类时,您将不得不更加复杂地调用代码,可能会调用以下代码:

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

    那段代码会导致错误,因为您无法将source1和source2映射到一起,而且映射可能取决于将源类映射到目标的顺序。我并不是在谈论如果您忘记进行1次映射或者如果您的映射在1个属性上具有冲突映射,互相覆盖。

    这些问题可能看起来很小,但在几个项目中,我面对使用自动映射库将ViewModel/Dto映射到Entity时,它比从未使用更加痛苦。

    以下是一些链接:


5
为了实现这一目的,我们编写了一个简单的映射程序。它通过名称进行映射,并忽略虚拟属性(因此它可以与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));
        }
    }
}

这段代码对我有效。 "PropertyCopyIgnoreAttribute" 抛出了无效引用异常,我将其删除以使其正常工作。无论如何,感谢分享。 :) - Andy Schmitt
@AndySchmitt 我相信你需要自己创建这个属性。 - Ryan Buddicom
不,我只是认为他忘记包含它了 :-) - Tor Thorbergsen
有没有一种方法可以将这个函数应用到一个数组上? - Koronos
1
谢谢Vanice,代码写得很好。还可以在https://github.com/m-ishizaki/PropertyCopy上找到。如果有更新,请告诉我。谢谢。 - Dinand

0

使用Newtonsoft.Json将视图模型序列化,然后反序列化为模型。

首先,我们需要对视图模型进行序列化:

var viewmodel = JsonConvert.SerializeObject(companyInfoViewModel);

然后将其反序列化为模型:

var model = JsonConvert.DeserializeObject<CompanyInfo>(viewmodel);

因此,所有数据都可以轻松地从ViewModel传递到Model。
一行代码:
var company = JsonConvert.DeserializeObject<CompanyInfo>(JsonConvert.SerializeObject(companyInfoViewModel));

0

我想解决你问题中的一个具体点,即“忘记某些属性并导致灾难发生”的问题。这种情况发生的原因是你的模型上没有构造函数,只有可以从任何地方设置(或不设置)的 setter。这对于防御性编码来说并不是一个好的方法。

我在所有我的模型中都使用如下的构造函数:

    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; }

正如您所看到的,我没有设置器,因此必须通过构造函数完成对象构建。如果您尝试创建一个没有所有必需参数的对象,编译器将会报错。

采用这种方法,就清楚了像 AutoMapper 这样的工具在从 ViewModel 到 Model 时是没有意义的,因为使用这种模式构建 Model 不再是简单的映射,而是关于构建您的对象。

另外,随着您的 Models 变得更加复杂,您会发现它们与您的 ViewModels 有很大的不同。ViewModels 倾向于是平面的,具有简单的属性,如字符串、整数、布尔等。而 Models 则经常包括自定义对象。您会注意到我的示例中有一个 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方向。在另一个方向上,即从Models到ViewModels,我经常使用混合方法,我会手动映射Person到FirstName、LastName,但对于简单属性,我会使用映射器。

编辑:根据下面的讨论,AutoMapper比我想象的更好。虽然我不会推荐它,但如果您使用它,请利用Construction和Configuration Validation等功能来帮助防止静默失败。


@LucianBargaoanu 谢谢,这比我从AutoMapper期望的还要多(我实际上使用Value Injecter),但它仍然没有解决从基元类型到自定义对象的映射问题。我知道你可以使用AutoMapper进行自定义类型转换,但此时你基本上已经创建了一个构建器/工厂,那么为什么不一开始就使用构建器/工厂方法而不是AutoMapper呢? - Louise Eggleton
http://docs.automapper.org/en/latest/Reverse-Mapping-and-Unflattening.html - Lucian Bargaoanu
@LucianBargaoanu,这取决于我的DTOs / ViewModels是否遵循特定的命名约定。我可以接受这一点,但如果拼写错误或属性缺失,编译器会抱怨吗?诚然,我的经验是基于ValueInjector的,但我发现在这些情况下映射器往往会悄悄失败。 - Louise Eggleton
@LucianBargaoanu 好的,既然您已经回答了我的疑虑,我会再次仔细看一下AutoMapper,但是还有一个问题:Jimmy Bogard是否仍然建议不要从DTO映射到Model?他曾经说过这不是预期的使用方式。那是旧信息吗? - Louise Eggleton
这不是关于@jbogard在做什么,而是关于你自己想要做什么 :) 你只需要理解这个工具以及它能为你做什么。 - Lucian Bargaoanu
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接