使用AutoMapper

时间:2017-01-02 17:05:26

标签: c# entity-framework asp.net-mvc-5 automapper

我有一个Supplier.cs实体及其ViewModel SupplierVm.cs。我正在尝试更新现有供应商,但我收到黄色死亡屏幕(YSOD)并显示错误消息:

  

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

想想我知道它为什么会发生,但我不确定如何修复它。这是screencast正在发生的事情。我认为我收到错误的原因是因为当AutoMapper 它的东西时,这种关系就会丢失。

CODE

以下是我认为相关的实体

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<Address> Addresses { get; set; } = new List<Address>();
  public virtual ICollection<Contact> Contacts { get; set; } = new List<Contact>();
}

public class Supplier : Business
{
  public virtual ICollection<PurchaseOrder> 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; }
}

这是我的 ViewModel

public class SupplierVm
{
  public SupplierVm()
  {
    Addresses = new List<AddressVm>();
    Contacts = new List<ContactVm>();
    PurchaseOrders = new List<PurchaseOrderVm>();
  }

  public int Id { get; set; }
  [Required]
  [Display(Name = "Company Name")]
  public string Name { get; set; }
  [Display(Name = "Tax Number")]
  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 = "Status")]
  public bool IsDeleted { get; set; }

  public IList<AddressVm> Addresses { get; set; }
  public IList<ContactVm> Contacts { get; set; }
  public IList<PurchaseOrderVm> PurchaseOrders { get; set; }

  public string ButtonText => Id != 0 ? "Update Supplier" : "Add Supplier";
}

我的 AutoMapper映射配置是这样的:

cfg.CreateMap<Supplier, SupplierVm>();
cfg.CreateMap<SupplierVm, Supplier>()
  .ForMember(d => d.Addresses, o => o.UseDestinationValue())
  .ForMember(d => d.Contacts, o => o.UseDestinationValue());
cfg.CreateMap<Contact, ContactVm>();
cfg.CreateMap<ContactVm, Contact>()
  .Ignore(c => c.Business)
  .Ignore(c => c.CreatedOn);
cfg.CreateMap<Address, AddressVm>();
cfg.CreateMap<AddressVm, Address>()
  .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");
}

这里是UpdateSupplier上的SupplierService.cs方法:

public void UpdateSupplier(SupplierVm supplier)
{
  var updatedSupplier = _supplierRepository.Find(supplier.Id);
  Mapper.Map(supplier, updatedSupplier); // I lose navigational property here
  _supplierRepository.Update(updatedSupplier);
  _supplierRepository.Save();
}

我已经完成了大量阅读,根据this blog post,我的工作应该有效!我还读过stuff like this,但我想在放弃使用AutoMapper更新实体之前先与读者核对。

4 个答案:

答案 0 :(得分:7)

原因

线......

Mapper.Map(supplier, updatedSupplier);

......远不止眼睛。

  1. 在映射操作期间,updatedSupplier懒惰地加载其集合(Addresses等),因为AutoMapper(AM)访问它们。您可以通过监视SQL语句来验证这一点。
  2. AM 通过从视图模型映射的集合替换这些已加载的集合。尽管UseDestinationValue设置,但仍会发生这种情况。 (就个人而言,我认为这种设置是不可理解的。)
  3. 这种替代会产生一些意想不到的后果:

    1. 它会将原始项目保留在附加到上下文的集合中,但不再包含在您所使用的方法的范围内。这些项目仍在Local个集合中(例如context.Addresses.Local )但现在被剥夺了他们的父母,因为EF已经执行了关系修正。他们的州是Modified
    2. 它将视图模型中的项目附加到Added状态的上下文中。毕竟,他们对上下文不熟悉。如果此时您在Address中预计会有context.Addresses.Local,那么您会看到2.但您只能看到调试器中添加的项目。
    3. 这些导致异常的无父&#39; Modified`项目。如果它没有,那么下一个意外就是你只需要更新就可以向数据库添加新项目。

      好的,现在是什么?

      那你怎么解决这个问题?

      A。我尝试尽可能地重播您的方案。对我来说,一个可能的修复包括两个修改:

      1. 禁用延迟加载。我不知道你会如何安排你的存储库,但在某个地方应该有一个像

        这样的行
        context.Configuration.LazyLoadingEnabled = false;
        

        执行此操作后,您只会拥有Added项,而不会隐藏Modified项。

      2. Added项标记为Modified。再次,&#34;某处&#34;,放置像

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

        ......等等。

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

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

        ...并将其及其所有子项标记为Modified。这非常昂贵&#34;但就更新而言,请参阅下一点。

        C。我认为更好的解决方法是将AM完全排除在外,并且手动绘制状态。我总是对使用AM进行复杂的映射方案持谨慎态度。首先,因为映射本身的定义距离使用它的代码很远,所以使代码难以检查。但主要是因为它带来了自己的做事方式。它并不总是清楚它是如何与其他微妙的操作相互作用的 - 比如变化跟踪。

        绘制国家是一个艰苦的过程。基础可以是像...这样的陈述。

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

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

        选项C可让您对最初预期更新的内容进行细粒度控制,而不是对选项B的全面更新。如有疑问,this可帮助您确定使用哪个选项。

答案 1 :(得分:1)

我搜索了所有stackoverflow答案和google搜索。最后我只是添加了'db.Configuration.LazyLoadingEnabled = false;'。线,对我来说效果很好。

 var message = JsonConvert.DeserializeObject<UserMessage>(@"{.....}");
        using (var db = new OracleDbContex())
        {
            db.Configuration.LazyLoadingEnabled = false;
            var msguser = Mapper.Map<BAPUSER>(message);
            var dbuser = db.BAPUSER.FirstOrDefault(w => w.BAPUSERID == 1111);
            Mapper.Map(msguser, dbuser);
            //  db.Entry(userx).State = EntityState.Modified;

            db.SaveChanges();
        }

答案 2 :(得分:0)

我多次得到这个问题,通常是这样的:

父引用上的FK Id与该FK实体上的PK不匹配。即如果您有Order表和OrderStatus表。当您将两者加载到实体中时,Order具有OrderStatusId = 1且OrderStatus.Id = 1.如果更改OrderStatusId = 2但不将OrderStatus.Id更新为2,则会出现此错误。要修复它,您需要加载Id of 2并更新引用实体,或者在保存之前将Order上的OrderStatus引用实体设置为null。

答案 3 :(得分:0)

我不确定这是否符合您的要求,但我建议您遵循。

从你的代码中看,你肯定会在映射的某个地方失去关系。

对我而言,作为UpdateSupplier操作的一部分,您实际上并未更新供应商的任何子详细信息。

如果是这种情况,我建议仅将更改的属性从SupplierVm更新为域供应商类。您可以编写一个单独的方法,将SupplierVm中的属性值分配给Supplier对象(这应该只更改非子属性,如Name,Description,Website,Phone等。)

然后执行db Update。这将使您免于被跟踪实体的混乱。

如果要更改供应商的子实体,我建议独立于供应商更新它们,因为从数据库中检索整个对象图需要执行大量查询,更新它也会对数据库执行不必要的更新查询。

独立更新实体将节省大量的数据库操作,并会增加应用程序的性能。

如果必须在一个屏幕中显示有关供应商的所有详细信息,您仍然可以使用整个对象图的检索。对于更新,我不建议更新整个对象图。

我希望这有助于解决您的问题。