Benvenuto nel mio universo di innovazione
Sono Cosmin Irimescu, Technical Leader con oltre 8 anni di esperienza nello sviluppo software, guidato da una profonda passione per la tecnologia e il suo impatto sull'umanità.
Iscriviti alla newsletter!
Resta al passo con .NET. Scopri, Impara, Sviluppa.Scopri gli Ultimi Articoli
Microservizi vs Monolite Modulare: Scegliere l’Architettura Giusta
Nella mia esperienza come architetto software, ho visto il panorama delle architetture evolversi nel corso degli anni.
Microservizi vs Monolite Modulare: Scegliere l’Architettura Giusta
Nella mia esperienza come architetto software, ho visto il panorama delle architetture evolversi nel corso degli anni. Mentre i microservizi hanno guadagnato popolarità e sono stati presentati come la soluzione a tutti i problemi, ho maturato una prospettiva più cauta riguardo al loro utilizzo nelle applicazioni web. In questo articolo, vorrei condividere le mie riflessioni su quando i microservizi sono realmente necessari e perché ritengo che un approccio più pragmatico, come l’architettura monolitica modulare, possa spesso essere la scelta migliore.
Il Fascino dei Microservizi
Quando i microservizi hanno iniziato a guadagnare terreno, è stato facile farsi trascinare dalle loro promesse di scalabilità infinita e flessibilità senza pari. L’idea di avere servizi autonomi, ognuno con il proprio ciclo di vita e la possibilità di utilizzare tecnologie diverse, sembrava la risposta a tutti i problemi dei monoliti ingombranti. Tuttavia, man mano che mi addentravo nell’implementazione dei microservizi, ho iniziato a rendermi conto che questa architettura portava con sé una serie di sfide non banali.
La Complessità Nascosta dei Microservizi
Uno degli aspetti che spesso viene sottovalutato quando si parla di microservizi è la complessità che introducono a livello di sistema. La gestione di decine o addirittura centinaia di servizi richiede un’infrastruttura robusta e competenze specifiche. Aspetti come la comunicazione tra servizi, il monitoraggio distribuito e il debugging diventano molto più complessi in un’architettura a microservizi. Ho visto team lottare per tenere sotto controllo questa complessità, investendo tempo e risorse preziose nella gestione dell’infrastruttura invece che nello sviluppo di funzionalità di valore per il business.
Microservizi: Quando Sono Realmente Necessari?
Nella mia esperienza, i microservizi sono realmente necessari solo in un sottoinsieme di casi. Servizi che richiedono una scalabilità estrema, come quelli di calcolo intensivo, possono trarre beneficio da un’architettura a microservizi. Tuttavia, per la maggior parte delle applicazioni web line-of-business, i requisiti di scalabilità non giustificano la complessità aggiuntiva introdotta dai microservizi. A meno che non si stia costruendo il prossimo Netflix o un sistema con un carico di lavoro eccezionale, un’architettura monolitica ben strutturata può soddisfare le esigenze di scalabilità e manutenibilità in modo più efficiente.
L’Alternativa Pragmatica: Architettura Monolitica Modulare
Negli anni, ho imparato ad apprezzare l’architettura monolitica modulare come un approccio pragmatico per la maggior parte delle applicazioni line-of-business. Questa architettura combina i benefici della modularità, come il basso accoppiamento e l’alta coesione, con la semplicità di un sistema unificato.
A differenza dei monoliti mal progettati che possono diventare uno “spaghetti code” ingestibile, un’architettura monolitica modulare ben strutturata è come una “lasagna code” appetitosa e ordinata. Suddividendo l’applicazione in strati (moduli) con responsabilità chiare e interfacce ben definite, possiamo ottenere molti dei vantaggi dei microservizi senza la complessità aggiuntiva. Ogni strato della nostra “lasagna code” ha un ruolo specifico e interagisce con gli altri strati in modo prevedibile, rendendo il sistema facile da capire e mantenere.
Ne è un esempio illuminante la presentazione di Steve Smith, rinomato sviluppatore ed educatore nella comunità .NET, in un recente episodio di “On .NET Live”. Nel suo talk intitolato “Modular Monoliths with ASP.NET Core“, Steve dimostra come strutturare un’applicazione monolitica in moduli indipendenti, ognuno con le proprie responsabilità e interfacce ben definite. Seguendo i principi SOLID e le best practice del clean architecture, Steve illustra come questo approccio promuova la separazione delle preoccupazioni e la manutenibilità del codice, pur mantenendo la semplicità di una singola unità di deployment. È una testimonianza convincente del potenziale dell’architettura monolitica modulare.
https://www.youtube.com/watch?v=VnIWtVdbwTg
Un altro vantaggio dell’architettura monolitica modulare è la sua flessibilità evolutiva. Se in futuro dovessero emergere esigenze di scalabilità più elevate, i moduli possono essere estratti come servizi indipendenti, consentendo una transizione graduale verso un’architettura più distribuita. Questo approccio incrementale riduce i rischi e permette di adattare l’architettura alle reali necessità del sistema nel tempo.
Conclusione
La mia esperienza mi ha insegnato che l’architettura software non è una taglia unica. Mentre i microservizi hanno il loro posto, ritengo che spesso vengano sovra-utilizzati nelle applicazioni web line-of-business. Prima di tuffarsi nell’adozione dei microservizi, è fondamentale valutare attentamente le reali esigenze di scalabilità e considerare se un’architettura monolitica modulare possa soddisfare tali esigenze in modo più efficiente.
Come architetti software, il nostro compito è quello di prendere decisioni informate, basate su una valutazione pragmatica dei requisiti, delle capacità del team e della complessità del dominio. Scegliere l’architettura giusta richiede un equilibrio tra l’innovazione e il pragmatismo, tenendo sempre presente l’obiettivo finale di fornire valore all’azienda in modo sostenibile.
In definitiva, credo che un approccio più cauto e graduale all’adozione dei microservizi, combinato con una solida architettura monolitica modulare, possa portare a risultati migliori per la maggior parte delle applicazioni web line-of-business. Questo approccio ci permette di beneficiare della modularità senza introdurre complessità non necessarie, consentendoci di concentrarci sulle funzionalità che contano davvero.
E tu, che ne pensi? Hai avuto esperienze simili con microservizi e monoliti modulari? Quali sono state le tue lezioni apprese nel navigare tra queste architetture? Condividi la tua opinione nei commenti qui sotto – sarei felice di continuare la conversazione e imparare dalle esperienze degli altri!
Repository Pattern in .NET
Il Repository Pattern è un potente strumento per strutturare l’accesso ai dati in un’applicazione .NET.
Repository Pattern in .NET
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
Inizia creando un nuovo progetto ASP.NET Core Web API:
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.
Questa organizzazione ti aiuterà a mantenere il tuo codice pulito e ben strutturato mentre implementi il Repository Pattern.
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à.
È importante notare che queste operazioni base sono quelle che io personalmente ritengo essenziali, altri progetti potrebbero richiedere operazioni diverse o aggiuntive. Sentiti libero di modificare questa interfaccia per adattarla alle specifiche esigenze del tuo progetto.
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.
Registrazione Controllers
A partire da .NET 8, i template ASP.NET Core Web API non includono più automaticamente la registrazione dei controller e il mapping delle rotte dei controller. Quindi, è necessario aggiungere manualmente queste configurazioni nel file Program.cs. Assicurati di includere le seguenti linee
// 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
Prima di testare l’implementazione, è necessario creare e applicare una migrazione per configurare il database.
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.
API Gateway con YARP in .NET
YARP (Yet Another Reverse Proxy) è una potente libreria .NET che permette di creare server reverse proxy ad alte prestazioni,
API Gateway con YARP in .NET
YARP (Yet Another Reverse Proxy) è una potente libreria .NET che permette di creare server reverse proxy ad alte prestazioni, pronti per la produzione e altamente personalizzabili.
Per gli sviluppatori impazienti di buttarsi nel codice, il progetto completo è disponibile su GitHub: APIGateway.NET
Perché usare YARP?
YARP (Yet Another Reverse Proxy) emerge come un’alternativa chiara e potente a Ocelot per l’implementazione di API Gateway in .NET. Offre una soluzione robusta e flessibile, particolarmente adatta per progetti moderni basati su architetture a microservizi.
- Semplicità e Chiarezza: A differenza di Ocelot, YARP offre un’API più intuitiva e una documentazione più completa. La sua struttura e il suo utilizzo sono progettati per essere più facilmente comprensibili, riducendo la curva di apprendimento per gli sviluppatori.
- Prestazioni Superiori: YARP è ottimizzato per offrire prestazioni elevate, superando Ocelot in scenari di carico intensivo. Supporta nativamente HTTP/2 e gRPC, rendendolo ideale per applicazioni moderne che richiedono alte prestazioni e bassa latenza.
- Maggiore Flessibilità: YARP permette una personalizzazione più granulare rispetto a Ocelot. Gli sviluppatori possono facilmente estendere e modificare il comportamento del proxy direttamente attraverso il codice .NET, offrendo un controllo più preciso sulla logica di routing e trasformazione delle richieste.
Implementazione Step-by-Step
Ora che abbiamo compreso cos’è YARP e perché usarlo, passiamo all’implementazione seguendo un approccio passo dopo passo.
1. Inizializzazione Soluzione
Usando Visual Studio o il tuo IDE preferito, iniziamo con la creazione della soluzione. A tal fine, è sufficiente creare due progetti ASP.NET Core Web API, come illustrato nell’immagine.
2. Configurazione Progetto APIGateway
Dopo aver creato i due progetti necessari per implementare il gateway, procediamo con la configurazione del progetto che fungerà da punto d’ingresso.
2.1 Configurazione YARP
Per installare la libreria YARP.ReverseProxy
è necessario aprire il NuGet Package Manager oppure utilizzare il seguente comando:
dotnet add package Yarp.ReverseProxy
2.2 Registrazione Servizio
Adesso che abbiamo installato la libreria, possiamo procedere con la sua registrazione. Per fare questo, facciamo riferimento al codice evidenziato.
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddControllers();
builder.Services.AddHealthChecks();
// YARP - Register Service
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
builder.Services.AddSwaggerGen();
// YARP - Configure RateLimiter Service to prevent DoS attacks
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("customPolicy", opt =>
{
opt.PermitLimit = 1;
opt.Window = TimeSpan.FromSeconds(4);
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 1;
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapControllers();
app.MapHealthChecks("healthy");
// YARP - Configure RateLimiter Application Part to prevent DoS attacks
app.UseRateLimiter();
// YARP - Add Application Part
app.MapReverseProxy();
await app.RunAsync();
RateLimiter
La componente RateLimiter non è indispensabile, ma integrarla aumenterà la sicurezza del nostro gateway. Con la configurazione attuale, il gateway introdurrà un ritardo di 4 secondi nel caso di chiamate simultanee.
2.3 Configurazione Rotte
Affinché il gateway operi correttamente, è indispensabile configurare le rotte delle diverse applicazioni che saranno ospitate. Modifichiamo il file appsettings.json
aggiungendo la seguente configurazione:
"ReverseProxy": {
"Routes": {
"get-customers": {
"ClusterId": "customers",
"RateLimiterPolicy": "customPolicy",
"Match": {
"Path": "/customers/all",
"Methods": [
"GET"
]
}
},
"create-customer": {
"ClusterId": "customers",
"Match": {
"Path": "/customers/create",
"Methods": [
"POST"
]
},
"Transforms": [
{
"RequestHeader": "X-Added-Website",
"Set": "https://CosminIrimescu.COM"
}
]
}
},
"Clusters": {
"customers": {
"Destinations": {
"customers/destination1": {
"Address": "http://localhost:5010/"
}
}
}
}
}
3. Configurazione Progetto APIGateway.Customers
Dopo aver configurato il gateway, passiamo ora all’implementazione del microservizio che esporrà le nostre API attraverso l’APIGateway. Questo progetto conterrà i controller e la logica di business che verranno accessibili tramite il nostro reverse proxy.
3.1 Implementazione CustomersController
Implementiamo ora un controller che esporrà due endpoint API essenziali: un GET per recuperare la lista dei customers e un POST per aggiungere nuovi customers, utilizzando una lista statica in memoria come semplice ‘database’ dimostrativo.
using Microsoft.AspNetCore.Mvc;
namespace APIGateway.Customers.Controllers;
[ApiController]
[Route("[controller]/[action]")]
public class CustomersController : ControllerBase
{
private static readonly List _customers = ["Cst1", "Cst2", "Cst3"];
[HttpGet]
public Task All()
{
return Task.FromResult(Ok(_customers));
}
[HttpPost]
public Task Create([FromQuery] string customer)
{
const string HeaderKeyName = "X-Added-Website";
Request.Headers.TryGetValue(HeaderKeyName, out var headerValue);
_customers.Add(customer);
return Task.FromResult(Ok(new
{
Customers = _customers,
CustomHeader = headerValue
}));
}
}
Header personalizzato
Come possiamo notare, nella funzione Create stiamo recuperando un header personalizzato, definito nella configurazione delle rotte all’interno del file appsettings.json del progetto APIGateway, e restituendolo come payload. Sebbene questo header non sia necessario, ci aiuta a comprendere le potenzialità di YARP.
3.2 Registrazione Controllers
Con l’introduzione di .NET 8, il template ASP.NET Core Web API non registra più automaticamente i controller. Pertanto, dobbiamo modificare il file Program.cs per aggiungere manualmente la registrazione e la mappatura dei controller, come mostrato nel codice seguente.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddControllers();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapControllers();
await app.RunAsync();
4. Test Gateway
Ora che l’implementazione è completata, siamo pronti ad avviare i due progetti e testare il gateway. Il progetto APIGateway è configurato per essere eseguito sulla porta 5000
, mentre APIGateway.Customers è impostato per girare sulla porta 5010
. Con entrambe le applicazioni in esecuzione, possiamo aprire un terminale e testare il gateway utilizzando i seguenti comandi curl
:
4.1 Creazione di un nuovo customer
curl --location --request POST 'http://localhost:5000/customers/create?customer=nuovo%20cst'
4.2 Elenco dei customers
curl --location 'http://localhost:5000/customers/all'
Conclusione
In questo articolo abbiamo esaminato come utilizzare YARP per implementare un gateway API in ASP.NET Core. Abbiamo configurato il progetto, creato i controller necessari e testato le API tramite il gateway, esplorando le potenzialità di YARP nella gestione delle richieste. Ora sei pronto a integrare un potente gateway nella tua architettura.
Ora Tocca a Te
- Condividi nei commenti le tue esperienze e le tue opinioni sull’uso di YARP per il routing delle API.
- Aiuta altri sviluppatori a scoprire YARP condividendo questo articolo.
- Iscriviti alla newsletter per ricevere altri contenuti interessanti su ASP.NET Core e migliorare le tue competenze!
Mettiti in Contatto
Newsletter
Iscriviti oggi stesso e avrai accesso a:
- Contenuti esclusivi e approfondimenti sullo sviluppo software e ultime tecnologie
- Consigli pratici per migliorare le tue competenze e la tua produttività
- Risorse utili come guide, tutorial e strumenti per semplificare il tuo lavoro
Contattami
Collaboriamo per il successo del tuo progetto
- Hai bisogno di supporto tecnico, consulenza o risoluzione di problemi specifici nel tuo progetto di sviluppo?
- Hai domande o dubbi riguardo a scelte tecniche, best practice o strategie di sviluppo?
- Stai cercando un partner di sviluppo affidabile per collaborare a un progetto innovativo?