EF (6 и Core) исторически не поддерживает эту стандартную конструкцию SQL.Скорее всего, из-за отсутствия стандартного метода LINQ и технических трудностей сопоставления Select(expr).Distinct().Count()
с ним.
Хорошо, что EF Core расширяется за счет замены многих его внутренних служб пользовательскими производными реализациями для переопределениянеобходимое поведение.Не просто, требует много сантехнического кода, но выполнимо.
Поэтому идея состоит в том, чтобы добавить и использовать простые пользовательские CountDistinct
методы, подобные этому
public static int CountDistinct<T, TKey>(this IQueryable<T> source, Expression<Func<T, TKey>> keySelector)
=> source.Select(keySelector).Distinct().Count();
public static int CountDistinct<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
=> source.Select(keySelector).Distinct().Count();
, и позволить EF Core каким-то образом перевестиих в SQL.На самом деле EF Core предоставляет простой способ определения (и даже пользовательского перевода) скалярных функций базы данных, но, к сожалению, его нельзя использовать для агрегатных функций, имеющих отдельный конвейер обработки.Поэтому нам нужно углубиться в инфраструктуру EF Core.
Полный код для этого для конвейера EF Core 2.x приведен в конце.Не уверен, стоит ли усилий, потому что EF Core 3.0 будет использовать полностью переписанный конвейер процесса запросов.Но это было интересно, и я также уверен, что его можно обновить для нового (надеюсь, более простого) конвейера.
В любом случае, все, что вам нужно, это скопировать / вставить код в новый файл кода в проекте.добавьте следующее в контекст OnConfiguring
override
optionsBuilder.UseCustomExtensions();
, который включит функциональность в инфраструктуру EF Core, а затем запрос, подобный этому,
var result = db.MyTable
.GroupBy(x => x.PersonID, x => new { VisitStartDate = x.VisitStart.Date })
.Select(g => new
{
Count = g.CountDistinct(x => x.VisitStartDate)
}).ToList();
, к счастью, будет переведен втребуемый
SELECT COUNT(DISTINCT(CONVERT(date, [x].[VisitStart]))) AS [Count]
FROM [MyTable] AS [x]
GROUP BY [x].[PersonID]
Обратите внимание на предварительный выбор выражения, необходимого для статистического метода.Это текущее ограничение / требование EF Core для всех агрегатных методов, а не только для наших.
Наконец, полный код, который творит чудеса:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.Expressions;
using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors;
using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Remotion.Linq;
using Remotion.Linq.Clauses;
using Remotion.Linq.Clauses.ResultOperators;
using Remotion.Linq.Clauses.StreamedData;
using Remotion.Linq.Parsing.Structure.IntermediateModel;
namespace Microsoft.EntityFrameworkCore
{
public static partial class CustomExtensions
{
public static int CountDistinct<T, TKey>(this IQueryable<T> source, Expression<Func<T, TKey>> keySelector)
=> source.Select(keySelector).Distinct().Count();
public static int CountDistinct<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
=> source.Select(keySelector).Distinct().Count();
public static DbContextOptionsBuilder UseCustomExtensions(this DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.ReplaceService<INodeTypeProviderFactory, CustomNodeTypeProviderFactory>()
.ReplaceService<IRelationalResultOperatorHandler, CustomRelationalResultOperatorHandler>();
}
}
namespace Remotion.Linq.Parsing.Structure.IntermediateModel
{
public sealed class CountDistinctExpressionNode : ResultOperatorExpressionNodeBase
{
public CountDistinctExpressionNode(MethodCallExpressionParseInfo parseInfo, LambdaExpression optionalSelector)
: base(parseInfo, null, optionalSelector) { }
public static IEnumerable<MethodInfo> GetSupportedMethods()
=> typeof(CustomExtensions).GetTypeInfo().GetDeclaredMethods("CountDistinct");
public override Expression Resolve(ParameterExpression inputParameter, Expression expressionToBeResolved, ClauseGenerationContext clauseGenerationContext)
=> throw CreateResolveNotSupportedException();
protected override ResultOperatorBase CreateResultOperator(ClauseGenerationContext clauseGenerationContext)
=> new CountDistinctResultOperator();
}
}
namespace Remotion.Linq.Clauses.ResultOperators
{
public sealed class CountDistinctResultOperator : ValueFromSequenceResultOperatorBase
{
public override ResultOperatorBase Clone(CloneContext cloneContext) => new CountDistinctResultOperator();
public override StreamedValue ExecuteInMemory<T>(StreamedSequence input) => throw new NotSupportedException();
public override IStreamedDataInfo GetOutputDataInfo(IStreamedDataInfo inputInfo) => new StreamedScalarValueInfo(typeof(int));
public override string ToString() => "CountDistinct()";
public override void TransformExpressions(Func<Expression, Expression> transformation) { }
}
}
namespace Microsoft.EntityFrameworkCore.Query.Internal
{
public class CustomNodeTypeProviderFactory : DefaultMethodInfoBasedNodeTypeRegistryFactory
{
public CustomNodeTypeProviderFactory()
=> RegisterMethods(CountDistinctExpressionNode.GetSupportedMethods(), typeof(CountDistinctExpressionNode));
}
public class CustomRelationalResultOperatorHandler : RelationalResultOperatorHandler
{
private static readonly ISet<Type> AggregateResultOperators = (ISet<Type>)
typeof(RequiresMaterializationExpressionVisitor).GetField("_aggregateResultOperators", BindingFlags.NonPublic | BindingFlags.Static)
.GetValue(null);
static CustomRelationalResultOperatorHandler()
=> AggregateResultOperators.Add(typeof(CountDistinctResultOperator));
public CustomRelationalResultOperatorHandler(IModel model, ISqlTranslatingExpressionVisitorFactory sqlTranslatingExpressionVisitorFactory, ISelectExpressionFactory selectExpressionFactory, IResultOperatorHandler resultOperatorHandler)
: base(model, sqlTranslatingExpressionVisitorFactory, selectExpressionFactory, resultOperatorHandler)
{ }
public override Expression HandleResultOperator(EntityQueryModelVisitor entityQueryModelVisitor, ResultOperatorBase resultOperator, QueryModel queryModel)
=> resultOperator is CountDistinctResultOperator ?
HandleCountDistinct(entityQueryModelVisitor, resultOperator, queryModel) :
base.HandleResultOperator(entityQueryModelVisitor, resultOperator, queryModel);
private Expression HandleCountDistinct(EntityQueryModelVisitor entityQueryModelVisitor, ResultOperatorBase resultOperator, QueryModel queryModel)
{
var queryModelVisitor = (RelationalQueryModelVisitor)entityQueryModelVisitor;
var selectExpression = queryModelVisitor.TryGetQuery(queryModel.MainFromClause);
var inputType = queryModel.SelectClause.Selector.Type;
if (CanEvalOnServer(queryModelVisitor)
&& selectExpression != null
&& selectExpression.Projection.Count == 1)
{
PrepareSelectExpressionForAggregate(selectExpression, queryModel);
var expression = selectExpression.Projection[0];
var subExpression = new SqlFunctionExpression(
"DISTINCT", inputType, new[] { expression.UnwrapAliasExpression() });
selectExpression.SetProjectionExpression(new SqlFunctionExpression(
"COUNT", typeof(int), new[] { subExpression }));
return new ResultTransformingExpressionVisitor<int>(
queryModelVisitor.QueryCompilationContext, false)
.Visit(queryModelVisitor.Expression);
}
else
{
queryModelVisitor.RequiresClientResultOperator = true;
var typeArgs = new[] { inputType };
var distinctCall = Expression.Call(
typeof(Enumerable), "Distinct", typeArgs,
queryModelVisitor.Expression);
return Expression.Call(
typeof(Enumerable), "Count", typeArgs,
distinctCall);
}
}
private static bool CanEvalOnServer(RelationalQueryModelVisitor queryModelVisitor) =>
!queryModelVisitor.RequiresClientEval && !queryModelVisitor.RequiresClientSelectMany &&
!queryModelVisitor.RequiresClientJoin && !queryModelVisitor.RequiresClientFilter &&
!queryModelVisitor.RequiresClientOrderBy && !queryModelVisitor.RequiresClientResultOperator &&
!queryModelVisitor.RequiresStreamingGroupResultOperator;
}
}