.net 尝试使用字符串变量SetPropertyCall时,ExecuteUpdate的拦截器崩溃

c8ib6hqw  于 2023-06-25  发布在  .NET
关注(0)|答案(2)|浏览(184)

我尝试在调用ExecuteUpdate时通过拦截器为所有模型设置包含编辑日期和editUserId的基础模型。但是当我尝试设置用户ID时,程序崩溃并出现以下异常:
valueExpression:无法转换“AppContext.user))”。其他信息:以下“SetProperty”无法转换:'SetProperty(p => p.Author,AppContext.user)'。
这是我的密码

internal class ExecuteUpdateInterceptor : IQueryExpressionInterceptor
{
    private List<(Type Type, Delegate Calls, Func<IEntityType, bool> Filter)> items = new();

    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;
                        var calls = (LambdaExpression)item.Calls.Method.GetGenericMethodDefinition()
                            .MakeGenericMethod(targetType.ClrType)
                            .Invoke(null, null);
                        body = calls.Body.ReplaceParameter(calls.Parameters[0], body);
                    }
                    if (body != setPropertyCalls.Body)
                    { 
                        //body
                        return call.Update(call.Object, new[] { call.Arguments[0], Expression.Lambda(body, parameter) }); 
                    }
                }
            }
            return queryExpression;
        }
        catch (Exception ex)
        {
            throw;
        }
    }
}

和/或

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.AddInterceptors(new ExecuteUpdateInterceptor()
                        .Add(SetBaseProperty<BassyBase>));
    }
    public static Guid? user = new Guid("D4DB8C0E-E21B-4456-8974-22B2FFE0F2C1");

    private static Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> SetBaseProperty<TSource>()
    where TSource : BassyBase
    {
        Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> props = s => s
            .SetProperty(p => p.ChangeDate, DateTimeOffset.UtcNow)
            .SetProperty(p => p.Author, user);

        return props;
    }

如果我试着用SetProperty(p => p.Author, Guid.NewGuid() or new Guid("XXX") )将其设置为内联,它就可以工作了。
但是当我试图从变量中获取字符串或guid时,它似乎试图发送变量的引用,而不是导致查询崩溃的值。

s1ag04yj

s1ag04yj1#

相当烦人的EF核心bug/限制。很长一段时间,我们(为表达式树操作编写扩展/解决方案的人)要求在查询翻译管道中提供扩展点,以便我们可以接收原始表达式树并在EF Core之前进行预处理/转换。我们最终得到了它,但事实证明,在典型的EF Core方式中,经过一些EF Core预处理,特别是查询的 * 参数化 *。
最后一个是非常重要的细节,因为它限制了您在拦截器中可以做的事情。通常使用捕获的变量或静态属性/字段/方法访问器的表达式如下所示(注意,我们在表达式树中,因此没有真实的的调用)

.SetProperty(p => p.Author, user)

被转换为(替换为)参数。但是由于拦截器是在参数化之后调用的,因此它们不会转换为参数,并且稍后会报告为未知/不支持的表达式是表达式树中的任何自定义方法调用。
Shorty,现在的方式是,你不能在结果表达式树中引入非常量表达式。为了使事情变得更难,C#没有提供从 * 变量 * 创建常量表达式的语法(即而不是使用const字段)。
所以,首先,最好在EF Core GitHub问题跟踪器中将此报告为错误并请求修复(人们会去投票)。其次,我看到的使用拦截器的唯一解决方法是将变量转换为常量。这可以通过手动构建表达式树来完成,或者像往常一样,通过创建带有附加参数“标记”的原型表达式,然后用常量值替换它们。
在你的例子中,它可能是这样的:

// Expression with user being a parameter
Expression<Func<SetPropertyCalls<TSource>, string, SetPropertyCalls<TSource>>> propsP = static (s, user) => s
    .SetProperty(p => p.ChangeDate, DateTimeOffset.UtcNow)
    .SetProperty(p => p.Author, user);

// The actual expression with user value being a constant
var props = Expression.Lambda<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>>(
    propsP.Body.ReplaceParameter(propsP.Parameters[1], Expression.Constant(user)),
    propsP.Parameters[0]);

return props;
k2arahey

k2arahey2#

您面临的问题与用于设置SetBaseProperty方法中Author属性的表达式的转换有关。当您使用变量(AppContext.user)而不是常量值时,查询转换将失败,因为它试图发送对变量的引用而不是其值。
若要解决此问题,可以通过在SetBaseProperty方法中创建局部变量来修改代码,并使用该变量设置Author属性。下面是代码的更新版本:

private static Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> SetBaseProperty<TSource>()
where TSource : BassyBase
{
Guid? author = user; // Create a local variable and assign the value

Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> props = s => s
    .SetProperty(p => p.ChangeDate, DateTimeOffset.UtcNow)
    .SetProperty(p => p.Author, author); // Use the local variable

return props;
}

通过创建局部变量author并将用户的值赋给它,可以确保查询转换将使用变量的值,而不是将其视为引用。这应该可以解决问题,并允许您使用AppContext.user值设置Author属性。

相关问题