Code first Entity Framework 6.1 Пользовательская агрегатная функция

У меня есть пользовательская функция CLR Aggregate на SQL Server для вычисления процентилей. Можно ли вызвать мою пользовательскую агрегатную функцию через Entity Framework? Как настроено сопоставление, чтобы разрешить это?

Я попытался использовать функции codefirst, аналогичные описанным в Entity Framework 6 Code First Custom Functions., однако функциям, по-видимому, разрешено принимать только параметры масштабирования, где моя функция является агрегатной функцией, поэтому ей нужно будет принимать список элементов (аналогично тому, как работают Sum, Averagg и Count).

Агрегатные функции имеют следующую сигнатуру, принимая значение, из которого мы хотим получить медиану, и процентиль (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

person Alex    schedule 30.10.2015    source источник
comment
Можете ли вы показать, что вы пробовали, и сигнатуру метода CLR?   -  person Moho    schedule 30.10.2015
comment
Из доступной документации я предполагаю, что атрибут должен быть [DbFunction("CodeFirstDatabaseSchema", "Percentile")], а подпись должна быть public static Double Percentile(Double value, Int16 tile).   -  person Solomon Rutzky    schedule 30.10.2015
comment
@srutzky это то, чему эквивалентна сигнатура функции SQL, но поскольку это совокупная функция, чтобы иметь возможность использовать ее в C#, сигнатура C# должна принимать список элементов, чтобы она могла его агрегировать, аналогично сигнатуре Sum()   -  person Alex    schedule 30.10.2015
comment
Я думаю, вы можете просто добавить IEnumerable‹› к обоим входным параметрам и проверить это, верно? :-) Я просмотрел исходный код этого проекта CodePlex, и UDA не поддерживаются. Но учитывая, что для типа возвращаемого значения не требуется обнаружение БД, как для наборов результатов, я подумал, что это может не иметь значения. Но я подозреваю, что вам нужно IEnumerable вокруг обоих входных параметров, а не только первого, верно?   -  person Solomon Rutzky    schedule 30.10.2015
comment
@srutzky первый параметр - это то, что мы группируем, второй параметр - это просто константа процентиля, который мы хотим.   -  person Alex    schedule 30.10.2015
comment
Существует встроенная функция EF function SqlFunctions.ChecksumAggregate, которая выполняет агрегат, поэтому это кажется возможным, но не уверен, как обновить внутреннюю модель функций EF, доступных в базе данных.   -  person Alex    schedule 30.10.2015


Ответы (1)


Как упоминал @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, что в базе данных есть функция медианы. Мы можем сделать это в нашем 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, которую я здесь не рассматривал.

person Alex    schedule 30.10.2015
comment
Вы также должны иметь возможность использовать: var edmFloatListType = PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.Single).GetCollectionType() - person trs79; 11.10.2016