在对 DbContext 执行数据查询时,实体要么映射到当前数据库,要么不。这个没有灰色区域。您要问的内容类似于Entity Framework - “Navigation Property” In A Different Data Context?。在这种情况下,它不是另一个 DbContext,但问题是一样的。
当您针对 DbContext 进行查询时,服务该查询涉及以下高级步骤:(除非查询针对缓存的结果集)
- EF 通过查阅
DbModelBuilder 配置中表达的映射将表达式转换为 SQL 命令
- SQL 被传递到数据库并进行评估
- 响应从数据库反序列化到 EF 模型对象中
所以最终,我们需要告诉 EF 忽略任何需要解析地址的查询路径(和.Includes),因为它将无法找到它们。 ObsoleteAttribute 是一个很好的工具,我们可以通过警告跟踪代码中的任何引用,同时仍然允许旧代码在您处于过渡状态时编译。这也是对任何新代码的提示,不要使用这个 legacy 参考。
尝试将此条件功能静默注入 DbContext 可能是可行的,但并不实用。混合方法的最佳解决方案是“固定”新 AddressProvider 实现并排。
一段时间后,当您删除所有以前对旧地址的DbSet 的引用和查询 并且想要删除旧的地址表时,您可以简单地将其从UserDbContext 中删除,确保您在映射中也忽略它。
只需将新的提供程序实现添加到DbContext,这样您就不必复制对每个调用的引用。
public class UserDbContext : IdentityDbContext<AspNetUsers>
{
#region Disposable AddressProvider
AddressProvider AddressProvider { get; private set; } = new AddressProvider(this);
/// <summary>
/// Dispose managed members such as AddressProvider, otherwise they will keep this context active
/// </summary>
protected override void Dispose(bool disposing)
{
if(this.AddressProvider != null)
{
try
{
this.AddressProvider.Dispose();
this.AddressProvider = null;
}
catch (Exception) { /*Ignore errors during dispose*/ }
}
base.Dispose(disposing);
}
#endregion Disposable AddressProvider
public DbSet<Address> Addresses { get; set; }
public DbSet<Person> Persons { get; set; }
}
public class AddressProvider : IDisposable
{
UserDbContext _userContext;
public readonly bool LoadFromService;
public readonly bool UpdateDatabase;
public AddressProvider(UserDbContext dbContext, bool loadFromService = true, bool updateDatabase = true)
{
_userContext = dbContext;
LoadFromService = loadFromService;
UpdateDatabase = updateDatabase;
}
#region IDiposable - Release the dbContext reference
public void Dispose()
{
_userContext = null;
}
#endregion IDiposable - Release the dbContext reference
public Address GetAddress(string userId)
{
if (LoadFromService)
{
return GetAddressFromAPI(userId);
}
return _userContext.Addresses.FirstOrDefault(x => x.UserId == userId);
}
public void SetAddress(Address address)
{
// To support Hybrid operations, we can update both the service and the database
if (LoadFromService)
{
SaveAddressInAPI(address);
}
if (UpdateDatabase)
{
var userId = address.UserId;
// It's probably simpler to delete any existing address for this user
foreach(var a in context.Addresses.Where(x => x.UserId == userId).ToList())
context.Entry(fund).State = System.Data.Entity.EntityState.Deleted;
_userContext.Addresses.Add(address);
_userContext.SaveChanges();
}
}
public void GetAddressFromAPI(string userId)
{
// this will be fetch from api via HttpClient
return new Address { UserId = userId };
//throw new NotImplementedException();
}
public void SaveAddressInAPI(Address address)
{
// this will be sent to API
//throw new NotImplementedException();
}
}
public class AspNetUsers : IdentityUser
{
public virtual ICollection<Person> Persons { get; set; }
[Obsolete("Stop using the Addresses collection directly, see 'GetAddress(UserDbContext)'")]
public virtual ICollection<Address> Addresses { get; set; }
public AspNetUsers()
{
Addresses = new List<Address>();
Persons = new List<Person>();
}
}
public static class AddressExtensions
{
public static Address GetAddress(this AspNetUsers user, UserDbContext context)
{
return context.AddressProvider.GetAddress(user.UserId);
}
public void SetAddress(this AspNetUsers user, Address address, UserDbContext context)
{
// ensure the UserId is set to this user
address.UserId = user.UserId;
context.AddressProvider.SetAddress(address);
}
}
这与您的原始实现没有太大区别,但至少提供程序实现被整齐地封装在 DbContext 中
static void Main(string[] args)
{
var context = new UserDbContext();
var user = context.Users.FirstOrDefault(x => x.UserId == "user12345");
// load the address
var address = user.GetAddress(context);
var newAddress = new Address("street", "city");
user.SetAddress(newAddress, context);
}
我不推荐这个,但您也可以添加一个LoadAddresses 方法来管理条件加载逻辑。我不喜欢这样,因为它允许您的太多代码像本身是 EF 上下文的一部分一样运行,而实际上它不是,但是没有它,答案就不会完整...
public static class MoreAddressExtensions
{
public static Address LoadAddress(this AspNetUsers user, UserDbContext context)
{
if (context.AddressProvider.LoadFromService)
{
if (this.Addresses == null)
this.Addresses = new HashSet<Address>();
else
this.Addresses.Clear();
var externalAddress = context.AddressProvider.GetAddress(user.UserId));
if (externalAddress != null)
this.Addresses.Add(externalAddress);
}
else
this.Addesses = context.Addresses.Where(x => x.UserId == userId).ToList();
}
}
那么这个就可以用了……
static void Main(string[] args)
{
var context = new UserDbContext();
var user = context.Users.FirstOrDefault(x => x.UserId == "user12345");
// load the address
user.LoadAddress(context);
var address = user.Addresses.FirstOrDefault();
var newAddress = new Address("street", "city");
user.SetAddress(newAddress, context);
}
稍后,当您想从上下文中删除表时,请确保从上下文中删除 public DbSet<Address> Addresses { get; set; },并忽略模型配置中的类型:
modelBuilder.Ignore<Address>();
如果您对永久混合方法感兴趣而不是转换,或者您有许多可能正在更新旧地址记录的现有代码路径,那么您还可以拦截 DbContext 上的 SaveChanges 方法,如果你这样做,那么你需要注意并避免现有代码的竞争条件,也许你删除以前的 SetAddress 逻辑,这是你可以如何去做的一个例子:
/// <summary> detect changes to Address Entities and redirect them through the provider </summary>
public override int SaveChanges()
{
foreach (var entry in this.ChangeTracker.Entries())
{
if (entry.Entity is Address a)
{
switch(entry.State)
{
case EntityState.Modified:
case EntityState.Added:
{
if (AddressProvider.LoadFromService)
{
if (String.IsNullOrEmpty(a.UserId))
a.UserId = a.User.Id;
AddressProvider.SaveAddressInAPI(a);
}
if (!AddressProvider.UpdateDatabase)
entry.State = EntityState.Unchanged;
break;
}
case EntityState.Deleted:
{
// no mention of how to handle delete, so we'll add a blank one
if (AddressProvider.LoadFromService)
{
string userId = a.UserId;
if (String.IsNullOrEmpty(a.UserId))
userId = a.User.Id;
AddressProvider.SaveAddressInAPI(new Address { UserId = userId });
}
if (!AddressProvider.UpdateDatabase)
entry.State = EntityState.Unchanged;
break;
}
case EntityState.Unchanged:
default:
continue;
}
}
}
return base.SaveChanges();
}