UseAsDataSource: "System.ArgumentException : Argument types do not match" when mapping A->B->C
CRogos opened this issue · 9 comments
I was struggling on an issue also reported a while ago on the AutoMapper repo:
UseAsDataSource: System.InvalidCastException when mapping A->B->C #2262
I was able to fix one "System.InvalidCastException" when using UseAsDataSource concatenated multiple times and it is now working for objects where the property type do not change (int->int, string->string) but I do run in another issue when the properties are mapped (int->string, string->guid, etc.) in certain constellations.
I wrote some minimalist unit tests
ChangePropertyName_SourceInjectedQuery (working):
[Source(int) -> B(int) -> Destination(int)]
GuidToString_SourceInjectedQuery (fails):
[Source(int)->B(int)->Destination(string)]
GuidToString2_SourceInjectedQuery (working):
[Source(int)->B(string)->Destination(string)]
The unit tests and the patch for the "System.InvalidCastException" can be found in my branch with
Patch fix System.InvalidCastException
Another hint might give, when using UseAsDataSource concatenated from an EF.Core source, I find the Guid serialized to the SQL sting as "3b0db672-f2ff-40ed-a883-6c5661a85c4b", even though I created a map that should serialize the value as "3B0DB672F2FF40EDA8836C5661A85C4B".
cfg.CreateMap<Guid, string>().ConvertUsing(s => s.ToString("N").ToUpper());
Looks to me that the values in the expression are not mapped with AutoMapper?
Maybe my findings help someone fixing the issue or maybe someone can give me hint how to solve this problem?
My goal is to have an Odata WebApi Model (Destination), separated from an Business layer (B) and a Legacy database model (Source) without loosing the query performance as "ToList()" does.
Please include a sample we can run and see fail.
thanks für you quick reply.
The patch for the "System.InvalidCastException"
replace with:
destResult = mapExpressions.Aggregate(sourceResult, Select).Cast<TDestination>();
Unit Tests
The patch solves this unit test:
[Source(int) -> B(int) -> Destination(int)]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using AutoMapper.Mappers;
using Shouldly;
using Xunit;
namespace AutoMapper.Extensions.ExpressionMapping.UnitTests.Impl
{
public class ChangePropertyName_SourceInjectedQuery : AutoMapperSpecBase
{
private readonly Source[] _source = new[]
{
new Source
{
SourceProp = 5,
},
new Source
{
SourceProp = 6,
},
new Source
{
SourceProp = 7,
}
};
private class Source
{
public int SourceProp { get; set; }
}
private class IntermediateLayer
{
public int IntermediateProp { get; set; }
}
private class Destination
{
public int DestinationProp { get; set; }
}
protected override MapperConfiguration Configuration { get; } = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Destination, IntermediateLayer>()
.ForMember(dest => dest.IntermediateProp, opt => opt.MapFrom(src => src.DestinationProp))
.ReverseMap()
;
cfg.CreateMap<IntermediateLayer, Source>()
.ForMember(dest => dest.SourceProp, opt => opt.MapFrom(src => src.IntermediateProp))
.ReverseMap()
;
});
[Fact]
public void Shoud_support_source_to_destination_result_toList()
{
IQueryable<IntermediateLayer> intermediateSource = _source.AsQueryable()
.UseAsDataSource(Mapper).For<IntermediateLayer>();
IQueryable<Destination> result = intermediateSource
.UseAsDataSource(Mapper).For<Destination>();
result.ToList();
}
}
}
and this case:
[Source(Guid)->B(string)->Destination(string)]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using AutoMapper.Mappers;
using Shouldly;
using Xunit;
namespace AutoMapper.Extensions.ExpressionMapping.UnitTests.Impl
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using AutoMapper.Mappers;
using Shouldly;
using Xunit;
namespace AutoMapper.Extensions.ExpressionMapping.UnitTests.Impl
{
public class GuidToString_SourceInjectedQuery : AutoMapperSpecBase
{
private readonly Source[] _source = new[]
{
new Source
{
ValueProp = new Guid("90875833-20f0-4735-bf3f-f92fa678bca6")
},
new Source
{
ValueProp = new Guid("3b0db672-f2ff-40ed-a883-6c5661a85c4b")
},
new Source
{
ValueProp = new Guid("fe2ed13d-d4b2-4773-bc47-4f3f83ae5175")
}
};
private class Source
{
public Guid ValueProp { get; set; }
}
private class IntermediateLayer
{
public string ValueProp { get; set; }
}
private class Destination
{
public string ValueProp { get; set; }
}
protected override MapperConfiguration Configuration { get; } = new MapperConfiguration(cfg =>
{
cfg.CreateMap<string, Guid>().ConvertUsing(s => new Guid(s));
cfg.CreateMap<Destination, IntermediateLayer>()
.ReverseMap()
;
cfg.CreateMap<IntermediateLayer, Source>()
.ReverseMap()
;
});
[Fact]
public void Shoud_support_source_to_intermediate_result_toList()
{
IQueryable<IntermediateLayer> result = _source.AsQueryable()
.UseAsDataSource(Mapper).For<IntermediateLayer>();
result.ToList();
}
[Fact]
public void Shoud_support_intermediate_to_destination_result_toList()
{
IQueryable<IntermediateLayer> intermediateSource = _source.AsQueryable()
.UseAsDataSource(Mapper).For<IntermediateLayer>()
// when persist source before next mapping it is always working
.ToArray().AsQueryable();
IQueryable<Destination> result = intermediateSource
.UseAsDataSource(Mapper).For<Destination>();
result.ToList();
}
[Fact]
public void Shoud_support_source_to_destination_result_toList()
{
IQueryable<IntermediateLayer> intermediateSource = _source.AsQueryable()
.UseAsDataSource(Mapper).For<IntermediateLayer>();
IQueryable<Destination> result = intermediateSource
.UseAsDataSource(Mapper).For<Destination>();
result.ToList();
}
}
}
But still fails with "System.ArgumentException" on this unit test:
[Source(Guid)->B(Guid)->Destination(string)]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using AutoMapper.Mappers;
using Shouldly;
using Xunit;
namespace AutoMapper.Extensions.ExpressionMapping.UnitTests.Impl
{
public class GuidToString2_SourceInjectedQuery : AutoMapperSpecBase
{
private readonly Source[] _source = new[]
{
new Source
{
ValueProp = new Guid("90875833-20f0-4735-bf3f-f92fa678bca6")
},
new Source
{
ValueProp = new Guid("3b0db672-f2ff-40ed-a883-6c5661a85c4b")
},
new Source
{
ValueProp = new Guid("fe2ed13d-d4b2-4773-bc47-4f3f83ae5175")
}
};
private class Source
{
public Guid ValueProp { get; set; }
}
private class IntermediateLayer
{
public Guid ValueProp { get; set; }
}
private class Destination
{
public string ValueProp { get; set; }
}
protected override MapperConfiguration Configuration { get; } = new MapperConfiguration(cfg =>
{
cfg.AddExpressionMapping();
cfg.CreateMap<string, Guid>().ConvertUsing(s => new Guid(s));
cfg.CreateMap<Destination, IntermediateLayer>()
.ReverseMap()
;
cfg.CreateMap<IntermediateLayer, Source>()
.ReverseMap()
;
});
[Fact]
public void Shoud_support_intermediate_to_destination_result_toList()
{
IQueryable<IntermediateLayer> intermediateSource = _source.AsQueryable()
.UseAsDataSource(Mapper).For<IntermediateLayer>()
// when persist source before next mapping it is always working
.ToArray().AsQueryable();
IQueryable<Destination> result = intermediateSource
.UseAsDataSource(Mapper).For<Destination>();
result.ToList();
}
[Fact]
public void Shoud_support_source_to_destination_result_toList()
{
IQueryable<IntermediateLayer> intermediateSource = _source.AsQueryable()
.UseAsDataSource(Mapper).For<IntermediateLayer>();
IQueryable<Destination> result = intermediateSource
.UseAsDataSource(Mapper).For<Destination>();
result.ToList();
}
}
}
You're already working in the right spot but I haven't found a solution. Maybe @TylerCarlson1 has some words of wisdom?
Calling .ToArray().AsQueryable()
works because that changes the queryable provider - similar to issue #66.
The following does work in all three cases if you add a helper extension:
[Fact]
public void Shoud_support_source_to_destination_result_using_query_toList()
{
IQueryable<IntermediateLayer> intermediateSource = _source.AsQueryable()
.GetQuery<IntermediateLayer, Source>(Mapper);
IQueryable<Destination> result = intermediateSource
.GetQuery<Destination, IntermediateLayer>(Mapper);
result.ToList();
}
Extension:
public static class MyHelper
{
public static IQueryable<TModel> GetQuery<TModel, TData>(this IQueryable<TData> query, IMapper mapper,
Expression<Func<TModel, bool>> filter = null,
Expression<Func<IQueryable<TModel>, IQueryable<TModel>>> queryFunc = null)
{
//Map the expressions
Expression<Func<TData, bool>> f = mapper.MapExpression<Expression<Func<TData, bool>>>(filter);
Func<IQueryable<TData>, IQueryable<TData>> mappedQueryFunc = mapper.MapExpression<Expression<Func<IQueryable<TData>, IQueryable<TData>>>>(queryFunc)?.Compile();
if (filter != null)
query = query.Where(f);
return mappedQueryFunc != null
? mapper.ProjectTo<TModel>(mappedQueryFunc(query))
: mapper.ProjectTo<TModel>(query);
}
}
There are examples for EF6 and EF Core in the OData extensions repo.
Thank you for your quick respones and the hint to the OData extention repo. This is indeed very helpful and solves another part of my problem.
The GetQuery extension is indeed working for the in memory scenario, but adding this to my project using EF core I get the error message why I've tried to switch to UseAsDataQuery instead of ProjectTo.
When using a Guid in the OData / Model layer which is mapped to a string in the DB layer i use one of this mapping.
CreateMap<string, Guid>().ConvertUsing(s => new Guid(s));
or
CreateMap<string, Guid>().ConvertUsing(s => Guid.Parse(s));
But both leads to an error message when executing this requests:
var result = await httpClient.GetAsync("http://localhost:14869/odata/person?$filter=Id eq 548d20c8-8999-4f75-a52c-139abd332b4b");
var result2 = await httpClient.GetAsync("http://localhost:14869/odata/person(2a6cd06e-437b-4272-8d9c-ac327eb878df)");
System.InvalidOperationException: The LINQ expression 'DbSet<Members>
.Where(m => new Guid(m.Guid) == __key_0)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.<VisitMethodCall>g__CheckTranslated|8_0(ShapedQueryExpression translated, <>c__DisplayClass8_0& )
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_0`1.<Execute>b__0()
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
at System.Linq.Queryable.First[TSource](IQueryable`1 source)
at OdataControllerBase`2.Get(Guid key, ODataQueryOptions`1 queryOptions) in C:\Data\CodeV\AkafliegFrankfurt\Core\Akaflieg.Startliste.Web\Controllers\OdataControllerBase.cs:line 52
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
HEADERS
=======
Accept: application/json
Connection: Keep-Alive
Host: localhost:14869
My hope of using UseAsDataQuery is that instead of converting the database string to the model type in SQL, the Guid parameter of the model is converted in code to a string before adding it to the SQL parameter.
I do not yet have a working unit test setup and I also need to get through all the other tickets. (I did not find the closed issues before, thank you for the link ;) )
Two things:
First, Expression Mapping does not support ConvertUsing
. You may want to use the following e.g.:
cfg.CreateMap<IntermediateLayer, Source>()
.ForMember(dest => dest.ValueProp, opt => opt.MapFrom(src => new Guid(src.ValueProp)))
.ReverseMap()
;
Second, it seems EF Core is having trouble converting new Guid(m.Guid)
to SQL. Maybe the following will work:
cfg.CreateMap<IntermediateLayer, Source>()
.ForMember(dest => dest.ValueProp, opt => opt.MapFrom(src => Guid.Parse(src.ValueProp)))
.ReverseMap()
;
If you need to, put up a repo with the failing code.
I was aware that not all ConvertUsing overloads are working with expressions, but my understanding is that the one I've used is fine.
void ConvertUsing(Expression<Func<TSource, TDestination>> mappingExpression);
Nevertheless I gave it a try and it wasn't working.
System.InvalidOperationException: The LINQ expression 'DbSet<Members>
.Where(m => Guid.Parse(m.Guid) == __key_0)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.<VisitMethodCall>g__CheckTranslated|8_0(ShapedQueryExpression translated, <>c__DisplayClass8_0& )
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_0`1.<Execute>b__0()
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
at System.Linq.Queryable.First[TSource](IQueryable`1 source)
at OdataControllerBase`2.Get(Guid key, ODataQueryOptions`1 queryOptions) in C:\Data\CodeV\AkafliegFrankfurt\Core\Akaflieg.Startliste.Web\Controllers\OdataControllerBase.cs:line 52
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.<Invoke>g__AwaitMatcher|8_0(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task`1 matcherTask)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
HEADERS
=======
Accept: application/json
Connection: Keep-Alive
Host: localhost:14869
I think the UseAsDataQuery is the right approach and I will dive into this direction a little deeper. But as this is only a leisure time project and Easter is nearly over it'll take some days.
Thank you so far. You already helped me a lot and it was motivating to keep on investigating further.
Yes I know. But this is the reason why ProjectTo is not helping here and I have to go the UseAsDataQuery approach.
The resulting query with ProjectTo is:
Select * from Person where Guid.Parse(ID) = 'af603f07-c6a8-4471-9c5d-39e85fc36de6'
But Guid.Parse is unknown.
My intention with UseAsDataQuery is that the resulting query looks like:
Select * from Person where ID = 'af603f07c6a844719c5d39e85fc36de6'
(g => g.ToString("N"))
So instead of converting the SQL output to the API format in SQL, SQL result should return the database model format and the result should be converted by the c# code.
Which currently gives a "InvalidCastException" as mentioned in #26 . I also do not understand why this ticket is closed? The problem still exists and should be visible to everyone. My understanding of closed is either it is not a bug (e.g. because it is not used correctly), or a patch has been applied. But this is both not the case.
In my second post, I made a suggestion to how the cast exception might be solved at this place, but deeper analysis already showed me that we than have the same issue in AutoMapper ProjectionExpression.ToCore Method which is implemented the same way. So maybe the correct solution is, that the result of For< TResult > must be castable to IQueryable< TResult > ?
A PR is welcome.