问题
Code to Reproduce Issue
I ran into a situation where an IQueryable.Where<TSource>
call was returning an IQueryable<TOther>
where TOther != TSource
. I put together some sample code to reproduce it:
using System;
using System.Collections.Generic;
using System.Linq;
namespace IQueryableWhereTypeChange {
class Program {
static void Main( string[] args ) {
var ints = new List<ChildQueryElement>();
for( int i = 0; i < 10; i++ ) {
ints.Add( new ChildQueryElement() { Num = i, Value = i.ToString() } );
}
IQueryable<ChildQueryElement> theIQ = ints.AsQueryable();
Object theObj = theIQ;
Type theObjElementType = ( (IQueryable<ParentQueryElement>) theObj ).ElementType;
Type theObjGenericType = ( (IQueryable<ParentQueryElement>) theObj ).GetType().GetGenericArguments()[ 0 ];
var iQ = ( (IQueryable<ParentQueryElement>) theObj );
var copy = iQ;
Type copyType1 = copy.GetType().GetGenericArguments()[ 0 ];
Type elementType1 = copy.ElementType;
copy = copy.Where( qe1 => true );
Type copyType2 = copy.GetType().GetGenericArguments()[ 0 ];
Type elementType2 = copy.ElementType;
Console.WriteLine( "theObjElementType : " + theObjElementType.ToString() );
Console.WriteLine( "theObjGenericType : " + theObjGenericType.ToString() );
Console.WriteLine( "copyType1 : " + copyType1.ToString() );
Console.WriteLine( "elementType1 : " + elementType1.ToString() );
Console.WriteLine( "copyType2 : " + copyType2.ToString() );
Console.WriteLine( "elementType2 : " + elementType2.ToString() );
}
}
public class ParentQueryElement {
public int Num { get; set; }
}
public class ChildQueryElement : ParentQueryElement {
public string Value { get; set; }
}
}
The output of this program is:
theObjElementType : IQueryableWhereTypeChange.ChildQueryElement
theObjGenericType : IQueryableWhereTypeChange.ChildQueryElement
copyType1 : IQueryableWhereTypeChange.ChildQueryElement
elementType1 : IQueryableWhereTypeChange.ChildQueryElement
copyType2 : IQueryableWhereTypeChange.ParentQueryElement
elementType2 : IQueryableWhereTypeChange.ParentQueryElement
Summary of Code Results
So, we store an IQueryable<ChildQueryElement>
in an Object
, then cast the object to IQueryable<ParentQueryElement>
, where the child type inherits from the parent type. At this point the object stored in the Object
variable still knows it is a collection of the child type. We then call Queryable.Where
on it, but the object that is returned is no longer aware that it contains the child type, and thinks it only contains the parent type.
Question
Why does this happen? Is there any way I can avoid this, other than skipping the step where it gets stored in an object? I ask this because I'm dealing with a third-party API that demands I pass it an Object
, and I don't want to have to rewrite a bunch of third-party code.
Updated Sample Code
After getting some advice from Jon Skeet, I tried this sample code, which uses a dynamic variable for copy. Replace the body of Main
with the following:
var ints = new List<ChildQueryElement>();
for( int i = 0; i < 10; i++ ) {
ints.Add( new ChildQueryElement() { Num = i, Value = i.ToString() } );
}
IQueryable<ChildQueryElement> theIQ = ints.AsQueryable();
Object theObj = theIQ;
Type theObjElementType = ( (IQueryable<ParentQueryElement>) theObj ).ElementType;
Type theObjGenericType = ( (IQueryable<ParentQueryElement>) theObj ).GetType().GetGenericArguments()[ 0 ];
var iQ = ( (IQueryable<ParentQueryElement>) theObj );
dynamic copy = iQ;
Type copyType1 = copy.GetType().GetGenericArguments()[ 0 ];
Type elementType1 = ((IQueryable)copy).ElementType;
Expression<Func<ParentQueryElement, bool>> del = qe => true;
copy = Queryable.Where( copy, del );
Type copyType2 = copy.GetType().GetGenericArguments()[ 0 ];
Type elementType2 = ((IQueryable)copy).ElementType;
Console.WriteLine( "theObjElementType : " + theObjElementType.ToString() );
Console.WriteLine( "theObjGenericType : " + theObjGenericType.ToString() );
Console.WriteLine( "copyType1 : " + copyType1.ToString() );
Console.WriteLine( "elementType1 : " + elementType1.ToString() );
Console.WriteLine( "copyType2 : " + copyType2.ToString() );
Console.WriteLine( "elementType2 : " + elementType2.ToString() );
Unfortunately, the output remains the same.
回答1:
Why does this happen?
Because the Where
call is receiving a type argument of ParentQueryElement
as TSource
. It creates a result based on TSource
as a new object... so you end up with something which "knows" about ParentQueryElement
instead of ChildQueryElement
.
It's easy enough to demonstrate this without going into LINQ at all:
using System;
public interface IWrapper<out T>
{
T Value { get; }
}
public class Wrapper<T> : IWrapper<T>
{
private readonly T value;
public Wrapper(T value)
{
this.value = value;
}
public T Value { get { return value; } }
}
class Program
{
static void Main(string[] args)
{
IWrapper<string> original = new Wrapper<string>("foo");
IWrapper<object> original2 = original;
IWrapper<object> rewrapped = Rewrap(original2);
Console.WriteLine(original2.GetType()); // Wrapper<string>
Console.WriteLine(rewrapped.GetType()); // Wrapper<object>
}
static IWrapper<T> Rewrap<T>(IWrapper<T> wrapper)
{
return new Wrapper<T>(wrapper.Value);
}
}
Is there any way I can avoid this, other than skipping the step where it gets stored in an object?
Well, you could call Where
dynamically, at which point the type argument will be inferred at execution time instead:
dynamic copy = ...;
Expression<Func<ChildQueryElement, bool>> filter = qe1 => true;
// Can't call an extension method "on" dynamic; call it statically instead
copy = Queryable.Where(copy, filter);
Note that the expression tree type needs to be Func<ChildQueryElement, bool>
as well... it's not clear to me whether that would be a problem for you.
回答2:
If you know for certain that the elements in the query are going to be of type ChildQueryElement
, maybe you could simply use the Cast
method?
copy = copy.Where(qe1 => true); // IQueryable<ParentQueryElement>
var copyCasted = copy.Cast<ChildQueryElement>(); // IQueryable<ChildQueryElement>
来源:https://stackoverflow.com/questions/25778701/why-does-this-queryable-where-call-change-the-queryables-type-parameter