当我需要 EF Core 中的 ROW_NUMBER 支持时,您的用例与我的非常相似。
例子:
// gets translated to
// ROW_NUMBER() OVER(PARTITION BY ProductId ORDER BY OrderId, Count)
DbContext.OrderItems.Select(o => new {
RowNumber = EF.Functions.RowNumber(o.ProductId, new {
o.OrderId,
o.Count
})
})
使用匿名类而不是数组
您要做的第一件事是从使用数组切换到匿名类,即您将调用从
DbContext.FullText(new[] { x.Col1, x.Col2, x.Col3 }, "keywords")
到
DbContext.FullText(new { x.Col1, x.Col2, x.Col3 }, "keywords")
参数的排序顺序将保持在查询中定义的顺序,
即new { x.Col1, x.Col2 } 将被翻译成Col1, Col2
和new { x.Col2, x.Col1 } 到Col2, Col1。
您甚至可以使用以下内容:new { x.Col1, _ = x.Col1, Foo = "bar" } 将被翻译为 Col1, Col1, 'bar'。
实现自定义IMethodCallTranslator
如果您需要一些提示,可以在Azure DevOps: RowNumber Support 上查看我的代码,或者如果您可以等待几天,那么我将提供有关自定义函数实现的博客文章。
更新(2019 年 7 月 31 日)
博文:
更新(2019 年 7 月 27 日)
感谢下面的 cmets,我看到需要进行一些澄清。
1) 正如下面评论中所指出的,还有另一种方法。使用HasDbFunction,我可以节省一些输入,例如使用 EF 注册翻译器的代码,但我仍然需要RowNumberExpression,因为该函数有 2 组参数(PARTITION BY 和 ORDER BY)和现有的SqlFunctionExpression 不支持。 (或者我错过了什么?)我选择IMethodCallTranslator 方法的原因是因为我希望在设置DbContextOptionsBuilder 而不是在OnModelCreating 中完成此功能的配置。也就是说,这是我的个人喜好。
最后,线程创建者也可以使用HasDbFunction 来实现所需的功能。就我而言,代码如下所示:
// OnModelCreating
var methodInfo = typeof(DemoDbContext).GetMethod(nameof(DemoRowNumber));
modelBuilder.HasDbFunction(methodInfo)
.HasTranslation(expressions => {
var partitionBy = (Expression[])((ConstantExpression)expressions.First()).Value;
var orderBy = (Expression[])((ConstantExpression)expressions.Skip(1).First()).Value;
return new RowNumberExpression(partitionBy, orderBy);
});
// the usage with this approach is identical to my current approach
.Select(c => new {
RowNumber = DemoDbContext.DemoRowNumber(
new { c.Id },
new { c.RowVersion })
})
2) 匿名类型不能强制执行其成员的类型,因此如果使用 integer 而不是 @ 调用函数,您可能会遇到运行时异常987654347@。尽管如此,它仍然是有效的解决方案。根据您正在为解决方案工作的客户,您的解决方案可能或多或少可行,最终决定权在于客户。不提供任何替代方案也是一种可能的解决方案,但并不令人满意。
特别是,如果不希望使用 SQL(因为您从编译器获得的支持更少),那么运行时异常毕竟可能是一个很好的折衷方案。
但是,如果妥协仍然不可接受,那么我们可以研究如何添加对数组的支持。
第一种方法可能是实现自定义 IExpressionFragmentTranslator 以将数组的处理“重定向”给我们。
请注意,这只是一个原型,需要更多调查/测试:-)
// to get into EF pipeline
public class DemoArrayTranslator : IExpressionFragmentTranslator
{
public Expression Translate(Expression expression)
{
if (expression?.NodeType == ExpressionType.NewArrayInit)
{
var arrayInit = (NewArrayExpression)expression;
return new DemoArrayInitExpression(arrayInit.Type, arrayInit.Expressions);
}
return null;
}
}
// lets visitors visit the array-elements
public class DemoArrayInitExpression : Expression
{
private readonly ReadOnlyCollection<Expression> _expressions;
public override Type Type { get; }
public override ExpressionType NodeType => ExpressionType.Extension;
public DemoArrayInitExpression(Type type,
ReadOnlyCollection<Expression> expressions)
{
Type = type ?? throw new ArgumentNullException(nameof(type));
_expressions = expressions ?? throw new ArgumentNullException(nameof(expressions));
}
protected override Expression Accept(ExpressionVisitor visitor)
{
var visitedExpression = visitor.Visit(_expressions);
return NewArrayInit(Type.GetElementType(), visitedExpression);
}
}
// adds our DemoArrayTranslator to the others
public class DemoRelationalCompositeExpressionFragmentTranslator
: RelationalCompositeExpressionFragmentTranslator
{
public DemoRelationalCompositeExpressionFragmentTranslator(
RelationalCompositeExpressionFragmentTranslatorDependencies dependencies)
: base(dependencies)
{
AddTranslators(new[] { new DemoArrayTranslator() });
}
}
// Register the translator
services
.AddDbContext<DemoDbContext>(builder => builder
.ReplaceService<IExpressionFragmentTranslator,
DemoRelationalCompositeExpressionFragmentTranslator>());
为了测试,我引入了另一个包含Guid[] 作为参数的重载。
虽然,这种方法在我的用例中根本没有意义 :)
public static long RowNumber(this DbFunctions _, Guid[] orderBy)
并调整了方法的使用
// Translates to ROW_NUMBER() OVER(ORDER BY Id)
.Select(c => new {
RowNumber = EF.Functions.RowNumber(new Guid[] { c.Id })
})