Need a generic EF method that accepts entity id and includes
Asked Answered
D

2

5

I'm using Entity Framework 5 and have a generic repository, within which are several methods like the two Get() methods below:

public TEntity GetById(int id)
{
    return DbSet.Find(id);
}

public TEntity Get(
    Expression<Func<TEntity, bool>> filter = null,
    IEnumerable<string> includePaths = null)
{
    IQueryable<TEntity> query = DbSet;

    if (filter != null)
    {
        query = query.Where(filter);
    }

    if (includePaths != null)
    {
        query = includePaths.Aggregate(query, (current, includePath) => current.Include(includePath));
    }

    return query.SingleOrDefault();
}

These are both very helpful, however when I want to make a slightly more complex GetById() call and retrieve some entity references at the same time, like so:

var user = _userRepository.GetById(
    id,
    new List<string> { "Roles", "Invoices" });

I end up having to roll out entity-specific (so non-generic) GetById(id, includes) calls for each entity so that I can access their specific Id fields in the lambda, i.e. UserId, or InvoiceId etc.

public User GetById(
    int id,
    IEnumerable<string> includes)
{
    return Get(
        (u => u.UserId == id),
        includes);
}

It seems that I can't, with my average EF skills, work out how to combine the goodness of DbSet.Find(id) with the .Include() call in a generic fashion.

So the question is - is there a way to write a generic EF method that I can use to get an entity by it's id and include some references, and in turn remove the need to write entity specific GetById(id, includes) calls like I've done above.

Thanks in advance.

Demote answered 15/1, 2013 at 19:39 Comment(1)
See this answer which explains why it doesn't make sense to combine Find and Include.Mcintyre
H
9

Heres how I do it in my generic repository:

    public T GetBy(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includes)
    {
        var result = GetAll();
        if (includes.Any())
        {
            foreach (var include in includes)
            {
                result = result.Include(include);
            }
        }
        return result.FirstOrDefault(predicate);
    }

Note this is using lambda includes and FirstOrDefault rather than find but the result is the same.

You can check out the full source for my generic repository here.

You can call this by the following:

var entity = myRepository.GetBy(e=>e.Id == 7, /*Includes*/ e=> e.ANavigationProperty, e=>e.AnotherNavProperty);

Edit:

I don't use generic repositories anymore, instead I use extension methods to build the query on the fly. I find this gets much better reuse. (see my article here on Composable Repositories)

Haga answered 15/1, 2013 at 19:43 Comment(4)
Hi Luke, could you show me an example of a call to your GetBy() method please?Demote
@bern i also wrote article on my generic repository a while ago with an example MVC solution. blog.staticvoid.co.nz/2011/10/13/…Haga
Thanks @LukeMcGregor you rock! I just plugged that into my code and it worked a treat. Also nice blog by the way, I was reading through it yesterday by coincidence. FYI: You can turn that "foreach" into this: "result = includes.Aggregate(result, (current, include) => current.Include(include));"Demote
@bern Awesome I might do that :) feel free to fork it and add improvements if you want as well. If you're interested in a little bit more substantial a use case of how I actually use my repository check out my blog source code which is also in githubHaga
D
-1

Include works on IQueryable<T> while FindAsync works on DbSet<T>. But we can improvise, in order to take in a generic id, you need to constrain it to being IEquatable to itself. Then you can use .Equals.

Consider the below implementation

public interface IEntity
{
    object?[] GetKeys();
}

public interface IEntity<out TId> : IEntity
{
    TId Id { get; }
}

public interface IReadOnlyRepository<TEntity, TId> where TEntity: class, IEntity<TId>
{
    Task<TEntity> GetAsync(TId id, bool includeDetails, CancellationToken cancellation);
    Task<IList<TEntity>> GetAllAsync(bool includeDetails, CancellationToken cancellation);
}

Let us implement GetAsync

Attempt #1

    public async Task<TEntity> GetAsync(TId id, bool includeDetails, CancellationToken cancellation)
    {
        var dbContext = await _dbContextAccessor.GetDbContextAsync(cancellation);
        var query = dbContext.Set<TEntity>().Where(e => e.Id == id);
        if (includeDetails)
            query = IncludeDetails(query);
        return await query.FirstOrDefaultAsync(cancellation);
    }

This gives a compiler error, since the equality operator is not defined for the generic type TId.

Attempt #2

    public async Task<TEntity> GetAsync(TId id, bool includeDetails, CancellationToken cancellation)
    {
        var dbContext = await _dbContextAccessor.GetDbContextAsync(cancellation);
        var query = dbContext.Set<TEntity>().Where(e => e.Id.Equals(id));
        if (includeDetails)
            query = IncludeDetails(query);
        return await query.FirstOrDefaultAsync(cancellation);
    }

This time we get it to compile, but blows up at runtime, since this is actually using object.Equals.

Attempt #3 (Solution)

Now we add a generic constraint to the TId generic type IEquatable<TId> as follows

    public interface IReadOnlyRepository<TEntity, TId> 
    where TEntity: class, IEntity<TId>
    where TId : IEquatable<TId>

This now works, the GetAsync implementation is the same as Attempt#2 but this time, IEquatable<TId>.Equals is being used instead of Object.Equals.

Dragnet answered 15/7, 2024 at 9:34 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.