Preprocessor Directives in C#
Jan 16, 2020 • 13 Minute Read
Introduction
In this guide you will learn how preprocessors work in C#. We will start by getting familiar with the concept of a preprocessor, then we will see how this concept fits into the language itself. Later we will look at the most common preprocessor directives and some examples in action.
The Preprocessor
This concept belongs to the realm of compiled languages. A compiled language is a language which takes high-level code, like C# or C, and with the help of a compiler translates it to machine code. The compiler is an abstraction layer between different architecture of manufacturers, and it knows how to translate blocks of code to different architectures like Intel or AMD processor instructions. The preprocessor itself does not generate the final machine code. As its name suggests, it is only preprocessing the code for the compiler. It contains directives which are evaluated in advance and have an impact on the compilation process. Preprocessors are usually treated as separate entities from compilers. Depending on the language, preprocessors range from fairly simple to more complex.
The prerpocessor found in C# is considered quite simple, providing fewer capabilities than, for example, the one found in the C programming language.
The workflow in C# that produces an executable application looks like this.
Preprocessor Directives
These directives must begin with the # symbol, and they do not contain the usual semicolon at the end of the line as they are not statements, so they are terminated by a new line.
Here is the list of preprocessors.
- #if
- #else
- #elif
- #endif
- #define
- #undef
- #warning
- #error
- #line
- #region
- #endregion
- #pragma
- #pragma warning
- #pragma checksum
Now we are going to take a look at what each of these directives mean. If you are familiar with bash scripting, or C programming, these concepts will be hauntingly familiar.
#if, #else, #elif, #endif
These are conditional directives. The #if directive is always enclosed by the #endif directive, and in between you can define different constructs. These can be conditional initialization of other components based on arguments, or basically anything that your application needs. You can further fine-tune your constructs with #else and #elif conditionals.
You are allowed to use the == or != operators to test only for bool values of true or false.
Let's see an example of this in action. The assumption from now on is that you have Visual Studio installed.
Begin with the following code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pluralsight
{
class Program
{
static void Main(string[] args)
{
#if DEBUG
System.Console.WriteLine("Preprocessors are cool, running in Debug mode!");
#else
System.Console.WriteLine("Preprocessors are still cool, even if you are not debugging!");
#endif
System.Console.ReadKey();
}
}
}
You have two options to build and run the solution: you either run it in RELEASE mode or DEBUG mode. By default the DEBUG mode is run, and this output is produced.
Preprocessors are cool, running in Debug mode!
If you switch this to run in RELEASE mode, you get the following output.
Preprocessors are still cool, even if you are not debugging!
Note how extra functionality can be added to our application and is dynamically picked up during preprocessing.
#define, #undef
These directives can be used to define and undefine directives for the preprocessors. The most common use case for this is when you would like to define extra conditions for the compilation itself. This is usually done in tandem with the #if directive to provide a more sophisticated compiling experience. If you do not want to litter your code with the #define directive, you have the option to use the -define argument to the compiler to do the same. This really depends on your application and situation. Both have their advantages and disadvantages.
Our task is to create a Tier-based build configuration with the help of preprocessors. We have three different tiers: PROD, TEST, and DEV. The rules say that we do NOT want TRACE and DEBUG enabled when building for PROD. We only want DEBUG enabled when building for TEST, and we want both DEBUG and TRACE enabled when building for DEV.
Here is our demonstration code for that.
#define DEV
#if PROD
#undef DEBUG
#undef TRACE
#endif
#if TEST
#undef TRACE
#endif
#if DEV
#define DEBUG
#define TRACE
#endif
using System;
namespace Pluralsight
{
class Program
{
static void Main(string[] args)
{
#if PROD
System.Console.WriteLine("Target is PROD!");
#elif TEST
System.Console.WriteLine("Target is TEST");
#elif DEV
System.Console.WriteLine("Target is DEV");
#endif
#if DEBUG
System.Console.WriteLine("DEBUG is ENABLED!");
#else
System.Console.WriteLine("DEBUG is DISABLED!");
#endif
#if TRACE
System.Console.WriteLine("TRACE is ENABLED!");
#else
System.Console.WriteLine("TRACE is DISABLED!");
#endif
System.Console.ReadKey();
}
}
}
There is a rule which says you cannot #define or #undef directives after the first token in a line. This only means you need to put all your directives regarding this before the rest of the code.
Upon executing the code as it is now with DEV settings, you will see the following output produced.
Target is DEV
DEBUG is ENABLED!
TRACE is ENABLED!
If you swap out the first line to #define TEST, you will see the following output.
Target is TEST
DEBUG is ENABLED!
TRACE is DISABLED!
Swapping out to #define PROD will produce the following output.
Target is PROD!
DEBUG is DISABLED!
TRACE is DISABLED!
This can be further improved by removing the first like and then simply passing the -define <Tier> as command line argument to the compiler.
#warning, #error
These directives allow you to issue either a warning or a terminating error message. When a warning is issued, it will only be present in the console logs, but the error level will break the compilation. This is extremely useful when you want to warn the user about outdated dependencies or prevent building of an incomplete solution which is terminated by your custom error message.
Let's modify our previous example so the compilation process breaks if the Tier is not defined or passed as a hardcoded parameter in our script!
#if PROD
#undef DEBUG
#undef TRACE
#endif
#if TEST
#undef TRACE
#endif
#if DEV
#define DEBUG
#define TRACE
#endif
#if !PROD && !TEST && !DEV
#error Cannot compile as the Tier is not specified
#else
#warning Running with hard coded Tier information
#endif
using System;
namespace Pluralsight
{
class Program
{
static void Main(string[] args)
{
#if PROD
System.Console.WriteLine("Target is PROD!");
#elif TEST
System.Console.WriteLine("Target is TEST");
#elif DEV
System.Console.WriteLine("Target is DEV");
#endif
#if DEBUG
System.Console.WriteLine("DEBUG is ENABLED!");
#else
System.Console.WriteLine("DEBUG is DISABLED!");
#endif
#if TRACE
System.Console.WriteLine("TRACE is ENABLED!");
#else
System.Console.WriteLine("TRACE is DISABLED!");
#endif
System.Console.ReadKey();
}
}
}
Note how the first #define <Tier> line was removed, and a conditional was introduced right above the using statement.
Simply running the above code will result in this customer error message, breaking our compilation.
Error CS1029 #error: 'Cannot compile as the Tier is not specified' Pluralsight C:\Users\dszabo\source\repos\Pluralsight\Pluralsight\Program.cs 15 Active
If we add the very first line back, we will receive the following warning.
Warning CS1030 #warning: 'Running with hard coded Tier information' Pluralsight C:\Users\dszabo\source\repos\Pluralsight\Pluralsight\Program.cs 17 Active
#line
This directive allows you to modify the compiler's line numbering and even the file name for errors and warnings. There are some situations when specific lines are removed from source code files, but you need the compiler to generate output based on the original line numbering, such as legacy and troubleshooting. This directive is very rarely used.
You have three types of arguments you can pass to this directive: default, hidden and filename. When you want the compiler to ignore the directive, you specify hidden.
Let's say we would like to modify the file name reported when a warning is generated.
using System;
namespace Pluralsight
{
#line 1 "Warning line.cs"
class Program
{
static void Main(string[] args)
{
#warning Warning from different filename
System.Console.ReadKey();
}
}
}
Executing this produces the following output, despite how you actually name your file.
Warning CS1030 #warning: 'Warning from different filename' Pluralsight C:\Users\dszabo\source\repos\Pluralsight\Pluralsight\Program.cs\Warning line.cs 5 Active
#region, #endregion
This directive is useful when you are working with Visual Studio Code Editor or Visual Studio itself. There is no special output from this directive. It allows you to mark specific regions, which will be expandable and collapsible by your editor of choice. In smaller projects, you rarely see developers using it, but on bigger projects allows you to group together parts of your code based on business logic or functionality, which comes in handy when you need to add stuff or troubleshoot something.
Let's look at the following example.
using System;
namespace Pluralsight
{
#region AnotherClass
class Another
{
private static void DoSomething()
{
System.Console.WriteLine("It's something!");
}
}
#endregion
#region MainStuff
class Program
{
static void Main(string[] args)
{
System.Console.ReadKey();
}
}
#endregion
}
Inserting the above code into one of the editors will give you the following look.
Note how the expand/collapse icon came to be marked by red arrows.
#pragma, #pragma warning, #pragma checksum
In simple words, the #pragma directive can be used to affect how the compile time reporting of warnings is handled and give special instructions to the compiler. There are two basic variants warning and checksum. When you are working with #pragma warning, you have the option to specify either disable or restore, followed either by a list of warnings that need to be acted upon or nothing, which assumes every type of warning. The checksum generates the checksum of source files, which help debugging ASP.NET pages.
For example, if you were to disable all warnings for a given app, you could define this pragma.
#pragma warning disable
Or you could also just disable specific warnings.
#pragma warning disable CS3020
If you want, you can enable specific warnings if they were disabled previously. You could also undo any disabled warnings with this line.
#pragma restore
In the below example, you will learn how to disable the " is assigned but its value is never used" warning.
using System;
namespace Pluralsight
{
#pragma warning disable 414,CS3021
class Program
{
int i = 1;
static void Main(string[] args)
{
System.Console.ReadKey();
}
}
}
Running the code does not produce warning messages, but if we were to remove the following line ...
#pragma warning disable 414, CS3021
... the following warning would pop up.
Warning CS0414 The field 'Program.i' is assigned but its value is never used Pluralsight C:\Users\dszabo\source\repos\Pluralsight\Pluralsight\Program.cs 8 Active
Conclusion
In this guide you have become familiar with all the preprocessor directives provided by the C# language. You have seen how preprocessor imbue compiled languages with extra functionality that helps customize build and compilation processes. Through basic examples, a use case was provided that demonstrated how these directives can be used, but how you use this functionality will depend on your current application and business needs. I hope this has been informative to you and I would like to thank you for reading it!