Conditional Attributes in .NET
31 December 2022

In the last couple of years, programming in .NET (.NET Core, Xamarin, MAUI) is multi-targeting different frameworks or platforms (NETCOREAPP, NET4, Windows, Android, iOS, . . .) and conditional compilation is used more than ever. By inertia, many of us who switched from C++ to C# continue to use preprocessor directives to control conditional compilation(#if - #endif). .NET is offering another approach that is in some cases a cleaner option.

Let’s define symbol LOAD_TEST to load more data for testing purposes. In this dummy example symbol is simply defined at the top of the file, but in the real life symbol would be defined in command-line options or environment variable. I intentionally didn’t want to use well known DEBUG symbol.

#define LOAD_TEST

using System.Diagnostics;

var calculator = new Calculator();

Console.WriteLine($"Pi: {calculator.Pi}");

public class Calculator
{
public string Pi = "3.1415926535";

public Calculator()
{
LoadTest1();

#if LOAD_TEST
LoadTest2();
#endif

Console.WriteLine("Creating Calculator");
}

[ConditionalAttribute("LOAD_TEST")]
void LoadTest1() // Must be void
{
Pi = "3.1415926535 8979323846 2643383279 5028841971 6939937510 5820974944";
}

void LoadTest2()
{
Pi = "3.1415926535 8979323846 2643383279 5028841971 6939937510 5820974944";
}
}

When symbol LOAD_TEST is defined, both functions will expand Pi with more digits. If symbol is not defined, the two approaches are similar but not quite the same.
Let’s observe ILSpy output below without LOAD_TEST defined.

// Calculator - This is output from ILSpy if symbol LOAD_TEST is not defined

public class Calculator
{
public string Pi = "3.1415926535";

public Calculator()
{
Console.WriteLine("Creating Calculator");
}

[Conditional("LOAD_TEST")]
private void LoadTest1()
{
Pi = "3.1415926535 8979323846 2643383279 5028841971 6939937510 5820974944";
}

private void LoadTest2()
{
Pi = "3.1415926535 8979323846 2643383279 5028841971 6939937510 5820974944";
}
}

Call to LoadTest1() is also compiled out even code itself is not polluted with #if - #endif like call to LoadTest2(). Using conditional attributes is sometimes a cleaner solution. Attribute can be applied to classes too and all usages of this class will be compiled out.

But there is a downside. Method guarded with conditional attribute must return void. Another downside of conditional attributes is that LoadTest1() is still present and compiled (only call is eliminated). If you are switching between frameworks like iOS/Android/Win in Maui app, method code must compile on all platforms and that is usually not the case. In this case better option is to use #if - #elif - #endif and make 3 different versions of the same method.