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.
If you know for certain that the elements in the query are going to be of type
ChildQueryElement
, maybe you could simply use theCast
method?Because the
Where
call is receiving a type argument ofParentQueryElement
asTSource
. It creates a result based onTSource
as a new object... so you end up with something which "knows" aboutParentQueryElement
instead ofChildQueryElement
.It's easy enough to demonstrate this without going into LINQ at all:
Well, you could call
Where
dynamically, at which point the type argument will be inferred at execution time instead: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.