在MVC中使用AutoMapper从ViewModel更新实体

20 浏览
0 Comments

在MVC中使用AutoMapper从ViewModel更新实体

我有一个名为Supplier.cs的实体和它的视图模型SupplierVm.cs。我正在尝试更新一个现有的供应商,但是我遇到了黄屏幕(YSOD)并显示以下错误消息:

操作失败:由于一个或多个外键属性是非空的,所以无法更改关系。当对关系进行更改时,相关的外键属性将被设置为null值。如果外键不支持null值,则必须定义一个新的关系,将外键属性分配给另一个非null值,或者删除无关的对象。

认为我知道为什么会发生这种情况,但是我不确定该如何解决。这是一个关于发生情况的录屏。我认为我得到这个错误的原因是当AutoMapper执行时,该关系丢失了。

代码

以下是我认为相关的实体

public abstract class Business : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string TaxNumber { get; set; }
    public string Description { get; set; }
    public string Phone { get; set; }
    public string Website { get; set; }
    public string Email { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime CreatedOn { get; set; }
    public DateTime? ModifiedOn { get; set; }
    public virtual ICollection
Addresses { get; set; } = new List
(); public virtual ICollection Contacts { get; set; } = new List(); } public class Supplier : Business { public virtual ICollection PurchaseOrders { get; set; } } public class Address : IEntity { public Address() { CreatedOn = DateTime.UtcNow; } public int Id { get; set; } public string AddressLine1 { get; set; } public string AddressLine2 { get; set; } public string Area { get; set; } public string City { get; set; } public string County { get; set; } public string PostCode { get; set; } public string Country { get; set; } public bool IsDeleted { get; set; } public DateTime CreatedOn { get; set; } public DateTime? ModifiedOn { get; set; } public int BusinessId { get; set; } public virtual Business Business { get; set; } } public class Contact : IEntity { public Contact() { CreatedOn = DateTime.UtcNow; } public int Id { get; set; } public string Title { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Phone { get; set; } public string Email { get; set; } public string Department { get; set; } public bool IsDeleted { get; set; } public DateTime CreatedOn { get; set; } public DateTime? ModifiedOn { get; set; } public int BusinessId { get; set; } public virtual Business Business { get; set; } }

这是我的视图模型

public class SupplierVm
{
    public SupplierVm()
    {
        Addresses = new List();
        Contacts = new List();
        PurchaseOrders = new List();
    }
    public int Id { get; set; }
    [Required]
    [Display(Name = "公司名称")]
    public string Name { get; set; }
    [Display(Name = "税号")]
    public string TaxNumber { get; set; }
    public string Description { get; set; }
    public string Phone { get; set; }
    public string Website { get; set; }
    public string Email { get; set; }
    [Display(Name = "状态")]
    public bool IsDeleted { get; set; }
    public IList Addresses { get; set; }
    public IList Contacts { get; set; }
    public IList PurchaseOrders { get; set; }
    public string ButtonText => Id != 0 ? "更新供应商" : "添加供应商";
}

我的AutoMapper映射配置如下:

cfg.CreateMap();
cfg.CreateMap()
    .ForMember(d => d.Addresses, o => o.UseDestinationValue())
    .ForMember(d => d.Contacts, o => o.UseDestinationValue());
cfg.CreateMap();
cfg.CreateMap()
    .Ignore(c => c.Business)
    .Ignore(c => c.CreatedOn);
cfg.CreateMap();
cfg.CreateMap()
    .Ignore(a => a.Business)
    .Ignore(a => a.CreatedOn);

最后,这是我的SupplierController的编辑方法:

[HttpPost]
public ActionResult Edit(SupplierVm supplier)
{
    if (!ModelState.IsValid) 
        return View(supplier);
    _supplierService.UpdateSupplier(supplier);
    return RedirectToAction("Index");
}

这是SupplierService.cs上的UpdateSupplier方法:

public void UpdateSupplier(SupplierVm supplier)
{
    var updatedSupplier = _supplierRepository.Find(supplier.Id);
    Mapper.Map(supplier, updatedSupplier); // 这里丢失了导航属性
    _supplierRepository.Update(updatedSupplier);
    _supplierRepository.Save();
}

我已经阅读了很多文章,根据这篇博客文章,我所做的应该可以工作!我还阅读了这样的文章,但在放弃AutoMapper更新实体之前,我想请读者们确认一下。

0
0 Comments

在使用MVC架构中,通过AutoMapper从ViewModel更新实体时出现了问题。问题的原因是数据库上下文的LazyLoadingEnabled属性被设置为true,导致在映射过程中出现了错误。为了解决这个问题,需要将LazyLoadingEnabled属性设置为false。

解决方法如下:

var message = JsonConvert.DeserializeObject(@"{.....}");
using (var db = new OracleDbContex())
{
    db.Configuration.LazyLoadingEnabled = false;
    var msguser = Mapper.Map(message);
    var dbuser = db.BAPUSER.FirstOrDefault(w => w.BAPUSERID == 1111);
    Mapper.Map(msguser, dbuser);
    db.SaveChanges();
}

以上代码片段中,首先通过JsonConvert反序列化UserMessage对象。然后创建一个OracleDbContex实例,并将其LazyLoadingEnabled属性设置为false。接下来,使用AutoMapper将ViewModel对象映射到实体对象msguser中。然后从数据库中获取一个具有特定BAPUSERID的BAPUSER实体对象dbuser。最后,使用AutoMapper将msguser的属性值映射到dbuser中,并通过调用SaveChanges方法将更改保存到数据库。

通过以上解决方法,我们可以成功地使用AutoMapper从ViewModel更新实体,并解决了LazyLoadingEnabled属性设置错误的问题。

0
0 Comments

问题的原因

下面这行代码...

Mapper.Map(supplier, updatedSupplier);

...实际上做了更多的事情。

  1. 在映射操作期间,updatedSupplier会因为AutoMapper (AM)的访问而懒加载它的集合(例如Addresses)。你可以通过监测SQL语句来验证这一点。
  2. AM会用从视图模型映射的集合替换这些已加载的集合。这发生在即使设置了UseDestinationValue的情况下。(就个人而言,我认为这个设置是难以理解的。)

这个替换有一些意想不到的后果:

  1. 它使得原始项目仍然附加在上下文的集合中,但不再在你所在的方法范围内。这些项目仍然存在于Local集合(例如context.Addresses.Local),但是由于EF执行了关系修复,它们已经失去了它们的父对象。它们的状态是Modified
  2. 它以Added状态将视图模型中的项目附加到上下文中。毕竟,它们对上下文来说是新的。如果此时你期望context.Addresses.Local中有1个Address,你会看到有2个。但是在调试器中,你只能看到被添加的项目。

正是这些没有父对象的“Modified”项目导致了异常的出现。如果没有这个异常,下一个惊喜将是在你只期望进行更新时却向数据库添加了新的项目。

那么现在怎么办?

你应该如何解决这个问题呢?

A. 我试图尽可能地重现你的场景。对我来说,可能的解决办法包括两个修改:

  1. 禁用懒加载。我不知道你如何在你的仓储中安排这一点,但是在某个地方应该有一行代码,像这样:

    context.Configuration.LazyLoadingEnabled = false;
    

    这样做,你只会有Added状态的项目,而不会有隐藏的Modified状态的项目。

  2. Added状态的项目标记为Modified。同样,在“某个地方”放置类似下面的代码:

    foreach (var addr in updatedSupplier.Addresses)
    {
        context.Entry(addr).State = System.Data.Entity.EntityState.Modified;
    }
    

    ...等等。

B. 另一种选择是将视图模型映射到新的实体对象...

  var updatedSupplier = Mapper.Map<Supplier>(supplier);

...并将其及其所有子对象标记为Modified。不过,这在更新方面的开销相对较大,可以参考下一个点。

C. 在我看来,一个更好的解决办法是完全不使用AM,而是手动绘制状态。我总是对在复杂的映射场景中使用AM持怀疑态度。首先,因为映射本身是在离它使用的代码很远的地方定义的,这使得代码难以检查。但主要是因为它带来了它自己的方式来做事情。它与其他敏感的操作(如更改跟踪)的交互方式并不总是清楚的。

手动绘制状态是一个费时的过程。基础可以是类似于下面的语句...

context.Entry(updatedSupplier).CurrentValues.SetValues(supplier);

...如果它们的名称匹配,它将supplier的标量属性复制到updatedSupplier。或者你可以使用AM(毕竟)将单个视图模型映射到它们的实体对应项上,但忽略导航属性。

选项C使你能够对要更新的内容进行精细控制,而不是像选项B那样进行全面的更新。当有疑问时,这篇文章可能会帮助你决定使用哪个选项。

谢谢Gert,这是一个很棒的答案!

嗨,我在问题中看到,用户将视图模型传递给了服务层。这样做好吗?

大多数情况下,视图模型只是一个DTO。对我来说,将DTO传递给服务层是可以的 - 实际上这就是它们的用途。如果视图模型真的是一个视图模型(包含INotifyPropertyChanged和/或指导视图的整个过程),那么不,我不会这样做,因为它可能会将UI库的依赖项泄露到服务层中。

0
0 Comments

问题的出现原因:父引用的外键Id与该外键实体的主键不匹配,即在加载实体时,Order的OrderStatusId = 1,而OrderStatus的Id = 1。如果将OrderStatusId更改为2,但没有更新OrderStatus的Id为2,那么就会出现此错误。

解决方法:要解决这个问题,可以通过以下两种方式之一来解决:

1. 加载Id为2的实体并更新引用实体。

2. 在保存之前将Order上的OrderStatus引用实体设置为null。

以下是使用AutoMapper在MVC中从ViewModel更新实体的示例代码:

// 定义ViewModel
public class OrderViewModel
{
    public int Id { get; set; }
    public int OrderStatusId { get; set; }
    // 其他属性...
}
// 定义实体
public class Order
{
    public int Id { get; set; }
    public int OrderStatusId { get; set; }
    public OrderStatus OrderStatus { get; set; }
    // 其他属性...
}
// 定义映射关系
Mapper.Initialize(cfg =>
{
    cfg.CreateMap()
        .ForMember(dest => dest.OrderStatus, opt => opt.Ignore());
});
// 在Controller中更新实体
public ActionResult UpdateOrder(OrderViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        // 从数据库加载原始实体
        var order = dbContext.Orders.SingleOrDefault(o => o.Id == viewModel.Id);
        // 使用AutoMapper将ViewModel映射到实体
        Mapper.Map(viewModel, order);
        // 设置OrderStatus为null
        order.OrderStatus = null;
        // 保存更改
        dbContext.SaveChanges();
        
        return RedirectToAction("Index");
    }
    return View(viewModel);
}

以上示例代码中,我们首先定义了一个OrderViewModel和一个Order实体。然后使用AutoMapper配置了它们之间的映射关系。在Controller的UpdateOrder方法中,我们首先从数据库加载原始实体,然后使用AutoMapper将ViewModel映射到实体。最后,我们将OrderStatus引用实体设置为null,并保存更改。

通过这种方式,我们可以使用AutoMapper轻松地从ViewModel更新实体,并且可以解决上述问题引起的错误。

0