实体框架未正确修改或删除子项 [英] Entity Framework not correctly modifying or deleting child items

查看:65
本文介绍了实体框架未正确修改或删除子项的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



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



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



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





这是我的模型类



NewFirmViewModel

 公共类NewFirmViewModel 
{
public int FirmId {组; }

公用字串FirmName {get; set;}

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

public ICollection< AddressViewModel>地址{get;组; }

public ICollection< WebsiteViewModel>网站{get;组; }

公共布尔hasIntralinks {get;组; }
}

AddressViewModel

 公共类AddressViewModel 
{
public int AddressId {get;组; }
公用字串Line1 {get;组; }
公用字串Line2 {get;组; }
公用字串Line3 {get;组; }
公用字符串Phone {get;组; }
public bool IsHeadOffice {get;组; }
public int FirmId {组; }
}

WebsiteViewModel

 公共类WebsiteViewModel 
{
private int FirmWebsiteId {get;组; }
私人字串WebsiteUrl {get;组; }
公用字符串用户名{get;组; }
公用字符串密码{get;组; }
public int FirmId {组; }
}

实体

 公共类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>();
地址=新的HashSet< ADDRESS>();
//人=新的HashSet< PERSON>();
//网站=新的HashSet< FIRM_WEBSITE>();
}

//公共十进制ID {get;组; }
//
//
//
//
公共字符串NAME {get;组; }
公用字串SHORT_NAME {get;组; }
公用字串ALTERNATE_NAME {get;组; }
公共字符串WEBSITE {get;组; }
公用字串WEBSITE_USERNAME {get;组; }
公用字串WEBSITE_PASSWORD {get;组; }
公共布尔? INTRALINKS_FIRM {get;组; }
公用字串NOTES_TEXT {get;组; }
公用字串NOTES_HTML {get;组; }
公共字符串HISTORY_TEXT {get;组; }
公共字符串HISTORY_HTML {get;组; }

公共字符串HISTORY_SUM_TEXT {get;组; }
公共字符串HISTORY_SUM_HTML {get;组; }

public Nullable< decimal> OLD_ORG_REF {get;组; }
public Nullable< decimal> SOURCE_ID {获取;组; }

[DisplayFormat(DataFormatString = PermalConstants.DateFormat)]
public Nullable< DateTime> DATE_FOUNDED {获取;组; }

公共虚拟ICollection< ADDRESS>地址{get;组; }

// public ICollection< FIRM_WEBSITE>网站{get;组; }
//公共ICollection< PERSON>人们{组; }

// public SOURCE SOURCE {get;组; }
// public ICollection< FIRM_PERSON> FIRM_PERSON {get;组; }
public ICollection< MANAGERSTRATEGY>管理策略{get;组; }
public ICollection< FIRM_ACTIVITY> FIRM_ACTIVITY {get;组; }
public ICollection< FIRM_REGISTRATION> FIRM_REGISTRATION {获取;组; }
//公共ICollection< ACTIVITY>活动{get;组; }
public ICollection< FIRM_WEBSITE>网站{get;组; }

public Nullable< int> KEY_CONTACT_ID {get;组; }
[未映射]
public ICollection< FIRM_AUM> AUMs
{
得到
{
返回this.FIRM_AUMs;
}
}
public ICollection< FIRM_AUM> FIRM_AUMs {get;组; }
}


地址

公共类地址:实体
{
public ADDRESS()
{
// DATE_CREATED = DateTime.Now;
}


公用字符串LINE1 {get;组; }
公用字串LINE2 {get;组; }
公用字串LINE3 {get;组; }
public int CITY_ID {get;组; }
公用字串POSTAL_CODE {get;组; }
公用字串SWITCHBOARD_INT {get;组; }
公共字符串注意{组; }
public int? OLD_ADDRESS_REF {get;组; }
public int? SOURCE_ID {获取;组; }

public int FIRM_ID {get;组; }
[ForeignKey( FIRM_ID)]
公共FIRM FIRM {get;组; }

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

公共ICollection< PERSON>人们{组; }

// public SOURCE SOURCE {get;组; }

public bool IS_HEAD_OFFICE {get;组; }
[未映射]
公共字符串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

公共类FIRM_WEBSITE:实体
{
public FIRM_WEBSITE()
{

}
私有字符串_WEBSITE_URL;

公共字符串WEBSITE_URL
{
get
{
if(string.IsNullOrEmpty(_WEBSITE_URL))
return _WEBSITE_URL;
try
{

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

返回ubuilder.Uri.AbsoluteUri;
}
捕获(UriFormatException ex)
{
return _WEBSITE_URL;
}

}
set {_WEBSITE_URL = value; }
}

公用字符串USERNAME {get;组; }
公用字串PASSWORD {get;组; }


public int FIRM_ID {get;组; }
[ForeignKey( FIRM_ID)]
公共FIRM FIRM {get;组; }
}

API控制器

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


var firmService = GetService< FIRM>();

if(model == null)返回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地址= firm.Addresses.Where(x => addressIds.Contains(x.ID))。ToList(); //我们要与该公司关联的所有地址。
//标识要从该公司删除的地址。
varaddressesToRemove = firm.Addresses.Where(x =>!addressIds.Contains(x.ID))。ToList();
foreach(varesTo地址中的变量地址)
firm.Addresses.Remove(address);

//标识要与该公司关联的地址。
var existingAddressIds = firm.Addresses.Select(x => x.ID).ToList();
var addresssToAdd =地址。其中(x =>!existingAddressIds.Contains(x.ID))。ToList();
foreach(addressToAdd中的变量地址)
firm.Addresses.Add(address);

firmService.Update(firm);
}
其他
{

}

return Ok(firm);
}

DbContext

 公共类Repo< T> :IRepo< T> T:Entity,new()
{
public readonly Db dbContext;

私人ILogger _logger;
私人IQueryable< T> lastQuery {get;组; }
私人布尔? _enablelazyloading;
私人IEntityWatcher< T> _watcherNotification;
私人布尔值_EnableChangeNotification;
公用字符串ID {get;组; }
专用字串_clientId;

#region构造函数
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(异常例外)
{
_logger.Error(更改通知无法在Repo中解析。Repo将继续运行而不会发出通知。,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实体= 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 {
///仅
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>>()。其中​​(x => x.NAME.Trim()==).Select(x => x.ID)。 AsEnumerable();
if(changeNotificationType == ChangeNotificationType.Added&& newLegacyStratImpl!= null&& newLegacyStratImpl.LEGACY_STRATEGY_ID.HasValue&!blankStrategies.Contains(newLegacyStratImpl.LEGACY_bbValue)
_action.Added(newEntity);
返回true;
}否则if(changeNotificationType == ChangeNotificationType.Deleted& newLegacyStratImpl!= null){
_action.Deleted(newEntity);
返回true;
}否则if(changeNotificationType == ChangeNotificationType.Modified&& newLegacyStratImpl!= null&& oldLegacyStratImpl!= null){
///需要走得更远,并确保遗留策略已更改,没有其他属性。
var hasChanged = newLegacyStratImpl.LEGACY_STRATEGY_ID!= oldLegacyStratImpl.LEGACY_STRATEGY_ID;
if(hasChanged){
_action.Modified(newEntity,oldEntity);
返回true;
} else {
返回false;
}
}
}
返回false; ///其他所有失败...
} catch(Exception ex){
_logger.Error(例如);
返回false;
}
}


解决方案

  firm.Addresses = model.Firm.Addresses; 
firm.Websites = model.Firm.Websites;

此...您实际上是在告诉上下文实例来处理由以下人员提供的地址和网站您的模型作为实体。上下文不了解这些实体,因此对它们的处理方式与您执行以下操作一样:

  foreach (model.Firm.Addresses中的变量地址)
{
firm.Addresses.Add(新地址{AddressId = address.AddressId,City = address.City,/ * ... * /});
}

就上下文而言,这些对象是新的。 / p>

作为一般规则,请避免将实体传递给客户端,并且切勿信任/接受实体从客户端返回。如果公司正在关联现有地址,则对于公司更新模型而言,AddressID列表已绰绰有余。 (假设如果用户创建或更新了地址的内容,则该内容将被单独保存。)如果用户可以通过Firm更新传递新地址,则需要合适的地址视图模型并检测新的或更新的条目。



解决上述问题的一种简单方法是使用 Attach()将实体与上下文相关联,但是我永远不要建议这样做,因为它相信该实体没有以意外的方式被修改。 (Plus会引发其他一些极端情况,例如上下文可能已经具有与该ID关联的实体)



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

  var addressIds = model.Firm.Addresses.Select(x => x。 AddressId).ToList(); 
var地址= dbContext.Addresses.Where(x => addressIds.Contains(x => x.AddressId))。ToList(); //我们要与该公司关联的所有地址。

//标识要从该公司删除的地址。
varaddresssToRemove = firm.Addresses.Where(x =>!addressIds.Contains(x.AddressId))。ToList();
//标识要与该公司关联的地址。
var addresssToAdd =地址
.Except(firm.Addresses,new LamdaComparer((a1,a2)=> a1.AddressId == a2.AddressId));

foreach(addressToRemove中的可变地址)
firm.Addresses.Remove(address);

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

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



可以找到LamdaComparer 此处



编辑:如果实现比较器存在问题。
如果没有LamdaComparer,您可以执行以下操作:

  //确定要与该公司关联的地址。 
var existingAddressIds = firm.Addresses.Select(x => x.AddressId).ToList();
var addresssToAdd =地址。其中(x =>!existingAddressIds.Contains(x.AddressId))。ToList();

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



尝试在存储库中抽象出EF的功能会适得其反。例如,要尝试对添加和删除的相关实体进行检查,就需要了解相关实体,而这并不是通用实现所知道的。接受EF是应用程序的核心部分,与.Net Framework是应用程序的核心部分没有什么不同。这使您可以利用EF的全部功能,而不必编写代码来试图隐藏诸如排序表达式,分页,reduce和map操作等之类的代码,或者只是因为这些功能可能泄漏 EF原理而没有利用这些功能。



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



也许这是您从其他开发人员那里继承下来的,或者是您从野外发现的示例构造而来的。复杂性必须有一个非常具体的目的,以证明它的存在。不幸的是,在大多数情况下,它是出于盲目的信念而添加的,它会解决某些将来的问题,或者仅仅是因为这是一个挑战。设计模式是作为传达相关概念的一种方式开发的,但已被视为所有 all 代码的外观的福音。重构,改进和合并代码是减少错误的好方法,但是在证明并理解代码目标之后,应该这样做。否则,这是过早的优化,并会导致诸如此类的令人头疼的问题。


I am trying save Firm object that contains addresses and websites. I have developed the ability to add and remove address controls from the UI using reactive forms in Angular 7. While saving the Firm object, it is creating additional entries for addresses and websites and not treating it as existing record.

So if I delete websites and addresess from the UI, I can see that I am passing the correct amount of array elements to the backend api. So I am rest assured that the issue is with Entity Framework.

So what I am trying to achieve is that if the user deletes addresses or websites from the client side, it should update the same when calling the update method in Entity Framework. I am using Entity Framework 6

UI - Where I can add multiple addressess

Here are my model classes

NewFirmViewModel

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

AddressViewModel

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

WebsiteViewModel

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

Entities

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 controller

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

DbContext

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

解决方案

        firm.Addresses = model.Firm.Addresses;
        firm.Websites=  model.Firm.Websites;

This... You are effectively telling this instance of the context to treat the Addresses and Websites provided by your "model" as entities. The context doesn't know about these entities so it treats them no differently than if you had done something like the following:

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

As far as the context is concerned, these objects are "new".

As a general rule, avoid passing entities to the client, and never trust/accept entities back from the client. If the Firm is associating existing addresses, then a list of AddressIDs is more than sufficient for the Firm update model. (Assuming that if the user created or updated an address' content, that would have been saved separately.) If the user can pass a new address with the Firm update, then you need a suitable address view model and detect the new or updated entries.

A simple apparent solution to the above problem is to associate the entities to the context using Attach() but I never recommend this because it trusts that the entity has not been modified in unintended ways. (Plus raises other edge cases that crop up such as where the context may already have an entity associated with that ID)

When updating child references like addresses where we are not updating address content as part of the firm update:

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

If you are potentially updating address details, then a bit more work will be needed, but the crux of the matter is that you can't trust the entities that you passed to the client and receive back via the model. View models should be POCO classes, not entities. To avoid issues like this, anything passed back from a view should be validated and the applicable entity(ies) should be loaded from the context handling the request.

The LamdaComparer can be found here.

Edit: If there are issues with implementing the comparer.. Without the LamdaComparer, you can do something like:

// 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();

Edit 2: Repository classes are helpful for enabling unit testing. Generic repository classes are evil. If you're not using unit testing, then I would avoid adding the complexity of trying to abstract away the EF functionality into a repository, especially a Generic repository. In your case, to avoid potentially breaking other areas of the code, I'd add a method called SaveChanges to your service which just calls the context's SaveChanges, and then instead of calling your service.Update(entiny) method, call service.SaveChanges().

Trying to abstract away EF's functionality in a repository is very much counter-productive. For instance to try and do the checks for added and removed related entities requires knowledge of the entity in question, which isn't knowledge that a Generic implementation would know. Accept that EF is a core part of your application, no different than the .Net Framework is a core part of your application. This allows you to leverage the full capability of EF without having to code around trying to hide things like sorting expressions, paging, reduce and map operations, etc. or simply not taking advantage of these features because they might "leak" EF-isms.

It's not to say that the Repo/Context Wrapping implementation your project has is bad or wrong, but it is complex and it has led to behaviour that is difficult to explain. From what I can see from the code you've provided is that it's geared towards treating entities as 2 separate roles, the model, and a detached representation of the model. IMO this violates Single Responsibility, an entity should represent the model, and nothing more. A ViewModel or DTO is the transport of relevant information to a view or external consumer, not the entity. Yes, EF provides functionality to detach/reattach, and copy values across between entities, but a key point I'd make against using this with entities that have been repurposed as view models is that view models/DTOs coming back from the client cannot be trusted. An entity exposes far more information than a client action may wish to update, but the entity coming back could contain changes to any of those values if intercepted by a debugger.

Maybe this is something you inherited from another developer, or something you've constructed from examples found out in the wild. Complexity has to serve a very specific purpose to justify it's existence. Unfortunately in most cases it is added in blind faith that it will solve some future problem or simply because it's a challenge. Design patterns were developed as a means to communicate related concepts, but have been taken as a gospel for what all code should look like. Refactoring, refining, and consolidating code is a good thing to reduce bugs, but it's something that should be done after the objective of the code is proven and understood. Otherwise it is premature optimization, and leads to head-scratching problems like this.

这篇关于实体框架未正确修改或删除子项的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆