Inleiding: De uitdaging van de data-toegangslaag Abstractie in .NET Core

Moderne .NET Core toepassingen hebben vaak interactie met data via meerdere opslag backends .relational databases, NoSQL winkels, in-memory caches, of derden API's. Naarmate de toepassing groeit, wordt een strakke koppeling tussen zakelijke logica en specifieke data toegang implementaties een onderhoudslast. Het veranderen van de onderliggende database provider of het toevoegen van een nieuw opslagmechanisme kan rimpelen door de hele codebase, forceren recompilatie en uitgebreide regressie testen. De Factory Pattern[] biedt een bewezen oplossing door het inkapselen van objecten, waardoor u kunt schakelen van de implementatie van data toegang zonder de verbruikscode te wijzigen. Dit artikel biedt een gezaghebbende, productie-ready gids om de Factory Pattern voor het implementeren van de data toegang laag abstraction in .NET Core, met code voorbeelden, beste praktijken, en integratie met de ingebouwde Dependentence Injection (DI) container.

Begrijpen van het Fabriekspatroon: Voorbij eenvoudige Objectcreatie

Het Fabriekspatroon is een creatief ontwerppatroon dat de verantwoordelijkheid voor het instantiëren van objecten toelegt aan een speciale fabrieksklasse. Dit patroon valt onder drie algemene variaties: Eenvoudige Fabriek, Fabrieksmethode en Abstract Factory. Voor het abstracteren van data-toegangslagen is de Eenvoudige Fabriek (of Statische Fabriek) vaak het meest pragmatische uitgangspunt, maar we zullen ook onderzoeken hoe we het kunnen ontwikkelen tot een flexibeler Abstract Factory wanneer meerdere productfamilies bestaan.

De primaire motivatie voor het gebruik van een fabriek in .NET Kerngegevenstoegang is het handhaven van het Open/Gesloten Principe: klassen moeten open zijn voor uitbreiding maar gesloten voor wijziging. Door alle datatoegangsobjectcreatie via een fabriek te kanaliseren, kunt u nieuwe implementaties introduceren (bijvoorbeeld het overschakelen van entiteitskern naar Dapper) zonder de bedrijfslogica aan te raken. Bovendien bevordert het Factory Pattern Dependent Inversion].High-level modules niet langer afhankelijk van low-level details; beide zijn afhankelijk van abstracties (interfaces).

Het Fabriekspatroon is geen zilveren kogel. Het is het meest effectief wanneer u een goed gedefinieerde reeks verwisselbare datatoegangsstrategieën hebt en de scheppingslogica moet isoleren van de rest van de toepassing. In eenvoudige monolithische toepassingen met één enkele gegevensbron is het mogelijk dat de bovenzijde van een fabriek niet gerechtvaardigd is.[

Ontwikkelen van de abstractie: Het Interface-contract

De eerste stap in het gebruik van het Factory Pattern voor datatoegang is het definiëren van een gemeenschappelijke interface die alle concrete repositories moeten implementeren. Deze interface dient als het contract tussen uw bedrijfslogica en de datalaag. In .NET Core, zo'n interface vaak in kaart brengen om standaard CRUD operaties, maar je kunt het aanpassen aan uw domeinbehoeften.

Voorbeeld: Een generische repository-interface

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);
}

Deze generieke interface werkt goed wanneer u consistente gegevensbewerkingen nodig hebt over verschillende entiteitstypen. Echter, voor eenvoud in dit artikel, zullen we vasthouden aan een niet-generische interface die werkt op één entiteit type. De principes blijven identiek.

Gespecialiseerde interfaces voor geavanceerde scenario's

In real-world projecten, je kan nodig hebben repository methoden die verder gaan dan de basis CRUD, zoals gepagineerde queries, filtering, of aggregatie. Overweeg het definiëren van afzonderlijke interfaces voor alleen-lezen en alleen-schrijven operaties om het Interface Segregation Principe te volgen. Bijvoorbeeld:

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);
}

Het Fabriekspatroon kan dan een gecombineerde implementatie produceren die voldoet aan beide interfaces wanneer nodig, of afzonderlijke objecten retourneren om te lezen en te schrijven als u kiest voor een CQRS-aanpak.

Uitvoering van de klassen voor toegang tot concrete gegevens

Zodra de interface is gedefinieerd, creëer je concrete implementaties voor elke data access technologie. Hieronder staan voorbeelden met Entity Framework Core en Dapper, twee van de meest voorkomende .NET Core data access frameworks.

Kernimplementatie van het entiteitskader

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
}

Merk op dat een instantie verwacht, die in een .NET Core toepassing meestal via de DI container wordt geïnjecteerd. Dit sluit aan bij het Fabriekspatroon: de fabriek heeft toegang tot de DI container nodig om dergelijke afhankelijkheden op te lossen.

Uitvoering van 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 });
 }
}

Beide implementaties voldoen aan hetzelfde contract maar gebruiken volledig verschillende mechanica. De fabriek zal beslissen welke te instantiëren op basis van runtime voorwaarden.

Bouwen van de fabriek: Van eenvoudig tot abstract

De fabriek zelf inkapselt de beslissingslogica. Een statische fabriek is voldoende wanneer de keuze van de implementatie uitsluitend afhankelijk is van een configuratiewaarde (bv. een app-instellingensleutel). Echter, wanneer de beslissing een runtime context vereist (gebruikersrol, huurder, functievlag), is een niet-statische fabriek die aanvullende parameters accepteert, meer geschikt.

Statische eenvoudige fabriek (configuratie-aandrijving)

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.")
 };
 }
}

Deze fabriek gebruikt de om types te instantiëren die afhankelijkheden hebben die geregistreerd zijn in de DI-container. wordt direct opgelost omdat deze reeds geregistreerd is (samen met ]). [] wordt gemaakt met ] die constructorinjectie behandelt zonder dat de opslag zelf handmatig geregistreerd hoeft te worden en alleen de afhankelijkheden ervan (bv. ) moeten in de container aanwezig zijn.

Abstract Factory voor meerdere productfamilies

Wanneer uw toepassing verschillende soorten objecten voor gegevenstoegang vereist (bijvoorbeeld een voor bestellingen, een voor inventaris, elk potentieel met behulp van een andere opslagmotor), wordt de eenvoudige fabriek onhandig. Een Abstract Factory definieert een interface voor het creëren van families van gerelateerde objecten zonder hun concrete klassen te specificeren. Elke betonfabriek produceert een complete set van objecten voor datatoegang voor een bepaalde technologiestapel.

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);
}

De Abstract Factory is krachtiger maar ook zwaarder. Reserveer het voor toepassingen waar je volledige datatoegangsstapels (bijvoorbeeld alle Entity Framework repositories vervangen door Dapper repositories) tegelijk moet uitwisselen, in plaats van op een kers-picking individuele implementaties.

Integratie van de Fabriek met .NET Core Afhankelijkheidsinjectie

De ware kracht van het Fabriekspatroon in .NET Core ontstaat wanneer je het combineert met de DI container. In plaats van het registreren van betonnen repository types, registreert de fabriek en laat het produceren van de juiste implementatie op aanvraag.

Registratie in Programma.cs (of Opstart.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 deze registratie wordt het beton van voorbijgaande aard geregistreerd (zodat de DI-container het binnen de fabriek kan oplossen). De registratie gebruikt een fabrieksdelegatie die de waarde leest van ] en delegeert naar de statische fabriek. Nu zal elke consument die injecteert de juiste implementatie krijgen zonder te weten welke het is.

Gebruik van Genoemde diensten voor ondersteuning voor multiprovider

Als uw toepassing meerdere repositories nodig heeft die gelijktijdig verschillende providers gebruiken (bijvoorbeeld één voor historische gegevens die Dapper gebruiken, en één voor real-time gegevens die gebruik maken van Entity Framework), kunt u fabrieksmethoden met naam registreren of een woordenboekpatroon gebruiken.

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

De fabriek kan dan een methode ontmaskeren die de juiste implementatie teruggeeft op basis van de parameter die je als afhankelijkheid kunt doorgeven met of een aangepast injectiepatroon.

Voordelen en scenario's in de reële wereld

Laten we concrete situaties onderzoeken waarin het Fabriekspatroon in .NET Core data toegang lagen schijnt.

1. Testen en slijmen

De bedrijfslogica van de eenheid testen wordt triviaal wanneer je een nep-repositorium kunt vervangen. De fabriek kan worden geconfigureerd bij de opstelling van de test om een nep- of geheugen-implementatie terug te geven. Bijvoorbeeld, tijdens integratietests, de configuratiesleutel instellen op en een fabriek hebben die een teruggeeft, ondersteund door een .

2. Multi-tenant toepassingen

Elke huurder kan een andere technologie voor dataopslag nodig hebben als gevolg van licentieverlening, legacybeperkingen of geografische distributie. Een fabriek kan de metadata van huurders op runtime onderzoeken en de juiste repository instantiëren.Misschien gebruikt de huurder SQL Server via Entity Framework, een andere gebruikt PostgreSQL via Dapper en een derde gebruikt Azure Cosmos DB.

3. Functie Schakelen en Geleidelijke Migratie

Wanneer u van de ene ORM naar de andere overstapt (bijvoorbeeld van het Entity Framework naar Dapper voor prestatiekritische vragen), kunt u het verkeer geleidelijk aan routeren. U kunt een feature flag systeem bouwen dat, voor een percentage van gebruikers of specifieke eindpunten, de nieuwe Dapper repository retourneert terwijl de meeste van de toepassing nog steeds gebruik maakt van Entity Framework. Als er problemen optreden, keer de vlag terug met nul codewijzigingen.

Testen van de Abstracted Data Access Layer

Een goed ontworpen fabriek maakt testen eenvoudig. U kunt een testfabriek creëren die spot implementaties of lichtgewicht in-geheugen versies van uw data repositories retourneert. Bijvoorbeeld:

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

De .9.] heeft gewoon een en implementeert de interface met behulp van in-geheugen operaties. De unit tests kunnen dan instant zakelijke diensten met deze testfabriek zonder dat er een database verbinding of spotraamwerk nodig. Integratie tests kunnen nog steeds de echte fabriek gebruiken om te valideren tegen een test database.

Beste praktijken en gemeenschappelijke valkuilen

Niet overtrekken

Het Factory Pattern voegt indirecte toevoegingen toe. Als uw toepassing nooit van data access providers zal wisselen, vergroot de abstractie alleen maar complexiteit zonder voordeel. Gebruik het alleen als u een duidelijke, actuele eis voor meerdere verwisselbare implementaties hebt.

Vermijd Stringly-Typed Providers

Het gebruik van magische tekenreeksen voor providernamen (zoals of ) is broos. In plaats daarvan, definieer een opsomming of gebruik een configuratie-object met sterke typen. Bijvoorbeeld:

public enum DataProvider
{
 EntityFramework,
 Dapper,
 InMemory
}

Levenstijd zorgvuldig beheren

Repositories hebben vaak verbindingen of DbContext-instellingen. Zorg ervoor dat de DI-container hun toepassingsgebied correct beheert. Voor webtoepassingen is een scoped lifetime (per request) meestal geschikt voor EF Core DbContext, maar Dapper-verbindingen kunnen tijdelijk of scoped nodig zijn op basis van de verbindingspooling strategie. De fabriek mag geen repositories voor onbepaalde tijd cachen tenzij ze staatloze zijn.

Overweeg om een fabriek met een register te gebruiken

Voor grote toepassingen, overwegen het implementeren van een Registratie Pattern naast de fabriek. Een register slaat vooraf geconfigureerde fabrieken die zijn gesleuteld door een bepaalde identificatie (bijv. huurder ID, logische naam). De klant vraagt het register voor de juiste fabriek, die vervolgens de repository maakt. Dit patroon is vooral nuttig in multi-tenant architecturen waar elke huurder een andere data access strategie kan hebben.

Vergelijking met alternatieve patronen

Het Fabriekspatroon is niet de enige manier om datatoegang te abstracteren. Hier vindt u hoe het zich verhoudt tot andere gemeenschappelijke benaderingen in .NET Core:

  • Strategiepatroon
  • Decoratorpatroon .. Handig voor het toevoegen van horizontale zorgen (caching, logging, retry) aan een repository zonder de code te wijzigen. De Factory kan de repository die het maakt versieren, combineren van beide patronen.
  • Eigenschappenpatroon (zonder fabriek) .Je injecteert direct een betonnen repository via DI-werken voor eenvoudige toepassingen.De Fabriek voegt flexibiliteit toe wanneer het betontype varieert.

Externe middelen en verdere lezing

Om uw begrip van het Fabriekspatroon en .NET Kerngegevens te verdiepen, verwijzen wij u naar de volgende gezaghebbende bronnen:

Conclusie: Bouwen voor verandering

Het Fabriekspatroon biedt een gedisciplineerde manier om variatie in datatoegangslagen te beheren, zodat u backends kunt uitwisselen, nieuwe technologieën kunt gebruiken en bedrijfslogica in isolatie kunt testen. In .NET Core, waarbij het Fabriekspatroon wordt gecombineerd met de ingebouwde DI-container, levert een schone, onderhoudsbare architectuur op die de SOLID-principes respecteert. Start klein: stel een interface in, implementeer twee betonklassen, creëer een statische fabriek en bedrad het door DI. Naarmate uw toepassing groeit, ontwikkelt u de fabriek tot een Abstract Factory of een door het register gedreven fabriek om meerdere families van repositories te behandelen. Door in deze abstractie te investeren, kunt u uw toegangscode voor gegevens voortaan beschermen tegen onvermijdelijke veranderingen in opslagvereisten en technologiekeuzes.