当我需要在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日):
博客文章:
-
使用IMethodCallTranslator创建Entity Framework Core自定义函数
-
使用HasDbFunction创建Entity Framework Core自定义函数
更新(2019年7月27日):
感谢下方的评论,我发现需要一些澄清。
1) 如下方评论所指出的一样,还有另一种方法。使用
HasDbFunction
可以省去与 EF 注册翻译器的代码敲打,但仍需要
RowNumberExpression
,因为该函数有两组参数(用于
PARTITION BY
和
ORDER BY
),而现有的
SqlFunctionExpression
不支持这一点。(或者我错过了什么?)我之所以选择使用
IMethodCallTranslator
的方法是因为我希望在设置
DbContextOptionsBuilder
时完成此功能的配置,而不是在
OnModelCreating
中完成。因此,这是我的个人喜好。
最终,线程创建者也可以使用
HasDbFunction
来实现所需功能。在我的情况下,代码将如下所示:
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);
});
.Select(c => new {
RowNumber = DemoDbContext.DemoRowNumber(
new { c.Id },
new { c.RowVersion })
})
2) 匿名类型无法强制其成员的类型,因此如果函数被调用时使用了例如
integer
而不是
string
,则可能会出现运行时异常。尽管如此,它仍然可以是有效的解决方案。根据您为之工作的客户,解决方案可能更或者更少可行,最终决策取决于客户。不提供任何替代方案也是一种可能的解决方案,但并不令人满意。
特别是,如果不希望使用SQL(因为编译器的支持更少),那么运行时异常可能是一个好的折衷方案。
但是,如果这个折衷方案仍然不可接受,那么我们可以研究如何添加对数组的支持。
第一种方法是实现自定义的IExpressionFragmentTranslator
来“重定向”数组的处理到我们这里。
请注意,这只是一个原型,需要更多的调查/测试:-)
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;
}
}
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);
}
}
public class DemoRelationalCompositeExpressionFragmentTranslator
: RelationalCompositeExpressionFragmentTranslator
{
public DemoRelationalCompositeExpressionFragmentTranslator(
RelationalCompositeExpressionFragmentTranslatorDependencies dependencies)
: base(dependencies)
{
AddTranslators(new[] { new DemoArrayTranslator() });
}
}
services
.AddDbContext<DemoDbContext>(builder => builder
.ReplaceService<IExpressionFragmentTranslator,
DemoRelationalCompositeExpressionFragmentTranslator>());
为了测试,我引入了另一个重载方法,包含Guid[]
作为参数。尽管在我的使用情况下这个方法完全没有意义 :)
public static long RowNumber(this DbFunctions _, Guid[] orderBy)
并调整了该方法的使用
.Select(c => new {
RowNumber = EF.Functions.RowNumber(new Guid[] { c.Id })
})
public static bool FullText(string[] propertyReferences, string fullText)
。 - alejandroldevnew [] { … }
不支持在表达式内部,因此我们的转换根本不会被调用。而且不幸的是,您无法在查询之外创建数组,因为您需要从x =>
访问x
。但是,创建一个带有多个重载(string, string)
、(string, string, string)
、(string, string, string, string)
等的函数相对容易。如果这对您有用,请告诉我。因为处理new [] {…}
参数将需要深入EF Core基础设施。 - Ivan Stoev