Why Does This Queryable.Where Call Change the Quer

2019-07-19 11:38发布

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.

2条回答
Fickle 薄情
2楼-- · 2019-07-19 12:05

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>
查看更多
萌系小妹纸
3楼-- · 2019-07-19 12:08

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.

查看更多
登录 后发表回答