Skip to main content

Conditional compilation symbols in C#

A few months ago, I created a scalable data migration tool for a customer using Dataflow. I’ll promise I write a blog post about it when time permits.

The tool was originally created with some assumptions about your primary key strategy; in this case that an identity column was used. However this week I was asked if the tool could be used for another application. The only problem that for this application a sequential guid was used for primary key values.

I could make the full application configurable to handle multiple key strategies but as this was a one shot migration, I decided to use a different strategy and use conditional compilation symbols to handle this scenario.

Let me first explain what conditional compilation symbols are…

What Are Conditional Compilation Symbols?

Conditional compilation symbols in C# are essentially preprocessor directives that allow the compiler to include or omit portions of code based on certain conditions. These symbols are particularly useful when you need to control how different builds behave without changing the underlying codebase.

The common use cases include:

  • Debug vs Release builds
  • Platform-specific code (e.g., Windows vs macOS vs Linux)
  • Experimental features or beta testing
  • Configuration management (e.g., enabling/disabling logging)

You use these compilation symbols in combination with the following directives in C#:

  • #define and #undef: These allow you to define or undefine symbols within a file.
  • #if, #elif, #else, and #endif: These are used to conditionally compile code based on whether a symbol is defined or not.

Let’s take a quick look at a basic example:

Here, when the DEBUG symbol is defined, the first Console.WriteLine statement will be included in the build. If it’s not defined, the second statement for release mode will be included instead.

Defining Conditional Compilation Symbols

You can define symbols in several places, making them flexible for various scenarios:

  • In Code: As seen in the example above, you can define them directly in the code file using #define:
  • Project Properties: This can be done directly in the csproj file:

or through the Visual Studio UI: Right click on your project -> Properties –> Build –> General:


  • Command Line: When compiling with dotnet build or csc, you can pass symbols as command-line arguments using –p:DefineConstants:

dotnet build -p:DefineConstants="PLATTELANDSLOKET"

Each method offers flexibility depending on where and how you need to apply these symbols.

As I mentioned in the introduction I used the approach above to conditionally switch between the 2 primary key strategies. So you'll find multiple #if#else,#endif directives spread out through the codebase:

Some heuristics when using conditional compilation symbols

While conditional compilation is a powerful feature, it can also lead to code that is harder to maintain if overused. Here are some best practices to follow:

  • Minimize Use: Only use conditional compilation where absolutely necessary. It can complicate code readability and maintainability. The example I shared was a good use case especially as the impact was limited. 
  • Keep Code Modular: Use conditional compilation at a high level (e.g., method or class) rather than for individual statements or lines. This keeps the logic clean and manageable.
  • Document it: Ensure that your conditional symbols are well-documented, either in your code comments or your project's documentation, so team members know why certain code paths are being excluded or included. It introduces another level of magic so make sure that people are aware that these symbols are available and/or in use.

Happy coding!

More information

Preprocessor directives - C# reference | Microsoft Learn

Dataflow (Task Parallel Library) - .NET | Microsoft Learn

SQL Server identity column - Simple Talk (red-gate.com)

Popular posts from this blog

Podman– Command execution failed with exit code 125

After updating WSL on one of the developer machines, Podman failed to work. When we took a look through Podman Desktop, we noticed that Podman had stopped running and returned the following error message: Error: Command execution failed with exit code 125 Here are the steps we tried to fix the issue: We started by running podman info to get some extra details on what could be wrong: >podman info OS: windows/amd64 provider: wsl version: 5.3.1 Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM Error: unable to connect to Podman socket: failed to connect: dial tcp 127.0.0.1:2655: connectex: No connection could be made because the target machine actively refused it. That makes sense as the podman VM was not running. Let’s check the VM: >podman machine list NAME         ...

Cache stampede: when our cache turned against us

While investigating some performance issues, we ran into an ASP.NET Core API that cached a fairly expensive aggregation query for 60 seconds. Under normal load, that was fine: one request rebuilds the cache, everyone else reads from it. Under peak load, dozens of requests would arrive in that same expiry window, all see a cache miss, and all fire the same expensive query in parallel. The database didn't like that. That was the moment when our caching layer stopped helping and started hurting. A burst of requests comes in at the same time, all miss the cache, and all go hammer the database or the downstream API at once. That's a cache stampede . The cache was supposed to protect our backend, and for a few hundred milliseconds it did the opposite. Why this happens IMemoryCache.GetOrCreate (and its async sibling) looks like it protects you, but it doesn't add any locking on its own. Look at the naive version: public async Task<Report> GetReportAsync(string key) ...

A complex system designed from scratch never works

A few years ago, I worked as an architect on a big mainframe rewrite. I still count it as one of my failures. Not because the technology was wrong, but because I couldn't convince the management team to simplify the approach. Years later, the organization is still struggling to get the new system up and running. I left the project at the time, because I couldn't put my name behind an approach that would take very long and cost a lot of money without a working system to show for it along the way. Gall’s Law That memory keeps coming back to me, because it's a textbook case of Gall's Law playing out in real life. Gall's Law , from John Gall's Systemantics , states it plainly: A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works, and it cannot be patched to make it work. You have to start over with a simple system that works. What does that mean in practice,...