J'essaie de construire quelque chose comme des requêtes conditionnelles pour obtenir uniquement les données nécessaires de la base de données sous-jacente.
Actuellement, j'ai la requête suivante (qui fonctionne bien)
var eventData = dbContext.Event.Select(t => new
{
Address = true ? new AnonymousEventGetAddress
{
AddressLine1 = t.Address.AddressLine1,
CityName = t.Address.AddressCityName
} : new AnonymousEventGetAddress(),
});
Si je le change en
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(),
});
Je reçois l'erreur suivante:
Le type 'AnonymousEventGetAddress' apparaît dans deux initialisations structurellement incompatibles au sein d'une même requête LINQ to Entities. Un type peut être initialisé à deux emplacements dans la même requête, mais uniquement si les mêmes propriétés sont définies dans les deux emplacements et si ces propriétés sont définies dans le même ordre.
Qu'est-ce que je fais de mal ici (à compter de la true
ça marche) et comment peut-on résoudre ce problème?
Je sais que changer la partie else
- en
new AnonymousEventGetAddress
{
AddressLine1 = null,
CityName = null
}
marchera. Mais si je change l'ordre des propriétés, cela échouera également.
La classe utilisée est définie comme suit:
public class AnonymousEventGetAddress : BaseAnonymousObject<AnonymousEventGetAddress>
{
public string AddressLine1 { get; set; }
public string CityName { get; set; }
}
alors que BaseAnonymousObject<AnonymousEventGetAddress>
est défini:
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();
}
Je ne sais pas pourquoi EF a une telle exigence, mais l'important est que cette exigence existe et que nous devions en tenir compte.
Le premier code fonctionne parce que true
est une constante de compilation, donc le compilateur le résout au moment de la compilation, aboutissant à l'une des deux expressions (en supprimant essentiellement l'opérateur ternaire). Alors que dans le second cas, il s'agit d'une variable, l'arborescence d'expression contient donc l'expression d'origine et échoue au moment de l'exécution en raison de la nécessité susmentionnée d'EF.
Il y a quelque temps, j'essayais de résoudre ce problème et des problèmes similaires (pour être honnête, principalement pour les filtres where
dynamiques) en implémentant une méthode personnalisée qui essaye de résoudre les variables bool. premier cas. Bien sûr, le code est expérimental et non testé, mais semble gérer correctement de tels scénarios, vous pouvez donc l'essayer. L'utilisation est assez simple:
var eventData = dbContext.Event.Select(t => new
{
Address = includeAddress ? new AnonymousEventGetAddress
{
AddressLine1 = t.Address.AddressLine1,
CityName = t.Address.AddressCityName
} : new AnonymousEventGetAddress(),
}).ReduceConstPredicates();
Et voici la méthode d'assistance utilisée:
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;
}
}
}
À mon avis, j'essaie toujours d'éviter de mettre des choses plus compliquées que de sélectionner des données dans IQueryable
car elles sont Expression
, ce qui signifie qu'elles ne sont jamais exécutées - elles sont compilées.
Donc, je voudrais aborder ce problème comme suit (cela a une belle simplicité):
Créez unDTOpour les données de retour:
public class EventDto
{
// some properties here that you need
public Address Address {get;set;}
}
Ensuite, je partagerais votre logique autour de la 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.
}
La méthode NoAddress
ou celle que vous souhaitez appeler sera similaire, mais sans Address
, et vous la mapperez.
Vous pouvez ensuite choisir lequel simplement:
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(); });
Si vous avez beaucoup de logique dans Select
, j’envisagerais alors de le déplacer dans un sproc où il sera plus facile de lire le code SQL.
Évidemment, ceci est juste un guide, vous donne une idée sur la façon de résoudre le problème.
Dans certaines circonstances, une solution de contournement simple peut être possible: indiquez le type sous différents types. Par exemple. faire 2 sous-classes de la classe d'origine. Cette solution de contournement est bien sûr assez sale, mais l’exigence de Linq est artificielle en soi. Dans mon cas, cela a aidé.
J'ai eu le même problème et j'ai trouvé la solution: ajouter .ToList () avant la fonction de sélection:
var eventData = dbContext.Event.ToList().Select(t => new
{
Address = includeAddress ? new AnonymousEventGetAddress
{
AddressLine1 = t.Address.AddressLine1,
CityName = t.Address.AddressCityName
} : new AnonymousEventGetAddress(),
});
Vous pouvez placer l'instruction conditionnelle dans chaque initialiseur de propriété.
var eventData = dbContext.Event.Select(t => new
{
Address = new AnonymousEventGetAddress
{
AddressLine1 = includeAddress ? t.Address.AddressLine1 : null,
CityName = includeAddress ? t.Address.AddressCityName : null
}
});