jeudi 23 juin 2022

Specific repository pattern with configurable fluent api

I'm trying to extend the capabilities of the Specific Repository Pattern by implementing fluent configuration for the navigation properties eager loading (on top of the automapping).

What I'm trying to achieve is to have this experience:

ISpecificRepository repo = new SpecificRepository()
var entries = repo.WithIncludes(x => x.NavProperty).GetAllTheEntriesWithLevel(5);

Here's my IRepository interface

public interface IRepository<TEntity, TDto>
{
    Task<List<TDto>> GetAllAsync(params Expression<Func<TDto, object>>[] properties);

    Task<TDto> GetAsync(object id);

    Task AddAsync(TDto dto);

    Task AddRangeAsync(IEnumerable<TDto> dtos);

    void Update(TDto dto);

    void Remove(TDto dto);

    void RemoveRange(IEnumerable<TDto> dtos);

    Task<bool> ContainsAsync(Expression<Func<TEntity, bool>> predicate);

    Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate);

    IRepository<TEntity, TDto> WithIncludes(params Expression<Func<TDto, object>>[] properties);
}

Now, I build a base abstract implementation called RepositoryBase

public abstract class RepositoryBase<TEntity, TDto> : IRepository<TEntity, TDto>
{
    protected readonly DbSet<TEntity> _set;

    protected readonly IMapper _mapper;

    private readonly DbContext _context;

    /// <summary>
    /// Backing field for PropsToInclude.
    /// </summary>
    private Expression<Func<TDto, object>>[] _properties;

    protected RepositoryBase(DbContext context, IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }

    /// <summary>
    /// Gets the rroperties to eager load in the resultset.
    /// </summary>
    protected Expression<Func<TDto, object>>[] PropsToInclude
    {
        get
        {
            if (_properties.Length == 0)
            {
                return Array.Empty<Expression<Func<TDto, object>>>();
            }

            Expression<Func<TDto, object>>[] propertiesToReturn =
                _properties.Select(x => Expression.Lambda<Func<TDto, object>>(x.Body, x.Parameters)).ToArray();
            _properties = Array.Empty<Expression<Func<TDto, object>>>();
            return propertiesToReturn;
        }

        private set
        {
            _properties = value;
        }
    }

    public async Task<List<TDto>> GetAllAsync(params Expression<Func<TDto, object>>[] properties)
    {
        IQueryable<TEntity> entities = _context.Set<TEntity>().AsQueryable();
        return await _mapper.ProjectTo<TDto>(entities, null, PropsToInclude).ToListAsync();
    }

    public async Task<TDto> GetAsync(object id)
    {
        TEntity entity = await _context.Set<TEntity>().FindAsync(id);
        return _mapper.Map<TDto>(entity);
    }

    public async Task AddAsync(TDto dto)
    {
        TEntity entity = _mapper.Map<TEntity>(dto);
        await _context.Set<TEntity>().AddAsync(entity);
    }

    public Task AddRangeAsync(IEnumerable<TDto> dtos)
    {
        IEnumerable<TEntity> entities = _mapper.Map<IEnumerable<TEntity>>(dtos);
        return _context.Set<TEntity>().AddRangeAsync(entities);
    }

    public void Update(TDto dto)
    {
        TEntity entity = _mapper.Map<TEntity>(dto);
        _context.Set<TEntity>().Attach(entity);
        _context.Entry(entity).State = EntityState.Modified;
    }

    public void Remove(TDto dto)
    {
        TEntity entity = _mapper.Map<TEntity>(dto);
        _context.Set<TEntity>().Remove(entity);
    }

    public void RemoveRange(IEnumerable<TDto> dtos)
    {
        IEnumerable<TEntity> entities = _mapper.Map<IEnumerable<TEntity>>(dtos);
        _context.Set<TEntity>().RemoveRange(entities);
    }

    public async Task<bool> ContainsAsync(Expression<Func<TEntity, bool>> predicate)
    {
        return await CountAsync(predicate) > 0;
    }

    public Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate)
    {
        return _context.Set<TEntity>().Where(predicate).CountAsync();
    }

    public IRepository<TEntity, TDto> WithIncludes(params Expression<Func<TDto, object>>[] properties)
    {
        PropsToInclude = properties;
        return this;
    }
}

Now, with the base methods implemented, I declare a specific Interface

public interface ISpecificRepository : IRepository<EntityA, MappedEntityA>
{
    Task<List<MappedEntityA>> GetAllTheEntriesWithLevel(int level);
}

And let the implementation to implement it and extends the base to inherit base implementations.

public class SpecificRepository : RepositoryBase<EntityA, MappedEntityA>, ISpecificRepository
{
    public SpecificRepository(DbContext context, IMapper mapper)
        : base(context, mapper)
    {
    }

    public async Task<List<MappedEntityA>> GetAllTheEntriesWithLevel(int level)
    {
        IQueryable<EntityA> entities= _set.Where(x => x.Level = level);
        return await _mapper.ProjectTo<MappedEntityA>(entities, null, PropsToInclude).ToListAsync();
    }
}

The problem with this approach is that the RepositoryBase expose a WithIncludes() method which returns an IRepository and then the specific repository implementation (that inherits it) does not have its specific methods after calling the method itself. I know that this implementation violates SRP but in which way can I achieve what I mentioned before?

Add: The desired is to centralize the implementation in a BaseClass (i.e. RepositoryBase) without the needing of implementation for each new Repository that I want to create.

Aucun commentaire:

Enregistrer un commentaire