【问题标题】:Entity Framework not correctly modifying or deleting child items实体框架未正确修改或删除子项
【发布时间】:2019-02-27 12:56:48
【问题描述】:

我正在尝试保存包含地址和网站的 Firm 对象。我已经开发了在 Angular 7 中使用反应式表单在 UI 中添加和删除地址控件的能力。在保存 Firm 对象的同时,它为地址和网站创建了额外的条目,而不是将其视为现有记录。

因此,如果我从 UI 中删除网站和地址,我可以看到我正在将正确数量的数组元素传递给后端 api。所以我放心,问题出在实体框架上。

所以我想要实现的是,如果用户从客户端删除地址或网站,它应该在调用实体框架中的更新方法时更新相同。我正在使用实体框架 6

UI - 我可以在其中添加多个地址

这是我的模型类

新FirmViewModel

 public class NewFirmViewModel
    {
        public int FirmId { get; set; }

        public string FirmName { get; set;}

        public Nullable<DateTime> DateFounded { get; set; }

        public ICollection<AddressViewModel> Addresses { get; set; }

        public ICollection<WebsiteViewModel> Websites { get; set; }

        public bool hasIntralinks { get; set; }
    }

地址视图模型

public class AddressViewModel
{
    public int AddressId { get; set; }
    public string Line1 { get; set; }
    public string Line2 { get; set; }
    public string Line3 { get; set; }
    public string Phone { get; set; }
    public bool IsHeadOffice { get; set; }
    public int FirmId { get; set; }
}

网站视图模型

public class WebsiteViewModel
{
    private int FirmWebsiteId { get; set; }
    private string WebsiteUrl { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public int FirmId { get; set; }
}

实体

public class FIRM: Entity,IHasAUMs<FIRM_AUM> 
    {
        public FIRM()
        {
            //this.FIRM_PERSON = new HashSet<FIRM_PERSON>();
            this.MANAGERSTRATEGies = new HashSet<MANAGERSTRATEGY>();
            this.FIRM_ACTIVITY = new HashSet<FIRM_ACTIVITY>();
            this.FIRM_AUMs = new HashSet<FIRM_AUM>();
            this.FIRM_REGISTRATION = new HashSet<FIRM_REGISTRATION>();
            //this.ACTIVITies = new HashSet<ACTIVITY>();
            Addresses = new HashSet<ADDRESS>();
            //People = new HashSet<PERSON>();   
           // Websites = new HashSet<FIRM_WEBSITE>();
        }

        //public decimal ID { get; set; }
        //
        //
        //
        //
        public string NAME { get; set; }
        public string SHORT_NAME { get; set; }
        public string ALTERNATE_NAME { get; set; }
        public string WEBSITE { get; set; }
        public string WEBSITE_USERNAME { get; set; }
        public string WEBSITE_PASSWORD { get; set; }
        public bool? INTRALINKS_FIRM { get; set; }        
        public string NOTES_TEXT { get; set; }
        public string NOTES_HTML { get; set; }
        public string HISTORY_TEXT { get; set; }
        public string HISTORY_HTML { get; set; }

        public string HISTORY_SUM_TEXT { get; set; }
        public string HISTORY_SUM_HTML { get; set; }

        public Nullable<decimal> OLD_ORG_REF { get; set; }
        public Nullable<decimal> SOURCE_ID { get; set; }

        [DisplayFormat(DataFormatString = PermalConstants.DateFormat)]
        public Nullable<DateTime> DATE_FOUNDED { get; set; }

        public virtual  ICollection<ADDRESS> Addresses { get; set; }

      //  public ICollection<FIRM_WEBSITE> Websites { get; set; }
        // public ICollection<PERSON> People { get; set; }

        //public SOURCE SOURCE { get; set; }
        // public ICollection<FIRM_PERSON> FIRM_PERSON { get; set; }
        public ICollection<MANAGERSTRATEGY> MANAGERSTRATEGies { get; set; }
        public ICollection<FIRM_ACTIVITY> FIRM_ACTIVITY { get; set; }
        public ICollection<FIRM_REGISTRATION> FIRM_REGISTRATION { get; set; }
        //public ICollection<ACTIVITY> ACTIVITies { get; set; }
        public ICollection<FIRM_WEBSITE> Websites { get; set; }

        public Nullable<int> KEY_CONTACT_ID { get; set; }
        [NotMapped]
        public ICollection<FIRM_AUM> AUMs
        {
            get
            {
                return this.FIRM_AUMs;
            }
        }
        public ICollection<FIRM_AUM> FIRM_AUMs { get; set; }
    }


    ADDRESS

      public class ADDRESS : Entity
    {
        public ADDRESS()
        {
            // DATE_CREATED = DateTime.Now;
        }


        public string LINE1 { get; set; }
        public string LINE2 { get; set; }
        public string LINE3 { get; set; }
        public int CITY_ID { get; set; }
        public string POSTAL_CODE { get; set; }
        public string SWITCHBOARD_INT { get; set; }
        public string NOTES { get; set; }
        public int? OLD_ADDRESS_REF { get; set; }
        public int? SOURCE_ID { get; set; }

        public int FIRM_ID { get; set; }
        [ForeignKey("FIRM_ID")]
        public FIRM FIRM { get; set; }

        [ForeignKey("CITY_ID")]
        public CITY City { get; set; }

        public ICollection<PERSON> People { get; set; }

        // public SOURCE SOURCE { get; set; }

        public bool IS_HEAD_OFFICE { get; set; }
        [NotMapped]
        public string AddressBlurb
        {
            get
            {
                return string.Join(",", new[] { LINE1, LINE2, City != null ? City.NAME : "", City != null && City.Country != null ? City.Country.NAME : "" }.Where(x => !string.IsNullOrEmpty(x)));
            }
        }
    }


    FIRM_WEBSITE

      public class FIRM_WEBSITE : Entity
    {
        public FIRM_WEBSITE()
        {

        }
        private string _WEBSITE_URL;

        public string WEBSITE_URL
        {
            get
            {
                if (string.IsNullOrEmpty(_WEBSITE_URL))
                    return _WEBSITE_URL;
                try
                {

                    var ubuilder = new System.UriBuilder(_WEBSITE_URL ?? "");

                    return ubuilder.Uri.AbsoluteUri;
                }
                catch (UriFormatException ex)
                {
                    return _WEBSITE_URL;
                }

            }
            set { _WEBSITE_URL = value; }
        }

        public string USERNAME { get; set; }
        public string PASSWORD { get; set; }


        public int FIRM_ID { get; set; }
        [ForeignKey("FIRM_ID")]
        public FIRM FIRM { get; set; }
    }

API 控制器

  [HttpPut]
    [SkipTokenAuthorization]
    [Route("api/firm/update")]
    public IHttpActionResult Update(NewFirmViewModel model)
    {


          var firmService = GetService<FIRM>();

        if (model == null) return StatusCode(HttpStatusCode.NotFound);

        var firm = firmService.GetWithIncludes(model.FirmId);

        if (firm != null)
        {
            firm.NAME = model.FirmName;
            firm.DATE_FOUNDED = model.DateFounded;
            firm.Addresses = model.Addresses.Select(x => new ADDRESS() {ID = x.AddressId, LINE1 = x.Line1, LINE2 = x.Line2, LINE3 = x.Line3, FIRM_ID = x.FirmId}).ToList();
            firm.Websites = model.Websites.Select(x => new FIRM_WEBSITE() {ID = x.FirmWebsiteId, WEBSITE_URL = x.WebsiteUrl, USERNAME = x.Username, PASSWORD = x.Password, FIRM_ID = x.FirmId}).ToList();


            var addressIds = model.Addresses.Select(x => x.AddressId).ToList();
            var addresses = firm.Addresses.Where(x => addressIds.Contains(x.ID)).ToList(); // All of the addresses we want to associate to this firm.
            // Identify addresses to remove from this firm.
            var addressesToRemove = firm.Addresses.Where(x => !addressIds.Contains(x.ID)).ToList();
            foreach (var address in addressesToRemove)
                firm.Addresses.Remove(address);

            // Identify addresses to associate to this firm.
            var existingAddressIds = firm.Addresses.Select(x => x.ID).ToList();
            var addressesToAdd = addresses.Where(x => !existingAddressIds.Contains(x.ID)).ToList();
            foreach (var address in addressesToAdd)
                firm.Addresses.Add(address);

            firmService.Update(firm);
        }
        else
        {

        }

        return Ok(firm);
}

数据库上下文

     public class Repo<T> : IRepo<T> where T : Entity, new()
        {
            public readonly Db dbContext;

            private ILogger _logger;
            private IQueryable<T> lastQuery { get; set; }
            private bool? _enablelazyloading;
            private IEntityWatcher<T> _watcherNotification;
            private bool _EnableChangeNotification;
            public string ID { get; set; }
            private string _clientId;

            #region Constructors
            public Repo(IDbContextFactory f)
            {
                if (typeof(T).GetCustomAttribute<SeparateDbContext>() != null)
                    dbContext = f.GetContext<T>();
                else
                    dbContext = f.GetContext();
                _logger = IoC.Resolve<ILogger>();
                try
                {
                    _watcherNotification = IoC.Resolve<IEntityWatcher<T>>();
                }
                catch (Exception ex)
                {
                    _logger.Error("Change Notification failed to resolve in Repo.  The Repo will continue to function without notification.", ex);

                }
            }
            public Repo() : this(new DbContextFactory()) { }
            #endregion

            public bool? EnableLazyLoading
            {
                get { return dbContext.EnableLazyLoading; }
                set { dbContext.EnableLazyLoading = value; }
            }

            public void SetClientId(string clientId)
            {
                var oc = dbContext.Database.Connection as OracleConnection;

                if (oc != null)
                {
                    oc.Open();
                    oc.ClientId = clientId;
                    oc.Close();
                }
            }


            public T Update(T obj)
            {
                _logger.Info("Repo.Update {0}", obj);
                var entity = Get(obj.ID);
                var oldEntity = new T();
                var entry = dbContext.Entry(entity);
                oldEntity.InjectFrom(entry.OriginalValues.ToObject());
                if (dbContext.Entry(obj).State == System.Data.Entity.EntityState.Detached)
                {
                    entry.CurrentValues.SetValues(obj);
                }
                    LogAllModifiedEntities(dbContext);
                dbContext.SaveChanges();
                if (_watcherNotification != null)
                    _watcherNotification.EntityChanged(ChangeNotificationType.Modified, entity, oldEntity);
                return Get(obj.ID);
            }


 public void EntityChanged(ChangeNotificationType changeNotificationType, T newEntity, T oldEntity) {
            if(_entityAuditEnabled) {
                var filter = IoC.Resolve<IEntityWatchFilter<T>>();
                filter.Filter(changeNotificationType, newEntity, oldEntity);
            }
        }
    }

   public bool Filter(ChangeNotificationType changeNotificationType, T newEntity, T oldEntity) {
            try {
                ///only 
                if(_WatchList.Contains(typeof(T).Name) || !_WatchList.Any()) {
                    var newLegacyStratImpl = newEntity as ILegacyStrategy;
                    var oldLegacyStratImpl = oldEntity as ILegacyStrategy;
                    var blankStrategies = IoC.Resolve<ICrudService<LEGACY_STRATEGY>>().Where(x => x.NAME.Trim() == "").Select(x => x.ID).AsEnumerable();
                    if(changeNotificationType == ChangeNotificationType.Added && newLegacyStratImpl != null && newLegacyStratImpl.LEGACY_STRATEGY_ID.HasValue && !blankStrategies.Contains(newLegacyStratImpl.LEGACY_STRATEGY_ID.Value)) {

                        _action.Added(newEntity);
                        return true;
                    } else if(changeNotificationType == ChangeNotificationType.Deleted && newLegacyStratImpl != null) {
                        _action.Deleted(newEntity);
                        return true;
                    } else if(changeNotificationType == ChangeNotificationType.Modified && newLegacyStratImpl != null && oldLegacyStratImpl != null) {
                        ///need to go the extra distance and make sure the legacy strategy was changed and not some other property.
                        var hasChanged = newLegacyStratImpl.LEGACY_STRATEGY_ID != oldLegacyStratImpl.LEGACY_STRATEGY_ID;
                        if(hasChanged) {
                            _action.Modified(newEntity, oldEntity);
                            return true;
                        } else {
                            return false;
                        }
                    }
                }
                return false;///all else fails...
            } catch(Exception ex) {
                _logger.Error(ex);
                return false;
            }
        }

【问题讨论】:

  • 如果不是因为赏金,这个问题将因缺少minimal reproducible example 而关闭。您显示调用隐藏代码的代码。如果我们必须猜测实际发生了什么,我们无法回答这个问题。
  • 我已经用更多信息更新了帖子。你能告诉我你在找什么吗
  • 您只是在添加更多噪音,而不是显示隐藏代码(如InjectFrom 等)和其他显示重要信息的代码,例如所涉及的上下文的生命周期。它看起来像是this 常见陷阱的另一个化身,但谁知道呢。另外,为什么实际上更新公司时方法名称为“CreateFirm”?

标签: .net entity-framework


【解决方案1】:
        firm.Addresses = model.Firm.Addresses;
        firm.Websites=  model.Firm.Websites;

这...您实际上是在告诉此上下文实例将您的“模型”提供的地址和网站视为实体。上下文不知道这些实体,因此它对待它们的方式与您执行以下操作时没有区别:

foreach(var address in model.Firm.Addresses)
{
   firm.Addresses.Add(new Address { AddressId = address.AddressId, City = address.City, /* ... */ });
}

就上下文而言,这些对象是“新的”。

作为一般规则,请避免将实体传递给客户端,并且永远不要信任/接受来自客户端的实体。如果公司正在关联现有地址,那么地址ID 列表对于公司更新模型来说已经绰绰有余了。 (假设如果用户创建或更新了地址的内容,那将被单独保存。)如果用户可以通过公司更新传递一个新地址,那么您需要一个合适的地址视图模型并检测新的或更新的条目.

上述问题的一个简单明显的解决方案是使用Attach() 将实体与上下文相关联,但我从不推荐这样做,因为它相信实体没有以非预期的方式被修改。 (此外还会引发其他边缘情况,例如上下文可能已经有一个与该 ID 关联的实体)

在更新子引用(例如地址)时,我们不更新地址内容作为公司更新的一部分:

var addressIds = model.Firm.Addresses.Select(x => x.AddressId).ToList();
var addresses = dbContext.Addresses.Where(x => addressIds.Contains(x => x.AddressId)).ToList(); // All of the addresses we want to associate to this firm.

// Identify addresses to remove from this firm.
var addressesToRemove = firm.Addresses.Where(x => !addressIds.Contains(x.AddressId)).ToList();
// Identify addresses to associate to this firm.
var addressesToAdd = addresses
        .Except(firm.Addresses, new LamdaComparer((a1,a2) => a1.AddressId == a2.AddressId));

foreach(var address in addressesToRemove)
    firm.Addresses.Remove(address);

if(addressesToAdd.Any())
    firm.Addresses.AddRange(addressesToAdd);

如果您可能要更新地址详细信息,则需要做更多工作,但问题的关键在于您不能信任传递给客户端并通过模型接收回来的实体。视图模型应该是 POCO 类,而不是实体。为避免此类问题,应验证从视图传回的任何内容,并应从处理请求的上下文中加载适用的实体。

LamdaComparer 可以在here找到。

编辑:如果实施比较器有问题.. 如果没有 LamdaComparer,您可以执行以下操作:

// Identify addresses to associate to this firm.
var existingAddressIds = firm.Addresses.Select(x => x.AddressId).ToList();
var addressesToAdd = addresses.Where(x => !existingAddressIds.Contains(x.AddressId)).ToList();

编辑 2:存储库类有助于启用单元测试。通用存储库类是邪恶的。如果您不使用单元测试,那么我将避免增加尝试将 EF 功能抽象到存储库中的复杂性,尤其是通用存储库。在您的情况下,为避免可能破坏代码的其他区域,我将向您的服务添加一个名为 SaveChanges 的方法,该方法仅调用上下文的 SaveChanges,然后调用 service.Update(enty) 方法而不是调用 service.SaveChanges ()。

试图在存储库中抽象出 EF 的功能会适得其反。例如,要尝试检查添加和删除的相关实体,需要了解相关实体,这不是通用实现会知道的知识。接受 EF 是应用程序的核心部分,就像 .Net Framework 是应用程序的核心部分一样。这使您可以利用 EF 的全部功能,而无需编写代码来尝试隐藏排序表达式、分页、reduce 和映射操作等内容,或者干脆不利用这些功能,因为它们可能会“泄露”EF 主义。

这并不是说您的项目的 Repo/Context Wrapping 实现是坏的或错误的,但它很复杂并且导致了难以解释的行为。从您提供的代码中我可以看出,它旨在将实体视为 2 个独立的角色、模型和模型的分离表示。 IMO 这违反了单一职责,实体应该代表模型,仅此而已。 ViewModel 或 DTO 是将相关信息传输到视图或外部使用者,而不是实体。是的,EF 提供了分离/重新附加以及在实体之间复制值的功能,但是我反对将其与已重新用作视图模型的实体一起使用的一个关键点是,不能从客户端返回的视图模型/DTO信任。实体公开的信息远远多于客户端操作可能希望更新的信息,但如果被调试器拦截,返回的实体可能包含对任何这些值的更改。

也许这是您从其他开发人员那里继承的东西,或者是您从野外发现的示例中构建的东西。复杂性必须服务于一个非常具体的目的来证明它的存在。不幸的是,在大多数情况下,它是盲目地添加的,它会解决一些未来的问题,或者仅仅是因为它是一个挑战。设计模式是作为交流相关概念的一种手段而开发的,但已被视为所有代码应该是什么样子的福音。重构、提炼和整合代码是减少错误的好方法,但它应该在代码的目标被证明和理解之后进行。否则它是过早的优化,并导致像这样令人头疼的问题。

【讨论】:

  • 嗨史蒂夫,我已经为公司、地址和网站创建了新的视图模型类。我试图实现你建议的关于删除地址的逻辑,它在迭代过程中删除地址时给了我一个错误。我已经用新的视图模型更新了帖子
  • 有什么异常?
  • 我看到了一个可能的问题。您的地址有一个不可为空的 FirmId,它还有一个我希望有 AddressId 的人的集合。如果您从公司中删除地址,EF 会想要删除该地址,但它不会如果其他记录(人)引用它,则能够。 (违反 FK 约束。)我相信您可能希望 Address 上的 FirmId 为 Nullable Int。这应该会改变行为,以便地址的 FirmId 在将其从公司中删除时将被清空,从而使人员引用保持不变。否则,您也必须将其从人员中删除。
  • 为了快速检查,我在地址实体中将 FirmId 设为可为空,但仍然出现异常。我得到的异常是 - $exception {“集合已修改;枚举操作可能无法执行。”} System.InvalidOperationException
  • 啊..我的错..我相信这是由于addressesToRemove的人口中缺少.ToList()。我已经更新了答案代码。它将尝试修改正在积极枚举的内容
猜你喜欢
  • 2011-09-08
  • 1970-01-01
  • 1970-01-01
  • 2012-08-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多