You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 02:01:28