Removendo código desnecessário das suas aplicações .NET

Código de debug esquecido no projeto é uma das formas mais simples de vazar informação em produção, e o C# oferece recursos nativos para evitar esse problema. Neste artigo, você vai conhecer a classe Debug, o atributo Conditional e as diretivas #if, que permitem desabilitar ou omitir trechos de código no build de Release.

Além disso, você vai entender o que cada abordagem realmente faz com o IL gerado. Essa diferença define quando usar uma ou outra.

O risco do código de debug em produção

Quem nunca esqueceu um Console.WriteLine no código e enviou tudo para produção? Essa é uma prática comum, mas pode ter consequências bem maiores do que um texto perdido no terminal.

Já houve casos de Console.WriteLine(connectionString) publicados em produção, imprimindo usuário e senha de acesso ao banco de dados. Em outras palavras, qualquer pessoa com acesso aos logs do servidor passa a ter acesso ao banco.

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

// Nunca faça isso: a string de conexão inclui usuário e senha
Console.WriteLine(connectionString);

O problema não está no Console.WriteLine em si. Na verdade, o risco nasce do código de debug que sobrevive ao ciclo de desenvolvimento e chega ao artefato publicado.

Em ambientes de nuvem, a saída padrão do processo costuma ir direto para a plataforma de logs. Por isso, um print esquecido pode ficar armazenado por meses e visível para muito mais gente do que deveria.

Debug.WriteLine e o código de debug condicional

Uma abordagem bem mais sutil é trocar o Console.WriteLine pelo Debug.WriteLine, do namespace System.Diagnostics. Dessa forma, a não ser que você compile o projeto em modo Debug, essa linha não existe em produção.

using System.Diagnostics;

public class PaymentService
{
    public void Process(decimal amount)
    {
        // Só é compilado quando o símbolo DEBUG está definido
        Debug.WriteLine($"Processing payment of {amount}");

        // Lógica real do pagamento
    }
}

Tudo o que está na classe Debug só executa no modo Debug. Com isso, o próprio compilador evita a exposição desse tipo de informação no build de Release.

Por que esse código de debug desaparece no Release

O segredo está no atributo ConditionalAttribute, que a Microsoft aplica a todos os métodos das classes Debug e Trace. Segundo a documentação oficial do ConditionalAttribute, o atributo instrui o compilador a ignorar a chamada, a menos que um símbolo de compilação específico esteja definido.

No caso da classe Debug, esse símbolo é o DEBUG. Por padrão, o sistema de build define o DEBUG apenas na configuração Debug, então as chamadas somem quando você compila em Release.

Já a classe Trace depende do símbolo TRACE. Contudo, a documentação do Visual Studio informa que o TRACE fica definido tanto no Debug quanto no Release, então Trace.WriteLine continua ativo em produção.

Portanto, para código de debug que não pode chegar ao servidor, use Debug, e não Trace. Essa confusão é mais comum do que parece.

A melhor maneira de evitar vazamentos

Ainda assim, a melhor proteção é simples: não imprima esse tipo de informação. Nenhum recurso do compilador substitui o cuidado de não escrever segredos na saída da aplicação.

Se você precisa obter uma configuração específica ou inspecionar algum valor em produção, prefira o Remote Debugger do Visual Studio. Ele permite anexar o depurador a um processo em outra máquina, sem deixar logs desnecessários no código.

Para registros que precisam existir em produção, use um logger estruturado com níveis bem definidos. O artigo sobre logging no ASP.NET mostra como configurar esses níveis.

O atributo Conditional para código de debug

Além do Debug.WriteLine e dos outros métodos da classe Debug, você pode decorar os seus próprios métodos com o atributo Conditional. Assim, o compilador só mantém as chamadas ao método quando o símbolo DEBUG está definido.

using System.Diagnostics;

public class DiagnosticsHelper
{
    [Conditional("DEBUG")]
    public void LogDiagnostics(string message)
    {
        Console.WriteLine($"[DEBUG] {message}");
    }
}

Esse método só tem efeito durante o modo Debug. Em Release, o compilador simplesmente remove todas as chamadas a LogDiagnostics, como se elas nunca tivessem existido.

Vale reforçar um detalhe importante. Como o compilador omite a chamada inteira, as expressões passadas como argumento também deixam de existir, o que evita custo de processamento com código de debug em produção.

Regras do atributo Conditional

O atributo tem algumas restrições impostas pela especificação do C#. A principal delas é que o método condicional precisa retornar void, porque o compilador não teria um valor para entregar quando a chamada some.

Além disso, o método precisa estar em uma classe ou struct. Ele também não pode implementar um método de interface, não pode usar o modificador override e não pode ter parâmetros out.

Você ainda pode aplicar o atributo a uma classe de atributo personalizada. Nesse caso, o compilador só grava o atributo nos metadados quando o símbolo está definido.

Debug.Assert no código de debug

Outro método útil da classe Debug é o Debug.Assert. Ele verifica uma condição e, quando ela é falsa, avisa o desenvolvedor durante a depuração.

public decimal CalculateDiscount(decimal price, decimal percentage)
{
    // Valida uma suposição apenas durante o desenvolvimento
    Debug.Assert(percentage is >= 0 and <= 100, "Percentage must be between 0 and 100");

    return price * percentage / 100;
}

Como todo método da classe Debug, o Assert também carrega o atributo Conditional("DEBUG"). Portanto, a verificação some no Release, junto com a expressão passada como argumento.

Isso faz do Debug.Assert uma ótima ferramenta para documentar suposições internas do código. Contudo, ele nunca deve substituir a validação de dados que chegam do usuário, porque essa validação precisa existir em produção.

Símbolos personalizados com define e undef

O ConditionalAttribute trabalha com símbolos do compilador, e você pode criar quantos símbolos quiser. Para isso, use a diretiva #define no topo do arquivo, antes de qualquer instrução que não seja uma diretiva.

#define CONDITION1
#define CONDITION2

using System;
using System.Diagnostics;

Com os símbolos definidos, use o Conditional para pedir ao compilador que ignore a chamada de um método. A exceção acontece quando o símbolo informado está definido.

[Conditional("CONDITION1")]
public static void Method1(int x)
{
    Console.WriteLine("CONDITION1 is defined");
}

[Conditional("CONDITION1"), Conditional("CONDITION2")]
public static void Method2()
{
    Console.WriteLine("CONDITION1 or CONDITION2 is defined");
}

Nesse exemplo, o compilador só mantém as chamadas a Method1 quando CONDITION1 está definido. Por outro lado, as chamadas a Method2 permanecem quando qualquer um dos dois símbolos existe, já que vários atributos Conditional se combinam com a lógica OR.

Da mesma forma, a diretiva #undef CONDITION1 remove o símbolo do arquivo. Com isso, a aplicação compila sem as chamadas a Method1, porque a condição deixou de valer.

Símbolos no arquivo de projeto

O #define vale apenas para o arquivo em que aparece. Quando você precisa de um símbolo para o projeto inteiro, a propriedade DefineConstants do .csproj resolve o problema.

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
  <!-- Mantém os símbolos existentes e adiciona um novo -->
  <DefineConstants>$(DefineConstants);VERBOSE_LOGS</DefineConstants>
</PropertyGroup>

Repare no uso de $(DefineConstants) antes do novo símbolo. Sem ele, você sobrescreveria o DEBUG e o TRACE que o build já define.

A diretiva #if DEBUG para blocos maiores

Além do atributo Conditional, o C# oferece as diretivas de pré-processamento #if, #elif, #else e #endif. Elas atendem trechos maiores de código de debug, e não apenas chamadas de método.

public void Method2()
{
#if CONDITION1
    // Este bloco só existe quando CONDITION1 está definido
    Console.WriteLine("Running with CONDITION1");
#endif
}

Um dos cenários mais comuns é usar o símbolo DEBUG, que o próprio ambiente do .NET define automaticamente na configuração Debug. Dessa forma, você toma decisões de compilação sem configurar nada.

#if DEBUG
public void LogDiagnostics(string message)
{
    Console.WriteLine($"[DEBUG] {message}");
}
#endif

As diretivas também aceitam os operadores &&, || e !, além de parênteses para agrupar símbolos. Por exemplo, #if DEBUG && !MYTEST só compila o bloco quando DEBUG existe e MYTEST não.

#if DEBUG
    Console.WriteLine("Verbose diagnostics enabled");
#else
    Console.WriteLine("Release mode: errors only");
#endif

O #else permite fornecer uma alternativa para o build de Release. Assim, cada configuração recebe exatamente o comportamento esperado.

Cuidado ao esconder um método inteiro

Quando você envolve um método inteiro em #if DEBUG, o método deixa de existir no Release. Consequentemente, todo lugar que chama esse método também precisa estar protegido, ou o build de Release quebra.

É justamente aqui que a documentação do C# recomenda o atributo Conditional. Segundo a Microsoft, ele é mais limpo, mais elegante e menos sujeito a erros do que envolver métodos em blocos #if e #endif.

O que vai para produção: #if DEBUG ou Conditional

Se as duas abordagens parecem fazer a mesma coisa, qual é a diferença real? Na verdade, existe uma diferença grande no IL que o compilador gera.

No caso do #if DEBUG, o código nem chega ao IL. O compilador o descarta antes mesmo da análise, como se aquelas linhas não existissem no arquivo.

No caso do Conditional, o corpo do método chega ao IL normalmente. A documentação do ConditionalAttribute explica que o compilador deixa de gerar apenas as chamadas ao método quando o símbolo não está definido.

Comparação lado a lado

A tabela resume o comportamento de cada recurso quando você compila em Release.

Recurso O que vai para o IL no Release Melhor uso
Debug.WriteLine Nada, as chamadas são omitidas Mensagens rápidas de diagnóstico
[Conditional("DEBUG")] O método, mas sem nenhuma chamada Métodos auxiliares de código de debug
#if DEBUG Nada do bloco protegido Blocos grandes, campos e classes inteiras
Trace.WriteLine As chamadas, porque TRACE segue definido Rastreamento que deve existir em produção

Quando escolher cada abordagem

Em resumo, se você quer de fato descartar o código de debug durante a compilação, o #if DEBUG é a melhor opção. Além disso, ele economiza alguns bytes no assembly final.

Por outro lado, o Conditional é a melhor opção quando o método precisa continuar disponível na compilação final. Isso é útil se, eventualmente, você precisar alternar entre Release e Debug para algum tipo de teste.

Existe ainda um detalhe sutil. O compilador avalia o símbolo do Conditional no ponto da chamada, e não no ponto em que você declarou o método.

Na prática, uma biblioteca compilada em Release pode expor um método [Conditional("DEBUG")]. Nesse caso, um projeto consumidor compilado em Debug continua chamando esse método normalmente, porque o corpo dele está no IL.

Como testar o código de debug no build de Release

Confiar no compilador é bom, mas conferir é melhor. Por isso, vale executar a aplicação em Release antes de publicar e observar se alguma mensagem de diagnóstico ainda aparece.

Pelo terminal, a CLI do .NET aceita a configuração como parâmetro. O mesmo projeto pode rodar nas duas configurações sem nenhuma alteração no código.

# Executa com o símbolo DEBUG definido
dotnet run -c Debug

# Executa com a configuração usada em produção
dotnet run -c Release

No Visual Studio, basta trocar a configuração da solução de Debug para Release na barra de ferramentas. Em seguida, execute a aplicação e acompanhe a janela de saída.

Se as mensagens do Debug.WriteLine aparecem no primeiro comando e somem no segundo, o código de debug está protegido. Caso algo ainda apareça em Release, procure por Console.WriteLine ou Trace.WriteLine esquecidos.

Além disso, a esteira de CI deve compilar sempre em Release. Dessa forma, o artefato publicado nunca carrega chamadas condicionadas ao símbolo DEBUG.

Boas práticas para código de debug

Os recursos do compilador ajudam, mas não substituem bons hábitos. Por isso, algumas práticas simples reduzem bastante o risco de vazamentos.

Primeiro, nunca escreva segredos na saída, nem mesmo em código de debug. Strings de conexão, tokens e senhas devem ficar fora de qualquer WriteLine.

Em seguida, guarde as configurações sensíveis em locais apropriados, fora do código-fonte. O artigo sobre o Configuration Manager do ASP.NET explica como a aplicação lê esses valores.

Também vale revisar os pull requests procurando prints esquecidos. Uma busca simples por Console.WriteLine antes do merge já evita boa parte dos problemas.

Por fim, trate o assunto como parte da segurança da aplicação. O artigo sobre boas práticas de segurança em APIs ASP.NET Core traz outras medidas que complementam esse cuidado.

Debug.WriteLine não é o mesmo que LogDebug

Uma confusão frequente envolve o método LogDebug da interface ILogger. Apesar do nome parecido, ele não depende do símbolo DEBUG e não desaparece no build de Release.

Na prática, a chamada ao LogDebug continua no IL, e quem decide se a mensagem aparece é o nível mínimo configurado para o logger. Em outras palavras, basta alguém baixar esse nível em produção para que todas as mensagens voltem a ser gravadas.

Por isso, o mesmo cuidado com dados sensíveis vale para os logs estruturados. Trate o LogDebug como código de produção, e não como código de debug que o compilador vai remover.

Conclusão

Código de debug que chega à produção pode expor dados sensíveis, como credenciais de banco de dados. Felizmente, o C# oferece três ferramentas para controlar o que vai para o build de Release.

O Debug.WriteLine resolve as mensagens rápidas, enquanto o atributo Conditional remove as chamadas aos seus próprios métodos de diagnóstico. Já o #if DEBUG elimina blocos inteiros antes mesmo de o compilador gerar o IL.

Escolha o #if DEBUG quando quiser descartar o código de vez. Escolha o Conditional quando o método precisar continuar no assembly, e lembre que a melhor proteção continua sendo não imprimir informação sensível.

FAQ

O Debug.WriteLine aparece no build de Release?

Não. Os métodos da classe Debug têm o atributo Conditional("DEBUG"), então o compilador omite as chamadas quando o símbolo DEBUG não está definido.

O Trace.WriteLine também some no Release?

Não. A classe Trace depende do símbolo TRACE, que fica definido por padrão tanto no Debug quanto no Release.

Posso usar Conditional em um método que retorna valor?

Não. A especificação do C# exige que um método condicional retorne void, caso contrário ocorre erro de compilação.

Qual a diferença entre #if DEBUG e Conditional?

O #if DEBUG remove o bloco antes da geração do IL. O Conditional mantém o corpo do método no IL e remove apenas as chamadas.

Como definir um símbolo para o projeto inteiro?

Use a propriedade DefineConstants no arquivo .csproj, sempre incluindo $(DefineConstants) para preservar os símbolos já existentes.