C# 实体框架委托实体从外部源添加/加载

C# 实体框架委托实体从外部源添加/加载,c#,.net,entity-framework,entity-framework-6,C#,.net,Entity Framework,Entity Framework 6,我正在尝试修改现有的实体框架类,以便从服务而不是基于config键的数据库进行读/写 几周内,我们希望将数据保存在服务和现有数据库中,稍后验证数据一致性,然后完全切换到服务 这是我心目中的方法: public class UserDbContext : IdentityDbContext<AspNetUsers> { public DbSet<Address> Addresses { get; set; } public DbSet<Person&g

我正在尝试修改现有的实体框架类,以便从服务而不是基于config键的数据库进行读/写

几周内,我们希望将数据保存在服务和现有数据库中,稍后验证数据一致性,然后完全切换到服务

这是我心目中的方法:

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 = 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对象,它都将被委托给其他类来加载实体而不是数据库?

在对DbContext执行数据查询时,实体要么映射到当前数据库,要么映射到当前数据库。这张照片上没有灰色区域。你所问的和我的相似。在本例中,它不是另一个DbContext,但问题是相同的

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

  • EF通过查阅
    DbModelBuilder
    配置中表示的映射,将表达式转换为SQL命令
  • SQL被传递到数据库并进行计算
  • 响应从数据库反序列化到EF模型对象中
  • 因此,最终,我们需要告诉EF忽略任何需要解析地址的查询路径(和
    .Include
    s),因为它将无法找到它们。对于这一点,
    obsoletateAttribute
    是一个很好的工具,我们可以通过警告跟踪代码中的任何引用,同时仍然允许在转换状态下编译旧代码。这也是对任何新代码的提示,不要使用这个遗留引用

    尝试并静默地将此条件功能注入DbContext可能是可行的,但这并不实际。混合方法的最佳解决方案是将新的
    AddressProvider
    实现并排“栓接”

    过了一段时间,当您删除了以前对旧地址集的所有引用和查询,并且希望删除旧地址表时,只需将其从
    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);
        }
    }
    
    我不建议这样做,但您也可以添加一个
    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();
        }
    }
    
    稍后,当您要从上下文amke中删除表时,请确保从上下文中删除
    public DbSet Addresses{get;set;}
    ,并忽略模型配置中的类型:

    modelBuilder.Ignore<Address>();
    

    因此,您希望禁用地址提供程序,或者您实现了该切换,因为您认为这样会更容易?从
    UserDbContext
    模型中完全删除地址,并将其转换为
    User
    /
    Person
    上的方法,以便通过提供程序获取或更新地址,要简单得多。@chrischaller,是的,但这取决于配置。我们希望在两周内使用现有数据库,并在两个位置写入数据并验证数据一致性,然后再完全切换到使用该服务,只是为了确认,
    AspNetUsers
    在DbContext(数据库)中有一个地址集合但是在服务中,对于每个
    AspNetUsers
    (用户)实例,只有一个
    地址
    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);       
    }
    
    modelBuilder.Ignore<Address>();
    
    /// <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();
    }