MSBuild
conditional compilation
build configuration
C# development
build automation

Defining conditional compilation symbols in MSBuild

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Conditional compilation symbols let you include or exclude code at compile time with directives such as #if and #endif. In MSBuild-based .NET projects, these symbols are usually defined through the DefineConstants property in the project file or on the command line. The important detail is that symbols are build configuration, not runtime switches.

Define Symbols in the Project File

The most common place to define symbols is the .csproj file.

xml
1<Project Sdk="Microsoft.NET.Sdk">
2  <PropertyGroup>
3    <TargetFramework>net8.0</TargetFramework>
4    <DefineConstants>TRACE;FEATURE_X</DefineConstants>
5  </PropertyGroup>
6</Project>

Those symbols can then be used in code:

csharp
#if FEATURE_X
Console.WriteLine("Feature X code compiled in.");
#endif

If the symbol is not defined during compilation, the guarded code is omitted entirely from the assembly.

Preserve Existing Symbols When Appending

A frequent mistake is overwriting the default symbols accidentally. If you want to add a custom symbol while keeping what is already defined, append to $(DefineConstants) instead of replacing it blindly.

xml
<PropertyGroup>
  <DefineConstants>$(DefineConstants);FEATURE_X</DefineConstants>
</PropertyGroup>

This matters because build configurations often already define symbols such as DEBUG and TRACE.

Use Conditions for Configuration-Specific Symbols

You can define different symbols for Debug and Release builds or for other conditions.

xml
1<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
2  <DefineConstants>$(DefineConstants);INTERNAL_DIAGNOSTICS</DefineConstants>
3</PropertyGroup>
4
5<PropertyGroup Condition="'$(Configuration)' == 'Release'">
6  <DefineConstants>$(DefineConstants);PUBLIC_BUILD</DefineConstants>
7</PropertyGroup>

That keeps the symbols aligned with the build mode rather than scattering configuration logic through the source code.

You Can Also Set Symbols From the Command Line

Sometimes the build pipeline, not the project file, should decide which symbols are active.

bash
dotnet build /p:DefineConstants="TRACE;FEATURE_X;CI_BUILD"

This is useful in CI or special packaging workflows where a one-off symbol should not live permanently in the project file.

Be careful, though: passing DefineConstants on the command line typically replaces the value unless you explicitly include everything you still need.

Multi-Targeting and Target-Specific Symbols

In multi-targeted projects, you can also condition symbols on the target framework.

xml
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
  <DefineConstants>$(DefineConstants);NET8_ONLY</DefineConstants>
</PropertyGroup>

That can be useful when platform support or APIs differ across targets. It keeps target-specific code explicit and avoids overly complicated runtime checks for compile-time differences.

Keep Symbols Focused

Conditional compilation is powerful, but too many symbols can make a codebase hard to reason about. A good rule is to reserve symbols for things that genuinely differ at compile time, such as:

  • target framework differences
  • internal diagnostics
  • build-flavor-only code

If the choice should happen while the program runs, configuration files or feature flags are usually better tools than compilation symbols.

Common Pitfalls

The first pitfall is overwriting DefineConstants and accidentally removing existing symbols such as DEBUG or TRACE.

Another issue is using compilation symbols for behavior that should really be runtime-configurable. Once code is compiled out, the running application cannot switch it back on.

Developers also sometimes define symbols in one project and expect them to apply automatically to every project in a solution. Each project's build inputs still need to be configured intentionally.

Finally, excessive nested #if logic can make source files difficult to read and maintain. Use symbols sparingly and name them clearly.

Summary

  • In MSBuild, conditional compilation symbols are typically defined through DefineConstants.
  • Append to $(DefineConstants) when you want to preserve existing symbols.
  • Use MSBuild conditions to vary symbols by configuration or target framework.
  • Command-line definitions are useful for CI and special builds, but they often replace the existing value.
  • Treat compilation symbols as compile-time switches, not as substitutes for runtime configuration.

Course illustration
Course illustration

All Rights Reserved.