Il Repository Pattern è un potente strumento per strutturare l’accesso ai dati in un’applicazione .NET. In questo tutorial, ti guiderò attraverso un’implementazione avanzata del pattern, creando un nuovo progetto. Potrai facilmente adattare questi passaggi al tuo progetto esistente.
Se preferisci vedere subito il codice completo, puoi trovare il progetto su GitHub: EF.RepositoryPattern.NET
Perché Usare il Repository Pattern?
Il Repository Pattern offre diversi vantaggi:
- Separazione delle Preoccupazioni: Isola la logica di accesso ai dati dal resto dell’applicazione, rendendo il codice più pulito e manutenibile.
- Testabilità Migliorata: Facilita la scrittura di unit test permettendo di sostituire facilmente le implementazioni reali con mock.
- Flessibilità e Scalabilità: Consente di cambiare la fonte dei dati o aggiungere nuove entità senza modificare il codice client, rendendo l’applicazione più scalabile.
Implementazione
Ora che hai capito i vantaggi, passiamo all’implementazione.
1. Inizializzazione del Progetto
dotnet new webapi -n EF.RepositoryPattern.NET
cd EF.RepositoryPattern.NET
Dopo aver creato il progetto, organizza la struttura delle cartelle come mostrato nell’immagine.
2. Installazione dei Pacchetti NuGet
Dopo aver creato il progetto, è necessario installare i seguenti pacchetti NuGet per supportare Entity Framework Core
, SQLite
e Serilog
:
dotnet add package Microsoft.EntityFrameworkCore -v 8.0.8
dotnet add package Microsoft.EntityFrameworkCore.Sqlite -v 8.0.8
dotnet add package Microsoft.EntityFrameworkCore.Sqlite.Core -v 8.0.8
dotnet add package Microsoft.EntityFrameworkCore.Tools -v 8.0.8
dotnet add package Serilog -v 4.0.1
3. Definizione delle Interfacce
In Interfaces
, crea i seguenti file:
IBaseEntity.cs
namespace EF.RepositoryPattern.NET.Interfaces;
public interface IBaseEntity;
Questa interfaccia vuota serve come marker per tutte le tue entità. Permette di vincolare i repository generici a lavorare solo con entità che implementano questa interfaccia.
IBaseRepository.cs
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace EF.RepositoryPattern.NET.Interfaces;
///
/// Base repository class providing common CRUD operations for entities.
///
/// The type of entity managed by the repository.
public interface IBaseRepository where TEntity : IBaseEntity
{
///
/// The UseLazyLoadingProxies property in Entity Framework is used to enable lazy loading of related entities.
/// When this property is enabled, related entities are not automatically loaded
/// when the main entity is loaded.
///
/// Used to enable or disable lazy loading.
public void UseAsLazyLoadingProxies(bool useAsLazyLoadingProxies);
///
/// Retrieves an IQueryable representing the given entity.
///
/// An IQueryable of TEntity.
IQueryable AsQueryable();
///
/// Retrieves an IQueryable as no tracking representing the given entity.
///
/// An IQueryable of TEntity.
public IQueryable AsNoTracking();
///
/// Retrieves all entities synchronously.
///
/// An IEnumerable of TEntity containing all entities.
IEnumerable GetAll();
///
/// Retrieves all entities asynchronously.
///
/// The cancellation token.
/// An IEnumerable of TEntity containing all entities.
Task> GetAllAsync(CancellationToken cancellationToken = default);
///
/// Adds a new entity synchronously.
///
/// The entity to add.
void Add(TEntity entity);
///
/// Adds a new entity asynchronously.
///
/// The entity to add.
/// The cancellation token.
Task AddAsync(TEntity entity, CancellationToken cancellationToken = default);
///
/// Adds a range of entities synchronously.
///
/// The entities to add.
void AddRange(IEnumerable entities);
///
/// Adds a range of entities asynchronously.
///
/// The entities to add.
/// The cancellation token.
Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default);
///
/// Updates an existing entity synchronously.
///
/// The old entity.
/// The new entity with updated values.
void Update(TEntity oldEntity, TEntity newEntity);
///
/// Updates an existing entity synchronously.
///
/// The entity to update.
void Update(TEntity entity);
///
/// Updates an existing entity asynchronously.
///
/// The entity to update.
/// The cancellation token.
Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default);
///
/// Updates an existing entity asynchronously.
///
/// The old entity.
/// The new entity with updated values.
/// The cancellation token.
Task UpdateAsync(TEntity oldEntity, TEntity newEntity, CancellationToken cancellationToken = default);
///
/// Updates a range of entities synchronously.
///
/// The entities to update.
void UpdateRange(IEnumerable entities);
///
/// Updates a range of entities asynchronously.
///
/// The entities to update.
/// The cancellation token.
Task UpdateRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default);
///
/// Deletes an entity synchronously.
///
/// The entity to delete.
void Delete(TEntity entity);
///
/// Deletes an entity asynchronously.
///
/// The entity to delete.
/// The cancellation token.
Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default);
///
/// Deletes a range of entities synchronously.
///
/// The entities to delete.
void DeleteRange(IEnumerable entities);
///
/// Deletes a range of entities asynchronously.
///
/// The entities to delete.
/// The cancellation token.
Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default);
///
/// Truncates the entity synchronously.
///
void Truncate();
///
/// Truncates the entity asynchronously.
///
/// The cancellation token.
Task TruncateAsync(CancellationToken cancellationToken = default);
}
Questa interfaccia generica definisce le operazioni base che ogni repository dovrebbe supportare. L’uso di generics permette di riutilizzare questa interfaccia per diverse entità.
ICustomersRepository.cs
namespace EF.RepositoryPattern.NET.Interfaces;
public interface ICustomersRepository : IBaseRepository where T : IBaseEntity;
Questa interfaccia estende IBaseRepository
per l’entità dei Customers. È possibile registrare CustomersRepository
anche utilizzando IBaseRepository
invece di ICustomersRepository
.
Tuttavia, è importante notare che se ci sono più implementazioni registrate con la stessa interfaccia, l’ultima registrata sovrascrive le precedenti. Quindi, se si hanno più repository che implementano IBaseRepository
, come ad esempio OrdersRepository
e ProductsRepository
, l’ultimo registrato sarà quello effettivamente utilizzato per tutte le dipendenze di IBaseRepository
.
Per evitare questo comportamento e assicurarsi che ogni implementazione sia correttamente associata alla propria interfaccia, è consigliabile creare un’interfaccia specifica per ogni repository, come fatto con ICustomersRepository
. In questo modo, si può registrare ogni repository con la sua interfaccia dedicata.
4. Implementazione del Repository Base
In Repositories
, crea i seguenti file:
BaseRepository.cs
#region Usings
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EF.RepositoryPattern.NET.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
#endregion
namespace EF.RepositoryPattern.NET.Repositories;
///
/// Base repository class providing common CRUD operations for entities.
///
/// The type of entity managed by the repository.
/// The type of the DbContext used by the repository.
public abstract class BaseRepository
where TEntity : class, IBaseEntity
where TContext : DbContext
{
private readonly TContext _dbContext;
private bool? _useAsLazyLoadingProxies;
///
/// Constructs an instance of the base repository with the provided DbContext.
///
/// The DbContext instance.
protected BaseRepository(TContext dbContext)
{
_dbContext = dbContext;
if (_useAsLazyLoadingProxies.HasValue)
_dbContext.ChangeTracker.LazyLoadingEnabled = _useAsLazyLoadingProxies.Value;
}
///
/// The UseLazyLoadingProxies property in Entity Framework is used to enable lazy loading of related entities.
/// When this property is enabled, related entities are not automatically loaded
/// when the main entity is loaded.
///
/// Used to enable or disable lazy loading.
public void UseAsLazyLoadingProxies(bool useAsLazyLoadingProxies)
{
_useAsLazyLoadingProxies = useAsLazyLoadingProxies;
}
///
/// Retrieves an IQueryable representing the given entity.
///
/// An IQueryable of TEntity.
public IQueryable AsQueryable()
{
Log.Logger.Information("Retrieving queryable for '{TEntity}'.", typeof(TEntity).Name);
return _dbContext.Set();
}
///
/// Retrieves an IQueryable as no tracking representing the given entity.
///
/// An IQueryable of TEntity.
public IQueryable AsNoTracking()
{
Log.Logger.Information("Retrieving queryable for '{TEntity}'.", typeof(TEntity).Name);
return _dbContext.Set().AsNoTracking();
}
///
/// Retrieves all entities synchronously.
///
/// An IEnumerable of TEntity containing all entities.
public virtual IEnumerable GetAll()
{
Log.Logger.Information("Retrieving all records from '{TEntity}'.", typeof(TEntity).Name);
return _dbContext.Set();
}
///
/// Retrieves all entities asynchronously.
///
/// The cancellation token.
/// An IEnumerable of TEntity containing all entities.
public virtual Task> GetAllAsync(CancellationToken cancellationToken = default)
{
Log.Logger.Information("Retrieving all records from '{TEntity}'.", typeof(TEntity).Name);
return Task.FromResult>(_dbContext.Set());
}
///
/// Adds a new entity synchronously.
///
/// The entity to add.
public virtual void Add(TEntity entity)
{
Log.Logger.Information("Adding record to '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Set().Add(entity);
_dbContext.SaveChanges();
}
///
/// Adds a new entity asynchronously.
///
/// The entity to add.
/// The cancellation token.
public virtual async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default)
{
Log.Logger.Information("Adding record to '{TEntity}'.", typeof(TEntity).Name);
await _dbContext.Set().AddAsync(entity, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
}
///
/// Adds a range of entities synchronously.
///
/// The entities to add.
public virtual void AddRange(IEnumerable entities)
{
Log.Logger.Information("Adding range records to '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Set().AddRange(entities);
_dbContext.SaveChanges();
}
///
/// Adds a range of entities asynchronously.
///
/// The entities to add.
/// The cancellation token.
public virtual async Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default)
{
Log.Logger.Information("Adding range records to '{TEntity}'.", typeof(TEntity).Name);
await _dbContext.Set().AddRangeAsync(entities, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
}
///
/// Updates an existing entity synchronously.
///
/// The entity to update.
public virtual void Update(TEntity entity)
{
Log.Logger.Information("Updating record from '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Update(entity);
_dbContext.SaveChanges();
}
///
/// Updates an existing entity synchronously.
///
/// The old entity.
/// The new entity with updated values.
public virtual void Update(TEntity oldEntity, TEntity newEntity)
{
Log.Logger.Information("Updating old record from '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Entry(oldEntity).State = EntityState.Detached;
_dbContext.Update(newEntity);
_dbContext.SaveChanges();
}
///
/// Updates an existing entity asynchronously.
///
/// The entity to update.
/// The cancellation token.
public virtual async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
{
Log.Logger.Information("Updating record from '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Update(entity);
await _dbContext.SaveChangesAsync(cancellationToken);
}
///
/// Updates an existing entity asynchronously.
///
/// The old entity.
/// The new entity with updated values.
/// The cancellation token.
public virtual async Task UpdateAsync(TEntity oldEntity, TEntity newEntity, CancellationToken cancellationToken = default)
{
Log.Logger.Information("Updating old record from '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Entry(oldEntity).State = EntityState.Detached;
_dbContext.Update(newEntity);
await _dbContext.SaveChangesAsync(cancellationToken);
}
///
/// Updates a range of entities synchronously.
///
/// The entities to update.
public virtual void UpdateRange(IEnumerable entities)
{
Log.Logger.Information("Updating range records from '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Set().UpdateRange(entities);
_dbContext.SaveChanges();
}
///
/// Updates a range of entities asynchronously.
///
/// The entities to update.
/// The cancellation token.
public virtual async Task UpdateRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default)
{
Log.Logger.Information("Updating range records from '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Set().UpdateRange(entities);
await _dbContext.SaveChangesAsync(cancellationToken);
}
///
/// Deletes an entity synchronously.
///
/// The entity to delete.
public virtual void Delete(TEntity entity)
{
Log.Logger.Information("Deleting record from '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Set().Remove(entity);
_dbContext.SaveChanges();
}
///
/// Deletes an entity asynchronously.
///
/// The entity to delete.
/// The cancellation token.
public virtual async Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default)
{
Log.Logger.Information("Deleting record from '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Set().Remove(entity);
await _dbContext.SaveChangesAsync(cancellationToken);
}
///
/// Deletes a range of entities synchronously.
///
/// The entities to delete.
public virtual void DeleteRange(IEnumerable entities)
{
Log.Logger.Information("Deleting range records from '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Set().RemoveRange(entities);
_dbContext.SaveChanges();
}
///
/// Deletes a range of entities asynchronously.
///
/// The entities to delete.
/// The cancellation token.
public virtual async Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default)
{
Log.Logger.Information("Deleting range records from '{TEntity}'.", typeof(TEntity).Name);
_dbContext.Set().RemoveRange(entities);
await _dbContext.SaveChangesAsync(cancellationToken);
}
///
/// Truncates the entity synchronously.
///
public virtual void Truncate()
{
Log.Logger.Information("Deleting range records from: {TEntity}", typeof(TEntity).Name);
_dbContext.Set().RemoveRange(_dbContext.Set());
_dbContext.SaveChanges();
}
///
/// Truncates the entity asynchronously.
///
/// The cancellation token.
public virtual async Task TruncateAsync(CancellationToken cancellationToken = default)
{
Log.Logger.Information("Deleting range records from: {TEntity}", typeof(TEntity).Name);
_dbContext.Set().RemoveRange(_dbContext.Set());
await _dbContext.SaveChangesAsync(cancellationToken);
}
}
Questa classe astratta fornisce un’implementazione di base per IBaseRepository
. L’uso di due generici (TEntity
e TContext
) permette di utilizzare lo stesso repository base con diversi tipi di entità e contesti di database.
CustomersRepository.cs
using EF.RepositoryPattern.NET.Contexts;
using EF.RepositoryPattern.NET.Interfaces;
namespace EF.RepositoryPattern.NET.Repositories;
public class CustomersRepository(CustomersDbContext context)
: BaseRepository(context), ICustomersRepository
where TEntity : class, IBaseEntity;
Questa classe concreta estende BaseRepository
specificamente per l’entità Customers
.
5. Definizione del Contesto del Database
In Contexts
, crea il seguente file:
CustomersDbContext.cs
namespace EF.RepositoryPattern.NET.Contexts;
public class CustomersDbContext(DbContextOptions dbContextOptions)
: DbContext(dbContextOptions)
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity(entity =>
{
entity.Property(e => e.Id).ValueGeneratedOnAdd();
});
}
}
Questo contesto specifico per i Customers
configura come l’entità CustomersEntity
deve essere mappata nel database.
6. Creazione dell'Entità
In Entities
, crea il seguente file:
CustomersEntity.cs
namespace EF.RepositoryPattern.NET.Entities;
public class CustomersEntity : IBaseEntity
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
Questa classe rappresenta l’entità Customers
nel sistema.
7. Implementazione del Controller
In Controllers
, crea il seguente file:
CustomersController.cs
namespace EF.RepositoryPattern.NET.Controllers;
[ApiController]
[Route("[controller]/[action]")]
public class CustomersController(ICustomersRepository customersRepository) : ControllerBase
{
[HttpGet]
public async Task GetCustomersAsync()
{
return Ok(await customersRepository.GetAllAsync());
}
[HttpPost]
public async Task CreateCustomersAsync(string firstName, string lastName, string email)
{
var newCustomer = new CustomersEntity
{
FirstName = firstName,
LastName = lastName,
Email = email
};
await customersRepository.AddAsync(newCustomer);
return Ok(newCustomer);
}
}
Questo controller utilizza il repository dei Customers
per gestire le richieste HTTP.
8. Registrazione dei Servizi
Program.cs
builder.Services.AddDbContext(options =>
options.UseSqlite("Data Source=Customers.db;"));
builder.Services.AddScoped(typeof(ICustomersRepository<>), typeof(CustomersRepository<>));
Questa configurazione registra il contesto del database e il repository dei Customers
nel container di dipendenze.
// Nel metodo ConfigureServices o nella parte di configurazione dei servizi
builder.Services.AddControllers();
// Nella parte di configurazione del middleware
app.MapControllers();
9. Migrazione e Test dell'Implementazione
9.1. Creazione della migrazione iniziale
dotnet ef migrations add --project EF.RepositoryPattern.NET/EF.RepositoryPattern.NET.csproj --startup-project EF.RepositoryPattern.NET/EF.RepositoryPattern.NET.csproj --context EF.RepositoryPattern.NET.Contexts.CustomersDbContext --configuration Release --verbose Initial --output-dir Migrations
Questo comando creerà una nuova migrazione chiamata “Initial” nella cartella Migrations.
9.2. Applicazione della migrazione
La migrazione verrà applicata automaticamente all’avvio dell’applicazione grazie al seguente codice da aggiunto in Program.cs
:
using (var serviceScope = app.Services.CreateScope())
{
var context = serviceScope.ServiceProvider.GetService();
context!.Database.Migrate();
}
Questo assicura che il database sia sempre aggiornato con l’ultima migrazione disponibile quando l’applicazione viene avviata.
9.3. Test dell'Implementazione
Ora che l’implementazione è completata, siamo pronti ad avviare il progetto e testare il repository. Il progetto EF.RepositoryPattern.NET è configurato per essere eseguito sulla porta 5000
. Con l’applicazione in esecuzione, possiamo aprire un terminale e testare il nostro controller utilizzando i seguenti comandi curl
:
Creazione di un nuovo customer
curl --location --request POST 'http://localhost:5000/customers/createcustomers?firstName=John&lastName=Doe&email=john.doe@example.com'
Elenco dei customers
curl --location 'http://localhost:5000/customers/getcustomers
Conclusione
Il Repository Pattern in .NET che hai implementato offre una solida base per gestire l’accesso ai dati nelle tue applicazioni. Questo potente strumento ti permette di separare le responsabilità, migliorare la testabilità e ottenere maggiore flessibilità nel gestire diverse fonti di dati.
Ricorda, come ogni pattern architetturale, l’efficacia del Repository Pattern dipende da come lo applichi. Valuta sempre attentamente il contesto del tuo progetto prima di decidere se e come implementarlo.
Ora Tocca a Te
Per consolidare la tua comprensione e ampliare le tue competenze con il Repository Pattern, ecco alcune sfide che puoi affrontare:
Estendi il Repository: Aggiungi metodi personalizzati a
ICustomersRepository
per gestire query più complesse, come la ricerca di clienti per nome o email.Implementa il Unit of Work Pattern: Combina il Repository Pattern con il Unit of Work Pattern per gestire transazioni che coinvolgono più repository.
Aggiungi la Validazione: Implementa la validazione delle entità prima di salvarle nel database.
Crea Test Unitari: Scrivi una suite di test unitari per il tuo repository utilizzando un framework di mocking.
Esplora altre Fonti di Dati: Prova a implementare il pattern con un database diverso, come MongoDB o PostgreSQL.
La pratica è fondamentale per padroneggiare questi concetti. Continua a sperimentare, a fare domande e a condividere le tue esperienze con la comunità degli sviluppatori.