Как назначить значение свойства IQueryable <T>? - PullRequest
5 голосов
/ 12 апреля 2011

Сначала я использую Entity Framework 4.1 Code.В моей сущности у меня есть три свойства даты / времени:

public class MyEntity
{
    [Key]
    public Id { get; set; }

    public DateTime FromDate { get; set; }

    public DateTime ToDate { get; set; }

    [NotMapped]
    public DateTime? QueryDate { get; set; }

    // and some other fields, of course
}

В базе данных у меня всегда есть заполненные даты От / До.Я запрашиваю их, используя простое предложение where.Но в результирующем наборе я хочу включить дату, для которой я запрашивал.Мне нужно сохранить это для работы какой-то другой бизнес-логики.

Я работаю над методом расширения, чтобы сделать это, но у меня возникают проблемы:

public static IQueryable<T> WhereDateInRange<T>(this IQueryable<T> queryable, DateTime queryDate) where T : MyEntity
{
    // this part works fine
    var newQueryable = queryable.Where(e => e.FromDate <= queryDate &&
                                            e.ToDate >= queryDate);

    // in theory, this is what I want to do
    newQueryable = newQueryable.Select(e =>
                                           {
                                               e.QueryDate = queryDate;
                                               return e;
                                           });
    return newQueryable;
}

Это нене работаетЭто работает, если я использую IEnumerable, но я хочу сохранить его как IQueryable, чтобы все выполнялось на стороне базы данных, и этот метод расширения все еще можно использовать в любой части другого запроса.Когда это IQueryable, я получаю ошибку компиляции следующего:

Лямбда-выражение с телом оператора не может быть преобразовано в дерево выражений

Если это был SQL,Я бы просто сделал что-то вроде этого:

SELECT *, @QueryDate as QueryDate
FROM MyEntities
WHERE @QueryDate BETWEEN FromDate AND ToDate

Итак, вопрос в том, как я могу преобразовать дерево выражений, которое мне уже нужно, чтобы включить это дополнительное присвоение свойства?Я изучил IQueryable.Expression и IQueryable.Provider.CreateQuery - где-то есть решение.Может быть, выражение присваивания можно добавить к существующему дереву выражений?Я недостаточно знаком с методами дерева выражений, чтобы понять это.Любые идеи?

Пример использования

Чтобы уточнить, цель состоит в том, чтобы иметь возможность выполнить что-то вроде этого:

var entity = dataContext.Set<MyEntity>()
                        .WhereDateInRange(DateTime.Now)
                        .FirstOrDefault();

И иметьDateTime.Now сохраняется в QueryDate результирующей строки, БЕЗ возврата из запроса к базе данных более одной строки.(С помощью решения IEnumerable несколько строк возвращаются до того, как FirstOrDefault выберет нужную нам строку.)

Другая идея

Я могу пойти дальше и отобразить QueryDate как реальное полеи установите для DatabaseGeneratedOption значение Computed.Но тогда мне понадобится какой-нибудь способ вставить «@QueryDate as QueryDate» в SQL, созданный операторами выбора EF.Поскольку он вычисляется, EF не будет пытаться предоставлять значения во время обновления или вставки.Так как же я могу добавить собственный SQL в операторы select?

Ответы [ 4 ]

3 голосов
/ 13 апреля 2011

Ладислав абсолютно прав.Но так как вы, очевидно, хотите, чтобы ответили на вторую часть вашего вопроса, вот как вы можете использовать Assign.Это не будет работать с EF.

using System;
using System.Linq;
using System.Linq.Expressions;

namespace SO5639951
{
    static class Program
    {
        static void Main()
        {
            AdventureWorks2008Entities c = new AdventureWorks2008Entities();
            var data = c.Addresses.Select(p => p);

            ParameterExpression value = Expression.Parameter(typeof(Address), "value");
            ParameterExpression result = Expression.Parameter(typeof(Address), "result");
            BlockExpression block = Expression.Block(
                new[] { result },
                Expression.Assign(Expression.Property(value, "AddressLine1"), Expression.Constant("X")),
                Expression.Assign(result, value)
                );

            LambdaExpression lambdaExpression = Expression.Lambda<Func<Address, Address>>(block, value);

            MethodCallExpression methodCallExpression = 
                Expression.Call(
                    typeof(Queryable), 
                    "Select", 
                    new[]{ typeof(Address),typeof(Address) } , 
                    new[] { data.Expression, Expression.Quote(lambdaExpression) });

            var data2 = data.Provider.CreateQuery<Address>(methodCallExpression);

            string result1 = data.ToList()[0].AddressLine1;
            string result2 = data2.ToList()[0].AddressLine1;
        }
    }
}

Обновление 1

Вот тот же код после некоторой настройки.Я прочитал выражение «Блок», которое EF захлебнуло в коде выше, чтобы продемонстрировать с абсолютной ясностью, что это выражение «Назначить», которое EF не поддерживает.Обратите внимание, что Assign в принципе работает с общими деревьями выражений, это EF-провайдер, который не поддерживает Assign.

using System;
using System.Linq;
using System.Linq.Expressions;

namespace SO5639951
{
    static class Program
    {
        static void Main()
        {
            AdventureWorks2008Entities c = new AdventureWorks2008Entities();

            IQueryable<Address> originalData = c.Addresses.AsQueryable();

            Type anonType = new { a = new Address(), b = "" }.GetType();

            ParameterExpression assignParameter = Expression.Parameter(typeof(Address), "value");
            var assignExpression = Expression.New(
                anonType.GetConstructor(new[] { typeof(Address), typeof(string) }),
                assignParameter,
                Expression.Assign(Expression.Property(assignParameter, "AddressLine1"), Expression.Constant("X")));
            LambdaExpression lambdaAssignExpression = Expression.Lambda(assignExpression, assignParameter);

            var assignData = originalData.Provider.CreateQuery(CreateSelectMethodCall(originalData, lambdaAssignExpression));
            ParameterExpression selectParameter = Expression.Parameter(anonType, "value");
            var selectExpression = Expression.Property(selectParameter, "a");
            LambdaExpression lambdaSelectExpression = Expression.Lambda(selectExpression, selectParameter);

            IQueryable<Address> finalData = assignData.Provider.CreateQuery<Address>(CreateSelectMethodCall(assignData, lambdaSelectExpression));

            string result = finalData.ToList()[0].AddressLine1;
        }        

        static MethodCallExpression CreateSelectMethodCall(IQueryable query, LambdaExpression expression)
        {
            Type[] typeArgs = new[] { query.ElementType, expression.Body.Type };
            return Expression.Call(
                typeof(Queryable),
                "Select",
                typeArgs,
                new[] { query.Expression, Expression.Quote(expression) });

        }
    }
}
2 голосов
/ 12 апреля 2011

Нет, я не думаю, что есть решение. Это правда, что вы можете изменить дерево выражений, но вы получите точно такое же исключение, которое вы получили с вашим запросом linq, потому что этот запрос фактически является тем, что вы будете строить в дереве выражений. Проблема не в дереве выражений, а в отображении. EF не может отобразить QueryData на результат. Более того, вы пытаетесь сделать проекцию. Проекция не может быть выполнена для сопоставленной сущности, и анонимный тип не может быть возвращен из метода.

Вы, конечно, можете сделать выбор, который вы упомянули, но просто не можете сопоставить его с вашей сущностью. Вы должны создать новый тип для этого:

var query = from x in context.MyData
            where x.FromDate <= queryDate && x.ToDate >= queryDate
            select new MyDateWrapper
                {
                   MyData = x,
                   QueryDate = queryDate
                };
1 голос
/ 12 июня 2018

Automapper имеет Queryable Extensions, я думаю, что он может решить ваши потребности.Вы можете использовать ProjectTo для вычисления свойства во время выполнения.

Ef Core 2 установить значение игнорируемого свойства во время выполнения

http://docs.automapper.org/en/stable/Queryable-Extensions.html

Пример конфигурации:

 configuration.CreateMap(typeof(MyEntity), typeof(MyEntity))
    .ForMember(nameof(Entity.QueryDate), opt.MapFrom(src => DateTime.Now));

Использование:

queryable.ProjectTo<MyEntity>();
0 голосов
/ 13 апреля 2011

Спасибо за все ценные отзывы.Похоже, что ответ "нет - вы не можете сделать это таким образом".

Итак - я нашел обходной путь.Это очень специфично для моей реализации, но делает свое дело.

public class MyEntity
{       
    private DateTime? _queryDate;

    [ThreadStatic]
    internal static DateTime TempQueryDate;

    [NotMapped]
    public DateTime? QueryDate
    {
        get
        {
            if (_queryDate == null)
                _queryDate = TempQueryDate;
            return _queryDate;
        }
    }

    ...       
}

public static IQueryable<T> WhereDateInRange<T>(this IQueryable<T> queryable, DateTime queryDate) where T : MyEntity
{
    MyEntity.TempQueryDate = queryDate;

    return queryable.Where(e => e.FromDate <= queryDate && e.ToDate >= queryDate);
}

Волшебство в том, что я использую статическое поле потока, чтобы кэшировать дату запроса, чтобы оно было доступно позже в том же потоке.Тот факт, что я получаю его обратно в геттер QueryDate, специфичен для моих потребностей.

Очевидно, что это не решение EF или LINQ для исходного вопроса, но оно достигает того же эффекта, удаляя его измир.

...