As armadilhas mais comuns ao trabalhar com SQL Server em aplicações .NET

As armadilhas mais comuns ao trabalhar com SQL Server em aplicações .NET
Publicado em 27/10/2026 por Admin

O SQL Server é uma das soluções de banco de dados mais utilizadas no ecossistema Microsoft e está presente em aplicações corporativas de diferentes dimensões. Integrado ao .NET, oferece recursos para persistência de dados, execução de consultas complexas, processamento de transações e construção de sistemas escaláveis.

Apesar dessa integração, trabalhar com SQL Server em aplicações .NET exige mais do que conhecer comandos SQL ou utilizar um ORM como o Entity Framework Core. Decisões aparentemente simples, como carregar todos os registros de uma tabela, construir uma consulta sem parâmetros ou manter uma transação aberta por muito tempo, podem provocar problemas de desempenho, segurança e confiabilidade.

Muitas dessas dificuldades não aparecem durante o desenvolvimento inicial. Uma consulta que funciona perfeitamente com algumas centenas de registros pode se tornar um gargalo quando a aplicação começa a processar milhões de linhas. Da mesma forma, uma implementação que parece segura em um ambiente de testes pode expor vulnerabilidades quando recebe dados de usuários reais.

Neste artigo, vamos explorar as armadilhas mais comuns ao trabalhar com SQL Server em aplicações .NET, entender por que elas acontecem e conhecer práticas que ajudam a construir sistemas mais seguros, eficientes e fáceis de manter.

1. Não utilizar consultas parametrizadas

Uma das práticas mais perigosas ao trabalhar com bancos de dados é construir comandos SQL concatenando diretamente valores recebidos da aplicação.

Esse problema aparece frequentemente em consultas construídas dinamicamente com strings em C#.

Considere o seguinte exemplo:

string sql = "SELECT * FROM Users WHERE Email = '" + email + "'";

Embora a consulta possa funcionar normalmente, ela apresenta um risco importante: se o valor de email vier de uma entrada não confiável, poderá alterar a estrutura do comando SQL.

Esse tipo de vulnerabilidade é conhecido como SQL Injection.

Um atacante pode explorar essa falha para modificar as condições da consulta e, dependendo das permissões disponíveis e do contexto de execução, acessar ou alterar informações indevidamente.

Como evitar esse problema?

Utilize consultas parametrizadas, separando os comandos SQL dos valores utilizados na execução.

Com ADO.NET e Microsoft.Data.SqlClient, por exemplo:

using Microsoft.Data.SqlClient;

const string sql = """
    SELECT UserId, Name, Email
    FROM Users
    WHERE Email = @Email;
    """;

using var connection = new SqlConnection(connectionString);

using var command = new SqlCommand(sql, connection);

command.Parameters.Add("@Email", System.Data.SqlDbType.NVarChar, 256)
    .Value = email;

connection.Open();

using var reader = command.ExecuteReader();

while (reader.Read())
{
    Console.WriteLine(reader["Name"]);
}

Nesse exemplo, o valor do e-mail é enviado como parâmetro, em vez de ser incorporado diretamente ao texto da consulta.

Além de ajudar a prevenir SQL Injection, a parametrização favorece a reutilização de planos de execução em cenários apropriados.

É importante lembrar que parâmetros SQL representam valores, não nomes de tabelas, colunas ou direções de ordenação. Quando esses elementos precisam ser dinâmicos, utilize uma lista explícita de opções permitidas.

E quando utilizamos Entity Framework Core?

O EF Core normalmente gera consultas parametrizadas quando os valores são utilizados corretamente em expressões LINQ.

Por exemplo:

var user = await context.Users
    .FirstOrDefaultAsync(u => u.Email == email);

Essa abordagem é preferível à construção manual de SQL por concatenação.

Entretanto, utilizar um ORM não elimina automaticamente todos os riscos. Consultas SQL brutas, especialmente quando construídas com interpolação inadequada, também exigem atenção.

O método FromSqlInterpolated, por exemplo, parametriza os valores interpolados, enquanto FromSqlRaw exige cuidados adicionais quando combinado com entradas dinâmicas.

A regra é simples: nunca confie em uma entrada externa apenas porque ela será utilizada em uma consulta ao banco de dados.

2. Ignorar os índices e os planos de execução

Uma consulta lenta nem sempre é resultado de uma implementação ruim em C#.

Em muitos casos, o problema está na maneira como o SQL Server acessa os dados.

Os índices permitem localizar registros sem precisar examinar todas as linhas de uma tabela em determinadas consultas. Quando os índices existentes não atendem aos padrões de acesso da aplicação, o banco pode precisar realizar operações mais custosas.

Imagine uma tabela de pedidos com milhões de registros e a seguinte consulta:

SELECT OrderId, CustomerId, OrderDate, Total
FROM Orders
WHERE CustomerId = @CustomerId;

Se a aplicação executa essa consulta frequentemente, um índice adequado sobre CustomerId pode ajudar a reduzir o trabalho necessário para localizar os pedidos de um cliente.

Entretanto, criar índices indiscriminadamente também é um erro. Cada índice adicional ocupa espaço e pode aumentar o custo de operações de inserção, atualização e exclusão.

Como identificar problemas de indexação?

Antes de criar ou modificar índices, investigue como a consulta está sendo executada.

Algumas ferramentas e recursos úteis incluem:

  • Plano de execução real ou estimado no SQL Server Management Studio.

  • Query Store para acompanhar consultas, planos e métricas de desempenho ao longo do tempo.

  • Métricas de duração, leituras lógicas e utilização de recursos.

  • Estatísticas e índices existentes nas tabelas envolvidas.

O plano de execução ajuda a identificar operações como varreduras de tabelas ou índices, buscas por índice, junções e ordenações.

Uma varredura completa não é necessariamente um problema: ela pode ser a estratégia mais eficiente quando a consulta precisa retornar uma parcela significativa dos dados. O importante é avaliar o custo e o comportamento no contexto real da aplicação.

Cuidado com filtros sobre colunas

Considere esta consulta:

SELECT OrderId, OrderDate
FROM Orders
WHERE YEAR(OrderDate) = 2026;

Aplicar uma função à coluna filtrada pode dificultar a utilização eficiente de um índice convencional sobre OrderDate.

Uma alternativa é expressar o filtro como um intervalo:

SELECT OrderId, OrderDate
FROM Orders
WHERE OrderDate >= '20260101'
  AND OrderDate < '20270101';

Essa forma permite que o SQL Server considere estratégias de acesso por intervalo quando existe um índice apropriado e o plano de execução favorece essa abordagem.

A lição é que pequenas alterações na escrita de uma consulta podem influenciar o plano de execução. Porém, qualquer otimização deve ser validada com os dados, os índices e a distribuição de valores do ambiente real.

3. Buscar mais dados do que a aplicação realmente precisa

Outra armadilha frequente é utilizar SELECT * ou carregar entidades completas quando a aplicação precisa apenas de algumas propriedades.

Considere uma API que apresenta uma lista de clientes contendo somente nome e e-mail.

Uma consulta como esta pode retornar muito mais dados do que o necessário:

SELECT *
FROM Customers;

Se a tabela tiver dezenas de colunas, incluindo campos de observações extensas ou informações que não são necessárias para a tela, a aplicação poderá transferir dados adicionais sem benefício prático.

O problema se agrava quando o conjunto de resultados cresce.

Selecione apenas as colunas necessárias

Uma consulta mais específica pode ser escrita assim:

SELECT CustomerId, Name, Email
FROM Customers;

No Entity Framework Core, a mesma ideia pode ser aplicada por meio de projeção:

var customers = await context.Customers
    .AsNoTracking()
    .Select(c => new CustomerDto
    {
        Id = c.CustomerId,
        Name = c.Name,
        Email = c.Email
    })
    .ToListAsync();

Nesse exemplo, a aplicação projeta os dados diretamente para um DTO, evitando materializar entidades completas quando elas não são necessárias.

O uso de AsNoTracking() também pode reduzir o trabalho do EF Core em consultas de leitura que não precisam acompanhar alterações nas entidades retornadas.

É importante observar que AsNoTracking() não substitui uma projeção adequada. Quando a consulta retorna apenas valores escalares ou um DTO sem entidades rastreáveis, o comportamento de rastreamento pode ter pouca ou nenhuma relevância para aquele resultado específico.

O objetivo deve ser reduzir o trabalho desnecessário, sem sacrificar a clareza do código.

4. Carregar todos os registros sem paginação

Imagine uma aplicação que apresenta uma lista de transações financeiras, pedidos ou produtos.

No início, a tabela possui poucos registros. Por isso, a implementação abaixo parece suficiente:

var orders = await context.Orders
    .ToListAsync();

O problema é que ToListAsync() materializa todos os resultados da consulta na memória.

Se a tabela crescer significativamente, essa estratégia poderá aumentar o consumo de memória, o tempo de resposta e a quantidade de dados transferidos entre o banco e a aplicação.

Em aplicações web, isso também pode prejudicar a experiência dos usuários e aumentar a pressão sobre os recursos do servidor.

Implemente paginação

Uma abordagem mais adequada é retornar somente os registros necessários para a página solicitada.

int pageNumber = 1;
int pageSize = 20;

var orders = await context.Orders
    .OrderByDescending(o => o.OrderDate)
    .ThenByDescending(o => o.OrderId)
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize)
    .Select(o => new OrderDto
    {
        Id = o.OrderId,
        Date = o.OrderDate,
        Total = o.Total
    })
    .ToListAsync();

Nesse exemplo, a consulta retorna até 20 pedidos, ordenados pela data e pelo identificador.

A segunda ordenação é importante para estabelecer uma ordem determinística quando vários pedidos possuem a mesma data.

Em uma API real, também é necessário validar pageNumber e pageSize, impedindo valores inválidos ou tamanhos excessivos.

Quando considerar paginação por cursor?

O uso de Skip() e Take() é comum e funciona bem em diversos cenários. Entretanto, a paginação baseada em deslocamento pode se tornar custosa em páginas muito distantes do início, porque o banco pode precisar percorrer uma quantidade significativa de registros antes de retornar os resultados desejados.

Para navegação sequencial em conjuntos de dados grandes, considere a paginação por chave, também chamada de keyset pagination.

Nesse modelo, a próxima página é obtida a partir dos valores do último registro da página anterior.

A estratégia pode reduzir o trabalho de navegação em determinados cenários, especialmente quando existe um índice compatível com a ordenação e os filtros.

A escolha depende dos requisitos da interface: navegar sequencialmente, saltar diretamente para páginas arbitrárias ou manter uma ordenação específica são necessidades diferentes.

5. Cair na armadilha do problema N+1 no Entity Framework Core

O problema N+1 é uma das fontes mais conhecidas de consultas excessivas em aplicações que utilizam ORMs.

Ele acontece quando a aplicação executa uma consulta inicial para carregar uma coleção de entidades e, depois, realiza consultas adicionais para cada entidade individualmente.

Considere um sistema com blogs e publicações.

Uma implementação conceitual pode começar assim:

var blogs = await context.Blogs.ToListAsync();

foreach (var blog in blogs)
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine(post.Title);
    }
}

Se o carregamento lento estiver habilitado e as publicações forem carregadas ao acessar blog.Posts, a aplicação poderá executar uma consulta inicial para os blogs e consultas adicionais para carregar as publicações de cada blog.

Com muitos blogs, o número de viagens ao banco de dados pode crescer consideravelmente.

O código parece simples, mas o custo de execução pode ser elevado.

Como evitar o problema N+1?

A solução depende dos dados necessários e da estrutura das entidades.

Quando a aplicação precisa carregar os blogs e suas publicações, uma possibilidade é utilizar carregamento antecipado:

var blogs = await context.Blogs
    .Include(b => b.Posts)
    .AsNoTracking()
    .ToListAsync();

Essa abordagem permite ao EF Core buscar os dados relacionados sem depender do carregamento individual a cada acesso à propriedade de navegação.

Entretanto, utilizar Include() indiscriminadamente também pode produzir consultas muito grandes, com dados repetidos ou relações complexas.

Em determinados cenários, uma projeção para DTOs pode ser mais adequada:

var blogs = await context.Blogs
    .AsNoTracking()
    .Select(b => new BlogDto
    {
        Id = b.BlogId,
        Name = b.Name,
        Posts = b.Posts
            .Select(p => new PostDto
            {
                Id = p.PostId,
                Title = p.Title
            })
            .ToList()
    })
    .ToListAsync();

A tradução dessa projeção depende do provedor, da versão do EF Core e do formato da consulta. Por isso, é importante verificar o SQL gerado e validar o comportamento no ambiente utilizado.

Em alguns casos, consultas divididas com AsSplitQuery() também ajudam a evitar a explosão cartesiana causada por múltiplas relações carregadas na mesma consulta. Em contrapartida, elas podem exigir viagens adicionais ao banco de dados.

Não existe uma única estratégia ideal para todos os casos. O importante é compreender quantas consultas estão sendo executadas e quanto dado está sendo transferido.

6. Não compreender o funcionamento do connection pooling

Abrir uma conexão com o SQL Server pode parecer uma operação simples, mas envolve recursos de rede, autenticação e comunicação com o servidor.

Para evitar o custo de estabelecer conexões físicas repetidamente, o provedor Microsoft.Data.SqlClient utiliza connection pooling por padrão em cenários usuais.

O pool permite reutilizar conexões físicas, reduzindo o custo de novas conexões.

Um erro comum é interpretar esse mecanismo como uma justificativa para manter conexões abertas durante toda a execução da aplicação.

Considere o seguinte exemplo:

var connection = new SqlConnection(connectionString);

connection.Open();

// Executa operações no banco de dados.

// A conexão permanece aberta.

Se a conexão não for fechada ou descartada adequadamente, ela poderá continuar ocupada e indisponível para outras operações.

Sob carga elevada, isso pode contribuir para a exaustão do pool de conexões.

Utilize o descarte adequado dos recursos

Prefira utilizar using para garantir que os recursos sejam liberados:

using var connection = new SqlConnection(connectionString);

await connection.OpenAsync();

// Executa operações no banco de dados.

Ao descartar uma conexão gerenciada pelo pool, a aplicação normalmente devolve a conexão física ao pool para reutilização, em vez de encerrá-la fisicamente.

Também é importante evitar manter conexões abertas enquanto a aplicação executa operações demoradas que não dependem do banco de dados.

Por exemplo, não é uma boa prática abrir uma conexão, executar uma consulta e mantê-la ocupada durante uma chamada HTTP a outro serviço se essa conexão não for necessária nesse intervalo.

A conexão deve permanecer aberta somente pelo tempo necessário para realizar as operações de banco de dados.

E quanto ao Entity Framework Core?

O EF Core administra o ciclo de vida das conexões utilizadas pelo contexto conforme a operação e a configuração.

Em muitos cenários comuns, o desenvolvedor não precisa abrir e fechar manualmente a conexão em cada consulta.

Ainda assim, é importante compreender o ciclo de vida do DbContext, evitar compartilhá-lo indiscriminadamente entre operações concorrentes e investigar erros relacionados ao esgotamento do pool.

7. Utilizar transações longas ou desnecessárias

As transações são fundamentais para preservar a consistência dos dados.

Elas permitem agrupar operações relacionadas de forma que o banco de dados respeite as garantias transacionais aplicáveis.

Entretanto, manter uma transação aberta por muito tempo pode causar problemas de concorrência e desempenho.

Imagine uma operação que:

  1. Abre uma transação.

  2. Atualiza registros no SQL Server.

  3. Chama uma API externa.

  4. Aguarda vários segundos pela resposta.

  5. Executa outra atualização.

  6. Confirma a transação.

Durante esse período, a transação pode manter bloqueios e recursos associados às operações realizadas no banco.

Dependendo do nível de isolamento e das operações executadas, outras transações podem ficar bloqueadas, aumentando o tempo de resposta e o risco de deadlocks.

Mantenha as transações curtas

Quando possível, execute as operações necessárias no banco de dados dentro de uma transação delimitada.

await using var transaction =
    await context.Database.BeginTransactionAsync();

try
{
    var order = await context.Orders
        .FirstAsync(o => o.OrderId == orderId);

    order.Status = "Processed";

    await context.SaveChangesAsync();

    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

Esse exemplo demonstra o controle explícito de uma transação, mas não significa que todas as atualizações precisam de uma transação manual.

O EF Core já utiliza transações automaticamente em determinadas operações de persistência, incluindo o comportamento padrão de SaveChanges() quando necessário.

Por isso, antes de adicionar uma transação explícita, avalie se ela realmente é necessária para garantir a atomicidade da operação.

Quando o fluxo envolve serviços externos, pode ser mais apropriado utilizar padrões como outbox transacional, processamento assíncrono ou compensação de operações, em vez de manter uma transação de banco de dados aberta durante toda a comunicação externa.

8. Misturar operações síncronas e assíncronas sem critério

Aplicações .NET que realizam operações de banco de dados frequentemente precisam lidar com operações de entrada e saída, conhecidas como I/O.

Consultas ao SQL Server podem exigir espera pela resposta do servidor. Em aplicações web com muitas requisições concorrentes, bloquear threads durante essas esperas pode reduzir a capacidade de processamento.

Por esse motivo, o EF Core e o Microsoft.Data.SqlClient disponibilizam operações assíncronas.

Considere o exemplo:

var orders = await context.Orders
    .Where(o => o.Status == "Pending")
    .ToListAsync();

O uso de ToListAsync() permite aguardar a operação sem bloquear a thread da mesma forma que uma chamada síncrona bloqueante.

Para persistência, o equivalente é utilizar SaveChangesAsync() quando apropriado.

Evite bloquear operações assíncronas

Uma armadilha comum é utilizar .Result ou .Wait() para aguardar operações assíncronas:

var orders = context.Orders
    .ToListAsync()
    .Result;

Esse padrão bloqueia a thread enquanto aguarda o resultado e pode contribuir para problemas de escalabilidade e, em determinados contextos, deadlocks.

Prefira propagar o uso de async e await ao longo do fluxo de execução.

Entretanto, operações assíncronas não tornam automaticamente uma consulta SQL mais rápida. Elas melhoram principalmente a forma como a aplicação utiliza seus recursos enquanto aguarda operações de I/O.

Também existem cenários específicos em que determinados caminhos assíncronos do provedor podem apresentar comportamento de desempenho inesperado. Se os resultados de uma medição forem surpreendentes, investigue a versão do provedor, o tipo de dados e o padrão de acesso antes de decidir pela troca entre APIs síncronas e assíncronas.

9. Ignorar os tipos de dados e as conversões implícitas

A escolha dos tipos de dados influencia o armazenamento, a precisão, a integridade e, em determinadas situações, o desempenho das consultas.

Um exemplo é utilizar parâmetros com tipos incompatíveis com as colunas comparadas.

Imagine uma coluna CustomerCode definida como VARCHAR, mas a aplicação envia um parâmetro Unicode com tipo e tamanho diferentes dos esperados.

Dependendo dos tipos envolvidos e das regras de precedência do SQL Server, pode ocorrer uma conversão implícita que afete o plano de execução.

Isso não significa que toda diferença de tipo provoca uma varredura de tabela. O comportamento depende dos tipos concretos, da expressão e do plano escolhido pelo otimizador.

Defina os tipos adequadamente

Com ADO.NET, especifique o tipo e o tamanho do parâmetro quando isso fizer sentido para a coluna:

command.Parameters.Add(
    "@CustomerCode",
    System.Data.SqlDbType.VarChar,
    20
).Value = customerCode;

Se a coluna for NVARCHAR, utilize o tipo correspondente, respeitando a definição do banco de dados.

Outros exemplos importantes incluem:

  • Utilizar decimal com precisão e escala adequadas para valores monetários.

  • Escolher entre date, datetime e datetime2 conforme a necessidade.

  • Evitar converter datas para strings para realizar filtros.

  • Não utilizar tipos numéricos com precisão insuficiente para os valores armazenados.

  • Alinhar os tipos das chaves utilizadas em relacionamentos.

Também é importante ter cuidado com datas e horários. O SQL Server oferece tipos com comportamentos diferentes, e a escolha entre eles deve considerar se a aplicação precisa armazenar apenas uma data, uma data e hora ou um instante com informação de fuso horário.

Uma modelagem adequada evita ambiguidades e reduz a necessidade de conversões desnecessárias.

10. Criar consultas dinâmicas sem controlar os planos de execução

O SQL Server utiliza um otimizador de consultas para escolher estratégias de execução.

Em muitos casos, os planos podem ser reutilizados, evitando a necessidade de recompilar consultas repetidamente.

Entretanto, nem todas as consultas apresentam o mesmo comportamento para diferentes valores de parâmetros.

Imagine uma tabela em que a maioria dos registros pertence a um único cliente, enquanto muitos outros clientes possuem poucos registros.

Uma consulta parametrizada pode apresentar desempenhos diferentes dependendo do valor utilizado, pois a distribuição dos dados influencia o custo das estratégias possíveis.

Esse cenário está relacionado ao fenômeno conhecido como parameter sniffing, no qual os valores dos parâmetros observados durante a compilação podem influenciar o plano escolhido.

É importante não confundir esse fenômeno com a parametrização em si. Parametrizar consultas continua sendo uma prática recomendada para segurança e manutenção.

Como investigar problemas relacionados a planos?

Quando uma consulta apresenta desempenho inconsistente, algumas verificações úteis incluem:

  • Comparar a duração e as leituras lógicas para diferentes valores de parâmetros.

  • Examinar os planos de execução.

  • Verificar estatísticas e distribuição dos dados.

  • Utilizar o Query Store para identificar alterações de plano e regressões.

  • Investigar recompilações e diferenças entre os ambientes.

Dependendo do diagnóstico, podem existir alternativas como melhorar índices, atualizar estatísticas, reescrever a consulta ou aplicar recursos específicos de controle de plano.

Hints como OPTION (RECOMPILE) podem ser úteis em situações justificadas, mas não devem ser aplicados indiscriminadamente. Eles alteram o comportamento de compilação e podem aumentar o custo de execução quando utilizados sem critério.

A recomendação é medir, compreender o comportamento da consulta e só então aplicar uma otimização.

11. Não monitorar consultas lentas em produção

Uma consulta pode funcionar perfeitamente durante os testes e apresentar problemas depois que a aplicação entra em produção.

Isso acontece porque o ambiente real apresenta características que nem sempre são reproduzidas localmente:

  • Maior volume de dados.

  • Mais usuários concorrentes.

  • Distribuição de valores diferente.

  • Maior frequência de determinadas operações.

  • Recursos compartilhados com outros processos.

  • Variações de latência entre a aplicação e o banco de dados.

Sem monitoramento, a equipe pode perceber o problema apenas quando os usuários começam a reclamar.

Utilize ferramentas de diagnóstico

O SQL Server oferece recursos que ajudam a identificar consultas problemáticas e acompanhar o comportamento das cargas de trabalho.

Entre eles estão:

  • Query Store: mantém informações sobre consultas, planos e estatísticas de execução, conforme a configuração e o período de retenção.

  • Extended Events: permite capturar eventos relevantes para investigar comportamentos específicos.

  • Planos de execução: ajudam a entender como as consultas são processadas.

  • DMVs: disponibilizam informações sobre sessões, solicitações, índices e outras áreas do funcionamento do servidor.

Na aplicação .NET, também é importante acompanhar métricas como duração das operações, frequência de erros, timeouts e utilização de conexões.

Os logs devem permitir relacionar uma requisição da aplicação às operações de banco de dados relevantes, sem expor dados sensíveis desnecessariamente.

Uma abordagem eficiente combina observabilidade na aplicação com diagnóstico no SQL Server.

Em vez de simplesmente aumentar o timeout quando uma consulta fica lenta, procure entender se o problema está na consulta, na concorrência, na infraestrutura ou em outro componente do sistema.

12. Utilizar o DbContext com um ciclo de vida inadequado

No Entity Framework Core, o DbContext representa uma unidade de trabalho para interagir com o banco de dados.

Ele mantém informações sobre entidades rastreadas, alterações pendentes e outros elementos necessários para executar operações de persistência.

Um erro comum é manter o mesmo contexto ativo durante períodos excessivamente longos ou utilizá-lo simultaneamente em várias operações.

Um contexto de longa duração pode acumular entidades rastreadas e aumentar o custo de gerenciamento do estado.

Além disso, o DbContext não foi projetado para executar operações concorrentes em paralelo na mesma instância.

Considere este exemplo:

var task1 = context.Customers.ToListAsync();
var task2 = context.Orders.ToListAsync();

await Task.WhenAll(task1, task2);

O código tenta executar duas operações simultaneamente utilizando a mesma instância de DbContext.

Essa abordagem não é suportada pelo EF Core e pode provocar uma exceção quando as operações se sobrepõem.

Como utilizar corretamente?

Em aplicações ASP.NET Core, uma configuração comum é registrar o contexto com ciclo de vida scoped:

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection")));

Dessa forma, o contexto normalmente acompanha o escopo da requisição, desde que não seja capturado por componentes com ciclo de vida mais longo.

Quando existe uma necessidade legítima de executar operações independentes em paralelo, cada operação deve utilizar sua própria instância de contexto.

O IDbContextFactory<TContext> é uma alternativa para cenários em que a aplicação precisa criar contextos sob demanda.

O ponto fundamental é tratar cada contexto como uma unidade de trabalho e evitar compartilhá-lo entre operações concorrentes.

13. Tratar exceções do SQL Server de forma genérica

Outro problema recorrente é capturar todas as exceções relacionadas ao banco de dados e tratá-las como se fossem equivalentes.

Considere o seguinte código:

try
{
    await context.SaveChangesAsync();
}
catch (Exception)
{
    return false;
}

Essa implementação esconde a causa real do erro.

Uma falha de conexão, uma violação de restrição de integridade, um timeout ou um problema de configuração exigem abordagens diferentes.

Ao transformar qualquer falha em um simples false, a aplicação dificulta o diagnóstico e pode impedir que a camada responsável tome a decisão correta.

Trate os erros de acordo com o contexto

Uma implementação mais adequada registra as informações necessárias e permite que a exceção seja tratada pela camada responsável:

try
{
    await context.SaveChangesAsync();
}
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex)
{
    logger.LogError(
        ex,
        "Erro ao persistir alterações no banco de dados.");

    throw;
}

Esse exemplo registra a falha e preserva a exceção original.

Em uma aplicação real, o tratamento deve considerar a arquitetura utilizada e a natureza do erro.

Por exemplo, violações de restrições únicas podem exigir uma resposta específica para o usuário. Já falhas transitórias de comunicação podem permitir uma nova tentativa, desde que a operação seja segura para repetição.

Também é importante evitar repetir automaticamente operações que possam produzir efeitos duplicados.

O uso de políticas de retry deve considerar a natureza da operação, os limites de tentativas, os intervalos entre elas e a possibilidade de a operação anterior ter sido concluída no banco apesar de a resposta não ter chegado à aplicação.

A regra é não esconder erros: registre o contexto necessário, preserve a causa original e defina uma estratégia de tratamento adequada.

14. Armazenar informações sensíveis na connection string sem os cuidados necessários

A connection string contém informações necessárias para estabelecer a conexão com o SQL Server.

Dependendo da configuração, ela pode incluir o endereço do servidor, o banco de dados, o método de autenticação e outras propriedades.

Um erro comum é manter credenciais diretamente no código-fonte:

var connectionString =
    "Server=sqlserver;Database=AppDb;User Id=appuser;Password=MinhaSenha;";

Essa abordagem pode expor informações sensíveis em repositórios, logs, imagens de contêineres ou arquivos distribuídos com a aplicação.

Utilize configurações e mecanismos de gestão de segredos

Em aplicações .NET, a connection string pode ser carregada por meio da configuração:

var connectionString =
    builder.Configuration.GetConnectionString("DefaultConnection");

Em desenvolvimento, o User Secrets pode ajudar a manter dados de configuração fora do código-fonte.

Em produção, utilize um mecanismo de gestão de segredos adequado ao ambiente, como um cofre de segredos gerenciado, variáveis de ambiente protegidas ou um mecanismo de autenticação integrado quando disponível e apropriado.

Em ambientes Azure, por exemplo, o Azure Key Vault pode fazer parte dessa estratégia.

Sempre que possível, prefira autenticação integrada ou baseada em identidade gerenciada, quando suportada pelo ambiente, em vez de distribuir credenciais estáticas.

Além disso, a conta utilizada pela aplicação deve possuir somente as permissões necessárias para executar suas operações.

Evite utilizar uma conta administrativa do SQL Server para as conexões da aplicação.

A proteção das credenciais deve ser acompanhada de controle de acesso, auditoria e políticas adequadas de rotação.

15. Acreditar que o ORM sempre gera a consulta mais eficiente

O Entity Framework Core simplifica o acesso aos dados e reduz a necessidade de escrever SQL manualmente em muitas situações.

Entretanto, ele não elimina a necessidade de compreender o SQL gerado.

Uma expressão LINQ pode ser simples de escrever e ainda produzir uma consulta que merece investigação em termos de desempenho.

Da mesma forma, uma consulta SQL escrita manualmente não é necessariamente mais eficiente apenas por não utilizar um ORM.

A escolha deve considerar a complexidade da operação, o comportamento esperado, a facilidade de manutenção e os resultados das medições.

Quando utilizar SQL manual?

Existem situações em que uma consulta SQL específica oferece melhor controle sobre a operação, principalmente quando envolve construções avançadas ou recursos particulares do SQL Server.

O EF Core permite executar consultas SQL por meio de APIs como FromSql e FromSqlRaw, respeitando as diferenças entre suas formas de utilização.

Também é possível utilizar Dapper ou ADO.NET diretamente quando a arquitetura e os requisitos do sistema justificam essa escolha.

Entretanto, introduzir SQL manual significa assumir responsabilidades adicionais, incluindo manutenção da consulta, parametrização, mapeamento dos resultados e testes.

Uma boa estratégia é começar pela solução mais simples que atende aos requisitos e investigar alternativas quando houver uma necessidade concreta.

O objetivo não é eliminar o ORM nem utilizá-lo para tudo. É escolher a abordagem mais adequada para cada operação.

16. Não testar o comportamento do banco com dados representativos

Testes que utilizam poucos registros são úteis para verificar regras de negócio, mas nem sempre revelam problemas de desempenho.

Uma consulta que funciona bem com dez registros pode apresentar outro comportamento com centenas de milhares de linhas.

Da mesma forma, testes que utilizam apenas dados uniformemente distribuídos podem não reproduzir problemas associados a valores muito frequentes ou pouco frequentes.

Por isso, testes de integração e avaliações de desempenho devem considerar cenários representativos da utilização real.

O que vale a pena testar?

Considere incluir:

  • Consultas com diferentes volumes de dados.

  • Paginação em conjuntos de dados maiores.

  • Relações com diferentes quantidades de registros associados.

  • Operações concorrentes de leitura e escrita.

  • Restrições de integridade e cenários de falha.

  • Comportamento diante de timeouts e falhas transitórias.

  • Tempo de resposta e consumo de recursos em operações críticas.

Também é importante testar com o mesmo mecanismo de banco de dados utilizado em produção quando o objetivo é validar o comportamento específico do SQL Server.

Provedores em memória podem ser úteis para determinados testes de unidade, mas não reproduzem fielmente recursos, traduções SQL, índices, bloqueios e planos de execução de um banco relacional real.

Para testes de integração, uma instância de SQL Server em ambiente isolado pode oferecer resultados mais representativos.

A quantidade de dados e a carga devem ser definidas de acordo com os requisitos e o perfil de utilização da aplicação, sem presumir que um único volume de teste seja suficiente para todos os cenários.

Boas práticas para trabalhar com SQL Server em aplicações .NET

Depois de conhecer as principais armadilhas, vale consolidar as práticas que ajudam a evitar esses problemas no dia a dia.

Segurança

  • Utilize consultas parametrizadas.

  • Valide as entradas recebidas pela aplicação.

  • Evite construir SQL com concatenação de valores externos.

  • Armazene credenciais em mecanismos apropriados.

  • Utilize contas com privilégios mínimos.

  • Evite registrar credenciais e dados pessoais desnecessários nos logs.

Desempenho

  • Analise os planos de execução das consultas críticas.

  • Crie índices com base nos padrões reais de acesso.

  • Retorne apenas as colunas necessárias.

  • Implemente paginação para conjuntos de dados grandes.

  • Investigue consultas adicionais geradas pelo ORM.

  • Avalie o impacto de operações de ordenação, filtros e junções.

  • Utilize métricas para identificar regressões.

Confiabilidade

  • Delimite corretamente o ciclo de vida das conexões.

  • Evite transações longas.

  • Trate erros de acordo com sua natureza.

  • Configure políticas de retry para cenários apropriados.

  • Não compartilhe instâncias de DbContext entre operações concorrentes.

  • Teste o comportamento diante de falhas e condições de concorrência.

Manutenção

  • Mantenha as consultas legíveis.

  • Utilize DTOs quando precisar controlar os dados retornados.

  • Revise as migrações e alterações de esquema.

  • Centralize a configuração do acesso ao banco de dados.

  • Documente decisões importantes de modelagem e desempenho.

  • Monitore a aplicação depois da publicação.

Essas práticas não eliminam todos os problemas possíveis, mas ajudam a construir uma base mais consistente para o desenvolvimento e a evolução de aplicações .NET.

Conclusão

Trabalhar com SQL Server em aplicações .NET envolve muito mais do que executar consultas e persistir objetos.

As decisões tomadas na camada de acesso a dados afetam diretamente a segurança, o desempenho, a confiabilidade e a capacidade de evolução do sistema.

Consultas sem parametrização, ausência de índices adequados, carregamento excessivo de dados, problema N+1, conexões mal gerenciadas e transações longas são exemplos de situações que podem passar despercebidas durante o desenvolvimento inicial e se tornar problemas importantes em produção.

A melhor forma de evitá-las é combinar boas práticas de programação com conhecimento sobre o funcionamento do banco de dados.

Isso significa compreender o SQL gerado pelo ORM, analisar planos de execução, monitorar consultas, testar com dados representativos e utilizar os recursos do .NET de maneira consciente.

Mais importante ainda: não otimize apenas com base em suposições. Identifique o problema, reúna evidências, aplique uma mudança e meça o resultado.

Essa abordagem permite desenvolver aplicações .NET que utilizam o SQL Server de maneira mais eficiente, segura e sustentável.

Se você trabalha com C#, Entity Framework Core, Dapper ou ADO.NET, vale revisar os pontos apresentados e identificar quais deles merecem atenção nos projetos em que atua.

Continue acompanhando o Backstage Tech para mais conteúdos sobre desenvolvimento .NET, bancos de dados, arquitetura de software e boas práticas de engenharia.