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

EF Core 7中自定义SqlExpressionFactory的替代方案咨询

EF Core 7+ 多语言翻译表查询扩展的适配方案

背景

我们的数据库采用两张表存储可翻译数据:一张存储默认语言数据(例如Product表),另一张以Translation为后缀存储翻译数据(例如ProductTranslation表)。之前开发了一个EF Core扩展,可自动将单表查询替换为左连接翻译表的子查询,实现多语言数据的自动加载。

原查询代码:

context.Product
    .Where(t => t.Description.Contains('free'))
    .OrderBy(t => t.Name)
    .ToList();

正常生成的SQL:

SELECT Id, Name, Description
FROM Product
WHERE Description like 'free'
ORDER BY Name

扩展后期望生成的SQL:

SELECT Id, Name, Description
FROM (
    SELECT o.Id, COALESCE(t.Name, o.Name) AS Name, COALESCE(t.Description, o.Description) AS Description
    FROM Product as o
    LEFT JOIN ProductTranslation as t on t.Id = o.Id and t.LanguageId = 4
) as d
WHERE Description like 'free'
ORDER BY Name

原有实现的问题

之前的方案是重写SqlExpressionFactory的Select方法,并通过IDbContextOptionsExtension替换默认服务:

internal sealed class TranslationSqlExpressionFactory : SqlExpressionFactory
{
    public override SelectExpression Select(IEntityType entityType)
    {
        if (IsTranslatable(entityType))
        {
            var query = TranslationRawSql(entityType);
            var fromSqlExpression = new FromSqlExpression(entityType.GetTableMappings().Single().Table, query, Expression.Constant(new object[0]));
            return base.Select(entityType, fromSqlExpression);
        }
        return base.Select(entityType);
    }
}
internal sealed class TranslationOptionsExtension : IDbContextOptionsExtension
{
    public void ApplyServices(IServiceCollection services)
    {
        services.AddTransient<ISqlExpressionFactory, TranslationSqlExpressionFactory>();
    }
}

但EF Core 7开始,各数据库提供商拥有专属的ISqlExpressionFactory实现(如SqlServerSqlExpressionFactory、SqliteSqlExpressionFactory),直接替换会覆盖提供商的特定逻辑,引发功能冲突。目前临时方案是为每个提供商编写对应子类,但维护成本极高。

更适配的替代方案

1. 装饰器模式包装提供商实现

不直接替换服务,而是用装饰器包裹原有提供商的ISqlExpressionFactory实现,仅在需要时拦截Select方法:

internal sealed class TranslationSqlExpressionFactoryDecorator : ISqlExpressionFactory
{
    private readonly ISqlExpressionFactory _innerFactory;

    public TranslationSqlExpressionFactoryDecorator(ISqlExpressionFactory innerFactory)
    {
        _innerFactory = innerFactory;
    }

    // 重写Select方法,注入翻译逻辑
    public SelectExpression Select(IEntityType entityType)
    {
        if (!IsTranslatable(entityType))
            return _innerFactory.Select(entityType);

        var baseSelect = _innerFactory.Select(entityType);
        var entityTable = baseSelect.Tables.Single() as TableExpression;
        if (entityTable == null) return baseSelect;

        var translationEntityType = GetTranslationEntityType(entityType);
        var translationTable = _innerFactory.Table(translationEntityType);

        // 构建JOIN条件:ID匹配 + 当前语言ID
        var joinCondition = _innerFactory.Equal(
            _innerFactory.Property(entityTable, entityType.FindPrimaryKey().Properties.Single()),
            _innerFactory.Property(translationTable, translationEntityType.FindProperty("Id")))
            .And(_innerFactory.Equal(
                _innerFactory.Property(translationTable, translationEntityType.FindProperty("LanguageId")),
                _innerFactory.Constant(GetCurrentLanguageId())));

        // 添加左连接
        baseSelect.AddJoin(JoinExpressionType.LeftOuter, translationTable, joinCondition);

        // 替换列:用COALESCE取翻译值或默认值
        foreach (var property in entityType.GetProperties())
        {
            var translationProperty = translationEntityType.FindProperty(property.Name);
            if (translationProperty == null) continue;

            var columnExpression = _innerFactory.Column(entityTable, property);
            var translationColumn = _innerFactory.Column(translationTable, translationProperty);
            var coalesceExpression = _innerFactory.Coalesce(translationColumn, columnExpression);

            baseSelect.ReplaceProjection(columnExpression, coalesceExpression);
        }

        return baseSelect;
    }

    // 其他ISqlExpressionFactory接口方法直接委托给_innerFactory
    public SqlExpression Add(SqlExpression left, SqlExpression right, RelationalTypeMapping typeMapping = null)
        => _innerFactory.Add(left, right, typeMapping);

    // ... 其余接口方法同理实现
}

在TranslationOptionsExtension中注册装饰器:

public void ApplyServices(IServiceCollection services)
{
    services.AddTransient<ISqlExpressionFactory>(sp =>
    {
        var providerFactory = sp.GetRequiredKeyedService<ISqlExpressionFactory>(sp.GetRequiredService<IDatabaseProvider>().Name);
        return new TranslationSqlExpressionFactoryDecorator(providerFactory);
    });
}

该方案保留提供商原生实现,仅注入翻译逻辑,无需为每个提供商编写子类。

2. 利用EF Core查询拦截器

通过IQueryExpressionInterceptor拦截查询表达式,修改查询树来添加翻译表左连接:

internal sealed class TranslationQueryInterceptor : IQueryExpressionInterceptor
{
    public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionInterceptorContext context)
    {
        return new TranslationExpressionVisitor(context).Visit(queryExpression);
    }

    private class TranslationExpressionVisitor : ExpressionVisitor
    {
        private readonly QueryExpressionInterceptorContext _context;

        public TranslationExpressionVisitor(QueryExpressionInterceptorContext context)
        {
            _context = context;
        }

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.DeclaringType == typeof(Queryable) && node.Arguments.Count > 0)
            {
                var source = node.Arguments[0];
                if (source is ConstantExpression constant && constant.Value is IQueryable queryable)
                {
                    var entityType = _context.Model.FindEntityType(queryable.ElementType);
                    if (entityType != null && IsTranslatable(entityType))
                    {
                        var translatedQuery = BuildTranslatedQuery(queryable, entityType, _context);
                        var newSource = Expression.Constant(translatedQuery);
                        var newArguments = new List<Expression> { newSource };
                        newArguments.AddRange(node.Arguments.Skip(1));
                        return Expression.Call(node.Method, newArguments);
                    }
                }
            }
            return base.VisitMethodCall(node);
        }

        private IQueryable BuildTranslatedQuery(IQueryable source, IEntityType entityType, QueryExpressionInterceptorContext context)
        {
            var dbContext = context.Context;
            var translationEntityType = GetTranslationEntityType(entityType);
            var translationDbSet = dbContext.GetType().GetProperty(translationEntityType.ClrType.Name)?.GetValue(dbContext) as IQueryable;

            if (translationDbSet == null) return source;

            var parameterO = Expression.Parameter(entityType.ClrType, "o");
            var parameterT = Expression.Parameter(translationEntityType.ClrType, "t");

            var idPropertyO = Expression.Property(parameterO, entityType.FindPrimaryKey().Properties.Single().Name);
            var idPropertyT = Expression.Property(parameterT, translationEntityType.FindProperty("Id").Name);
            var languageIdPropertyT = Expression.Property(parameterT, translationEntityType.FindProperty("LanguageId").Name);
            var languageIdConstant = Expression.Constant(GetCurrentLanguageId());

            var joinCondition = Expression.AndAlso(
                Expression.Equal(idPropertyO, idPropertyT),
                Expression.Equal(languageIdPropertyT, languageIdConstant));

            // 构建投影:对可翻译属性使用COALESCE
            var projectionProperties = new List<MemberBinding>();
            foreach (var property in entityType.GetProperties())
            {
                var translationProperty = translationEntityType.FindProperty(property.Name);
                if (translationProperty == null)
                {
                    projectionProperties.Add(Expression.Bind(property.PropertyInfo, Expression.Property(parameterO, property.Name)));
                    continue;
                }

                var propertyO = Expression.Property(parameterO, property.Name);
                var propertyT = Expression.Property(parameterT, translationProperty.Name);
                var coalesce = Expression.Coalesce(propertyT, propertyO);
                projectionProperties.Add(Expression.Bind(property.PropertyInfo, coalesce));
            }

            var projection = Expression.MemberInit(Expression.New(entityType.ClrType), projectionProperties);

            // 动态构建左连接查询表达式
            var query = source.Provider.CreateQuery(
                Expression.Call(
                    typeof(Queryable),
                    "GroupJoin",
                    new[] { entityType.ClrType, translationEntityType.ClrType, typeof(object) },
                    source.Expression,
                    translationDbSet.Expression,
                    Expression.Lambda(idPropertyO, parameterO),
                    Expression.Lambda(idPropertyT, parameterT),
                    Expression.Lambda(
                        Expression.Call(
                            typeof(Enumerable),
                            "DefaultIfEmpty",
                            new[] { translationEntityType.ClrType },
                            Expression.Parameter(typeof(IEnumerable<>).MakeGenericType(translationEntityType.ClrType), "tj")),
                        Expression.Parameter(typeof(IEnumerable<>).MakeGenericType(translationEntityType.ClrType), "tj")))
                .Call(
                    typeof(Queryable),
                    "SelectMany",
                    new[] { entityType.ClrType, translationEntityType.ClrType, entityType.ClrType },
                    Expression.Parameter(typeof(IQueryable<>).MakeGenericType(typeof(IGrouping<,>).MakeGenericType(typeof(object), translationEntityType.ClrType))),
                    Expression.Lambda(projection, parameterO, parameterT)));

            return query;
        }
    }
}

在DbContext中注册拦截器:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.AddInterceptors(new TranslationQueryInterceptor());
}

该方案侵入性低,直接修改查询树,兼容性最好,无需关注提供商差异。

3. 实体映射到翻译视图

将原实体映射到包含左连接逻辑的数据库视图,结合参数传递实现多语言查询:

首先创建数据库视图(或动态生成):

CREATE VIEW ProductTranslated AS
SELECT o.Id, COALESCE(t.Name, o.Name) AS Name, COALESCE(t.Description, o.Description) AS Description
FROM Product o
LEFT JOIN ProductTranslation t ON t.Id = o.Id AND t.LanguageId = @LanguageId

在EF Core中配置实体映射:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .ToView("ProductTranslated")
        .HasQueryFilter(p => EF.Property<int>(p, "LanguageId") == GetCurrentLanguageId());
}

该方案适合简单场景,动态语言切换需结合DbCommandInterceptor替换参数。

总结

  • 装饰器模式:灵活度高,保留提供商原生逻辑,适合深度定制SQL生成的场景。
  • 查询拦截器:侵入性低,兼容性强,无需关注提供商差异,推荐优先使用。
  • 视图映射:适合静态语言场景,依赖数据库视图,动态切换需额外处理。

内容的提问来源于stack exchange,提问作者vernou

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 00:54:57