2

Previously (when using .net 4.5.2 and EF 6). I have had a generic Get method that accepted a number of includes as follows;

public abstract class DataContext : IdentityDbContext<ApplicationUser, ApplicationRole, int>, IDataContext
{
    public DataContext(DbContextOptions options)
    : base(options)
    {
    }

    // reduced for brevity

    public T Get<T>(int id, params Expression<Func<T, object>>[] includes) where T : class, IEntity
    {
        return this.Set<T>().Include(includes).FirstOrDefault(x => x.Id == id);
    }

I would then call for example;

context.Get<Job>(id, 
    x => x.Equipment,
    x => x.Equipment.Select(y => y.Type));

To include the Job.Equipment and also the Job.Equipment.Type.

However, when I have ported this over to asp.net core 2. I have tried the same generic approach, but if I try to include a sub-entity I get the following error;

The property expression 'x => {from Equipment y in x.Equipment select [y].Type}' is not valid. The expression should represent a property access: 't => t.MyProperty'. For more information on including related data, see http://go.microsoft.com/fwlink/?LinkID=746393.

Can anyone suggest how I can work around this to include sub entities in my Generic Get<T> method with Entity Framework Core 2?

Update

From looking at the documents there is an additional include method

include(string navigationPropertyPath)

I added the following method;

    public T Get<T>(int id, string[] includes) where T : class, IEntity
    {
        var result = this.Set<T>().AsQueryable();

        foreach(var include in includes)
        {
            result = result.Include(include);
        }

        return result.FirstOrDefault(x => x.Id == id);
    }

Which does work, although I am not convinced on the efficiency here?

Matthew Flynn
  • 3,661
  • 7
  • 40
  • 98
  • There's no `Include` overload that accepts an `Expression>[]` in Entity Framework Core, or is that how you use it in EF6? – Camilo Terevinto May 02 '18 at 15:18
  • `.Include` works correct, but it seemsthat selecting sub entities has changed in core 2? (the code that worked fine prviously no do not work). Also I found the link https://learn.microsoft.com/en-us/ef/core/querying/related-data which seems to suggest a `.ThenInclude` – Matthew Flynn May 02 '18 at 15:21
  • Yes, you need to use `ThenInclude`, but your code does not even compile for me on EF Core 2 – Camilo Terevinto May 02 '18 at 15:21
  • That's odd it compiling for me no problem. I edits to include my `DbContext` extension. I have noticed there is an `Include(string navigationPropertyPath)` which could possibly do the job. I'll give that a test and update with my results. – Matthew Flynn May 02 '18 at 15:35
  • I can confirm using the above method does work. However, I am not sure on the efficiency of my method here? (i've updated the question to list it) – Matthew Flynn May 02 '18 at 15:56
  • Efficiency is ok. Just the type safety (as with any `string`) is not. – Ivan Stoev May 02 '18 at 16:18
  • @IvanStoev Do you recommend anything to try to cover on this? – Matthew Flynn May 02 '18 at 16:27
  • 1
    Another solution is to convert the lambda expressions to a path as string: https://stackoverflow.com/a/47063432/861716 – Gert Arnold May 03 '18 at 06:54

2 Answers2

8

EF Core Include / ThenInclude pattern cannot be represented by Expression<Func<T, object>>[] like in EF6.

Looking at the source code of one of the EF Core extensions - Microsoft.EntityFrameworkCore.UnitOfWork, which claims to be

A plugin for Microsoft.EntityFrameworkCore to support repository, unit of work patterns, and multiple database with distributed transaction supported.

looks like the intended pattern for includes should be based on Func<IQueryable<T>, IIncludableQueryable<T, object>>:

public T Get<T>(int id, Func<IQueryable<T>, IIncludableQueryable<T, object>> include = null) where T : class, IEntity
{
    var result = this.Set<T>().AsQueryable();

    if (include != null)
        result = include(result);

    return result.FirstOrDefault(x => x.Id == id);
}

The drawback is that it adds EF Core dependency on the caller and requires using Microsoft.EntityFrameworkCore;. Which in your case is not essential since you are extending the DbContext.

The usage with your example would be:

context.Get<Job>(id, q => q
    .Include(x => x.Equipment)
        .ThenInclude(y => y.Type));
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Would this allow for multiple includes? for example if I need to include multiple complex entities in a return? As with EF6 I could simple chain the includes to pull back what I need? – Matthew Flynn May 02 '18 at 18:08
  • Yes, it does. In EF Core you would chain multiple `Include` / `ThenInclude`. Every `Include` is restarting from the root. Example [here](https://stackoverflow.com/questions/40181872/ef-core-include-on-multiple-sub-level-collections/40185150#40185150) and in the documentation. – Ivan Stoev May 02 '18 at 19:39
  • How do you think to make dynamic this? I would to have this in a BaseRepository thath is a base class of some repositories and I wouldn't write all the implementations for each repo, but I can't find a way to make the thenInclude dynamic, any suggestions? – RSadocchi Apr 29 '19 at 06:48
  • @RSadocchi Not sure what you mean by "make dynamic", may be you could give an example. In the above example (and the link), the `Include` / `ThenInclude` is provided externally by the *caller* of the repo method. – Ivan Stoev Apr 29 '19 at 07:35
  • @IvanStoev thanks for your answer, please look https://stackoverflow.com/questions/55898559/how-to-make-dynamic-inclusion-of-navigation-properties for a code sample – RSadocchi Apr 29 '19 at 09:20
0

You can do something like this:

   public abstract class DataContext : IdentityDbContext<ApplicationUser, ApplicationRole, int>, IDataContext
    {
        public DataContext(DbContextOptions options)
        : base(options)
        {
        }

        // reduced for brevity

        public T Get<T>(int id, Func<IQueryable<T>, IIncludableQueryable<T, object>> includes = null) where T : class, IEntity
        {
            IQueryable<T> queryable = this.Set<T>();

            if (includes != null)
            {
                queryable = includes(queryable);
            }

            return queryable.FirstOrDefault(x => x.Id == id);
        }
    }

   context.Get<Job>(id, includes: source => source.Include(x => x.Equipment).ThenInclude(x => x.Type));