Introdução: O desafio da Abstração da Camada de Acesso de Dados no Núcleo .NET

As aplicações modernas do .NET Core frequentemente interagem com dados através de várias infra- estruturas de armazenamento — bancos de dados relacionais, lojas NoSQL, caches de memória ou APIs de terceiros. À medida que a aplicação cresce, o acoplamento apertado entre a lógica de negócios e implementações específicas de acesso de dados torna- se uma carga de manutenção. A alteração do fornecedor de base de dados subjacente ou a adição de um novo mecanismo de armazenamento pode ondular através de toda a base de códigos, forçando a recompilação e testes de regressão extensivos. O Padrão de Fábrica] oferece uma solução comprovada, encapsulando a criação de objetos, permitindo- lhe alternar as implementações de acesso de dados sem alterar o código consumidor. Este artigo fornece um guia autoritário e pronto para a produção para implementar o Padrão de Fábrica para abstração de camadas de dados em .NET Core, com exemplos de código, melhores práticas e integração com o recipiente de Injecção de Dependencia (DI) incorporado.

Compreendendo o padrão de fábrica: Além da criação de objetos simples

No seu núcleo, o Padrão de Fábrica é um padrão de design criacional que delega a responsabilidade de instanciar objetos para uma classe de fábrica dedicada. Este padrão se enquadra em três variações comuns: Fábrica Simples, Método de Fábrica e Fábrica Abstrata. Para abstrair camadas de acesso de dados, a Fábrica Simples (ou Fábrica Estática) é muitas vezes o ponto de partida mais pragmático, mas também vamos explorar como evoluí-lo em uma Fábrica Abstrata mais flexível quando existirem várias famílias de produtos.

A motivação primária para usar uma fábrica no acesso de dados .NET Core é manter o Princípio Aberto/Fechado: as classes devem estar abertas para extensão mas fechadas para modificação. Ao canalizar toda a criação de objetos de acesso de dados através de uma fábrica, você pode introduzir novas implementações (por exemplo, mudar do Núcleo de Framework de Entidade para o Dapper) sem tocar na lógica de negócios. Adicionalmente, o Padrão de Fábrica promove Inversão de Dependência— módulos de alto nível não dependem mais de detalhes de baixo nível; ambos dependem de abstrações (interfaces).

O padrão de fábrica não é uma bala de prata. É mais eficaz quando você tem um conjunto bem definido de estratégias de acesso de dados intercambiáveis e precisa isolar a lógica de criação do resto da aplicação. Em aplicações monolíticas simples com uma única fonte de dados, a sobrecarga de uma fábrica pode não ser justificada.

Projetando a Abstração: O Contrato de Interface

O primeiro passo para usar o Padrão de Fábrica para o acesso de dados é definir uma interface comum que todos os repositórios de concreto devem implementar. Esta interface serve como o contrato entre a lógica de seu negócio e a camada de dados. No .NET Core, essa interface frequentemente mapeia as operações padrão CRUD, mas você pode adaptá-la às suas necessidades de domínio.

Exemplo: Uma interface genérica do repositório

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 interface genérica funciona bem quando você precisa de operações de dados consistentes entre diferentes tipos de entidade. No entanto, para simplificar neste artigo, vamos manter uma interface não genérica que opera em um único tipo de entidade. Os princípios permanecem idênticos.

Interfaces especializadas para cenários avançados

Nos projetos do mundo real, você poderá precisar de métodos de repositório que vão além do CRUD básico, como consultas paginadas, filtragem ou agregação. Considere definir interfaces separadas para operações somente de leitura e somente de gravação para seguir o Princípio de Segregação de Interface. Por exemplo:

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

O Padrão de Fábrica pode então produzir uma implementação combinada que satisfaça ambas as interfaces quando necessário, ou retornar objetos separados para leitura e escrita se você escolher uma abordagem CQRS.

Implementação de Classes de Acesso a Dados de Concreto

Uma vez definida a interface, você cria implementações concretas para cada tecnologia de acesso a dados. Abaixo estão exemplos usando Entity Framework Core e Dapper, dois dos frameworks de acesso de dados mais comuns .NET Core.

Implementação do núcleo do quadro de entidade

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 uma instância , que em uma aplicação .NET Core é tipicamente injetada através do recipiente DI. Isso se alinha com o padrão de fábrica: a fábrica precisará de acesso ao recipiente DI para resolver tais dependências.

Implementação Atrevida

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 as implementações cumprem o mesmo contrato, mas usam mecânica totalmente diferente. A fábrica decidirá qual a instanciar com base nas condições de execução.

Construindo a Fábrica: Do Simples ao Abstrato

A própria fábrica encapsula a lógica de decisão. Uma fábrica estática é suficiente quando a escolha da implementação depende apenas de um valor de configuração (por exemplo, uma chave de configuração do aplicativo). No entanto, quando a decisão requer contexto de execução (papel do usuário, locatário, bandeira de recurso), uma fábrica não estática que aceita parâmetros adicionais é mais apropriada.

Fábrica simples estática (configuração-dirigida)

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 usa o para instanciar tipos que têm dependências registradas no recipiente DI. é resolvido diretamente porque ele já está registrado (junto com ]). é criado usando que lida com injeção construtor sem exigir registro manual do próprio repositório – apenas suas dependências (por exemplo, ]) precisam estar no recipiente.

Fábrica Abstrata para Famílias de Produto Múltiplo

Quando sua aplicação requer diferentes tipos de objetos de acesso a dados (por exemplo, um para pedidos, outro para inventário, cada um potencialmente usando um motor de armazenamento diferente), a Fábrica Simples torna-se descontrolada.Uma ]Resumo Fábrica define uma interface para criar famílias de objetos relacionados sem especificar suas classes de concreto.Cada fábrica de concreto produz um conjunto completo de objetos de acesso a dados para uma pilha de tecnologia específica.

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

A Fábrica Abstrata é mais poderosa, mas também mais pesada. Reserve-a para aplicações onde você precisa trocar pilhas de acesso de dados inteiras (por exemplo, substituindo todos os repositórios de Framework de Entidades por repositórios Dapper) de uma vez, em vez de implementar individualmente.

Integrando a Fábrica com .NET injeção de dependência de núcleo

A verdadeira força do padrão de fábrica em .NET Core emerge quando você combina com o recipiente DI. Em vez de registrar tipos de repositório de concreto, registre a fábrica e deixe-a produzir a implementação adequada sob demanda.

Inscrição em Program.cs (ou 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);
});

Neste registro, o concreto é registrado de forma transitória (para que o contêiner DI possa resolvê-lo dentro da fábrica). O registro usa um delegado de fábrica que lê o valor de e delegados para a fábrica estática. Agora qualquer consumidor que injete vai obter a implementação correta sem saber qual é.

Usando Serviços Nomeados para Suporte Multi-Provider

Se o seu aplicativo precisar de repositórios ] múltiplos usando diferentes provedores simultaneamente (por exemplo, um para dados históricos usando Dapper, e um para dados em tempo real usando Entity Framework), você pode registrar métodos de fábrica nomeados ou usar um padrão de dicionário.

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

A fábrica pode então expor um método que retorna a implementação adequada com base no parâmetro , que você pode passar como dependência usando ou um padrão de injeção personalizado.

Benefícios e cenários do mundo real

Vamos examinar situações concretas onde o Padrão de Fábrica brilha em camadas de acesso de dados .NET Core.

1. Testes e zombar

A lógica de negócio de teste de unidade torna-se trivial quando você pode substituir um repositório simulado. A fábrica pode ser configurada na configuração de teste para retornar uma implementação simulada ou em memória. Por exemplo, durante os testes de integração, defina a chave de configuração para e tenha uma fábrica que retorna uma suportada por um .

2. Aplicações Multi-Tenant

Cada inquilino pode exigir uma tecnologia de armazenamento de dados diferente devido a licenciamento, restrições legados ou distribuição geográfica.Uma fábrica pode examinar os metadados do inquilino em tempo de execução e instanciar o repositório apropriado – talvez um inquilino use SQL Server via Entity Framework, outro usa PostgreSQL via Dapper, e um terceiro usa Azure Cosmos DB.

3. Comutadores de Característica e Migração Gradual

Quando migrando de um ORM para outro (por exemplo, de Entity Framework para Dapper para consultas críticas de desempenho), a fábrica permite- lhe encaminhar gradualmente o tráfego. Você pode criar um sistema de sinalização de recursos que, para uma porcentagem de usuários ou terminais específicos, retorna o novo repositório Dapper, enquanto a maioria da aplicação ainda usa o Entity Framework. Se surgirem problemas, reverta a bandeira com alterações de código zero.

Testando a Camada de Acesso Abstrata de Dados

Uma fábrica bem projetada torna os testes simples. Você pode criar uma fábrica de testes que retorna implementações simuladas ou versões leves em memória de seus repositórios de dados. Por exemplo:

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

O simplesmente detém um e implementa a interface usando operações de memória. Os testes de unidade podem então instanciar serviços de negócios com esta fábrica de testes sem exigir uma conexão de banco de dados ou frameworks de zombaria. Os testes de integração ainda podem usar a fábrica real para validar contra um banco de dados de teste.

Melhores práticas e armadilhas comuns

Não-Abstract

O Padrão de Fábrica adiciona indireta. Se sua aplicação nunca mudar de provedor de acesso de dados, a abstração só aumenta a complexidade para nenhum benefício. Use-o somente quando você tiver um requisito claro e atual para múltiplas implementações intercambiáveis.

Evite fornecedores digitados com caracteres

Usar strings mágicas para nomes de provedores (como [[FLT: 31]]] ou [[FLT: 32]]) é frágil. Em vez disso, defina uma enumeração ou use um objeto de configuração com tipos fortes. Por exemplo:

public enum DataProvider
{
 EntityFramework,
 Dapper,
 InMemory
}

Gerencie cuidadosamente a vida inteira

Os repositórios frequentemente mantêm as conexões ou instâncias do DbContext. Certifique- se de que o recipiente DI gere corretamente seu escopo. Para aplicações web, uma vida útil útil (por solicitação) é geralmente apropriada para o EF Core DbContext, mas as conexões Dapper podem precisar de transientes ou escopo baseado na estratégia de agrupamento de conexões. A fábrica não deve armazenar repositórios de cache indefinidamente, a menos que eles não sejam apátridas.

Considere usar uma fábrica com registro

Para aplicações grandes, considere implementar um Padrão de Registro ao lado da fábrica. Um registro armazena fábricas pré-configuradas com chave de algum identificador (por exemplo, identificação do inquilino, nome lógico). O cliente pede ao registro para a fábrica apropriada, que então cria o repositório. Este padrão é especialmente útil em arquiteturas multi-doentes onde cada inquilino pode ter uma estratégia de acesso de dados diferente.

Comparação com padrões alternativos

O padrão de fábrica não é a única maneira de abstrair o acesso de dados. Veja como ele se compara com outras abordagens comuns em .NET Core:

  • Padrão Estratégico – Semelhante ao Factory, mas o foco é encapsular algoritmos (por exemplo, diferentes estratégias de classificação ou filtragem) em vez de criar objetos. O Padrão Fábrica é um padrão criacional; o Padrão Estratégia é comportamental. Eles podem complementar-se mutuamente: uma fábrica pode retornar uma estratégia.
  • Padrão de Decorador – Útil para adicionar preocupações de corte transversal (caching, loging, retry) a um repositório sem modificar seu código. A Fábrica pode decorar o repositório que cria, combinando ambos os padrões.
  • Padrão de repositório (sem fábrica) – Injetar diretamente um repositório de concreto via DI trabalha para aplicações simples.A Fábrica adiciona flexibilidade quando o tipo de concreto varia.

Recursos externos e leituras posteriores

Para aprofundar a sua compreensão do padrão de fábrica e acesso de dados .NET Core, consulte as seguintes fontes autoritárias:

Conclusão: Construção para a mudança

O Padrão de Fábrica fornece uma maneira disciplinada de gerenciar a variação nas camadas de acesso de dados, permitindo que você troque backends de armazenamento, adote novas tecnologias e teste a lógica de negócios em isolamento. No núcleo .NET, combinando o Padrão de Fábrica com o contêiner DI incorporado produz uma arquitetura limpa e mantendível que respeite os princípios SOLID. Comece pequeno: defina uma interface, implemente duas classes de concreto, crie uma fábrica estática e transfira-a através do DI. À medida que sua aplicação cresce, evolua a fábrica para uma Fábrica Abstrata ou uma fábrica orientada para registro para lidar com várias famílias de repositórios. Ao investir nesta abstração, você pode proteger seu código de acesso de dados de futuro contra mudanças inevitáveis nos requisitos de armazenamento e nas escolhas de tecnologia.