21

Closed

Support projections to strongly typed collections in LINQ to Entities

description

Consider the repro below. This code attempts to execute a query which projects into DTO objects. One DTO object (BlogDto) contains a collection of the other DTO (PostDto). If the property that represents this collection is type as IEnumerable<PostDto> then everything is fine because the select returns an IEnumerable<PostDto> and LINQ to Entities handles it.

However, if the property is types as List<Dto> or some other strongly typed collection, then the query must be changed so that the Select in the projection returns a List. (If this is not done C# won't compile it.) This can be attempted either with a call to ToList or a cast. But LINQ to Entities does not support either of these things, even though the underlying collection is a List<PostDto>. LINQ to Entities should be able to detect these patterns and determine that they are no-ops.

More details can be found in this discussion and the linked blog article: http://entityframework.codeplex.com/discussions/430165

This bug is related and could possibly be considered a duplicate:
http://entityframework.codeplex.com/workitem/111

    public class BlogDto
    {
        public int Id { get; set; }
        public string Title { get; set; }

        // Change this to IEnumerbale<PostDto> and remove the cast/ToList below to make work
        public virtual List<PostDto> Posts { get; set; }
    }

    public class PostDto
    {
        public int Id { get; set; }
        public string Title { get; set; }

        public int BlogId { get; set; }
    }

    public class Blog
    {
        public int Id { get; set; }
        public string Title { get; set; }

        public virtual ICollection<Post> Posts { get; set; }
    }

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }

        public int BlogId { get; set; }
        public virtual Blog Blog { get; set; }
    }

    public class BlogContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            using (var context = new BlogContext())
            {
                context.Posts.Add(new Post {Title = "Open source FTW", Blog = new Blog {Title = "One Unicorn"}});
                context.SaveChanges();
            }

            using (var context = new BlogContext())
            {
                var blogs = context.Blogs.Select(
                    b => new BlogDto
                             {
                                 Id = b.Id,
                                 Title = b.Title,
                                 // If Posts is typed as IEnumerbale<PostDto> then cast can be removed and query will work
                                 // Note that ToList is another option here that also currently doesn't work
                                 Posts = (List<PostDto>)b.Posts.Select(
                                     p => new PostDto
                                              {
                                                  Id = p.Id,
                                                  Title = p.Title
                                              })
                             });
                
                foreach (var blog in blogs)
                {
                    Console.WriteLine("Blog: {0}", blog.Title);
                    foreach (var post in blog.Posts)
                    {
                        Console.WriteLine("  Post: {0}", post.Title);
                    }
                }
            }
        }
    }
Current stack trace:

Unhandled Exception: System.NotSupportedException: Unable to cast the type 'System.Collections.Generic.IEnumerable1' to
type 'System.Collections.Generic.List
1'. LINQ to Entities only supports casting EDM primitive or enumeration types.
at System.Data.Objects.ELinq.ExpressionConverter.ValidateAndAdjustCastTypes(TypeUsage toType, TypeUsage fromType, Typ
e toClrType, Type fromClrType)
at System.Data.Objects.ELinq.ExpressionConverter.GetCastTargetType(TypeUsage fromType, Type toClrType, Type fromClrTy
pe, Boolean preserveCastForDateTime)
at System.Data.Objects.ELinq.ExpressionConverter.CreateCastExpression(DbExpression source, Type toClrType, Type fromC
lrType)
at System.Data.Objects.ELinq.ExpressionConverter.ConvertTranslator.TranslateUnary(ExpressionConverter parent, UnaryEx
pression unary, DbExpression operand)
at System.Data.Objects.ELinq.ExpressionConverter.UnaryTranslator.TypedTranslate(ExpressionConverter parent, UnaryExpr
ession linq)
at System.Data.Objects.ELinq.ExpressionConverter.TypedTranslator1.Translate(ExpressionConverter parent, Expression l
inq)
at System.Data.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)
at System.Data.Objects.ELinq.ExpressionConverter.MemberInitTranslator.TypedTranslate(ExpressionConverter parent, Memb
erInitExpression linq)
at System.Data.Objects.ELinq.ExpressionConverter.TypedTranslator
1.Translate(ExpressionConverter parent, Expression l
inq)
at System.Data.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)
at System.Data.Objects.ELinq.ExpressionConverter.TranslateLambda(LambdaExpression lambda, DbExpression input)
at System.Data.Objects.ELinq.ExpressionConverter.TranslateLambda(LambdaExpression lambda, DbExpression input, DbExpre
ssionBinding& binding)
at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.OneLambdaTranslator.Translate(ExpressionConvert
er parent, MethodCallExpression call, DbExpression& source, DbExpressionBinding& sourceBinding, DbExpression& lambda)
at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.SelectTranslator.Translate(ExpressionConverter
parent, MethodCallExpression call)
at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.SequenceMethodTranslator.Translate(ExpressionCo
nverter parent, MethodCallExpression call, SequenceMethod sequenceMethod)
at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, Meth
odCallExpression linq)
at System.Data.Objects.ELinq.ExpressionConverter.TypedTranslator1.Translate(ExpressionConverter parent, Expression l
inq)
at System.Data.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)
at System.Data.Objects.ELinq.ExpressionConverter.Convert()
at System.Data.Objects.ELinq.ELinqQueryState.GetExecutionPlan(Nullable
1 forMergeOption)
at System.Data.Objects.ObjectQuery1.GetResults(Nullable1 forMergeOption)
at System.Data.Objects.ObjectQuery1.System.Collections.Generic.IEnumerable<T>.GetEnumerator()
at System.Data.Entity.Internal.Linq.InternalQuery
1.GetEnumerator()
at System.Data.Entity.Infrastructure.DbQuery`1.System.Collections.Generic.IEnumerable<TResult>.GetEnumerator()
at ConsoleApplication18.Program.Main(String[] args) in c:\Stuff\ConsoleApplication18\ConsoleApplication18\Program.cs:
line 72
Closed Jun 7, 2013 at 6:14 PM by maumar

comments

jarrettv wrote Jan 24, 2013 at 1:42 AM

This feature would decrease lines of code and increase performance for projections. example
[HttpGet]
[OutputCache(Duration=0)]
public ActionResult Edit(int id)
{
    if (!Identity.Current.IsModerator())
        throw new UnauthorizedAccessException();

    var chapters = this.db.ChapterSets
        .Where(x => x.Id == id)
        .SelectMany(x => x.Chapters)
        .Select(y => new ChapterDto()
            {
                Number = y.Number,
                Name = y.Name,
                Time = y.Time,
            });
    var cs = this.db.ChapterSets
        .Where(x => x.Id == id)
        .Select(x => new ChapterSetDto()
        {
            Id = id,
            Title = x.Title,
            AltTitle = x.AltTitle,
            LangCode = x.LangCode,
            HideLastChapter = x.HideLastChapter,
        }).Single();
    cs.Chapters = chapters.ToList();

    return this.PartialView(cs);
}

jarrettv wrote Jan 24, 2013 at 1:47 AM

With this feature.
[OutputCache(Duration=0)]
public ActionResult Edit(int id)
{
    if (!Identity.Current.IsModerator())
        throw new UnauthorizedAccessException();

    var cs = this.db.ChapterSets
        .Where(x => x.Id == id)
        .Select(x => new ChapterSetDto()
        {
            Id = id,
            Title = x.Title,
            AltTitle = x.AltTitle,
            LangCode = x.LangCode,
            HideLastChapter = x.HideLastChapter,
            Chapters = x.Chapters.Select(y => new ChapterDto()
            {
                Number = y.Number,
                Name = y.Name,
                Time = y.Time,
            }).ToList() // <-- new feature
        }).Single();

    return this.PartialView(cs);
}

brentmckendrick wrote Jan 31, 2013 at 6:33 AM

So I had some time today to try my hand at going through the EF source. I created a simple passthrough for ToList inside of the fork: http://entityframework.codeplex.com/SourceControl/network/forks/brentmckendrick/Issue808/changeset/b18e48b3e51f

This solves the problem described.

I feel it would be better if you could instantiate dictionaries, lists or arrays, however my knowledge of the inner workings of EF is very minimal and I'm not sure how to go about that.

ajcvickers wrote Feb 8, 2013 at 9:36 PM

Hi Brent,

We have taken a look at the code and it seems like something we would accept as a pull request. The pull request instructions are here: http://entityframework.codeplex.com/wikipage?title=Contributing You will need to submit a CLA if you haven't done so already. Also, please remember to add tests as part of the pull request.

Thanks,
Arthur

sunnfun wrote Feb 15, 2013 at 5:34 PM

This would be an incredibly rich feature and probably should have been in the original version.

jarrettv wrote Mar 27, 2013 at 7:50 PM

Any updates on this feature? I didn't see a pull request accepted.

erinursofa wrote Apr 30, 2013 at 9:05 AM

your suggestion.... very helpful!, thanks.... It's work for me... :D

brentmckendrick wrote May 9, 2013 at 2:25 PM

Sorry it took me a while guys, I've been very busy. Here is the pull request https://entityframework.codeplex.com/SourceControl/network/forks/brentmckendrick/Issue808Fix/contribution/4689

ajcvickers wrote May 31, 2013 at 9:34 PM

Fixed in b0de1dacdb2c by brentmckendrick

Issue 808 - Strongly typed nested projections

maumar wrote Jun 7, 2013 at 6:14 PM

Added tests in 39dfe83c3d564ab2c0484be1f0baeb7cefda6870