Author avatar

Dániel Szabó

Preprocessor Directives in C#

Dániel Szabó

  • Jan 16, 2020
  • 13 Min read
  • 4,552 Views
  • Jan 16, 2020
  • 13 Min read
  • 4,552 Views
Languages Frameworks and Tools
C#

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.

compilation

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.

  1. #if
  2. #else
  3. #elif
  4. #endif
  5. #define
  6. #undef
  7. #warning
  8. #error
  9. #line
  10. #region
  11. #endregion
  12. #pragma
  13. #pragma warning
  14. #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.

1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Text;
5using System.Threading.Tasks;
6
7namespace Pluralsight
8{
9    class Program
10    {
11        static void Main(string[] args)
12        {
13            #if DEBUG
14            System.Console.WriteLine("Preprocessors are cool, running in Debug mode!");
15            #else
16            System.Console.WriteLine("Preprocessors are still cool, even if you are not debugging!");
17            #endif
18            System.Console.ReadKey();
19        }
20    }
21}
csharp

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.

1Preprocessors are cool, running in Debug mode!
bash

If you switch this to run in RELEASE mode, you get the following output.

1Preprocessors are still cool, even if you are not debugging!
bash

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.

1#define DEV
2#if PROD
3#undef DEBUG
4#undef TRACE
5#endif
6#if TEST
7#undef TRACE
8#endif
9#if DEV
10#define DEBUG
11#define TRACE
12#endif
13
14using System;
15
16namespace Pluralsight
17{
18    class Program
19    {
20        static void Main(string[] args)
21        {
22            #if PROD
23            System.Console.WriteLine("Target is PROD!");          
24            #elif TEST
25            System.Console.WriteLine("Target is TEST");
26            #elif DEV
27            System.Console.WriteLine("Target is DEV");
28            #endif
29            #if DEBUG
30            System.Console.WriteLine("DEBUG is ENABLED!");
31            #else
32            System.Console.WriteLine("DEBUG is DISABLED!");
33            #endif
34            #if TRACE
35            System.Console.WriteLine("TRACE is ENABLED!");
36            #else
37            System.Console.WriteLine("TRACE is DISABLED!");
38            #endif
39            System.Console.ReadKey();
40        }
41    }
42}
csharp

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.

1Target is DEV
2DEBUG is ENABLED!
3TRACE is ENABLED!
bash

If you swap out the first line to #define TEST, you will see the following output.

1Target is TEST
2DEBUG is ENABLED!
3TRACE is DISABLED!
bash

Swapping out to #define PROD will produce the following output.

1Target is PROD!
2DEBUG is DISABLED!
3TRACE is DISABLED!
bash

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!

1#if PROD
2#undef DEBUG
3#undef TRACE
4#endif
5#if TEST
6#undef TRACE
7#endif
8#if DEV
9#define DEBUG
10#define TRACE
11#endif
12
13#if !PROD && !TEST && !DEV
14#error Cannot compile as the Tier is not specified
15#else
16#warning Running with hard coded Tier information
17#endif
18
19using System;
20
21namespace Pluralsight
22{
23    class Program
24    {
25        static void Main(string[] args)
26        {
27#if PROD
28            System.Console.WriteLine("Target is PROD!");          
29#elif TEST
30            System.Console.WriteLine("Target is TEST");
31#elif DEV
32            System.Console.WriteLine("Target is DEV");
33#endif
34#if DEBUG
35            System.Console.WriteLine("DEBUG is ENABLED!");
36#else
37            System.Console.WriteLine("DEBUG is DISABLED!");
38#endif
39#if TRACE
40            System.Console.WriteLine("TRACE is ENABLED!");
41#else
42            System.Console.WriteLine("TRACE is DISABLED!");
43#endif
44            System.Console.ReadKey();
45        }
46    }
47}
csharp

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.

1Error	CS1029	#error: 'Cannot compile as the Tier is not specified'	Pluralsight	C:\Users\dszabo\source\repos\Pluralsight\Pluralsight\Program.cs	15	Active
bash

If we add the very first line back, we will receive the following warning.

1Warning	CS1030	#warning: 'Running with hard coded Tier information'	Pluralsight	C:\Users\dszabo\source\repos\Pluralsight\Pluralsight\Program.cs	17	Active
bash

#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.

1using System;
2
3namespace Pluralsight
4{
5#line 1 "Warning line.cs"
6    class Program
7    {
8        static void Main(string[] args)
9        {
10            #warning Warning from different filename
11            System.Console.ReadKey();
12        }
13    }
14}
csharp

Executing this produces the following output, despite how you actually name your file.

1Warning	CS1030	#warning: 'Warning from different filename'	Pluralsight	C:\Users\dszabo\source\repos\Pluralsight\Pluralsight\Program.cs\Warning line.cs	5	Active
bash

#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.

1using System;
2
3namespace Pluralsight
4{
5    #region AnotherClass
6    class Another
7    {
8        private static void DoSomething()
9        {
10            System.Console.WriteLine("It's something!");
11        }
12    }
13    #endregion
14    #region MainStuff
15    class Program
16    {
17        static void Main(string[] args)
18        {
19            System.Console.ReadKey();
20        }
21    }
22    #endregion
23}
csharp

Inserting the above code into one of the editors will give you the following look.

region

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.

1#pragma warning disable
csharp

Or you could also just disable specific warnings.

1#pragma warning disable CS3020
csharp

If you want, you can enable specific warnings if they were disabled previously. You could also undo any disabled warnings with this line.

1#pragma restore
csharp

In the below example, you will learn how to disable the " is assigned but its value is never used" warning.

1using System;
2
3namespace Pluralsight
4{
5    #pragma warning disable 414,CS3021  
6    class Program
7    {
8        int i = 1;
9        static void Main(string[] args)
10        {
11            System.Console.ReadKey();
12        }
13    }
14
15}
csharp

Running the code does not produce warning messages, but if we were to remove the following line ...

1#pragma warning disable 414, CS3021
csharp

... the following warning would pop up.

1Warning	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
bash

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!