调用ExecuteUpdate时,Interceptor中SetPropertyCall用变量值致崩溃
解决EF Core ExecuteUpdate拦截器中变量无法翻译的问题
问题原因
当在SetProperty中引用静态变量AppContext.user时,生成的表达式树是字段访问表达式(MemberExpression),EF Core无法将内存中的静态字段引用转换为数据库能识别的SQL常量,因此抛出翻译失败的异常。而内联常量(如new Guid("XXX"))会被直接编译为ConstantExpression,EF Core可以正常转换。
解决方案
通过表达式访问器,将表达式树中的静态字段引用替换为当前的常量值,让EF Core能识别并转换为SQL常量。
步骤1:实现表达式访问器
创建一个表达式访问器,用于替换指定字段引用为常量值:
internal class ReplaceFieldWithConstantVisitor : ExpressionVisitor { private readonly FieldInfo _targetField; private readonly object _constantValue; public ReplaceFieldWithConstantVisitor(FieldInfo targetField, object constantValue) { _targetField = targetField; _constantValue = constantValue; } protected override Expression VisitMember(MemberExpression node) { if (node.Member == _targetField) { return Expression.Constant(_constantValue, node.Type); } return base.VisitMember(node); } } // 辅助方法:替换表达式中的参数 public static class ExpressionExtensions { public static Expression ReplaceParameter(this Expression expression, ParameterExpression oldParam, Expression newParam) { return new ParameterReplacer(oldParam, newParam).Visit(expression); } private class ParameterReplacer : ExpressionVisitor { private readonly ParameterExpression _oldParam; private readonly Expression _newParam; public ParameterReplacer(ParameterExpression oldParam, Expression newParam) { _oldParam = oldParam; _newParam = newParam; } protected override Expression VisitParameter(ParameterExpression node) { return node == _oldParam ? _newParam : base.VisitParameter(node); } } }
步骤2:修改拦截器逻辑
在拦截器处理ExecuteUpdate表达式时,替换SetBaseProperty中的user字段引用为当前常量值:
internal class ExecuteUpdateInterceptor : IQueryExpressionInterceptor { private List<(Type Type, Delegate Calls, Func<IEntityType, bool> Filter)> items = new(); private readonly FieldInfo _userField = typeof(AppContext).GetField(nameof(AppContext.user), BindingFlags.Public | BindingFlags.Static); public ExecuteUpdateInterceptor Add<TSource>( Func<Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>>> source, Func<IEntityType, bool> filter = null) { items.Add((typeof(TSource), source, filter)); return this; } Expression IQueryExpressionInterceptor.QueryCompilationStarting( Expression queryExpression, QueryExpressionEventData eventData) { try { if (queryExpression is MethodCallExpression call && call.Method.DeclaringType == typeof(RelationalQueryableExtensions) && (call.Method.Name == nameof(RelationalQueryableExtensions.ExecuteUpdate) || call.Method.Name == nameof(RelationalQueryableExtensions.ExecuteUpdateAsync))) { var setPropertyCalls = (LambdaExpression)((UnaryExpression)call.Arguments[1]).Operand; var body = setPropertyCalls.Body; var parameter = setPropertyCalls.Parameters[0]; var targetType = eventData.Context?.Model.FindEntityType(parameter.Type.GetGenericArguments()[0]); if (targetType != null) { foreach (var item in items) { if (!item.Type.IsAssignableFrom(targetType.ClrType)) continue; if (item.Filter != null && !item.Filter(targetType)) continue; // 获取通用的SetProperty调用表达式 var calls = (LambdaExpression)item.Calls.Method.GetGenericMethodDefinition() .MakeGenericMethod(targetType.ClrType) .Invoke(null, null); // 替换表达式中的user字段为当前常量值 if (_userField != null) { var currentUserId = _userField.GetValue(null); calls = (LambdaExpression)new ReplaceFieldWithConstantVisitor(_userField, currentUserId).Visit(calls); } // 合并表达式 body = calls.Body.ReplaceParameter(calls.Parameters[0], body); } if (body != setPropertyCalls.Body) { return call.Update(call.Object, new[] { call.Arguments[0], Expression.Lambda(body, parameter) }); } } } return queryExpression; } catch (Exception ex) { throw; } } }
可选优化:动态获取当前用户(ASP.NET Core场景)
如果是ASP.NET Core项目,静态变量存储用户ID不是最佳实践,建议注入IHttpContextAccessor来获取当前请求的用户ID:
- 拦截器注入
IHttpContextAccessor:
private readonly IHttpContextAccessor _httpContextAccessor; public ExecuteUpdateInterceptor(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; }
- 替换用户值的逻辑改为:
// 从HttpContext获取当前用户ID(示例,根据实际认证方式调整) var currentUserId = Guid.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)); calls = (LambdaExpression)new ReplaceFieldWithConstantVisitor(_userField, currentUserId).Visit(calls);
验证效果
修改完成后,拦截器会自动将SetProperty(p => p.Author, user)中的user替换为当前的常量值,生成的表达式树包含常量,EF Core可以正常翻译为SQL语句,不会再抛出翻译异常。
内容的提问来源于stack exchange,提问作者Kyeman
相关产品推荐
相关产品推荐

