Reputation: 7951
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
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
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
.
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.
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
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