Qualche tempo fa con un amico si stava discutendo di `Repository Pattern` e di tutto quello che gli gira intorno, tutta la disquisizione ruotava intorno a quale fosse il ruolo di un `repository` in un mondo orientato a CQRS. Siamo dopo un po’ di scambi di opinioni giunti alle seguenti questa conclusioni.

Un repository deve

  1. consentire di caricare un aggregato data la sua chiave primaria;
  2. consentire di aggiungere una nuova istanza di un aggregato;
  3. persistere le modifiche apportate ad un aggregato;
  4. rappresentare una Unit Of Work;

Un repository non deve

  1. consentire di eseguire query di nessun tipo;
  2. forzare la dichiarazione di intenti: Non è responsabilità di chi usa un aggregato sapere se deve persistere o meno delle modifiche, se il repository è una UoW la responsabilità è sua;

Se pensiamo di esprimere i requisiti di cui sopra con del codice C# lo possiamo fare usando la seguente interfaccia ad esempio:

namespace Sample.Repository.Pattern
{
public interface IRepository<T> where T : SomeConstraint
{
void Add(T entity);
T GetById(int Id);
void CommitChanges();
}
}

Che sebbene sembri soddisfare tutti i nostri requisiti non soddisfa il punto ‘4’ delle cose che un repository deve fare: Unit of Work.

Perché?

Provate a pensare come usereste l’interfaccia di cui sopra:

IRepository<Person> peopleRepo = …
 
var aPerson = peopleRepo.GetById( 123 );
aPerson.ChangeName( “new person name” );
 
peopleRepo.CommitChanges();

Se il repository è una UoW non dobbiamo dichiarare che vogliamo aggiornare l’istanza di Person che abbiamo appena modificato, lo sa già quindi ci basta a e avanza dichiarare che vogliamo salvare, CommitChanges().

Ma, osservate il seguente scenario:

IRepository<Person> peopleRepo = …
IRepository<Company> companiesRepo = …
 
var aPerson = peopleRepo.GetById( 123 );
aPerson.ChangeName( “new person name” );
 
var aCompany = companiesRepo.GetById( 456 );
aCompany.RegisterVATNumber( “vat number” );
 
peopleRepo.CommitChanges();
companiesRepo.CommitChanges();

Non è più una singola Unit Of Work e l`unico che abbiamo per garantire l’atomicità delle operazioni di cui sopra è racchiudere il codice in un blocco `using` con un `TransactionScope`.

Sebbene non sia un gran problema, e non è neanche sbagliato, la cosa che ci sta forzando ad andare in quella direzione non è l’architettura ma meramente la scelta di design che abbiamo fatto per l’API del nostro repository.

Se lo disegnassimo così?

namespace Sample.Repository.Pattern
{
public interface IRepository
{
void Add<T>(T entity) where T : SomeConstraint;
T GetById<T>(int Id) where T : SomeConstraint;
void CommitChanges();
}
}

La modifica è in apparenza banale, ci siamo limitati a spostare i generics dalla dichiarazione dell’interfaccia ai singoli membri della stessa, con la differenza fondamentale che adesso possiamo scrivere questo:

IRepository repo = …
 
var aPerson = repo.GetById<Person>( 123 );
aPerson.ChangeName( “new person name” );
 
var aCompany = repo.GetById<Company>( 456 );
aCompany.RegisterVATNumber( “vat number” );
 
repo.CommitChanges();

Che oltre a farci risparmiare un apio di righe di codice, ma chi se ne frega, ci permette di soddisfare elegantemente anche il punto “4”.

Volete vederlo in azione?

.m