Reputation: 2458
I want to modify existing generic repository to add optional select
functionality like in Entity Framework Core.
Desired result:
private readonly IUnitOfWork _unit;
// ...
// without using select functionality
IEnumerable<Entity> entities = await _unit.Account.AllAsync();
Entity? x = await _unit.Account.SingleAsync(x => x == id);
// using select functionality
IEnumerable<DTO> y = await _unit.Account.AllAsync(select: x => new DTO
{
Name = x.Name
});
DTO? y = await _unit.Account.SingleAsync(x => x == id, select: x => new DTO
{
Name = x.Name
});
I tried to implement solution from this question Select specific columns in a generic repository function but parameter was required and I want it to be optional.
For simplicity I only leave methods to which I want to add this functionality in generic repository:
in IBaseRepository.cs
public interface IBaseRepository<T> where T : BaseEntity
{
Task<IEnumerable<T>> AllAsync(
Expression<Func<T, bool>>? filter = null,
Func<IQueryable<T>, IOrderedQueryable<T>>? order = null,
Func<IQueryable<T>, IIncludableQueryable<T, object>>? include = null,
int skip = 0,
int take = int.MaxValue,
Track track = Track.NoTracking);
Task<T?> SingleAsync(
Expression<Func<T, bool>> filter, Func<IQueryable<T>,
IIncludableQueryable<T, object>>? include = null,
Track track = Track.Tracking);
}
in BaseRepository.cs
public class BaseRepository<T> : IBaseRepository<T> where T : BaseEntity
{
private readonly DataContext _context;
internal DbSet<T> _set;
public BaseRepository(DataContext context)
{
_context = context;
_set = context.Set<T>();
}
public async Task<IEnumerable<T>> AllAsync(
Expression<Func<T, bool>>? filter = null,
Func<IQueryable<T>, IOrderedQueryable<T>>? order = null,
Func<IQueryable<T>, IIncludableQueryable<T, object>>? include = null,
int skip = 0, int take = int.MaxValue, Track track = Track.NoTracking)
{
IQueryable<T> query = _set;
switch (track)
{
case Track.NoTracking:
query = query.AsNoTracking();
break;
case Track.NoTrackingWithIdentityResolution:
query = query.AsNoTrackingWithIdentityResolution();
break;
default:
query = query.AsTracking();
break;
}
query = skip == 0 ? query.Take(take) : query.Skip(skip).Take(take);
query = filter is null ? query : query.Where(filter);
query = order is null ? query : order(query);
query = include is null ? query : include(query);
return await query.ToListAsync();
}
public async Task<T?> SingleAsync(
Expression<Func<T, bool>> filter,
Func<IQueryable<T>, IIncludableQueryable<T, object>>? include = null,
Track track = Track.Tracking)
{
IQueryable<T> query = _set;
switch (track)
{
case Track.NoTracking:
query = query.AsNoTracking();
break;
case Track.NoTrackingWithIdentityResolution:
query = query.AsNoTrackingWithIdentityResolution();
break;
default:
query = query.AsTracking();
break;
}
query = filter is null ? query : query.Where(filter);
query = include is null ? query : include(query);
return await query.SingleOrDefaultAsync();
}
}
in BaseEntity.cs
(every class-dbtable would inherit from BaseEntity
)
public abstract class BaseEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
public DateTime CreatedAt { get; set; } = DateTime.Now;
public DateTime? UpdatedAt { get; set; }
}
Upvotes: 1
Views: 2720
Reputation: 34663
Ask yourself honestly: "Why do I believe I need a Generic Repository?"
A: "Because I want to add a layer of abstraction over EF so I can replace it if needed."
A: "Because I don't want to pollute my business logic with domain knowledge or EF-specific knowledge."
A: "Because I want to be able to introduce a point of abstraction so I can more easily write unit tests."
.
IQueryable<T> All();
IQueryable<T> Single(int id);
That's it. Your consumer can support filtering, sorting, pagination, exists check, count, projection, and even determine for itself whether the overhead of an async
operation is necessary. It's dead simple to mock out, adding just a pinch of complexity to support passing back data for async
consumption. The repository method can impose base-level filtering such as IsActive
for soft-delete systems, or checking authorization for the current user and factoring that into the results returned.
Where you want to introduce a solution to satisfy "DNRY" (Do not repeat yourself) or enforce a level of conformity in your application, or between identical (not merely similar) concerns such as a Web API service and a web application, that can be done by handing off to a common service that fetches and packages data in a consistent way using the repository to return DTOs. Otherwise treat the repository like you would an MVC Controller, scope it to be responsible for one consumer, and only have that one reason to change.
The typical argument against returning IQueryable
is that it is a weak or leaky abstraction and hands the consumer a loaded shotgun. This is 100% true that it is a weak abstraction, this makes it very easy to mock out. If you don't need to mock it out for unit tests, You just "ain't gonna need it." As far as giving developers a loaded weapon, IMHO it is far, far better for a project to give implementation developers the tools they need to be able to write efficient expressions against the domain and the training/knowledge how to do it properly, and correct mistakes if and when they are found; than it is to try and abstract the technology away to "dumb it down" or in essence, end up introducing just as much complexity and ability to make a mess, just with your name across the interface rather than EF.
Upvotes: 11