Leverage the C# preprocessor
C and C++ developers carry a mental model of the preprocessor as a separate pass—a crude text-substitution engine that runs before the compiler proper. C# doesn’t work that way. Its preprocessor directives are resolved during lexical analysis, as part of the normal compilation pipeline. The practical effect is the same in most cases, but one important limitation follows from the difference: #define in C# cannot define macros. You can define symbols, but not code.
Defining and testing symbols
The #define directive creates a named symbol that can be tested later in the same file:
#define DEBUG
using System;
public class MyClass
{
public static void Main()
{
#if DEBUG
Console.WriteLine("DEBUG symbol is defined!");
#endif
}
}
The #if / #endif pair wraps code that should only compile when the symbol is active. If DEBUG isn’t defined, the compiler sees nothing between those directives. The #undef directive does the inverse—it deactivates a symbol for the remainder of the file, which is useful when you want to suppress a symbol that was defined elsewhere.
File scope vs. project scope
A #define in source code applies only to that file. This is intentional: file-local symbols let you control behaviour without affecting the rest of the project.
For project-wide symbols, use the build configuration instead. In Visual Studio, go to Project Properties → Build and edit the Conditional Compilation Constants field. Any symbol defined there is visible across all files in that configuration—which is how DEBUG and RELEASE behave by default. You can add your own: FEATURE_EXPERIMENTAL, INTERNAL_BUILD, whatever the project requires.
Where this is actually useful
The most common use is exactly what the example shows: diagnostic output that should never reach production. A slightly more sophisticated pattern is capability flags that allow a single codebase to produce different builds—a feature-limited public release and a full-featured internal one, for example—without maintaining separate branches.
It’s a narrow tool. Don’t reach for it when a runtime flag or a configuration value would do the job. But for compile-time separation of concerns, the C# preprocessor does exactly what it needs to.