Skip to content

C# - Preprocessor Directives

Preprocessor directives are special instructions for the C# compiler that are processed before the actual compilation of your source code begins. They all start with a # symbol and must be the only instruction on a line. Unlike the preprocessor in C++, C# directives are not used for creating macros but are primarily used for conditional compilation, code organization, and influencing compiler warnings and errors.

Conditional Compilation: #if, #elif, #else, #endif

Section titled “Conditional Compilation: #if, #elif, #else, #endif”

Conditional directives allow you to include or exclude parts of your code from compilation based on whether certain symbols are defined. This is extremely useful for creating different build configurations, such as DEBUG and RELEASE.

Symbols can be defined at the top of a file using #define or, more commonly, in your project’s build settings (.csproj file). Visual Studio automatically defines DEBUG for Debug builds.

// To test this, you can define the symbol in your .csproj file or at the top of this file:
// #define FEATURE_PREMIUM
using System;
public class Program
{
public static void Main()
{
Console.WriteLine("Welcome to the application!");
#if FEATURE_PREMIUM
Console.WriteLine("Premium features are enabled.");
ConnectToPremiumService();
#elif DEBUG
Console.WriteLine("Running in DEBUG mode. No premium features.");
#else
Console.WriteLine("Running in RELEASE mode.");
#endif
Console.WriteLine("Application finished.");
}
#if FEATURE_PREMIUM
public static void ConnectToPremiumService()
{
// Logic for premium features would go here
Console.WriteLine("--> Connecting to premium backend...");
}
#endif
}

You can use the logical operators ==, !=, &&, and || to create more complex conditions.

The #define directive defines a symbol. Its scope is limited to the file in which it is defined. The #undef directive removes a symbol’s definition.

#define EXPERIMENTAL_FEATURE
#if EXPERIMENTAL_FEATURE
Console.WriteLine("Experimental code is included.");
#endif
#undef EXPERIMENTAL_FEATURE
#if EXPERIMENTAL_FEATURE
// This block will not be compiled because the symbol was undefined.
Console.WriteLine("This will never be printed.");
#endif

Best Practice: For project-wide symbols like DEBUG or RELEASE, it’s better to define them in the project settings (<DefineConstants> in the .csproj file) rather than using #define in every file.

The #region and #endregion directives allow you to define a collapsible block of code in IDEs like Visual Studio or JetBrains Rider. This can help organize long files into logical sections.

using System;
public class DataService
{
#region Public Properties
public string ConnectionString { get; set; }
public int Timeout { get; set; }
#endregion
#region Public Methods
public void Connect()
{
// ...
}
public void Disconnect()
{
// ...
}
#endregion
}

Caution: While #region can be useful, overusing it can be a sign that a class is too large and has too many responsibilities. It’s often better to refactor a large class into smaller, more focused classes than to hide its complexity inside regions.

Controlling Compiler Output: #warning and #error

Section titled “Controlling Compiler Output: #warning and #error”

These directives allow you to generate custom compiler warnings and errors from specific locations in your code.

  • #warning: Generates a CS1030 level one warning. The compilation will succeed.
  • #error: Generates a CS1029 error and stops the compilation immediately.
public class LegacyApiWrapper
{
public void OldMethod()
{
#warning "This method is obsolete and will be removed in the next version. Use NewMethod() instead."
// ...
}
public void CriticalMethod()
{
#if !NET6_0_OR_GREATER
#error "This method requires .NET 6 or newer to compile."
#endif
// ...
}
}

The #nullable directive is crucial for modern C# development as it sets the nullable annotation context and nullable warning context. This feature helps you write safer code by making you more explicit about whether a reference type can or cannot be null.

  • #nullable enable: Turns on the nullable reference type feature. The compiler will warn you if you assign null to a non-nullable variable (e.g., string) or if you dereference a potentially null variable (e.g., string?).
  • #nullable disable: Disables the feature, reverting to the pre-C# 8.0 behavior where any reference type could be null without warning.
  • #nullable restore: Restores the context to the project-level setting.
// It's best to enable this feature project-wide in your .csproj file:
// <Nullable>enable</Nullable>
#nullable enable // Activating nullable context for this code block
public class UserProfile
{
public string Name { get; set; } // Non-nullable: must not be null
public string? MiddleName { get; set; } // Nullable: can be null
public UserProfile(string name)
{
Name = name;
}
public void PrintNameLength()
{
Console.WriteLine($"Name length: {Name.Length}"); // Safe to access
// Console.WriteLine($"Middle name length: {MiddleName.Length}"); // Compiler WARNING: MiddleName may be null!
if (MiddleName != null)
{
Console.WriteLine($"Middle name length: {MiddleName.Length}"); // Safe after check
}
}
}

The #line directive modifies the line number and, optionally, the file name that the compiler reports in warnings and errors. This is primarily used by code-generation tools to map generated C# code back to the original source file (e.g., a Razor .cshtml file generating C#).