mikolaj semeniuk
mikolaj semeniuk

Reputation: 2458

Modifying existing generic repository pattern to add select

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

Answers (1)

Steve Py
Steve Py

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."

  • You are a victim of a self-fulfilling prophecy; Handcuffing yourself and your application's capability to leverage EF to it's fullest. The solution will either be so limited or complex and problematic that EF will be considered too slow or incapable of doing what is needed and needs to be replaced with something someone convinces you is better/faster.

A: "Because I don't want to pollute my business logic with domain knowledge or EF-specific knowledge."

  • This is a lie. Passing expressions into your repository pollutes the domain with domain and EF-specific knowledge & limitations. If the expressions are passed to EF's Linq methods, they must conform to the domain and they must be palatable to EF. You remove the reference to EF, but still impose the EF's limitations on your consumer. You cannot have references to methods in the expressions, or use unmapped properties. EF and your chosen provider need to be able to translate these expressions down to SQL, otherwise you are left writing even more code to introduce your own expression parser and adapter.

A: "Because I want to be able to introduce a point of abstraction so I can more easily write unit tests."

  • This is a good reason, but it can be solved in a considerably more simple approach:

.

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

Related Questions