Introduction
Debug.WriteLine is completely removed from Release builds in .NET. The Debug class in System.Diagnostics is decorated with [Conditional("DEBUG")], which means the compiler strips all Debug.* calls when the DEBUG symbol is not defined. In Release configuration, DEBUG is not defined by default, so Debug.WriteLine statements produce zero overhead — no method call, no string formatting, no output. For logging in Release builds, use Trace.WriteLine, a logging framework (Serilog, NLog), or ILogger.
How It Works
1using System.Diagnostics;
2
3public void ProcessOrder(Order order)
4{
5 Debug.WriteLine($"Processing order: {order.Id}"); // REMOVED in Release
6 Debug.Assert(order != null, "Order cannot be null"); // REMOVED in Release
7
8 // Business logic...
9 Trace.WriteLine($"Order processed: {order.Id}"); // KEPT in Release
10}
Compiler Behavior
1// In Debug configuration (DEBUG symbol defined):
2Debug.WriteLine("Hello"); // Compiled and executed
3
4// In Release configuration (DEBUG symbol NOT defined):
5// Debug.WriteLine("Hello"); // Entire line is removed by the compiler
6// The compiled IL contains no trace of this call
The [Conditional("DEBUG")] attribute on the Debug class tells the C# compiler to exclude calls to its methods when the DEBUG preprocessor symbol is absent.
Debug vs Trace vs ILogger
1using System.Diagnostics;
2using Microsoft.Extensions.Logging;
3
4public class OrderService
5{
6 private readonly ILogger<OrderService> _logger;
7
8 public OrderService(ILogger<OrderService> logger)
9 {
10 _logger = logger;
11 }
12
13 public void Process(Order order)
14 {
15 // Debug.WriteLine — REMOVED in Release
16 Debug.WriteLine($"Debug: Processing {order.Id}");
17
18 // Trace.WriteLine — KEPT in Release (controlled by TRACE symbol)
19 Trace.WriteLine($"Trace: Processing {order.Id}");
20
21 // ILogger — KEPT in Release, configurable level
22 _logger.LogDebug("Processing {OrderId}", order.Id);
23 _logger.LogInformation("Order {OrderId} processed", order.Id);
24 }
25}
| Method | Debug Build | Release Build | Configurable |
Debug.WriteLine | Active | Removed | No (compile-time) |
Trace.WriteLine | Active | Active | Listeners in config |
ILogger.LogDebug | Active | Active | Log level in config |
Console.WriteLine | Active | Active | No (always runs) |
Verifying the Behavior
1public class DebugTest
2{
3 public static void Main()
4 {
5 Console.WriteLine("Console: Always visible");
6 Debug.WriteLine("Debug: Only in Debug builds");
7 Trace.WriteLine("Trace: In both builds");
8
9 #if DEBUG
10 Console.WriteLine("Compiled with DEBUG symbol");
11 #else
12 Console.WriteLine("Compiled WITHOUT DEBUG symbol");
13 #endif
14 }
15}
1# Debug build
2dotnet run -c Debug
3# Console: Always visible
4# Debug: Only in Debug builds (visible in Output window)
5# Trace: In both builds
6# Compiled with DEBUG symbol
7
8# Release build
9dotnet run -c Release
10# Console: Always visible
11# Trace: In both builds
12# Compiled WITHOUT DEBUG symbol
Using Trace for Release Logging
1using System.Diagnostics;
2
3// Add trace listeners in app.config
4// <system.diagnostics>
5// <trace autoflush="true">
6// <listeners>
7// <add name="file" type="System.Diagnostics.TextWriterTraceListener"
8// initializeData="app.log" />
9// </listeners>
10// </trace>
11// </system.diagnostics>
12
13public void ProcessData()
14{
15 Trace.TraceInformation("Starting data processing");
16 Trace.TraceWarning("Large dataset detected");
17 Trace.TraceError("Processing failed for record 42");
18
19 // Trace with category
20 Trace.WriteLine("Custom message", "MyCategory");
21}
Trace is controlled by the TRACE preprocessor symbol, which is defined in both Debug and Release configurations by default.
Custom Conditional Methods
1// Create your own conditionally compiled methods
2[Conditional("DEBUG")]
3public static void DebugLog(string message)
4{
5 Console.WriteLine($"[DEBUG] {DateTime.Now:HH:mm:ss} {message}");
6 File.AppendAllText("debug.log", $"{DateTime.Now}: {message}\n");
7}
8
9[Conditional("VERBOSE")]
10public static void VerboseLog(string message)
11{
12 Console.WriteLine($"[VERBOSE] {message}");
13}
14
15// Usage
16DebugLog("Processing started"); // Removed in Release
17VerboseLog("Step 1 complete"); // Only if VERBOSE symbol is defined
1<!-- Define custom symbols in .csproj -->
2<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
3 <DefineConstants>DEBUG;TRACE;VERBOSE</DefineConstants>
4</PropertyGroup>
5<PropertyGroup Condition="'$(Configuration)' == 'Release'">
6 <DefineConstants>TRACE</DefineConstants>
7</PropertyGroup>
Modern Logging with ILogger
1// Program.cs (.NET 6+)
2var builder = WebApplication.CreateBuilder(args);
3
4// Configure logging levels per environment
5// appsettings.Development.json
6// { "Logging": { "LogLevel": { "Default": "Debug" } } }
7
8// appsettings.Production.json
9// { "Logging": { "LogLevel": { "Default": "Warning" } } }
10
11var app = builder.Build();
12
13app.MapGet("/", (ILogger<Program> logger) =>
14{
15 logger.LogDebug("Debug message"); // Only in Development
16 logger.LogInformation("Info message"); // Development + Staging
17 logger.LogWarning("Warning message"); // All environments
18 logger.LogError("Error message"); // All environments
19 return "OK";
20});
ILogger is the recommended approach for .NET applications. Log levels are configured per environment without recompilation.
Common Pitfalls
Expecting Debug.WriteLine in production logs: Debug.WriteLine produces no output in Release builds — not even in the Output window. If you need logging in production, use Trace, ILogger, or a logging framework like Serilog.
Side effects in Debug calls: Debug.WriteLine(ExpensiveComputation()) — the entire expression including ExpensiveComputation() is removed in Release. If the method has side effects your code depends on, those effects disappear in Release builds.
Assuming Trace is always active: Trace requires the TRACE preprocessor symbol. While it is defined by default in both Debug and Release configurations, it can be removed. Verify your .csproj defines TRACE in Release.
String formatting overhead: Debug.WriteLine($"Processing {JsonSerializer.Serialize(largeObject)}") — in Debug builds, the string is constructed even though the output may go nowhere (no listener attached). Use Debug.WriteLineIf or check Debugger.IsAttached for expensive formatting.
Confusing #if DEBUG with [Conditional("DEBUG")]: #if DEBUG removes the code block at compile time. [Conditional("DEBUG")] removes the call sites but keeps the method definition. With [Conditional], the method exists in the assembly but is never called.
Summary
Debug.WriteLine is completely removed from Release builds — zero overhead
The [Conditional("DEBUG")] attribute causes the compiler to strip all Debug.* calls when DEBUG is not defined
Use Trace.WriteLine for logging that persists in Release builds
Use ILogger (Microsoft.Extensions.Logging) for configurable, environment-aware logging
Never rely on side effects inside Debug.* calls — they are removed in Release
#if DEBUG removes code blocks; [Conditional("DEBUG")] removes call sites