【问题标题】:Entity Framework delegate entities Add/Load from external sources实体框架委托实体从外部源添加/加载
【发布时间】:2021-08-13 14:35:39
【问题描述】:

我正在尝试修改现有的实体框架类,以便根据配置键从服务而不是数据库中读取/写入。

几个星期我们想将数据保存在服务和现有数据库的地方,稍后验证数据一致性然后完全切换到服务。

这是我想到的方法:

public class UserDbContext : IdentityDbContext<AspNetUsers>
{
    public DbSet<Address> Addresses { get; set; }
    public DbSet<Person> Persons { get; set; }
}

public class AspNetUsers : IdentityUser
{
    public virtual ICollection<Person> Persons { get; set; }
    public virtual ICollection<Address> Addresses { get; set; }
    
    public AspNetUsers()
    {
        Addresses = new List<Address>();
        Persons = new List<Person>();
    }
}

我们今天如何从数据库访问

class Program
{
    static void Main(string[] args)
    {
       var context = new UserDbContext();
       var address = context.Addresses.FirstOrDefault(x => x.UserId == "user12345"); // Load
       
       var newAddress = new Address("street", "city");
       context.Addresses.Add(newAddress);
       context.SaveChanges();
    }
}

但我们已将地址部分移至单独的服务,并希望在现有代码中使用它,而不是从数据库中读取

class Program
{
    static void Main(string[] args)
    {
       var context = new UserDbContext();
       var address = new AddressProvider().GetAddress("user12345"); // Load
       
       var newAddress = new Address("street", "city");
       new AddressProvider().SaveAddress(newAddress);
       
       // How to delegate the Address from database to service here?
       var user = context.Users.FirstOrDefault(x => x.UserId == "user12345"); 
       user.Addresses.FirstOrDefault();
       user.Addresses.Add(newAddress);
    }
}

public class AddressProvider
{
    public Address GetAddress(string userId, UserDbContext context)
    {
        bool loadFromService = true; // will be from config

        if (loadFromService) 
        {
            return new Address(); // this will be fetch from api via HttpClient
        }

        return context.Addresses.FirstOrDefault(x => x.UserId == userId);
    }
  
    public void SaveAddress(Address address, UserDbContext context)
    {
        bool loadFromService = true; // will be from config

        if (loadFromService)
        {
            SaveAddress(address); // this will be sent to API
            return;
        }

        context.Addresses.Add(newAddress);
        context.SaveChanges();
    }
}

现在的问题是,当通过导航属性加载此地址时,我们无法直接使用服务。

有没有办法使用任何实体框架功能来委派这个?那么,无论何时调用 Address 对象,它都会被委托给其他类来加载实体而不是数据库?

【问题讨论】:

  • 所以您希望允许禁用地址提供程序,或者您是否实现了该开关,因为您认为这样会更容易?从UserDbContext 模型中完全删除地址并将其转换为User/Person 上的方法以通过提供程序获取或更新地址会简单得多。
  • @ChrisSchaller,是的,但它基于配置。我们希望使用现有数据库几周,并在两个地方都写入数据并验证数据一致性,然后再完全切换到使用该服务
  • 确认一下,AspNetUsersDbContext (Database) 中有一个地址集合,但在服务中只会有一个每个 AspNetUsers (User) 实例的单个 Address

标签: c# .net entity-framework entity-framework-6


【解决方案1】:

在对 DbContext 执行数据查询时,实体要么映射到当前数据库,要么。这个没有灰色区域。您要问的内容类似于Entity Framework - “Navigation Property” In A Different Data Context?。在这种情况下,它不是另一个 DbContext,但问题是一样的。

当您针对 DbContext 进行查询时,服务该查询涉及以下高级步骤:(除非查询针对缓存的结果集

  1. EF 通过查阅 DbModelBuilder 配置中表达的映射将表达式转换为 SQL 命令
  2. SQL 被传递到数据库并进行评估
  3. 响应从数据库反序列化到 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&lt;Address&gt; 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();
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    相关资源
    最近更新 更多