Тип появляется в двух структурно несовместимых инициализациях внутри одного запроса LINQ to Entities

Я пытаюсь построить нечто вроде условных запросов, чтобы получить только необходимые данные из базовой базы данных.

В настоящее время у меня есть следующий запрос (который отлично работает)

var eventData = dbContext.Event.Select(t => new
    {
        Address = true ? new AnonymousEventGetAddress
        {
            AddressLine1 = t.Address.AddressLine1,
            CityName = t.Address.AddressCityName
        } : new AnonymousEventGetAddress(),
    });

Если я изменю его на

var includeAddress = true; // this will normally be passed as param

var eventData = dbContext.Event.Select(t => new
    {
        Address = includeAddress ? new AnonymousEventGetAddress
        {
            AddressLine1 = t.Address.AddressLine1,
            CityName = t.Address.AddressCityName
        } : new AnonymousEventGetAddress(),
    });

Я получаю следующую ошибку:

Тип 'AnonymousEventGetAddress' появляется в двух структурно несовместимых инициализациях в одном запросе LINQ to Entities. Тип может быть инициализирован в двух местах в одном запросе, но только если в обоих местах установлены одни и те же свойства, и эти свойства заданы в одном порядке.

Что я делаю неправильно здесь (как с true он работает) и как это можно исправить?

Я знаю, что изменение else -part на

new AnonymousEventGetAddress
{
    AddressLine1 = null,
    CityName = null
}

будет работать. Но если я изменю порядок свойств, это также не сработает.

Используемый класс определяется следующим образом:

public class AnonymousEventGetAddress : BaseAnonymousObject<AnonymousEventGetAddress>
{
    public string AddressLine1 { get; set; }
    public string CityName { get; set; }
}

тогда как BaseAnonymousObject<AnonymousEventGetAddress> определяется:

public abstract class BaseAnonymousObject<TAnonymous>
    where TAnonymous : BaseAnonymousObject<TAnonymous>
{
    // this is used in case I have to return a list instead of a single anonymous object
    public static Expression<Func<IEnumerable<TAnonymous>>> Empty => () => new TAnonymous[]{}.AsEnumerable();
}

Ответ 1

Я не знаю, почему EF имеет такое требование, но важно то, что это требование существует, и мы должны его учитывать.

Первый код работает, потому что true является константой времени компиляции, поэтому компилятор разрешает ее во время компиляции, заканчивая одним из двух выражений (в основном удаляя тройной оператор). Хотя во втором случае это переменная , поэтому дерево выражений содержит исходное выражение и не работает во время выполнения из-за вышеупомянутого требования к EF.

Некоторое время назад я пытался решить эту и подобные проблемы (если честно, в основном для динамических фильтров where), реализовав собственный метод, который пытается разрешить переменные bool, тем самым делая во время выполнения нечто похожее на то, что делает компилятор в первом случае. Конечно, код экспериментальный и не проверенный, но, похоже, правильно обрабатывает такие сценарии, поэтому вы можете попробовать. Использование довольно просто:

var eventData = dbContext.Event.Select(t => new
    {
        Address = includeAddress ? new AnonymousEventGetAddress
        {
            AddressLine1 = t.Address.AddressLine1,
            CityName = t.Address.AddressCityName
        } : new AnonymousEventGetAddress(),
    }).ReduceConstPredicates();

И здесь используется вспомогательный метод:

public static partial class QueryableExtensions
{
    public static IQueryable<T> ReduceConstPredicates<T>(this IQueryable<T> source)
    {
        var visitor = new ConstPredicateReducer();
        var expression = visitor.Visit(source.Expression);
        if (expression != source.Expression)
            return source.Provider.CreateQuery<T>(expression);
        return source;
    }

    class ConstPredicateReducer : ExpressionVisitor
    {
        int evaluateConst;
        private ConstantExpression TryEvaluateConst(Expression node)
        {
            evaluateConst++;
            try { return Visit(node) as ConstantExpression; }
            finally { evaluateConst--; }
        }
        protected override Expression VisitConditional(ConditionalExpression node)
        {
            var testConst = TryEvaluateConst(node.Test);
            if (testConst != null)
                return Visit((bool)testConst.Value ? node.IfTrue : node.IfFalse);
            return base.VisitConditional(node);
        }
        protected override Expression VisitBinary(BinaryExpression node)
        {
            if (node.Type == typeof(bool))
            {
                var leftConst = TryEvaluateConst(node.Left);
                var rightConst = TryEvaluateConst(node.Right);
                if (leftConst != null || rightConst != null)
                {
                    if (node.NodeType == ExpressionType.AndAlso)
                    {
                        if (leftConst != null) return (bool)leftConst.Value ? (rightConst ?? Visit(node.Right)) : Expression.Constant(false);
                        return (bool)rightConst.Value ? Visit(node.Left) : Expression.Constant(false);
                    }
                    else if (node.NodeType == ExpressionType.OrElse)
                    {

                        if (leftConst != null) return !(bool)leftConst.Value ? (rightConst ?? Visit(node.Right)) : Expression.Constant(true);
                        return !(bool)rightConst.Value ? Visit(node.Left) : Expression.Constant(true);
                    }
                    else if (leftConst != null && rightConst != null)
                    {
                        var result = Expression.Lambda<Func<bool>>(Expression.MakeBinary(node.NodeType, leftConst, rightConst)).Compile().Invoke();
                        return Expression.Constant(result);
                    }
                }
            }
            return base.VisitBinary(node);
        }
        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (evaluateConst > 0)
            {
                var objectConst = node.Object != null ? TryEvaluateConst(node.Object) : null;
                if (node.Object == null || objectConst != null)
                {
                    var arguments = new object[node.Arguments.Count];
                    bool canEvaluate = true;
                    for (int i = 0; i < arguments.Length; i++)
                    {
                        var argumentConst = TryEvaluateConst(node.Arguments[i]);
                        if (canEvaluate = (argumentConst != null))
                            arguments[i] = argumentConst.Value;
                        else
                            break;
                    }
                    if (canEvaluate)
                    {
                        var result = node.Method.Invoke(objectConst != null ? objectConst.Value : null, arguments);
                        return Expression.Constant(result, node.Type);
                    }
                }
            }
            return base.VisitMethodCall(node);
        }
        protected override Expression VisitUnary(UnaryExpression node)
        {
            if (evaluateConst > 0 && (node.NodeType == ExpressionType.Convert || node.NodeType == ExpressionType.ConvertChecked))
            {
                var operandConst = TryEvaluateConst(node.Operand);
                if (operandConst != null)
                {
                    var result = Expression.Lambda(node.Update(operandConst)).Compile().DynamicInvoke();
                    return Expression.Constant(result, node.Type);
                }
            }
            return base.VisitUnary(node);
        }
        protected override Expression VisitMember(MemberExpression node)
        {
            object value;
            if (evaluateConst > 0 && TryGetValue(node, out value))
                return Expression.Constant(value, node.Type);
            return base.VisitMember(node);
        }
        static bool TryGetValue(MemberExpression me, out object value)
        {
            object source = null;
            if (me.Expression != null)
            {
                if (me.Expression.NodeType == ExpressionType.Constant)
                    source = ((ConstantExpression)me.Expression).Value;
                else if (me.Expression.NodeType != ExpressionType.MemberAccess
                    || !TryGetValue((MemberExpression)me.Expression, out source))
                {
                    value = null;
                    return false;
                }
            }
            if (me.Member is PropertyInfo)
                value = ((PropertyInfo)me.Member).GetValue(source);
            else
                value = ((FieldInfo)me.Member).GetValue(source);
            return true;
        }
    }
}

Ответ 2

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

Ответ 3

Вы можете поместить условный оператор в каждый инициализатор свойств.

var eventData = dbContext.Event.Select(t => new
{
    Address = new AnonymousEventGetAddress
    {
        AddressLine1 = includeAddress ? t.Address.AddressLine1 : null,
        CityName = includeAddress ? t.Address.AddressCityName : null
    }
});

Ответ 4

По-моему, я всегда стараюсь уклониться от создания чего-то более сложного, чем выбор данных в IQueryable, потому что они Expression, что означает, что они никогда не исполняются - они скомпилированы.

Итак, я бы решил эту проблему так (это имеет приятный облик простоты):

Создайте DTO для возвращаемых данных:

public class EventDto
{
    // some properties here that you need

    public Address Address {get;set;}
}

Затем я разделил бы вашу логику вокруг includeAddress

public IEnumerable<EventDto> IncludeAddress(DbContext dbContext)
{
    return dbContext.Event.Select(t => new
    {
        // select out your other properties here

        Address = new
        {
            AddressLine1 = t.Address.AddressLine1,
            CityName = t.Address.AddressCityName
        },
    }).ToList().Select(x => new EventDto { Address = Address });
    // put what ever mapping logic you have up there, whether you use AutoMapper or hand map it doesn't matter.
}

Метод NoAddress или то, что вы хотите вызвать, будет выглядеть аналогично, но без Address, и вы вернете его обратно.

Затем вы можете выбрать, какой именно:

var eventDtos = new List<EventDto>();

if (includeAddress)
   eventDtos.AddRange(this.IncludeAddress(dbContext));
else
   eventDtos.AddRange(this.NoAddress(dbContext));

eventDtos.ForEach(e => { if (e.Address == null) e.Address = new Address(); });

Если у вас Select есть много логики, я бы подумал о том, чтобы переместить его в sproc, где будет легче читать SQL.

Очевидно, что это всего лишь руководство, дает вам представление о том, как решить эту проблему.

Ответ 5

Для будущих читателей этот ТА-дубликат (добавленный год спустя) стал ключом к решению моих проблем:

Тип появляется в двух структурно несовместимых инициализациях в одном запросе LINQ to Entities

Когда вы смотрите на это, сообщение об ошибке совершенно ясно. Не путайте порядок инициализации, если вы создаете объект более одного раза в одном выражении Linq. Для меня это было именно то, что я делал. При синхронизации инициализаций свойств между двумя вызовами экземпляров компилятор снова получил солнечный свет.

В этом случае:

new AnonymousEventGetAddress
{
    AddressLine1 = t.Address.AddressLine1,
    CityName = t.Address.AddressCityName
} 

отличается от

new AnonymousEventGetAddress()

В OP-запросе версии 1 можно с уверенностью сказать, что аберрантная инициализация в ветки else никогда не могла произойти из-за условия true, почему его, вероятно, отбросили, для версии 2, которая не должна была произойти, и мы осталось два порядка инициализации, свойства 1 и 2, а не свойства вообще. Это должно сделать это:

includeAddress
? new AnonymousEventGetAddress
{
    AddressLine1 = t.Address.AddressLine1,
    CityName = t.Address.AddressCityName
}
: new AnonymousEventGetAddress
{
    AddressLine1 = null,
    CityName = null
}

Ответ 6

У меня была та же проблема, и я нашел решение, чтобы добавить .ToList() перед функцией выбора:

var eventData = dbContext.Event.ToList().Select(t => new
{
    Address = includeAddress ? new AnonymousEventGetAddress
    {
        AddressLine1 = t.Address.AddressLine1,
        CityName = t.Address.AddressCityName
    } : new AnonymousEventGetAddress(),
});