我正在尝试保存包含地址和网站的Firm
对象。我已经开发了使用Angular 7中的反应形式从UI添加和删除地址控件的功能。在保存Firm
对象的同时,它正在为地址和网站创建其他条目,而不是将其视为现有记录。
因此,如果我从UI删除网站并添加地址,则可以看到我正在将正确数量的数组元素传递给后端api。因此,我可以放心,问题出在实体框架上。
因此,我要实现的目标是,如果用户从客户端删除地址或网站,则在调用Entity Framework中的update方法时,应该对其进行更新。我正在使用Entity Framework 6
UI-我可以在其中添加多个地址的地址
这是我的模型课
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; }
}
实体
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);
}
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;
}
}
答案 0 :(得分:0)
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, /* ... */ });
}
就上下文而言,这些对象是“新的”。
通常,请避免将实体传递给客户端,并且切勿信任/接受实体从客户端返回。如果公司正在关联现有地址,则对于公司更新模型而言,AddressID列表已绰绰有余。 (假设用户创建或更新了地址的内容,则该内容将被单独保存。)如果用户可以通过“公司”更新传递新地址,则需要合适的地址视图模型并检测新的或更新的条目。
解决上述问题的一个简单的明显方法是使用Attach()
将实体与上下文相关联,但是我从不建议这样做,因为它相信该实体并未以意外的方式被修改。 (Plus还会引发其他一些极端情况,例如上下文可能已经具有与该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类,而不是实体。为避免出现此类问题,应验证从视图传回的所有内容,并应从处理请求的上下文中装入适用的实体。
可以找到here的LamdaComparer。
编辑:如果实现比较器存在问题。 没有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(entiny)方法,而是调用service.SaveChanges ()。
试图在存储库中抽象出EF的功能会适得其反。例如,要尝试对添加和删除的相关实体进行检查,需要了解所讨论的实体,而这并不是通用实现所知道的。接受EF是应用程序的核心部分,与.Net Framework是应用程序的核心部分没有什么不同。这使您可以利用EF的全部功能,而不必编写代码来试图隐藏诸如排序表达式,分页,reduce和map操作等之类的代码,或者只是因为这些功能可能“泄漏” EF原理而没有利用这些功能。
这并不是说您的项目所执行的Repo / Context Wrapping实现是坏的还是错误的,但是它很复杂并且导致行为难以解释。从您提供的代码中可以看出,它旨在将实体视为两个独立的角色,模型和模型的分离表示。 IMO违反了单一责任制,一个实体应该代表该模型,仅此而已。 ViewModel或DTO是将相关信息传输到视图或外部使用者,而不是实体。是的,EF提供了分离/重新附加以及在实体之间复制值的功能,但是我要针对与已重新用作视图模型的实体使用此功能的一个重点是,不能从客户端返回的视图模型/ DTO值得信赖。实体公开的信息远远超出客户端操作可能希望更新的信息,但是如果被调试器拦截,返回的实体可能包含对那些值的更改。
也许这是您从另一个开发人员那里继承而来的,或者是您从野外发现的示例中构造的。复杂性必须有一个非常特殊的目的,以证明它的存在。不幸的是,在大多数情况下,它是出于盲目的信念而添加的,它将解决一些将来的问题,或者仅仅是因为这是一个挑战。设计模式是作为传达相关概念的一种方式开发的,但已被视为所有 all 代码的福音。重构,改进和整合代码是减少错误的好方法,但是在证明和理解了代码目标之后,应该这样做。否则,这是过早的优化,并会导致像这样的头抓问题。