Code first Entity Framework 6.1 自定义聚合函数

Code first Entity Framework 6.1 Custom Aggregate Function

我在 SQL 服务器上有一个自定义 CLR 聚合函数来计算百分位数。是否可以通过 Entity Framework 调用我的自定义聚合函数?映射是如何配置的?

我曾尝试使用类似于 中描述的 codefirstfunctions,但是这些函数似乎只允许采用缩放器参数,其中我的函数是一个聚合函数,因此需要获取一个列表项(类似于 Sum、Averagg 和 Count 的工作方式)。

聚合函数具有以下签名,采用我们想要的中位数和百分位数的值(50 是中位数,25 是下四分位数,75 是上四分位数)

CREATE AGGREGATE [dbo].[Percentile]
(@value [float], @tile [smallint])
RETURNS[float]
EXTERNAL NAME [SqlFuncs].[Percentile]
GO

我尝试添加 DbFunctionAttribute,但不完全确定如何首先使用代码将其连接到 entity framework 商店模型。

[DbFunction("SqlServer", "Percentile")]

public static double? Percentile(IEnumerable<int?> arg, int tile)
{
    throw new NotSupportedException("Direct calls are not supported.");
}

我正在寻找的是能够写出类似

的东西
paymentsTable
    .GroupBy(x=>x.CustomerId)
    .Select(new{
            Median = MyDbContext.Percentile(x.Select(g=>g.Amount), 50)
    });

这将映射到 SQL 喜欢

SELECT [dbo].[Percentile](Amount, 50) as Median
FROM Payments
GROUP BY CustomerId

正如@srutzky 在评论中提到的那样,EF 似乎不喜欢绑定到具有多个参数的聚合函数。因此,您必须将百分位数函数更改为中值函数或您感兴趣的任何固定百分位数(您需要更新 SqlClr 函数以便参数也匹配)

public class MySqlFunctions
{
    [DbFunction("dbo", "Median")]
    public static float? Median(IEnumerable<float?> arg)
    {
        throw new NotSupportedException("Direct calls are not supported.");
    }
}

下一步是让 EF 知道数据库有一个名为 median 的函数,我们可以在 DbContext 中执行此操作。创建一个新的约定来访问 dbModel 然后我们在 dbModel 中添加函数。您必须确保参数和参数类型与 SQL 和 C# 函数完全匹配。

public class EmContext : DbContext
{    
    ...

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        //Register a convention so we can load our function
        modelBuilder.Conventions.Add(new AddMedianFunction());

        ...

    }

    public class AddMedianFunction : IConvention, IStoreModelConvention<EntityContainer>
    {
        public void Apply(EntityContainer item, DbModel dbModel)
        {
            //these parameter types need to match both the database method and the C# method for EF to link
            var edmFloatType = PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.Single);

            //CollectionType constructor is internal making it impossible to get a collection type. 
            //We resort to reflection instantiation.
            var edmFloatListType = CreateInstance<CollectionType>(edmFloatType);

            var medianfunction = EdmFunction.Create("Median", "dbo", DataSpace.SSpace, new EdmFunctionPayload
            {
                ParameterTypeSemantics = ParameterTypeSemantics.AllowImplicitConversion,
                IsComposable = true,
                IsAggregate = true,
                Schema = "dbo",
                ReturnParameters = new[]
                {
                    FunctionParameter.Create("ReturnType", edmFloatType, ParameterMode.ReturnValue)
                },
                Parameters = new[]
                {
                    FunctionParameter.Create("input", edmFloatListType, ParameterMode.In),
                }
            }, null);

            dbModel.StoreModel.AddItem(medianfunction);
            dbModel.Compile();       
        }

        public static T CreateInstance<T>(params object[] args)
        {
            var type = typeof(T);
            var instance = type.Assembly.CreateInstance(
                type.FullName, false,
                BindingFlags.Instance | BindingFlags.NonPublic,
                null, args, null, null);
            return (T)instance;
        }
    }
}

所有这些都准备就绪后,您应该能够按预期调用您的函数

paymentsTable
    .GroupBy(x=>x.CustomerId)
    .Select(new{
            Median = MySqlFunctions.Median(x.Select(g=>g.Amount))
    });

注意:我已经假设您已经加载了 SqlClr 函数,我没有在此处介绍