repository pattern

in Architetture Software, ASPNET Core

Repository Pattern in Dotnet Core

Reading Time: 4 minutes

Sviluppato applicazioni che richiedono operazioni CRUD è necessario creare classi e metodi dedicati alla singola logica. Non sempre lo sviluppo della logica CRUD avviene utilizzando pattern specifici, ma al crescere della complessità dell’architettura è molto consigliata l’applicazione di una standardizzazione di ogni repository.

Alla base dell’implementazione di un repository c’è sempre la definizione di un’interfaccia. All’interno dell’interfaccia vengono inserite tutte le firme dei metodi che dovranno essere implementati all’interno dei singoli repository.

namespace ContractInterfaces
{
    public interface IRepositoryBase<T>
    {
        IQueryable<T> FindAll();
        IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression);
        void Create(T entity);
        void Update(T entity);
        void Delete(T entity);
    }
}

Nel codice precedente sono stati definiti i metodi che dovranno essere necessariamente creati all’interno dei repository che erediteranno dall’intefaccia IRepositoryBase. Da notare il metodo

IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression);

che accetta come parametro una Expression, che semplificando molto non è altro che la condizione che deve essere utilizzata come filtro. La condizione è un’expression che ritorna un bool.

L’intefaccia IRepositoryBase viene utilizzata per la creazione di una classe abstract (cioè una classe che non può essere istanziata, ma solo derivata). Dalla classe abstract verrano definite tutte le classi dei repository.

 namespace Repository
{
    public abstract class RepositoryBase<T> : IRepositoryBase<T> where T : class
    {
        protected RepositoryContext RepositoryContext { get; set; }
 
        public RepositoryBase(RepositoryContext repositoryContext)
        {
            this.RepositoryContext = repositoryContext;
        }
 
        public IQueryable<T> FindAll()
        {
            return this.RepositoryContext.Set<T>().AsNoTracking();
        }
 
        public IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression)
        {
            return this.RepositoryContext.Set<T>().Where(expression).AsNoTracking();
        }
 
        public void Create(T entity)
        {
            this.RepositoryContext.Set<T>().Add(entity);
        }
 
        public void Update(T entity)
        {
            this.RepositoryContext.Set<T>().Update(entity);
        }
 
        public void Delete(T entity)
        {
            this.RepositoryContext.Set<T>().Remove(entity);
        }
    }
}

Nella definizione della classe astratta viene anche definito il vincolo del sul parametro generico T, imponenendo che sia una classe. L’utilizzo del parametro generico T garantisce la riusabilità della classe astratta in contesti differenti.

Oltre alla definizione del RepositoryContext (l’oggetto che ci consente di accedere ai dati nelle tabelle dei database), è da notare l’utilizzo di AsNoTracking() per i metodi a sola lettura (per velocizzare le operazioni in sola lettura).

Creazione della classe Repository

A questo punto è possibile creare ogni singola classe Repository facendola ereditare dalla classe astratta. Inoltre, ogni singolo repository potrebbe avere la necessità di implementare funzioni specifiche: in questo caso sarà necessario implementare specifiche interfacce. Ogni singola interfaccia dovrà ereditare da IRepositoryBase:

using Entities.Models;
 
namespace Contracts
{
    public interface IAccountRepository : IRepositoryBase<Account>
    {
    }
}

Nell’esempio è stata definita l’interfaccia per il repository degli Account, che eredita da IRepositoryBase con classe entity Account. L’interfaccia IAccountRepository può essere modificata integrandola con meotodi specifici.

A questo punto non resta che definire la classe del repository vera e propria:

using Contracts;
using Entities;
using Entities.Models;
 
namespace Repository
{
    public class AccountRepository : RepositoryBase<Account>, IAccountRepository
    {
        public AccountRepository(RepositoryContext repositoryContext)
            :base(repositoryContext)
        {
        }
    }
}

che erediterà tutti i metodi della classe astratta RepositoryBase (con il passaggio della classe Entity (nel nostro caso Account) e dovrà definire tutti i metodi presenti all’interno della propria interfaccia IAccountRepository. Da notare il costruttore della classe che accetterà in ingresso il context passandolo alla costruttore della classe base.

Utilizzando questo pattern abbiamo standardizzato e reso facilmente mantenibile il codice dei singoli repository.

Riassumendo i passi che sono stati seguiti per la creazione delle classi del repository sono:

  • creazione dell’interfaccia generica “base”
  • creazione della classe abstract “base” che eredita dall’interfaccia “base”
  • creazione dell’interfaccia del singolo repository ereditando dalla IRepositoryBase<T>
  • creazione della singola classe del repository

Creazione del Wrapper per i repository

Utilizzando la DI di DotNet Core possiamo iniettare i nostri repository all’interno dei costruttori dei controller. Se devono essere utilizzati solo pochi repository si può ipotizzare di inserirli direttamente all’interno della firma del costruttore. Ma se i repository da iniettare fossero molti? Al crescere della complessità dell’applicazione, cresce anche la difficoltà nell’aggiunta di tutti i repository. Da qui la necessità di creare una classe wrapper da iniettare direttamente nel costruttore del controller.

Definiamo una nuova interfaccia:

namespace Contracts
{
    public interface IRepositoryWrapper
    {
        IOwnerRepository Owner { get; }
        IAccountRepository Account { get; }
        void Save();
    }
}

chiamata IRepositoryWrapper che restituisce due oggetti derivati dall’interfaccia IOwnerRepository e IAccountRepository. Inoltre, all’interno della stessa interfaccia abbiamo indicato il metodo Save(). L’aggiunta di un metodo Save all’interno dell’interfaccia è una pratica piuttosto comune: consente di eseguire una serie di metodi provenienti dai repository del wrapper e persistere le modifiche una volta terminati. Ovviamente, questo è possibile, perchè nei metodi CRUD definiti all’interno della classe base non è stata indicata un’operazione di salvataggio.

Proviamo ad implementare un repository wrapper sulla base dell’interfaccia IRepositoryWrapper:

Nublic class RepositoryWrapper : IRepositoryWrapper
    {
        private RepositoryContext _repoContext;
        private IOwnerRepository _owner;
        private IAccountRepository _account;
 
        public IOwnerRepository Owner {
            get {
                if(_owner == null)
                {
                    _owner = new OwnerRepository(_repoContext);
                }
 
                return _owner;
            }
        }
 
        public IAccountRepository Account {
            get {
                if(_account == null)
                {
                    _account = new AccountRepository(_repoContext);
                }
 
                return _account;
            }
        }
 
        public RepositoryWrapper(RepositoryContext repositoryContext)
        {
            _repoContext = repositoryContext;
        }
 
        public void Save()
        {
            _repoContext.SaveChanges();
        }
    }
}

Nulla di particolarmente difficile: ogni property verifica l’esistenza di un’instanza del repository e nel caso non esista procede con la creazione di un nuovo oggetto, restituendolo.

Nel costruttore del wrapper viene indicato passato come parametro il context per accedere ai dati. L’implementazione del metodo Save() consente di salvare tutte le modifiche pending presenti nel contesto.

A questo punto il nostro wrapper è pronto per essere iniettato all’interno dei controller.

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private IRepositoryWrapper _repoWrapper;
 
    public ValuesController(IRepositoryWrapper repoWrapper)
    {
        _repoWrapper = repoWrapper;
    }
    // GET api/values
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

Rimane ancora da istruire dotnet core per effettuare la DI al momento del bisogno. E’ necessario modificare il file Startup.cs, e modificare il metodo ConfigureServices aggiungendo:

services.AddScoped<IRepositoryWrapper, RepositoryWrapper>();

In questo modo, ogni volta che verrà effettuato l’accesso al controller, l’oggetto repoWrapper (il parametro del costruttore) verrà istanziato utilizzando la classe repositoryWrapper.

Il codice dell’esempio (console application) è disponibile su github.