Introduzione: La sfida dell'astrazione del livello di accesso ai dati in .NET Core

Le applicazioni di base di ModernjeNET spesso interagiscono con i dati attraverso più backend di archiviazione — database relazionali, negozi di NoSQL, cache in memoria o API di terze parti. Come l'applicazione cresce, stretto accoppiamento tra logica aziendale e specifiche implementazioni di accesso ai dati diventa un onere di manutenbilità.

Capire il modello di fabbrica: oltre la creazione di oggetti semplici

Al suo centro, il modello di fabbrica è un modello di design creatore che delega la responsabilità degli oggetti istantanei a una classe di fabbrica dedicata. Questo modello cade sotto tre varianti comuni: Semplice fabbrica, Metodo di fabbrica e fabbrica astratta. Per astratto strati di accesso dei dati, il Simple Factory[] (o fabbrica statica) è spesso il punto di partenza più pragmatico, ma esploreremo anche come evolverlo in più famiglie astratto.

La motivazione principale per l'utilizzo di una fabbrica in .NET Core data access è di sostenere il Principio aperto / chiuso: le classi dovrebbero essere aperte per estensione ma chiuse per la modifica. Con la canalizzazione di tutti i dati di accesso creazione di oggetti attraverso una fabbrica, è possibile introdurre nuove implementazioni (ad esempio, passare da Entity Framework Core a Dapper) senza toccare la logica aziendale più lunga.

Il modello di fabbrica non è un proiettile d'argento. È più efficace quando si dispone di un insieme ben definito di strategie di accesso ai dati intercambiabili e la necessità di isolare la logica di creazione dal resto dell'applicazione. In semplici applicazioni monolitiche con una singola fonte di dati, la testa di una fabbrica potrebbe non essere giustificata.

Progettare l'astrazione: Il contratto di interfaccia

Il primo passo nell'utilizzo del modello di fabbrica per l'accesso ai dati è quello di definire un'interfaccia comune che tutti i repository concreti devono implementare. Questa interfaccia serve come contratto tra la logica aziendale e lo strato di dati. In .NET Core, tale interfaccia spesso mappa alle operazioni standard CRUD, ma è possibile adattarlo alle esigenze del dominio.

Esempio: un'interfaccia di repository generica

public interface IDataRepository<TKey, TEntity> where TEntity : class
{
 Task<IEnumerable<TEntity>> GetAllAsync();
 Task<TEntity?> GetByIdAsync(TKey id);
 Task AddAsync(TEntity entity);
 Task UpdateAsync(TEntity entity);
 Task DeleteAsync(TKey id);
}

Questa interfaccia generica funziona bene quando avete bisogno di operazioni di dati coerenti tra diversi tipi di entità. Tuttavia, per semplicità in questo articolo, ci attaccheremo con un'interfaccia non-generica che opera su un singolo tipo di entità.

Interfacce speciali per scenari avanzati

Nei progetti del mondo reale, è possibile che sia necessario disporre di metodi di repository che vadano oltre il CRUD di base, come query impaginate, filtraggio o aggregazione.

public interface IDataReader<TEntity>
{
 Task<IEnumerable<TEntity>> QueryAsync(Expression<Func<TEntity, bool>> predicate);
 Task<TEntity?> GetByIdAsync(int id);
}

public interface IDataWriter<TEntity>
{
 Task InsertAsync(TEntity entity);
 Task UpdateAsync(TEntity entity);
 Task DeleteAsync(int id);
}

Il modello di fabbrica può quindi produrre un'implementazione combinata che soddisfa entrambe le interfacce quando necessario, o restituire oggetti separati per leggere e scrivere se si sceglie un approccio CQRS.

Implementazione di corsi di accesso dati concreti

Una volta che l'interfaccia è definita, si creano implementazioni concrete per ogni tecnologia di accesso ai dati. Di seguito sono esempi utilizzando []Entity Framework Core e Dapper], due dei più comuni framework di accesso ai dati .NET Core.

Attuazione del core del quadro di ammissione

public class EfDataRepository : IDataRepository
{
 private readonly AppDbContext _context;

 public EfDataRepository(AppDbContext context)
 {
 _context = context;
 }

 public async Task<IEnumerable<DataItem>> GetAllAsync()
 {
 return await _context.Set<DataItem>().AsNoTracking().ToListAsync();
 }

 public async Task<DataItem?> GetByIdAsync(int id)
 {
 return await _context.Set<DataItem>().FindAsync(id);
 }

 // Additional methods omitted for brevity
}

Si noti che si aspetta un'istanza ], che in un'applicazione .NET Core viene in genere iniettata tramite il contenitore DI. Questo si allinea con il modello di fabbrica: la fabbrica avrà bisogno di accedere al contenitore DI per risolvere tali dipendenze.

Attuazione del dapper

public class DapperDataRepository : IDataRepository
{
 private readonly IDbConnection _connection;
 private readonly string _connectionString;

 public DapperDataRepository(IConfiguration configuration)
 {
 _connectionString = configuration.GetConnectionString("DefaultConnection");
 _connection = new SqlConnection(_connectionString);
 }

 public async Task<IEnumerable<DataItem>> GetAllAsync()
 {
 var sql = "SELECT * FROM DataItems";
 return await _connection.QueryAsync<DataItem>(sql);
 }

 public async Task<DataItem?> GetByIdAsync(int id)
 {
 var sql = "SELECT * FROM DataItems WHERE Id = @Id";
 return await _connection.QueryFirstOrDefaultAsync<DataItem>(sql, new { Id = id });
 }
}

Entrambe le implementazioni adempiono allo stesso contratto ma utilizzano meccanica completamente diversa. La fabbrica deciderà quale istantaneo in base alle condizioni di runtime.

Edilizia della fabbrica: da semplice a astratto

Una fabbrica statica è sufficiente quando la scelta dell'implementazione dipende esclusivamente da un valore di configurazione (ad esempio, una chiave di impostazione dell'app). Tuttavia, quando la decisione richiede un contesto di runtime (ruolo utente, inquilino, flag), una fabbrica nonstatica che accetta parametri aggiuntivi è più appropriata.

Fabbrica semplice statica (configurazione-drive)

public static class DataRepositoryFactory
{
 public static IDataRepository Create(IServiceProvider serviceProvider, string provider)
 {
 return provider switch
 {
 "EntityFramework" => serviceProvider.GetRequiredService<EfDataRepository>(),
 "Dapper" => ActivatorUtilities.CreateInstance<DapperDataRepository>(serviceProvider),
 _ => throw new NotSupportedException($"Data provider '{provider}' is not supported.")
 };
 }
}

Questa fabbrica utilizza i tipi di istantaneo che hanno dipendenze registrate nel contenitore DI. ] viene risolto direttamente perché è già registrato (insieme a ]). viene creato utilizzando che gestisce l'iniezione del costruttore senza richiedere la registrazione manuale del repository stesso—solo le sue dipendenze (e.

Fabbrica astratta per le famiglie di prodotti multipli

Quando la vostra applicazione richiede diversi tipi di oggetti di accesso ai dati (ad esempio, uno per gli ordini, un altro per l'inventario, ciascuno potenzialmente utilizzando un motore di archiviazione diverso), la Semplice Fabbrica diventa indisturbabile. Un Abstract Factory[]] definisce un'interfaccia per creare famiglie di oggetti correlati senza specificare le loro classi di cemento.

public interface IDataAccessFactory
{
 IDataRepository CreateOrderRepository();
 IDataRepository CreateInventoryRepository();
 // etc.
}

public class EfDataAccessFactory : IDataAccessFactory
{
 private readonly AppDbContext _context;
 public EfDataAccessFactory(AppDbContext context) => _context = context;

 public IDataRepository CreateOrderRepository() => new EfOrderRepository(_context);
 public IDataRepository CreateInventoryRepository() => new EfInventoryRepository(_context);
}

public class DapperDataAccessFactory : IDataAccessFactory
{
 private readonly string _connectionString;
 public DapperDataAccessFactory(IConfiguration configuration) => _connectionString = configuration.GetConnectionString("DefaultConnection");

 public IDataRepository CreateOrderRepository() => new DapperOrderRepository(_connectionString);
 public IDataRepository CreateInventoryRepository() => new DapperInventoryRepository(_connectionString);
}

La fabbrica astratta è più potente ma anche più pesante.Riserva per applicazioni in cui è necessario scambiare interi stack di accesso dati (ad esempio, sostituire tutti i repository Entity Framework con repository Dapper) in una sola volta, piuttosto che ciliegia-picking singole implementazioni.

Integrazione della fabbrica con l'iniezione di dipendenza da nucleo .NET

La vera forza del modello di fabbrica in .NET Core emerge quando lo si combina con il contenitore DI. Invece di registrare tipi di repository concreti, registrare la fabbrica e lasciare che si produce l'implementazione appropriata su richiesta.

Registrazione in Program.cs (o Startup.cs)

builder.Services.AddTransient<EfDataRepository>();
builder.Services.AddTransient<IDataRepository>(sp =>
{
 var config = sp.GetRequiredService<IConfiguration>();
 var provider = config.GetValue<string>("DataProvider");
 return DataRepositoryFactory.Create(sp, provider);
});

In questa registrazione, il cemento ] è registrato in modo transitorio (in modo che il contenitore DI può risolverlo all'interno della fabbrica). La registrazione utilizza un delegato di fabbrica che legge il valore da e delegati alla fabbrica statica. Ora qualsiasi consumatore che inietta ] otterrà la corretta implementazione senza sapere quale è.

Utilizzo di servizi nominati per il supporto multiprovider

Se la vostra applicazione ha bisogno multiple[]] repository utilizzando diversi provider simultaneamente (ad esempio, uno per i dati storici utilizzando Dapper, e uno per i dati in tempo reale utilizzando Entity Framework), è possibile registrare i metodi di fabbrica nominati o utilizzare un modello di dizionario.

builder.Services.AddSingleton<IDataRepositoryFactory>(sp =>
{
 var config = sp.GetRequiredService<IConfiguration>();
 var providers = config.GetSection("DataProviders").Get<Dictionary<string, string>>();
 return new DataRepositoryFactory(sp, providers);
});

La fabbrica può quindi esporre un metodo che restituisce l'implementazione appropriata in base al parametro , che è possibile passare come dipendenza utilizzando o un modello di iniezione personalizzato.

Vantaggi e scenari reali

Esaminiamo situazioni concrete in cui il modello di fabbrica brilla negli strati di accesso ai dati .NET Core.

1. Test e Mocking

La logica aziendale di test unità diventa banale quando si può sostituire un repository mock. La fabbrica può essere configurata in configurazione di prova per restituire un mock o un'implementazione in-memory. Ad esempio, durante i test di integrazione, impostare il chiave di configurazione a e avere una fabbrica che restituisce un supportato da un ]]].

2. Applicazioni multi-conduttore

Ogni inquilino potrebbe richiedere una tecnologia diversa data store a causa di licenze, vincoli legacy o distribuzione geografica. Una fabbrica può esaminare i metadati dell'inquilino a runtime e istazionare il repository appropriato, forse un inquilino utilizza SQL Server tramite Entity Framework, un altro usa PostgreSQL tramite Dapper, e un terzo usa Azure Cosmos DB.

3. Toggles caratteristica e la migrazione graduale

Quando migrate da un ORM all'altro (ad esempio, da Entity Framework a Dapper per domande critiche alle prestazioni), la fabbrica permette di indirizzare il traffico gradualmente. È possibile costruire un sistema di bandiere che, per una percentuale di utenti o di endpoint specifici, restituisce il nuovo repository Dapper mentre la maggior parte dell'applicazione utilizza ancora Entity Framework. Se si presentano problemi, deviare la bandiera con zero modifiche di codice.

Testare il livello di accesso astratto ai dati

Una fabbrica ben progettata rende i test semplici. È possibile creare una fabbrica di test che restituisce implementazioni di mock o versioni leggere in memoria dei repository di dati.

public class TestDataRepositoryFactory
{
 public static IDataRepository CreateInMemory()
 {
 return new InMemoryDataRepository();
 }
}

Il ] contiene semplicemente un e implementa l'interfaccia utilizzando operazioni in-memoria. I test delle unità possono quindi istantanare i servizi aziendali con questa fabbrica di test senza richiedere una connessione di database o di framework di mocking.

Migliori Pratiche e Pitfalls Comuni

Non troppo assurda

Se la tua applicazione non commuta mai i fornitori di accesso ai dati, l'astrazione aumenta solo la complessità senza alcun vantaggio. Usalo solo quando hai un chiaro requisito attuale per implementazioni intercambiabili multiple.

Evitare Fornitori a prova di stress

L'utilizzo di stringhe magiche per i nomi dei fornitori (come ] o ]) è fragile, invece, definire un'enumerazione o utilizzare un oggetto di configurazione con tipi forti.

public enum DataProvider
{
 EntityFramework,
 Dapper,
 InMemory
}

Gestire la vita con attenzione

Per applicazioni web, una durata di validità (per richiesta) è di solito appropriata per EF Core DbContext, ma le connessioni Dapper possono avere bisogno di transient o di scopo basato sulla strategia di connessione pooling. La fabbrica non deve memorizzare indefinitamente i repository a meno che non siano senza stato.

Considerare l'utilizzo di una fabbrica con un Registro di sistema

Per le grandi applicazioni, si consideri l'implementazione di un Modello di registrazione[]] accanto alla fabbrica. Un registro memorizza le fabbriche preconfigurate chiave da qualche identificatore (ad esempio, ID inquilino, nome logico). Il client chiede il registro per la fabbrica appropriata, che poi crea il repository. Questo modello è particolarmente utile nelle architetture multi-tenant dove ogni inquilino può avere una strategia di accesso dati diversa.

Confronto con schemi alternativi

Il modello di fabbrica non è l'unico modo per accedere ai dati astratti. Ecco come si confronta con altri approcci comuni in .NET Core:

  • Strategy Pattern[[] – Simile alla fabbrica, ma l'attenzione è rivolta ad algoritmi incapsulanti (ad esempio, strategie di selezione o filtraggio diverse) piuttosto che alla creazione di oggetti. Il modello di fabbrica è un modello creativo; il modello di strategia è comportamentale. Possono integrarsi a vicenda: una fabbrica potrebbe restituire una strategia.
  • Decorator Pattern[ – Utile per aggiungere le preoccupazioni di taglio incrociato (caching, logging, retry) a un repository senza modificare il suo codice. La fabbrica può decorare il repository che crea, combinando entrambi i modelli.
  • Modello repository (senza fabbrica)[] – Iniettare direttamente un repository concreto tramite lavori DI per applicazioni semplici. La fabbrica aggiunge flessibilità quando il tipo di cemento varia.

Risorse esterne e lettura

Per approfondire la comprensione del modello di fabbrica e dell'accesso ai dati del core .NET, fare riferimento alle seguenti fonti autorevoli:

Conclusione: Edificio per Cambiare

Il modello di fabbrica fornisce un modo disciplinato per gestire la variazione degli strati di accesso ai dati, consentendo di scambiare backend di storage, adottare nuove tecnologie e testare la logica aziendale in isolamento. In .NET Core, combinando il modello di fabbrica con il contenitore integrato DI produce un'architettura pulita e manutenbile che rispetta i principi SOLID. Iniziare piccolo: definire un'interfaccia, implementare due classi di cemento, creare una fabbrica statica, e cablare attraverso l'evoluzione.