Voglio passare attraverso le relazioni per stringa.
Ho una Persona, un'Opera e una Posizione che sono connesse Persona N: 1 Lavoro e Lavoro 1: N Posizione (ogni persona può avere 1 lavoro e un lavoro può avere molte posizioni).
Input per il mio metodo:
Quindi devo chiamare con le espressioni: 1. nell'elenco delle persone un elenco.Seleziona (x => x.lavoro) 2. su quel risultato un elenco.SelezionaMany (x => x.Locations)
Ottengo un errore quando eseguo Expression.Call sul metodo SelectMany (su TODO)
var selectMethod = typeof(Queryable).GetMethods().Single(a => a.Name == "SelectMany" &&
a.GetGenericArguments().Length == 2 &&
a.MakeGenericMethod(typeof(object), typeof(object)).GetParameters()[1].ParameterType ==
typeof(Expression<Func<object, IEnumerable<object>>>));
var par = Expression.Parameter(origType, "x");
var propExpr = Expression.Property(par, property);
var lambda = Expression.Lambda(propExpr, par);
var firstGenType = reflectedType.GetGenericArguments()[0];
//TODO: why do I get an exception here?
selectExpression = Expression.Call(null,
selectMethod.MakeGenericMethod(new Type[] {origType, firstGenType}),
new Expression[] { queryable.Expression, lambda});
Ottengo questa eccezione:
System.ArgumentException: 'Espressione di tipo' System.Func
2[GenericResourceLoading.Data.Work,System.Collections.Generic.ICollection
1 [GenericResourceLoading.Data.Location]] 'non può essere utilizzato per il parametro di tipo' System.Linq.Expressions1[System.Func
2 [GenericResourceLoading.Data.Work, System.Collections.Generic.IEnumerable1[GenericResourceLoading.Data.Location]]]' of method 'System.Linq.IQueryable
1 [GenericResourceLoading.Data.Location] SelectMany [Lavoro, posizione] (System.Linq.IQueryable1[GenericResourceLoading.Data.Work], System.Linq.Expressions.Expression
1 [System.Func2[GenericResourceLoading.Data.Work,System.Collections.Generic.IEnumerable
1 [GenericResourceLoading .Data.Location]]]) ''
Il mio codice completo sembra così:
public void LoadGeneric(IQueryable<Person> queryable, string relations)
{
var splitted = relations.Split('.');
var actualType = typeof(Person);
IQueryable actual = queryable;
foreach (var property in splitted)
{
actual = LoadSingleRelation(actual, ref actualType, property);
}
MethodInfo enumerableToListMethod = typeof(Enumerable).GetMethod("ToList", BindingFlags.Public | BindingFlags.Static);
var genericToListMethod = enumerableToListMethod.MakeGenericMethod(new[] { actualType });
var results = genericToListMethod.Invoke(null, new object[] { actual });
}
private IQueryable LoadSingleRelation(IQueryable queryable, ref Type actualType, string property)
{
var origType = actualType;
var prop = actualType.GetProperty(property, BindingFlags.Instance | BindingFlags.Public);
var reflectedType = prop.PropertyType;
actualType = reflectedType;
var isGenericCollection = reflectedType.IsGenericType && reflectedType.GetGenericTypeDefinition() == typeof(ICollection<>);
MethodCallExpression selectExpression;
if (isGenericCollection)
{
var selectMethod = typeof(Queryable).GetMethods().Single(a => a.Name == "SelectMany" &&
a.GetGenericArguments().Length == 2 &&
a.MakeGenericMethod(typeof(object), typeof(object)).GetParameters()[1].ParameterType ==
typeof(Expression<Func<object, IEnumerable<object>>>));
var par = Expression.Parameter(origType, "x");
var propExpr = Expression.Property(par, property);
var lambda = Expression.Lambda(propExpr, par);
var firstGenType = reflectedType.GetGenericArguments()[0];
//TODO: why do I get an exception here?
selectExpression = Expression.Call(null,
selectMethod.MakeGenericMethod(new Type[] {origType, firstGenType}),
new Expression[] { queryable.Expression, lambda});
}
else
{
var selectMethod = typeof(Queryable).GetMethods().Single(a => a.Name == "Select" &&
a.MakeGenericMethod(typeof(object), typeof(object)).GetParameters()[1].ParameterType ==
typeof(Expression<Func<object, object>>));
var par = Expression.Parameter(origType, "x");
var propExpr = Expression.Property(par, property);
var lambda = Expression.Lambda(propExpr, par);
selectExpression = Expression.Call(null,
selectMethod.MakeGenericMethod(new Type[] {origType, reflectedType}),
new Expression[] {queryable.Expression, lambda});
}
var result = Expression.Lambda(selectExpression).Compile().DynamicInvoke() as IQueryable;
return result;
}
Sta fallendo perché il metodo SelectMany<TSource, TResult>
aspetta
Expression<Func<TSource, IEnumerable<TResult>>>
mentre stai passando
Expression<Func<TSource, ICollection<TResult>>>
Questi non sono gli stessi e l' Expression<TDelegate>
non è convertibile nel primo semplicemente perché Expression<TDelegate>
è una classe e le classi sono invarianti.
Prendendo il tuo codice, il tipo di risultato lambda atteso è come questo:
var par = Expression.Parameter(origType, "x");
var propExpr = Expression.Property(par, property);
var firstGenType = reflectedType.GetGenericArguments()[0];
var resultType = typeof(IEnumerable<>).MakeGenericType(firstGenType);
Ora puoi utilizzare Expression.Convert
per modificare (trasmettere) il tipo di proprietà:
var lambda = Expression.Lambda(Expression.Convert(propExpr, resultType), par);
o (il mio preferito) utilizzare un altro overload del metodo Expression.Lambda
con tipo di delegato esplicito (ottenuto tramite Expression.GetFuncType
):
var lambda = Expression.Lambda(Expression.GetFuncType(par.Type, resultType), propExpr, par);
Ciascuno di questi risolverà il tuo problema originale.
Ora prima di ottenere la prossima eccezione, la seguente riga:
var genericToListMethod = enumerableToListMethod.MakeGenericMethod(new[] { actualType });
è anche errato (perché quando si passa "Work.Locations", actualType
sarà ICollection<Location>
, non Location
che ToList
aspetta), quindi deve essere cambiato in:
var genericToListMethod = enumerableToListMethod.MakeGenericMethod(new[] { actual.ElementType });
In generale è possibile rimuovere la variabile actualType
e utilizzare sempre IQueryable.ElementType
a tale scopo.
Infine come bonus, non è necessario trovare manualmente le definizioni dei metodi generici. Expression.Call
ha un sovraccarico speciale che ti permette di "chiamare" facilmente metodi generici (e non solo) statici per nome. Ad esempio, la "chiamata" SelectMany
sarebbe così:
selectExpression = Expression.Call(
typeof(Queryable), nameof(Queryable.SelectMany), new [] { origType, firstGenType },
queryable.Expression, lambda);
e chiamare Select
è simile.
Inoltre non è necessario creare espressioni lambda aggiuntive, compilarle e richiamarle dinamicamente per ottenere IQueryable
risultante. Lo stesso può essere ottenuto utilizzando il metodo IQueryProvider.CreateQuery
:
//var result = Expression.Lambda(selectExpression).Compile().DynamicInvoke() as IQueryable;
var result = queryable.Provider.CreateQuery(selectExpression);
Usi il tuo metodo con un tipo di ICollection<T>
, ma la tua espressione accetta un IEnumerable<T>
come input. E SelectMany()
accetta come input IQueryable<T>
. Sia IQueryable<T>
che ICollection<T>
sono derivati da IEnumerable<T>
, ma se hai bisogno di un IQueryable<T>
puoi dare un ICollection<T>
.
Questo sarebbe lo stesso del seguente esempio:
class MyIEnumerable
{ }
class MyICollection : MyIEnumerable
{ }
class MyIQueryable : MyIEnumerable
{ }
private void MethodWithMyIQueryable(MyIQueryable someObj)
{ }
private void DoSth()
{
//valid
MethodWithMyIQueryable(new MyIQueryable());
//invalid
MethodWithMyIQueryable(new MyICollection());
}
Condividono la stessa eredità dall'oggetto, ma non hanno ancora eredità lineare l'una rispetto all'altra.
Prova a trasmettere / convertire il tuo ICollection<T>
a IEnumerable<T>
e poi darlo come parametro.