C# has pre-processing directives but no separate preprocessor. The compiler handles the
directives while it splits the file into tokens (C# specification clause 6.5). There are
no macros and no #include. A symbol is either defined or not, and has no
value.
Rules for every directive
- A directive starts with
#, which must be the first non-white-space character on its line. White space may separate the#from the name:# if DEBUGis valid. - A directive takes up a whole line and cannot be split across lines.
- A single-line comment (
//) may end a directive line. Delimited comments (/* */) are not allowed on it. - Names are case-sensitive.
- Directives are not tokens. They are separate from the grammar, so they may appear in the middle of a declaration or expression, on lines of their own.
| Directive | Purpose | Clause |
|---|---|---|
#define, #undef | Define and undefine symbols | 6.5.4 |
#if, #elif, #else, #endif | Conditional compilation | 6.5.5 |
#error, #warning | Diagnostics | 6.5.6 |
#region, #endregion | Regions for editors | 6.5.7 |
#line | Line numbers | 6.5.8 |
#nullable | Nullable context | 6.5.9 |
#pragma | Compiler instructions | 6.5.10 |
#!, #: | File-based apps; ignored by the compiler | none |
Defining symbols
#define identifier
#undef identifier
A conditional compilation symbol is defined or undefined. #define and
#undef must come before the first token of the file. They apply from that
point to the end of the file, so a symbol set in one file has no effect on another.
A symbol may be defined more than once, and it may be undefined when it was never
defined; neither is an error (clause 6.5.4).
The DefineConstants project property (-define option) defines
symbols for every file. In one file #undef still removes it. The symbols
true and false cannot be defined.
Conditional compilation
#if expression
code
#elif expression
code
#else
code
#endif
Each of these directives is a line of its own, and they nest. The compiler compiles the
section of the first condition that is true, or the #else section if none
is. All other sections are skipped: their text is not turned into tokens, but
the directives in them are still checked and nesting still counts, so an inner
#if needs its #endif even in a skipped section.
Expressions
A pre-processing expression (clause 6.5.3) tests symbols. It is not a C# expression and can use only these forms:
| Form | Meaning |
|---|---|
DEBUG | True if the symbol is defined |
true, false | Constants |
!x | Not |
x && y, x || y | And, or |
x == y, x != y | Equal, not equal, comparing the true or false value of each side |
( x ) | Grouping |
#define VERBOSE
#if DEBUG && !VERBOSE
Console.WriteLine("debug");
#elif (DEBUG == true) || VERBOSE
Console.WriteLine("verbose");
#else
Console.WriteLine("quiet");
#endif
Predefined symbols
The C# compiler defines no symbols by itself. The .NET SDK defines these for SDK-style projects, according to the target framework:
| Target | Symbols |
|---|---|
| Build configuration | DEBUG (Debug builds), TRACE |
| .NET Framework | NETFRAMEWORK, NET481, NET48, and so on, down to NET20 |
| .NET Standard | NETSTANDARD, NETSTANDARD2_1, NETSTANDARD2_0, and so on, down to NETSTANDARD1_0 |
| .NET 5 and later, and .NET Core | NET, NET10_0, NET9_0, and so on, then NETCOREAPP, NETCOREAPP3_1, and so on, down to NETCOREAPP1_0 |
| OS-specific target frameworks | ANDROID, BROWSER, IOS, MACCATALYST, MACOS, TVOS, WINDOWS, and versioned forms such as IOS15_1 |
Each versioned symbol also has an _OR_GREATER form, available in .NET 5
SDKs and later: NET8_0_OR_GREATER is defined when targeting .NET 8 or
any later .NET. The versionless symbols, such as NET, are defined for
every version.
Diagnostics
#error text
#warning text
These stop or warn the compilation at this point, with the text after the directive
name as the message. #error reports CS1029. #warning reports
CS1030. The text is not parsed, so it need not be quoted, and the whole line is the
message.
The compiler treats #error version specially. It reports CS8304, with a
message naming the compiler and language versions in use, which is the quickest way to
find out what a project is compiled with.
#if !NET8_0_OR_GREATER
#error This code needs .NET 8 or later.
#endif
#warning Remove this workaround before release.
Regions
#region [text]
#endregion [text]
#region and #endregion mark a block that editors can fold.
They have no effect on compiled code. They must be paired and may nest. A region may not
overlap an #if block, but one may be fully inside the other. Any text after
the directive is a name for the region.
#region Helpers
private static int Twice(int x) => x * 2;
#endregion
Line numbers
#line changes the line numbers and file names that the compiler reports.
Code generators use it to point error messages back at the original source.
| Form | Effect |
|---|---|
#line 200 | The next line is reported as line 200 |
#line 200 "Special.cs" | Also reports a different file name |
#line default | Returns to the real numbering and file name |
#line hidden | Hides the following lines from a debugger until the next #line; errors there are still reported with normal numbers |
#line (1, 1) - (5, 60) 10 "partial.cs" | Maps a span of the next line: start line and column, end line and column, an optional character offset, and a file name (C# 10) |
In the span form, the start and end are the line and column in the original file. The optional offset gives the column of the next line that maps to the first column of the span.
_builder.Add("Time: ");
#line (2, 6) - (2, 27) 15 "page.razor"
_builder.Add(DateTime.Now);
page.razorNullable context
#nullable (C# 8) sets the two flags of the
nullable context: the
annotation flag decides whether ? on a reference type has effect,
and the warning flag decides whether nullable warnings are given. A directive
applies from its line to the next directive or the end of the file. It overrides the
project’s Nullable setting.
| Directive | Annotations | Warnings |
|---|---|---|
#nullable enable | enabled | enabled |
#nullable disable | disabled | disabled |
#nullable restore | project setting | project setting |
#nullable enable annotations | enabled | unchanged |
#nullable disable annotations | disabled | unchanged |
#nullable restore annotations | project setting | unchanged |
#nullable enable warnings | unchanged | enabled |
#nullable disable warnings | unchanged | disabled |
#nullable restore warnings | unchanged | project setting |
The project property Nullable takes the values enable,
disable, warnings and annotations. If it is not
set, the default is disable. Files marked as generated (see
File structure) start
disabled regardless of the project, and can opt in with a directive.
Pragmas
#pragma pragma-name pragma-arguments
#pragma gives the compiler instructions for the file. The two pragmas
below are recognised; the compiler warns about any other.
#pragma warning
#pragma warning disable [warning-list]
#pragma warning restore [warning-list]
The list is comma-separated warning numbers, such as 414, CS3021; the
CS prefix is optional. With no list, the directive applies to all
warnings. disable takes effect from the next line and the warning is back
on the line after the restore. With no restore, the warning
returns at the first line of any later file in the same compilation.
#pragma warning disable 414, CS3021
[CLSCompliant(false)]
public class C { int i = 1; }
#pragma warning restore CS3021
#pragma warning disable format and #pragma warning restore
format stop and resume Visual Studio’s automatic code formatting for a block.
#pragma checksum
#pragma checksum "filename" "{guid}" "checksum-bytes"
This records a checksum for a source file in the PDB debug information, so that a
debugger can check that it has the right file. Tools that generate source, such as
ASP.NET for .aspx pages, use it for the original file. The GUID names the
hash algorithm, and the bytes are an even number of hexadecimal digits; an odd number
gives a warning and the directive is ignored.
File-based app directives
From the .NET 10 SDK, a single C# file can be built and run with no project file (see
File structure). The C#
compiler ignores any directive starting with #! or #:. The
SDK reads them, and they produce warnings in a normal project build.
| Directive | Purpose |
|---|---|
#!/usr/bin/env dotnet | Runs the file directly on Unix-like systems; must be the first line of the file |
#:package Name@1.2.3 | Adds a NuGet package reference; @* means the latest version, and the version may be left out only with central package management |
#:project path | References a project file, or a folder holding one |
#:property Name=Value | Sets an MSBuild property, such as PublishAot=false |
#:sdk Microsoft.NET.Sdk.Web | Chooses the project SDK; the default is Microsoft.NET.Sdk |
#:include path | Includes another file, with a literal path or a glob pattern (.NET 11 Preview 3 and .NET SDK 10.0.300 onwards) |