代码优先实体框架6.1自定义聚合函数

时间:2015-10-30 13:42:34

标签: entity-framework aggregate-functions sqlclr entity-framework-6.1

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

我尝试使用类似于Entity Framework 6 Code First Custom Functions所描述的codefirstfunctions,但是函数似乎只允许使用scaler参数,其中我的函数是一个聚合函数,因此需要获取一个项目列表(类似于Sum,Averagg和Count的工作方式。)

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

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

我尝试添加DbFunctionAttribute,但不完全确定如何首先使用代码将其连接到实体框架存储模型。

[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

1 个答案:

答案 0 :(得分:2)

正如@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函数