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.
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.
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.
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.
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}
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!
If you switch this to run in RELEASE
mode, you get the following output.
1Preprocessors 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.
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}
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!
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!
Swapping out to #define PROD
will produce the following output.
1Target is PROD!
2DEBUG is DISABLED!
3TRACE 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.
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}
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
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
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}
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
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}
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.
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
Or you could also just disable specific warnings.
1#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.
1#pragma restore
In the below example, you will learn how to disable the "
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}
Running the code does not produce warning messages, but if we were to remove the following line ...
1#pragma warning disable 414, CS3021
... 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
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!