Introducción: El reto de la abstración de acceso a datos en .NET Core

Moderno .NET Las aplicaciones básicas a menudo interactúan con datos a través de múltiples backends de almacenamiento: bases de datos relacionales, tiendas NoSQL, caches en memoria o API de terceros. A medida que la aplicación crece, el acoplamiento estrecho entre la lógica empresarial y las implementaciones específicas de acceso a datos se convierte en una carga de mantenimiento.

Comprender el patrón de fábrica: más allá de la creación de objetos simples

En su núcleo, el Patrón de Fábrica es un patrón de diseño creacional que delega la responsabilidad de instantánear objetos a una clase de fábrica dedicada. Este patrón cae bajo tres variaciones comunes: fábrica simple, método de fábrica y fábrica abstracta. Para la extracción de capas de acceso a datos, la Fábrica simple (o Fábrica de productos de estadio) es a menudo el punto de inicio más pragmático, pero evolucionaremos a las familias flexibles.

La principal motivación para usar una fábrica en .NET Core data access es mantener el Principio abierto/Closed: las clases deben estar abiertas para la extensión pero cerradas para la modificación. Mediante la canalización de todos los datos acceso a la creación de objetos a través de una fábrica, puede introducir nuevas implementaciones (por ejemplo, conmutación de la clave marco de Entidad a la fábrica) sin tocar la lógica de negocio promover la Patternidad[LT2

El Patrón de la fábrica no es una bala de plata. Es más eficaz cuando usted tiene un conjunto bien definido de estrategias de acceso de datos intercambiables y necesita aislar la lógica de la creación del resto de la aplicación. En aplicaciones monolíticas simples con una única fuente de datos, la parte superior de una fábrica puede no ser justificada.

Diseño de la Abstracción: El Contrato de Interfaz

El primer paso en utilizar el Patrón de Fábrica para el acceso a los datos es definir una interfaz común que todos los repositorios de hormigón deben implementar. Esta interfaz sirve como el contrato entre su lógica de negocio y la capa de datos. En .NET Core, tal interfaz a menudo mapas a las operaciones estándar de CRUD, pero puede adaptarlo a sus necesidades de dominio.

Ejemplo: Una interfaz de repositorio genérico

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

Esta interfaz genérica funciona bien cuando necesita operaciones de datos consistentes en diferentes tipos de entidades. Sin embargo, para la simplicidad en este artículo, nos adheriremos con una interfaz no-génita que opera en un tipo de entidad único. Los principios siguen siendo idénticos.

Interfaces especializadas para escenarios avanzados

En proyectos del mundo real, es posible que necesite métodos de repositorio que vayan más allá de la CRUD básica, como consultas con paginas, filtración o agregación. Considere la definición de interfaces separadas para operaciones sólo lectura y escritura para seguir el principio de la Segregación Interface. Por ejemplo:

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

El Patrón de Fábrica puede producir una implementación combinada que satisface ambas interfaces cuando sea necesario, o devolver objetos separados para leer y escribir si elige un enfoque CQRS.

Implementación de Clases de Acceso a Datos Concretos

Una vez definida la interfaz, crea implementaciones concretas para cada tecnología de acceso a datos. A continuación se presentan ejemplos utilizando ]Intity Framework Core y Dapper, dos de los marcos de acceso a datos básicos más comunes.NET.

Aplicación básica del Marco de Entidades

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
}

Observe que ] espera una instancia , que en una aplicación .NET Core se inyecta normalmente a través del contenedor DI. Esto se alinea con el Patrón de fábrica: la fábrica necesitará acceso al contenedor DI para resolver tales dependencias.

Aplicación de 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 });
 }
}

Ambas implementaciones cumplen el mismo contrato pero utilizan mecánicas completamente diferentes. La fábrica decidirá cuál a instantánea basado en condiciones de tiempo de funcionamiento.

Construyendo la fábrica: De Simple a Resumen

La propia fábrica encapsula la lógica de decisión. Una fábrica estática es suficiente cuando la elección de la implementación depende únicamente de un valor de configuración (por ejemplo, una clave de configuración de la aplicación). Sin embargo, cuando la decisión requiere un contexto de tiempo de ejecución (función de usuario, inquilino, bandera de características), una fábrica no estática que acepta parámetros adicionales es más apropiada.

Fábrica simple estática (configuración-construcción)

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

Esta fábrica utiliza para instantánear tipos que tienen dependencias registradas en el contenedor DI. ] se resuelve directamente porque ya está registrada (junto con ). se crea utilizando que maneja la inyección de constructor sin requerir registro manual del propio repositorio—sólo sus dependencias (por ejemplo,] [FLT] [

Abstract Factory for Multiple Product Families

Cuando su aplicación requiere diferentes tipos de objetos de acceso a datos (por ejemplo, uno para pedidos, otro para inventario, cada uno potencialmente utilizando un motor de almacenamiento diferente), la fábrica simple se vuelve inescrutable. Una Fábrica abstracta define una interfaz para crear familias de objetos relacionados sin especificar sus clases de hormigón. Cada fábrica de hormigón produce un conjunto completo de objetos de acceso a datos para una pila de tecnología particular.

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 fábrica de abstracts es más potente pero también más pesada. Reserve para aplicaciones donde necesita cambiar pilas de acceso completo de datos (por ejemplo, reemplazando todos los repositorios del Marco de Entidades con repositorios Dapper) de inmediato, en lugar de realizar implementaciones individuales de cereza.

Integrando la fábrica con .NET Core Dependency Injection

La verdadera fuerza del Patrón de Fábrica en .NET Core emerge cuando lo combina con el contenedor DI. En lugar de registrar tipos de depósito de concreto, registre la fábrica y permita que produzca la implementación adecuada a la demanda.

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

En este registro, el hormigón se registra de manera transitoria (para que el contenedor DI pueda resolverlo dentro de la fábrica). El registro utiliza un delegado de fábrica que lee el valor de y delegados a la fábrica estática. Ahora cualquier consumidor que inyecte obtendrá la correcta implementación sin saber cuál es.

Utilizando servicios designados para soporte multiproveedor

Si su aplicación necesita multiple repositorios que utilizan diferentes proveedores simultáneamente (por ejemplo, uno para datos históricos utilizando Dapper, y otro para datos en tiempo real utilizando el Marco de Entidad), puede registrar métodos de fábrica o utilizar un patrón de diccionario.

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 fábrica puede entonces exponer un método que devuelve la implementación adecuada basado en el parámetro , que puede pasar como dependencia utilizando o un patrón de inyección personalizado.

Beneficios y Escenarios en el Mundo Real

Examinemos situaciones concretas donde el patrón de fábrica brilla en capas de acceso a datos .NET Core.

1. Pruebas y burlas

La lógica de la unidad de prueba de negocio se vuelve trivial cuando se puede sustituir un repositorio de mock. La fábrica se puede configurar en la configuración de prueba para devolver una mock o la implementación en memoria. Por ejemplo, durante las pruebas de integración, establecer la clave de configuración a y tener una fábrica que devuelve un respaldado por un ]].

2. Aplicaciones de múltiples arrendatarios

Cada inquilino puede requerir una tecnología de almacenamiento de datos diferente debido a la concesión de licencias, restricciones heredadas o distribución geográfica. Una fábrica puede examinar los metadatos del inquilino en tiempo de ejecución e instantánea el repositorio apropiado, tal vez un inquilino utiliza SQL Server a través de Entity Framework, otro utiliza PostgreSQL a través de Dapper, y un tercero utiliza Azure Cosmos DB.

3. Moda de actividad y migración gradual

Cuando migramos de una ORM a otra (por ejemplo, desde el Marco de Entidades a Dapper para consultas de rendimiento crítica), la fábrica le permite trazar el tráfico gradualmente. Puede crear un sistema de banderas que, por un porcentaje de usuarios o puntos de final específicos, devuelve el nuevo repositorio Dapper mientras que la mayoría de la aplicación todavía utiliza el Marco de Entidad. Si surgen problemas, vuelva a la bandera con cero cambios de código.

Pruebas de la capa de acceso a datos extraídos

Una fábrica bien diseñada hace pruebas directas. Puede crear una fábrica de pruebas que devuelve implementaciones de mock o versiones de memoria ligera de sus repositorios de datos. Por ejemplo:

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

El simplemente tiene un y implementa la interfaz utilizando operaciones en memoria. Los exámenes de unidad pueden luego instantánear los servicios de negocios con esta fábrica de pruebas sin requerir una conexión de base de datos o marcos de burla. Los exámenes de integración todavía pueden utilizar la fábrica real para validar contra una base de datos de prueba.

Mejores prácticas y saltos comunes

No sobre-Abstract

El Patrón de Fábrica añade indirecta. Si su aplicación nunca cambiará los proveedores de acceso a datos, la abstracción sólo aumenta la complejidad sin ningún beneficio. Úsalo sólo cuando usted tiene un requisito claro y actual para múltiples implementaciones intercambiables.

Evitar proveedores de tipo ajustado

Usar cadenas mágicas para nombres de proveedores (como o ]) es frágil. En cambio, definir una enumeración o utilizar un objeto de configuración con tipos fuertes. Por ejemplo:

public enum DataProvider
{
 EntityFramework,
 Dapper,
 InMemory
}

Gestionar tiempo de vida con cuidado

Los depósitos suelen tener conexiones o instancias DbContext. Asegúrese de que el contenedor DI gestiona correctamente su alcance. Para aplicaciones web, una vida útil (por solicitud) es generalmente apropiada para EF Core DbContext, pero las conexiones Dapper pueden necesitar transito o encapsulado sobre la base de la estrategia de conexión. La fábrica no debe cachear los repositorios indefinidamente a menos que estén apátridas.

Considerar usar una fábrica con un registro

Para aplicaciones grandes, considere la implementación de un Patrón de registro] junto a la fábrica. Un registro almacena fábricas preconfiguradas claves por algún identificador (por ejemplo, ID de inquilino, nombre lógico). El cliente pide el registro para la fábrica apropiada, que luego crea el repositorio. Este patrón es especialmente útil en arquitecturas multi-tenant donde cada estrategia de acceso de datos puede tener un acceso diferente.

Comparación con patrones alternativos

El Patrón de Fábrica no es la única manera de acceder a datos abstractos. Así es como se compara con otros enfoques comunes en .NET Core:

  • Patrón de Estadio] – Similar a la Fábrica, pero el enfoque es encapsulador algoritmos (por ejemplo, diferentes estrategias de clasificación o filtración) en lugar de la creación de objetos. El Patrón de Fábrica es un patrón de creación; el Patrón de Estrategia es conductual. Pueden complementarse mutuamente: una fábrica podría devolver una estrategia.
  • Patrón de decorador] – Útil para añadir preocupaciones transversales (caching, logging, retry) a un repositorio sin modificar su código. La fábrica puede decorar el repositorio que crea, combinando ambos patrones.
  • Patrón de depósito (sin fábrica) – Inyecte directamente un repositorio de hormigón a través de obras de DI para aplicaciones sencillas. La fábrica añade flexibilidad cuando el tipo de hormigón varía.

Recursos externos y lectura ulterior

Para profundizar su comprensión del Patrón de Fábrica y el acceso a los datos .NET Core, consulte las siguientes fuentes autorizadas:

Conclusión: Edificio para el Cambio

El Patrón de Fábrica proporciona una forma disciplinada de gestionar la variación en las capas de acceso a datos, lo que le permite cambiar los backends de almacenamiento, adoptar nuevas tecnologías y probar la lógica de negocio en aislamiento. En . CoreNET, combinar el Patrón de Fábrica con el contenedor DI incorporado produce una arquitectura limpia y sostenible que respeta los principios de SOLID.