EF中基于唯一键查询泛型DbContext实现单向数据同步
基于唯一键实现跨DbContext实体同步
我正在实现单向同步功能,拥有WebContext和StagingContext两个上下文。每次调用SaveChangesAsync()时,需将WebContext中更新的数据同步到StagingContext。
特性定义
[AttributeUsage(AttributeTargets.Class)] public class SyncEntityAttribute : Attribute { public Type Target { get; set; } }
现有DbContext代码
public override async Task<int> SaveChangesAsync(CancellationToken cancellation = default) { await SyncEntityAsync(stagingContext, cancellation); return await base.SaveChangesAsync(cancellation); } private async Task SyncEntityAsync<T>(T dbContext, CancellationToken cancellation = default) where T : DbContext { ChangeTracker.DetectChanges(); var entries = ChangeTracker.Entries() .Where(x => Attribute.GetCustomAttribute(x.Entity.GetType(), typeof(SyncEntityAttribute)) != null) .ToList(); try { foreach (var entry in entries) { if (entry.State == EntityState.Detached || entry.State == EntityState.Unchanged) continue; var attribute = Attribute.GetCustomAttribute(entry.Entity.GetType(), typeof(SyncEntityAttribute)) as SyncEntityAttribute; if (attribute == null) continue; var uniqueKeys = entry.Properties.Where(x => x.Metadata.IsUniqueIndex()).ToList(); var targetEntity = await GetQueryable(dbContext, attribute.TargetEntity); // 尚未实现,需返回基于泛型类型按唯一键过滤的单个实体 mapper.Map(entry.Entity, targetEntity); switch (entry.State) { case EntityState.Added: dbContext.Add(targetEntity); break; case EntityState.Deleted: if (targetEntity.HasProperty("IsActive")) { targetEntity.TrySetProperty("IsActive", false); } break; } } } catch (Exception ex) { Log.Error(ex.Message); } }
需求与问题
需要同步所有标记SyncEntity特性的实体模型,查询StagingContext中的数据判断是否已存在。要通过特性的Target指定的类型进行查询,若仅用主键查询可使用DbContext的FindAsync方法,但希望实现类似FindAsync的功能,改用从entry.Properties.Where(x => x.Metadata.IsUniqueIndex()).ToList()获取的唯一键进行过滤,想找类似扩展方法的实现但没找到。
尝试的实现代码及异常
private static async Task<object> _GetQueryable<T>(DbContext dbContext, List<PropertyEntry> uniqueKeys) where T : class { if (uniqueKeys is null) throw new ArgumentNullException(); if (uniqueKeys.Count <= 0) throw new ArgumentNullException(); var p = Expression.Parameter(typeof(T)); var filters = new List<Expression>(uniqueKeys.Count); foreach (var key in uniqueKeys) { var wrapper = Expression.Constant(Activator.CreateInstance(typeof(Wrapper<>).MakeGenericType(key.CurrentValue.GetType()), key.CurrentValue)); var value = Expression.Property(wrapper, "Value"); filters.Add(Expression.Equal(p, value)); } var body = filters.Aggregate((c, n) => Expression.AndAlso(c, n)); var predicate = Expression.Lambda<Func<T, bool>>(body, p); return await dbContext.Set<T>().FirstOrDefaultAsync(predicate); }
执行filters.Add(Expression.Equal(p, value));时抛出异常:The binary operator Equal is not defined for the types 'Web.Gateway.Core.StagingModels.User' and 'System.String'.,原因是代码错误地将唯一键值与整个实体对象进行比较,而非实体的对应属性。
可用的最终代码
private static Task<object> GetQueryable(DbContext dbContext, Type entityType, List<PropertyEntry> uniqueKeys) { return (Task<object>)typeof(SharedContext).GetMethod(nameof(_GetQueryable), BindingFlags.NonPublic | BindingFlags.Static) .MakeGenericMethod(entityType) .Invoke(null, new object[] { dbContext, uniqueKeys }); } private static async Task<object> _GetQueryable<T>(DbContext dbContext, List<PropertyEntry> uniqueKeys) where T : class { if (uniqueKeys is null) throw new ArgumentNullException(); if (uniqueKeys.Count <= 0) throw new ArgumentNullException(); var entityType = typeof(T); var p = Expression.Parameter(entityType); var filters = new List<Expression>(uniqueKeys.Count); foreach (var key in uniqueKeys) { var wrapper = Expression.Constant(Activator.CreateInstance(typeof(Wrapper<>).MakeGenericType(key.CurrentValue.GetType()), key.CurrentValue)); var value = Expression.Property(wrapper, "Value"); filters.Add(Expression.Equal(Expression.Property(p, entityType.GetProperty(key.Metadata.Name)), value)); } var body = filters.Aggregate((c, n) => Expression.AndAlso(c, n)); var predicate = Expression.Lambda<Func<T, bool>>(body, p); return await dbContext.Set<T>().FirstOrDefaultAsync(predicate); }
内容的提问来源于stack exchange,提问作者Rashid
相关产品推荐
相关产品推荐

