C# preprocessor directives
Although the compiler doesn't have a separate preprocessor, the directives described in this section are processed as if there were one. You use them to help in conditional compilation. Unlike C and C++ directives, you can't use these directives to create macros. A preprocessor directive must be the only instruction on a line.
Nullable context
The #nullable
preprocessor directive sets the nullable annotation context and nullable warning context. This directive controls whether nullable annotations have effect, and whether nullability warnings are given. Each context is either disabled or enabled.
Both contexts can be specified at the project level (outside of C# source code) adding the Nullable
element to the PropertyGroup
element. The #nullable
directive controls the annotation and warning contexts and takes precedence over the project-level settings. A directive sets the context(s) it controls until another directive overrides it, or until the end of the source file.
The effect of the directives is as follows:
#nullable disable
: Sets the nullable annotation and warning contexts to disabled.#nullable enable
: Sets the nullable annotation and warning contexts to enabled.#nullable restore
: Restores the nullable annotation and warning contexts to project settings.#nullable disable annotations
: Sets the nullable annotation context to disabled.#nullable enable annotations
: Sets the nullable annotation context to enabled.#nullable restore annotations
: Restores the nullable annotation context to project settings.#nullable disable warnings
: Sets the nullable warning context to disabled.#nullable enable warnings
: Sets the nullable warning context to enabled.#nullable restore warnings
: Restores the nullable warning context to project settings.
Conditional compilation
You use four preprocessor directives to control conditional compilation:
#if
: Opens a conditional compilation, where code is compiled only if the specified symbol is defined.#elif
: Closes the preceding conditional compilation and opens a new conditional compilation based on if the specified symbol is defined.#else
: Closes the preceding conditional compilation and opens a new conditional compilation if the previous specified symbol isn't defined.#endif
: Closes the preceding conditional compilation.
The C# compiler compiles the code between the #if
directive and #endif
directive only if the specified symbol is defined, or not defined when the !
not operator is used. Unlike C and C++, a numeric value to a symbol can't be assigned. The #if
statement in C# is Boolean and only tests whether the symbol has been defined or not. For example, the following code is compiled when DEBUG
is defined:
#if DEBUG
Console.WriteLine("Debug version");
#endif
The following code is compiled when MYTEST
is not defined:
#if !MYTEST
Console.WriteLine("MYTEST is not defined");
#endif
You can use the operators ==
(equality) and !=
(inequality) to test for the bool
values true
or false
. true
means the symbol is defined. The statement #if DEBUG
has the same meaning as #if (DEBUG == true)
. You can use the &&
(and), ||
(or), and !
(not) operators to evaluate whether multiple symbols have been defined. You can also group symbols and operators with parentheses.
The following is a complex directive that allows your code to take advantage of newer .NET features while remaining backward compatible. For example, imagine that you're using a NuGet package in your code, but the package only supports .NET 6 and up, as well as .NET Standard 2.0 and up:
#if (NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER)
Console.WriteLine("Using .NET 6+ or .NET Standard 2+ code.");
#else
Console.WriteLine("Using older code that doesn't support the above .NET versions.");
#endif
#if
, along with the #else
, #elif
, #endif
, #define
, and #undef
directives, lets you include or exclude code based on the existence of one or more symbols. Conditional compilation can be useful when compiling code for a debug build or when compiling for a specific configuration.
A conditional directive beginning with an #if
directive must explicitly be terminated with an #endif
directive. #define
lets you define a symbol. By using the symbol as the expression passed to the #if
directive, the expression evaluates to true
. You can also define a symbol with the DefineConstants compiler option. You can undefine a symbol with #undef
. The scope of a symbol created with #define
is the file in which it was defined. A symbol that you define with DefineConstants or with #define
doesn't conflict with a variable of the same name. That is, a variable name shouldn't be passed to a preprocessor directive, and a symbol can only be evaluated by a preprocessor directive.
#elif
lets you create a compound conditional directive. The #elif
expression will be evaluated if neither the preceding #if
nor any preceding, optional, #elif
directive expressions evaluate to true
. If an #elif
expression evaluates to true
, the compiler evaluates all the code between the #elif
and the next conditional directive. For example:
#define VC7
//...
#if DEBUG
Console.WriteLine("Debug build");
#elif VC7
Console.WriteLine("Visual Studio 7");
#endif
#else
lets you create a compound conditional directive, so that, if none of the expressions in the preceding #if
or (optional) #elif
directives evaluate to true
, the compiler will evaluate all code between #else
and the next #endif
. #endif
(#endif) must be the next preprocessor directive after #else
.
#endif
specifies the end of a conditional directive, which began with the #if
directive.
The build system is also aware of predefined preprocessor symbols representing different target frameworks in SDK-style projects. They're useful when creating applications that can target more than one .NET version.
Target Frameworks | Symbols | Additional symbols (available in .NET 5+ SDKs) |
Platform symbols (available only when you specify an OS-specific TFM) |
---|---|---|---|
.NET Framework | NETFRAMEWORK , NET481 , NET48 , NET472 , NET471 , NET47 , NET462 , NET461 , NET46 , NET452 , NET451 , NET45 , NET40 , NET35 , NET20 |
NET48_OR_GREATER , NET472_OR_GREATER , NET471_OR_GREATER , NET47_OR_GREATER , NET462_OR_GREATER , NET461_OR_GREATER , NET46_OR_GREATER , NET452_OR_GREATER , NET451_OR_GREATER , NET45_OR_GREATER , NET40_OR_GREATER , NET35_OR_GREATER , NET20_OR_GREATER |
|
.NET Standard | NETSTANDARD , NETSTANDARD2_1 , NETSTANDARD2_0 , NETSTANDARD1_6 , NETSTANDARD1_5 , NETSTANDARD1_4 , NETSTANDARD1_3 , NETSTANDARD1_2 , NETSTANDARD1_1 , NETSTANDARD1_0 |
NETSTANDARD2_1_OR_GREATER , NETSTANDARD2_0_OR_GREATER , NETSTANDARD1_6_OR_GREATER , NETSTANDARD1_5_OR_GREATER , NETSTANDARD1_4_OR_GREATER , NETSTANDARD1_3_OR_GREATER , NETSTANDARD1_2_OR_GREATER , NETSTANDARD1_1_OR_GREATER , NETSTANDARD1_0_OR_GREATER |
|
.NET 5+ (and .NET Core) | NET , NET9_0 , NET8_0 , NET7_0 , NET6_0 , NET5_0 , NETCOREAPP , NETCOREAPP3_1 , NETCOREAPP3_0 , NETCOREAPP2_2 , NETCOREAPP2_1 , NETCOREAPP2_0 , NETCOREAPP1_1 , NETCOREAPP1_0 |
NET8_0_OR_GREATER , NET7_0_OR_GREATER , NET6_0_OR_GREATER , NET5_0_OR_GREATER , NETCOREAPP3_1_OR_GREATER , NETCOREAPP3_0_OR_GREATER , NETCOREAPP2_2_OR_GREATER , NETCOREAPP2_1_OR_GREATER , NETCOREAPP2_0_OR_GREATER , NETCOREAPP1_1_OR_GREATER , NETCOREAPP1_0_OR_GREATER |
ANDROID , BROWSER , IOS , MACCATALYST , MACOS , TVOS , WINDOWS ,[OS][version] (for example IOS15_1 ),[OS][version]_OR_GREATER (for example IOS15_1_OR_GREATER ) |
Note
- Versionless symbols are defined regardless of the version you're targeting.
- Version-specific symbols are only defined for the version you're targeting.
- The
<framework>_OR_GREATER
symbols are defined for the version you're targeting and all earlier versions. For example, if you're targeting .NET Framework 2.0, the following symbols are defined:NET20
,NET20_OR_GREATER
,NET11_OR_GREATER
, andNET10_OR_GREATER
. - The
NETSTANDARD<x>_<y>_OR_GREATER
symbols are only defined for .NET Standard targets, and not for targets that implement .NET Standard, such as .NET Core and .NET Framework. - These are different from the target framework monikers (TFMs) used by the MSBuild
TargetFramework
property and NuGet.
Note
For traditional, non-SDK-style projects, you have to manually configure the conditional compilation symbols for the different target frameworks in Visual Studio via the project's properties pages.
Other predefined symbols include the DEBUG
and TRACE
constants. You can override the values set for the project using #define
. The DEBUG symbol, for example, is automatically set depending on your build configuration properties ("Debug" or "Release" mode).
The following example shows you how to define a MYTEST
symbol on a file and then test the values of the MYTEST
and DEBUG
symbols. The output of this example depends on whether you built the project on Debug or Release configuration mode.
#define MYTEST
using System;
public class MyClass
{
static void Main()
{
#if (DEBUG && !MYTEST)
Console.WriteLine("DEBUG is defined");
#elif (!DEBUG && MYTEST)
Console.WriteLine("MYTEST is defined");
#elif (DEBUG && MYTEST)
Console.WriteLine("DEBUG and MYTEST are defined");
#else
Console.WriteLine("DEBUG and MYTEST are not defined");
#endif
}
}
The following example shows you how to test for different target frameworks so you can use newer APIs when possible:
public class MyClass
{
static void Main()
{
#if NET40
WebClient _client = new WebClient();
#else
HttpClient _client = new HttpClient();
#endif
}
//...
}
Defining symbols
You use the following two preprocessor directives to define or undefine symbols for conditional compilation:
#define
: Define a symbol.#undef
: Undefine a symbol.
You use #define
to define a symbol. When you use the symbol as the expression that's passed to the #if
directive, the expression will evaluate to true
, as the following example shows:
#define VERBOSE
#if VERBOSE
Console.WriteLine("Verbose output version");
#endif
Note
In C#, primitive constants should be defined using the const
keyword. A const
declaration creates a static
member that can't be modified at runtime. The #define
directive can't be used to declare constant values as is typically done in C and C++. If you have several such constants, consider creating a separate "Constants" class to hold them.
Symbols can be used to specify conditions for compilation. You can test for the symbol with either #if
or #elif
. You can also use the ConditionalAttribute to perform conditional compilation. You can define a symbol, but you can't assign a value to a symbol. The #define
directive must appear in the file before you use any instructions that aren't also preprocessor directives. You can also define a symbol with the DefineConstants compiler option. You can undefine a symbol with #undef
.
Defining regions
You can define regions of code that can be collapsed in an outline using the following two preprocessor directives:
#region
: Start a region.#endregion
: End a region.
#region
lets you specify a block of code that you can expand or collapse when using the outlining feature of the code editor. In longer code files, it's convenient to collapse or hide one or more regions so that you can focus on the part of the file that you're currently working on. The following example shows how to define a region:
#region MyClass definition
public class MyClass
{
static void Main()
{
}
}
#endregion
A #region
block must be terminated with an #endregion
directive. A #region
block can't overlap with an #if
block. However, a #region
block can be nested in an #if
block, and an #if
block can be nested in a #region
block.
Error and warning information
You instruct the compiler to generate user-defined compiler errors and warnings, and control line information using the following directives:
#error
: Generate a compiler error with a specified message.#warning
: Generate a compiler warning, with a specific message.#line
: Change the line number printed with compiler messages.
#error
lets you generate a CS1029 user-defined error from a specific location in your code. For example:
#error Deprecated code in this method.
Note
The compiler treats #error version
in a special way and reports a compiler error, CS8304, with a message containing the used compiler and language versions.
#warning
lets you generate a CS1030 level one compiler warning from a specific location in your code. For example:
#warning Deprecated code in this method.
#line
lets you modify the compiler's line numbering and (optionally) the file name output for errors and warnings.
The following example shows how to report two warnings associated with line numbers. The #line 200
directive forces the next line's number to be 200 (although the default is #6), and until the next #line
directive, the filename will be reported as "Special". The #line default
directive returns the line numbering to its default numbering, which counts the lines that were renumbered by the previous directive.
class MainClass
{
static void Main()
{
#line 200 "Special"
int i;
int j;
#line default
char c;
float f;
#line hidden // numbering not affected
string s;
double d;
}
}
Compilation produces the following output:
Special(200,13): warning CS0168: The variable 'i' is declared but never used
Special(201,13): warning CS0168: The variable 'j' is declared but never used
MainClass.cs(9,14): warning CS0168: The variable 'c' is declared but never used
MainClass.cs(10,15): warning CS0168: The variable 'f' is declared but never used
MainClass.cs(12,16): warning CS0168: The variable 's' is declared but never used
MainClass.cs(13,16): warning CS0168: The variable 'd' is declared but never used
The #line
directive might be used in an automated, intermediate step in the build process. For example, if lines were removed from the original source code file, but you still wanted the compiler to generate output based on the original line numbering in the file, you could remove lines and then simulate the original line numbering with #line
.
The #line hidden
directive hides the successive lines from the debugger, such that when the developer steps through the code, any lines between a #line hidden
and the next #line
directive (assuming that it isn't another #line hidden
directive) will be stepped over. This option can also be used to allow ASP.NET to differentiate between user-defined and machine-generated code. Although ASP.NET is the primary consumer of this feature, it's likely that more source generators will make use of it.
A #line hidden
directive doesn't affect file names or line numbers in error reporting. That is, if the compiler finds an error in a hidden block, the compiler will report the current file name and line number of the error.
The #line filename
directive specifies the file name you want to appear in the compiler output. By default, the actual name of the source code file is used. The file name must be in double quotation marks ("") and must be preceded by a line number.
Beginning with C# 10, you can use a new form of the #line
directive:
#line (1, 1) - (5, 60) 10 "partial-class.cs"
/*34567*/int b = 0;
The components of this form are:
(1, 1)
: The start line and column for the first character on the line that follows the directive. In this example, the next line would be reported as line 1, column 1.(5, 60)
: The end line and column for the marked region.10
: The column offset for the#line
directive to take effect. In this example, the 10th column would be reported as column one. That's where the declarationint b = 0;
begins. This field is optional. If omitted, the directive takes effect on the first column."partial-class.cs"
: The name of the output file.
The preceding example would generate the following warning:
partial-class.cs(1,5,1,6): warning CS0219: The variable 'b' is assigned but its value is never used
After remapping, the variable, b
, is on the first line, at character six, of the file partial-class.cs
.
Domain-specific languages (DSLs) typically use this format to provide a better mapping from the source file to the generated C# output. The most common use of this extended #line
directive is to re-map warnings or errors that appear in a generated file to the original source. For example, consider this razor page:
@page "/"
Time: @DateTime.NowAndThen
The property DateTime.Now
was typed incorrectly as DateTime.NowAndThen
. The generated C# for this razor snippet looks like the following, in page.g.cs
:
_builder.Add("Time: ");
#line (2, 6) - (2, 27) 15 "page.razor"
_builder.Add(DateTime.NowAndThen);
The compiler output for the preceding snippet is:
page.razor(2, 2, 2, 27)error CS0117: 'DateTime' does not contain a definition for 'NowAndThen'
Line 2, column 6 in page.razor
is where the text @DateTime.NowAndThen
begins. That's noted by (2, 6)
in the directive. That span of @DateTime.NowAndThen
ends at line 2, column 27. That's noted by the (2, 27)
in the directive. The text for DateTime.NowAndThen
begins in column 15 of page.g.cs
. That's noted by the 15
in the directive. Putting all the arguments together, and the compiler reports the error in its location in page.razor
. The developer can navigate directly to the error in their source code, not the generated source.
To see more examples of this format, see the feature specification in the section on examples.
Pragmas
#pragma
gives the compiler special instructions for the compilation of the file in which it appears. The instructions must be supported by the compiler. In other words, you can't use #pragma
to create custom preprocessing instructions.
#pragma warning
: Enable or disable warnings.#pragma checksum
: Generate a checksum.
#pragma pragma-name pragma-arguments
Where pragma-name
is the name of a recognized pragma and pragma-arguments
is the pragma-specific arguments.
#pragma warning
#pragma warning
can enable or disable certain warnings.
#pragma warning disable warning-list
#pragma warning restore warning-list
Where warning-list
is a comma-separated list of warning numbers. The "CS" prefix is optional. When no warning numbers are specified, disable
disables all warnings and restore
enables all warnings.
Note
To find warning numbers in Visual Studio, build your project and then look for the warning numbers in the Output window.
The disable
takes effect beginning on the next line of the source file. The warning is restored on the line following the restore
. If there's no restore
in the file, the warnings are restored to their default state at the first line of any later files in the same compilation.
// pragma_warning.cs
using System;
#pragma warning disable 414, CS3021
[CLSCompliant(false)]
public class C
{
int i = 1;
static void Main()
{
}
}
#pragma warning restore CS3021
[CLSCompliant(false)] // CS3021
public class D
{
int i = 1;
public static void F()
{
}
}
#pragma checksum
Generates checksums for source files to aid with debugging ASP.NET pages.
#pragma checksum "filename" "{guid}" "checksum bytes"
Where "filename"
is the name of the file that requires monitoring for changes or updates, "{guid}"
is the Globally Unique Identifier (GUID) for the hash algorithm, and "checksum_bytes"
is the string of hexadecimal digits representing the bytes of the checksum. Must be an even number of hexadecimal digits. An odd number of digits results in a compile-time warning, and the directive is ignored.
The Visual Studio debugger uses a checksum to make sure that it always finds the right source. The compiler computes the checksum for a source file, and then emits the output to the program database (PDB) file. The debugger then uses the PDB to compare against the checksum that it computes for the source file.
This solution doesn't work for ASP.NET projects, because the computed checksum is for the generated source file, rather than the .aspx file. To address this problem, #pragma checksum
provides checksum support for ASP.NET pages.
When you create an ASP.NET project in Visual C#, the generated source file contains a checksum for the .aspx file, from which the source is generated. The compiler then writes this information into the PDB file.
If the compiler doesn't find a #pragma checksum
directive in the file, it computes the checksum and writes the value to the PDB file.
class TestClass
{
static int Main()
{
#pragma checksum "file.cs" "{406EA660-64CF-4C82-B6F0-42D48172A799}" "ab007f1d23d9" // New checksum
}
}