Author avatar

Dániel Szabó

Preprocessor Directives in C#

Dániel Szabó

  • Jan 16, 2020
  • 13 Min read
  • 153 Views
  • Jan 16, 2020
  • 13 Min read
  • 153 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
        }
    }
}
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.

1
Preprocessors are cool, running in Debug mode!
bash

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

1
Preprocessors 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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();
        }
    }
}
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.

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

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

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

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

1
2
3
Target is PROD!
DEBUG is DISABLED!
TRACE 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#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();
        }
    }
}
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.

1
Error	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.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;

namespace Pluralsight
{
#line 1 "Warning line.cs"
    class Program
    {
        static void Main(string[] args)
        {
            #warning Warning from different filename
            System.Console.ReadKey();
        }
    }
}
csharp

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
}
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;

namespace Pluralsight
{
    #pragma warning disable 414,CS3021  
    class Program
    {
        int i = 1;
        static void Main(string[] args)
        {
            System.Console.ReadKey();
        }
    }

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

1
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
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!

3