1

I have a DbSet<T> that I need to project onto a different type. For the projection, I use Select.

If I call directly Select on my DbSet<T> like:

private IEnumerable<PersonPOCO> _getPersons(ILocator loc)
{
    using(var service = loc.GetService())
    {
        return service.GetPersons().Select(p => Mapper.ToPoco(p));
    }
}

This will throw a NotSupportedException because Select is not recognized by entity framework (which is kinda obvious).

So I'll need to "concretize" the list by calling ToList() :

private IEnumerable<PersonPOCO> _getPersons(ILocator loc)
{
    using(var service = loc.GetService())
    {
        return service.GetPersons()
                      .ToList()
                      .Select(p => Mapper.ToPoco(p));
    }
}

If I leave it like this, Select being 'lazy' will only be evaluated when the list is really enumerated. But by that time, the Context used by the service will have been disposed (outside of the using statement) so I will have a DbContextDisposedException.

So I need to directly enumerate the list inside the using

private IEnumerable<PersonPOCO> _getPersons(ILocator loc)
{
    using(var prov = loc.GetService())
    {
        return prov.GetPersons()
            .ToList()
            .Select(p => Mapper.ToPOCO(p))
            .ToList();
    }
}

Is there no way I can avoid calling ToList() twice in the same instruction?

Arthur Attout
  • 2,701
  • 2
  • 26
  • 49
  • 3
    EF does 'recognise' `Select`, it's the `Mapper.ToPoco` that it doesn't know how to process. What is that? – DavidG May 11 '21 at 08:51
  • The second `ToList` is unnecessary, because the first will enumerate the collection immediately. Only the `Select` is lazily evaluated, which will be operating on an in-memory list at that point. – Johnathan Barclay May 11 '21 at 08:54
  • 1
    Technically what you need (in case you can't rewrite `Select` to be translatable by replacing mapper call with mapping expression) before `Select` is `AsEnumerable()`, not `ToList()` . – Ivan Stoev May 11 '21 at 08:55
  • 1
    @JohnathanBarclay well, *yes*, but if you're going to buffer the data locally, you might as well do it *after* the projections, so that if it is consumed multiple times the projection isn't repeated – Marc Gravell May 11 '21 at 08:55
  • @MarcGravell Yeah, you're right about that. The comment was more because OP thought that omitting the second `ToList` would cause an exception. – Johnathan Barclay May 11 '21 at 09:00
  • Is there anything I can do to make `Mapper.ToPoco` "recognized" by EF ? For the record, this is only a static method that returns a `new ParamTypePOCO` with nothing fancy, only primitives. – Arthur Attout May 11 '21 at 09:09
  • You could change the `ToPoco` method to return an `Expression` or use AutoMapper which has some [EF features](https://docs.automapper.org/en/stable/Queryable-Extensions.html) – DavidG May 11 '21 at 09:11
  • @ArthurAttout, maybe this my answer will be helpful for you: https://stackoverflow.com/a/66386142/10646316 – Svyatoslav Danyliv May 11 '21 at 13:53
  • +1 on Automapper.. `ProjectTo` will work the SQL down to just what is needed to populate your DTO. (No need to reinvent the wheel) – Steve Py May 11 '21 at 21:29

1 Answers1

1

The problem here is that EF doesn't know what to do with DataViewMapper.ToPOCO. You can use AsEnumerable() to switch to LINQ-to-Objects:

using (var prov = arg.GetProvider())
{
    return prov.GetParamTypes()
        .AsEnumerable()
        .Select(p => DataViewMapper.ToPOCO(p))
        .ToList();
}

This avoids an intermediate list/array/etc allocation.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • The thing is I see `AsEnumerable` just as boilerplate as `ToList`, so it doesn't save me from an additional instruction to make my query. I'll go with you suggestion to change `DataViewMapper.ToPOCO` return an `Expression`, which would actually save me an instruction. – Arthur Attout May 11 '21 at 09:15