Bern
Bern

Reputation: 7951

Need a generic EF method that accepts entity id and includes

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.

Upvotes: 5

Views: 5184

Answers (2)

Cogent
Cogent

Reputation: 402

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.

Upvotes: -1

undefined
undefined

Reputation: 34238

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)

Upvotes: 9

Related Questions