J'utilise Dynamic Linq pour effectuer certaines requêtes (désolé mais c'est ma seule option). Par conséquent, IQueryable
un IQueryable
au lieu d'un IQueryable<T>
. Dans mon cas, je veux un IQueryable<Thing>
où Thing
est un type concret.
Ma requête est en tant que telle:
public IQueryable<Thing> Foo(MyContext db)
{
var rootQuery = db.People.Where(x => x.City != null && x.State != null);
var groupedQuery = rootQuery.GroupBy("new ( it.City, it.State )", "it", new []{"City", "State"});
var finalLogicalQuery = groupedQuery.Select("new ( Count() as TotalNumber, Key.City as City, Key.State as State )");
var executionDeferredResults = finalLogicalQuery.Take(10); // IQueryable
IQueryable<Thing> executionDeferredTypedThings = ??; // <--- Help here!!!!
return executionDeferredTypedThings;
}
My Thing.cs:
public class Thing
{
public int TotalNumber { get; set; }
public string City { get; set; }
public string State { get; set; }
}
Oui, je sais que la chose ci-dessus peut être faite sans Dynamic Linq mais j'ai une certaine variabilité en cours que j'ai simplifiée d'ici. Je peux le faire fonctionner avec ma variabilité si mon type de retour est simplement IQueryable
mais je ne peux pas comprendre comment convertir en IQueryable<Thing>
tout en conservant l'exécution différée et en gardant Entity Framework heureux. J'ai la Select
dynamique renvoyant toujours quelque chose (avec les données correctes) qui ressemble à une Thing
. Mais je ne peux tout simplement pas comprendre comment renvoyer le IQueryable<Thing>
et je pourrais y utiliser de l'aide. Merci!!
Sur la base de la suggestion de Rex M , j'essaie maintenant d'utiliser AutoMapper pour résoudre ce problème (même si je ne suis pas engagé dans cette approche et que je suis prêt à essayer d'autres approches). Pour l'approche AutoMapper, je le fais comme tel:
IQueryable<Thing> executionDeferredTypedThings = executionDeferredResults.ProjectTo<Thing>(); // <--- Help here!!!!
Mais cela se traduit par une InvalidOperationException:
Carte manquante de DynamicClass2 à Thing. Créez à l'aide de Mapper.CreateMap.
Le fait est que, bien que j'aie défini Thing
, je n'ai pas défini DynamicClass2
et en tant que tel, je ne peux pas le mapper.
IQueryable<Thing> executionDeferredTypedThings = db.People.Provider.CreateQuery<Thing>(executionDeferredResults.Expression);
Cela donne une InvalidCastException et semble être le même problème sous-jacent que les échecs de l'AutoMapper ci-dessus:
Impossible de caster un objet de type 'System.Data.Entity.Infrastructure.DbQuery'1 [DynamicClass2]' en type 'System.Linq.IQueryable'1 [MyDtos.Thing]'.
Si je comprends bien, la méthode d'extension suivante devrait faire le travail pour vous
public static class DynamicQueryableEx
{
public static IQueryable<TResult> Select<TResult>(this IQueryable source, string selector, params object[] values)
{
if (source == null) throw new ArgumentNullException("source");
if (selector == null) throw new ArgumentNullException("selector");
var dynamicLambda = System.Linq.Dynamic.DynamicExpression.ParseLambda(source.ElementType, null, selector, values);
var memberInit = dynamicLambda.Body as MemberInitExpression;
if (memberInit == null) throw new NotSupportedException();
var resultType = typeof(TResult);
var bindings = memberInit.Bindings.Cast<MemberAssignment>()
.Select(mb => Expression.Bind(
(MemberInfo)resultType.GetProperty(mb.Member.Name) ?? resultType.GetField(mb.Member.Name),
mb.Expression));
var body = Expression.MemberInit(Expression.New(resultType), bindings);
var lambda = Expression.Lambda(body, dynamicLambda.Parameters);
return source.Provider.CreateQuery<TResult>(
Expression.Call(
typeof(Queryable), "Select",
new Type[] { source.ElementType, lambda.Body.Type },
source.Expression, Expression.Quote(lambda)));
}
}
(Note latérale: Franchement, je n'ai aucune idée de l'argument des values
, mais je l'ai ajouté pour correspondre à la signature de la méthode DynamicQueryable.Select
correspondante.)
Donc, votre exemple deviendra quelque chose comme ça
public IQueryable<Thing> Foo(MyContext db)
{
var rootQuery = db.People.Where(x => x.City != null && x.State != null);
var groupedQuery = rootQuery.GroupBy("new ( it.City, it.State )", "it", new []{"City", "State"});
var finalLogicalQuery = groupedQuery.Select<Thing>("new ( Count() as TotalNumber, Key.City as City, Key.State as State )"); // IQueryable<Thing>
var executionDeferredTypedThings = finalLogicalQuery.Take(10);
return executionDeferredTypedThings;
}
Comment ça fonctionne
L'idée est assez simple.
L'implémentation de la méthode Select
dans DynamicQueryable
ressemble à ceci
public static IQueryable Select(this IQueryable source, string selector, params object[] values)
{
if (source == null) throw new ArgumentNullException("source");
if (selector == null) throw new ArgumentNullException("selector");
LambdaExpression lambda = DynamicExpression.ParseLambda(source.ElementType, null, selector, values);
return source.Provider.CreateQuery(
Expression.Call(
typeof(Queryable), "Select",
new Type[] { source.ElementType, lambda.Body.Type },
source.Expression, Expression.Quote(lambda)));
}
Ce qu'il fait est de créer dynamiquement une expression de sélecteur et de la lier à la méthode Select
source. Nous adoptons exactement la même approche, mais après avoir modifié l'expression de sélecteur créée par l'appel DynamicExpression.ParseLambda
.
La seule exigence est que la projection utilise la syntaxe "new (...)" et que les noms et types des propriétés projetées correspondent , ce qui, je pense, correspond à votre cas d'utilisation.
L'expression retournée est quelque chose comme ça
(source) => new TargetClass
{
TargetProperty1 = Expression1(source),
TargetProperty2 = Expression2(source),
...
}
où TargetClass
est une classe générée dynamiquement.
Tout ce que nous voulons, c'est conserver la partie source et remplacer simplement cette classe / propriétés cible par la classe / propriétés souhaitée.
Quant à la mise en œuvre, les affectations de propriété sont d'abord converties avec
var bindings = memberInit.Bindings.Cast<MemberAssignment>()
.Select(mb => Expression.Bind(
(MemberInfo)resultType.GetProperty(mb.Member.Name) ?? resultType.GetField(mb.Member.Name),
mb.Expression));
puis le new DynamicClassXXX { ... }
est remplacé par
var body = Expression.MemberInit(Expression.New(resultType), bindings);
Vous pouvez utiliser les extensions de requête d'AutoMapper pour produire un IQueryable qui enveloppe le IQueryable sous-jacent, préservant ainsi le IQueryProvider de l'IQueryable d'origine et l'exécution différée, mais ajoute un composant de mappage / traduction au pipeline pour convertir d'un type à un autre.
Il existe également UseAsDataSource d'AutoMapper qui facilite certains scénarios d'extension de requête courants.