Conditional Compilation and Directives

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. The examples are complete console programs that start with Option Strict On; they were built with warnings treated as errors and run in the .NET 10 SDK container (Linux, arm64), once per configuration where the result depends on it. The multi-targeting example also builds a net481 assembly, which was compiled but not run, because .NET Framework does not run on Linux; that is labelled where it matters. Where an example is meant to fail, the compiler messages shown are the ones the SDK printed. Every console example has the root namespace ShelfApp, because some CLR and compiler messages include it.

Conditional compilation lets one source tree produce different assemblies: a Debug build with extra checks, a Release build without them, a net481 build that avoids APIs the old framework lacks, a build with an optional feature switched on. Visual Basic does it with a small set of directives, lines that start with # and that the compiler reads while it scans the file, before it analyses any statement. This page covers every directive the language has, the constants that drive them, how projects define constants, the Conditional attribute that works at the call site, and what is different from C#, whose directive set is larger. The attribute syntax is on Attributes, Caller Information and Reflection; the build properties are on Projects, MSBuild and the dotnet CLI. For the C# directive set, see Preprocessor Directives and Compilation (C#).

The directives at a glance

Directive What it does Scope of its effect

#If …​ #ElseIf …​ #Else …​ #End If

includes or excludes whole blocks of source according to conditional-compilation constants

the lines between the directives

#Const

defines a conditional-compilation constant inside a source file

that file only, from that line on

#Region …​ #End Region

names a collapsible block; no effect on the compiled code

editor outlining only

#ExternalSource …​ #End ExternalSource

tells the compiler to report the enclosed lines as coming from another file and line number

diagnostics, debug information and caller information

#ExternalChecksum

associates a checksum with an external file named in #ExternalSource

debug information

#Disable Warning / #Enable Warning

switches individual compiler or analyzer warnings off and on for a stretch of source (Visual Basic 14)

from the directive to the matching enable or the end of the file

That is the complete list: the compiler’s own diagnostic for a stray #define or #pragma names these six (see The C# directives that Visual Basic does not have). Directives are not statements. Each one must be alone on its line, in the first position of the logical line, with no other code before it. #If blocks may be placed anywhere that a statement, a declaration or a whole file-level construct may start, and they may nest. Their conditions are not Visual Basic expressions; they use a small separate language, described next.

Conditional blocks: #If, #ElseIf, #Else and #End If

The block syntax mirrors the If statement, with the directive keywords in front:

#If condition Then
    ' compiled when the condition is True
#ElseIf otherCondition Then
    ' compiled when the first is False and this is True
#Else
    ' compiled when none of the above is True
#End If

The first program uses the two constants that every Debug build has, DEBUG and TRACE. It was built and run twice, as dotnet run (the Debug configuration) and as dotnet run -c Release:

Option Strict On

Module Program
    Private Function BuildKind() As String
#If DEBUG Then
        Return "Debug"
#Else
        Return "Release"
#End If
    End Function

    Sub Main()
        Console.WriteLine("Shelf catalogue, " & BuildKind() & " build")
#If DEBUG Then
        Console.WriteLine("Diagnostics: extra checks are compiled in")
#End If
#If TRACE Then
        Console.WriteLine("Trace calls are compiled in")
#End If
    End Sub
End Module

Debug configuration:

Shelf catalogue, Debug build
Diagnostics: extra checks are compiled in
Trace calls are compiled in

Release configuration (-c Release):

Shelf catalogue, Release build
Trace calls are compiled in

The differences between the two runs are not decided when the program starts: the Release assembly does not contain the strings or the calls that were in the excluded blocks. That is the property that makes conditional compilation different from an If statement, and it is the subject of the section on Compile time or run time.

The rules of the block form

  • Every #If needs a #End If; every #ElseIf and #Else needs an open #If; #Else is last, and #ElseIf cannot follow it.

  • There is no single-line form. #If DEBUG Then Console.WriteLine("x") is not a one-line If; the compiler reads it as an #If block that never ends. Nothing else may share a line with a directive.

  • Then is accepted but, in the SDK tested here, was not required: #If DEBUG on its own compiled and selected the block. Write it anyway; the documented syntax has it, and the editor reformats to it.

  • The block contents must be complete logical statements or declarations. You cannot conditionally compile only the attribute list of a method, but you can conditionally declare the whole method, attributes included. Learn’s own example puts the WebMethod attribute inside each branch of a pair of complete declarations.

  • Excluded text is skipped, not compiled. The compiler looks in it for directives and for nothing else, so it may contain code that does not compile, names that do not exist, or text that is not Visual Basic at all.

  • #ElseIf is the only spelling documented. In the SDK tested here, #Else If was also accepted and behaved like #ElseIf. Do not rely on it.

  • Conditions are always evaluated with Option Compare Binary, whatever the file says.

The next two programs show three of these rules. The first file has Option Compare Text, which makes "COMMUNITY" = "community" true at run time, and the #If conditions still compare in binary mode. It also shows that a normal Const is invisible to the directives, and the first block that is skipped contains text that is not Visual Basic:

Option Strict On
Option Compare Text

#Const Edition = "Community"

Module Program
    Private Const Level As Integer = 3

    Sub Main()
        Console.WriteLine("Run time, Option Compare Text:    " & ("COMMUNITY" = "community"))
#If Edition = "community" Then
        Console.WriteLine("Compile time: case-insensitive match")
#Else
        Console.WriteLine("Compile time: always compared with Option Compare Binary, no match")
#End If
#If Level = 3 Then
        Console.WriteLine("A Const is visible to #If")
#Else
        Console.WriteLine("A Const is not visible to #If: Level is undefined there")
#End If
    End Sub
End Module
Run time, Option Compare Text:    True
Compile time: always compared with Option Compare Binary, no match
A Const is not visible to #If: Level is undefined there
Option Strict On

#Region "Domain"
Friend Class Book
    Friend ReadOnly Property Title As String

    Friend Sub New(title As String)
        Me.Title = title
    End Sub
End Class
#End Region

Module Program
#Region "Helpers"
    Private Function Shout(text As String) As String
#Region "Inside a method"
        Return text.ToUpperInvariant()
#End Region
    End Function

#Region "Nested"
    Private Function Quiet(text As String) As String
        Return text.ToLowerInvariant()
    End Function
#End Region
#End Region

    Sub Main()
#If False Then
        This text is not Visual Basic at all. It is skipped, so it is never parsed.
        Dim broken As = "unterminated
#ElseIf False Then
        More skipped text
#Else
        Console.WriteLine("The #Else branch is compiled")
#End If
        Dim book As New Book("Dune")
        Console.WriteLine(Shout(book.Title) & " / " & Quiet(book.Title))
    End Sub
End Module
The #Else branch is compiled
DUNE / dune

Source: Const directive

Errors in the block structure

Each group of errors is a separate failing program, because the first structural error can hide the rest. The compiler messages:

Mistake Message printed by the SDK

A missing #End If

BC30012: '#If' block must end with a matching '#End If'.

#ElseIf after #Else

BC32030: '#ElseIf' cannot follow '#Else' as part of a '#If' block.

#Else with no #If

BC30028: '#Else' must be preceded by a matching '#If' or '#ElseIf'. (and, because the #If of the program was missing, a second BC30012)

#End If with no #If

BC30013: '#ElseIf', '#Else', or '#End If' must be preceded by a matching '#If'.

A one-line #If

BC30012 on the line, plus BC30205: End of statement expected. at the code after Then

The complete list of messages, with their causes, is in the Error reference at the end of the page.

Conditional-compilation constants and #Const

A conditional-compilation constant is a name with a value that exists only for the compiler. It is not a variable and not a Const: it cannot be read from ordinary code, and an ordinary Const cannot be read from a directive (the last block of the program above printed that). A constant comes from one of three places: the SDK, the project file or the command line, or a #Const line in a source file. This section is about the third.

#Const name = expression

The expression is made of literals, other conditional constants and operators. Unlike C#, where a symbol is only defined or not defined, a Visual Basic constant has a value of type Boolean, a number, a String, a Date or Nothing. The program below defines constants of several types and tests each one. All the #Const lines but the last are at file level, before the module; the last one is inside Main, redefines Level, and the final test spells the name in lower case:

Option Strict On

#Const Verbose = True
#Const Level = 3
#Const Edition = "Community"
#Const Ratio = 1.5
#Const Stamp = #2026-10-19#
#Const Loud = Verbose And Level > 2
#Const Label = Edition & "-Edition"
#Const Pick = If(Level > 2, "high", "low")
#Const Bits = (Level << 2) Or 1
#Const Half = Level \ 2

Module Program
    Sub Main()
#If Verbose Then
        Console.WriteLine("Verbose is True")
#End If
#If Level >= 3 Then
        Console.WriteLine("Level is at least 3")
#End If
#If Level Then
        Console.WriteLine("An Integer constant is True when it is not zero")
#End If
#If Edition = "Community" Then
        Console.WriteLine("Edition is Community")
#End If
#If Ratio > 1 Then
        Console.WriteLine("Ratio is greater than 1")
#End If
#If Stamp > #2026-01-01# Then
        Console.WriteLine("Stamp is in 2026 or later")
#End If
#If Loud Then
        Console.WriteLine("Loud is True")
#End If
#If Label = "Community-Edition" Then
        Console.WriteLine("Label is Community-Edition")
#End If
#If Pick = "high" Then
        Console.WriteLine("Pick is high")
#End If
#If Bits = 13 Then
        Console.WriteLine("Bits is 13")
#End If
#If Half = 1 Then
        Console.WriteLine("Half is 1")
#End If
#If Missing Then
        Console.WriteLine("Missing is True")
#Else
        Console.WriteLine("Missing is not defined, so it is False")
#End If
#If Not Missing Then
        Console.WriteLine("Not Missing is True")
#End If
#Const Level = 7
#If Level = 7 Then
        Console.WriteLine("Level was redefined and is now 7")
#End If
#If level = 7 Then
        Console.WriteLine("Names are not case sensitive: level is 7")
#End If
    End Sub
End Module
Verbose is True
Level is at least 3
An Integer constant is True when it is not zero
Edition is Community
Ratio is greater than 1
Stamp is in 2026 or later
Loud is True
Label is Community-Edition
Pick is high
Bits is 13
Half is 1
Missing is not defined, so it is False
Not Missing is True
Level was redefined and is now 7
Names are not case sensitive: level is 7

Source: Const directive

What the run shows:

  • A constant that is used but never defined has the value Nothing, which a condition treats as False. A misspelled constant therefore silently removes a block instead of causing an error (see Pitfalls checklist).

  • #Const is evaluated in order. The earlier blocks saw Level = 3 and the last ones saw Level = 7, even though the redefinition is textually after the code that ran first. A constant used before it is defined is Nothing at that point.

  • Names are not case sensitive, as everywhere in Visual Basic: level and Level are the same constant, and so are debug and DEBUG.

  • An Integer or Double is True when it is not zero, so #If Level Then works; a String can only be compared, never used as a condition.

  • A #Const line can also appear inside a procedure; Learn says there are no strict placement rules. Put the lines at the top of the file, where they are visible.

What an expression may contain

The condition and the value of a #Const may use literals, constants and these operators. I tested each of them in a one-line #Const and a #If on SDK 10.0.402:

Allowed Examples Notes

Arithmetic

Level * 2.5, Level \ 2, Level Mod 2, Level ^ 2, -Level

Shifts

(Level << 2) Or 1, Level >> 1

Comparison

Level >= 3, Level <> 3, "a" < "b", #2026-10-19# > #2026-01-01#

strings and dates compare as in Option Compare Binary

Logical

Not, And, Or, Xor, AndAlso, OrElse

Concatenation of strings

Edition & "-Edition"

a String joined with an Integer (Edition & Level) is rejected, see below

If operator

If(Level > 2, "high", "low")

Conversion function

CInt(2.5), CBool(Level)

CStr is rejected

Literals

True, 3000000000, 1.5D, "a"c, #2026-10-19#, Nothing

And these are rejected; the program below contains one of each (an ordinary function call, a method call, Like, concatenating a number, and two bad comparisons):

Option Strict On

#Const Level = 3
#Const Edition = "Community"
#Const Joined = Edition & Level
#Const Length = Len(Edition)
#Const Text = Level.ToString()
#Const Matches = Edition Like "C*"

Module Program
    Sub Main()
#If Edition Then
        Console.WriteLine("A String cannot be used as a Boolean condition")
#End If
#If Level = "3" Then
        Console.WriteLine("A String cannot be compared with an Integer")
#End If
    End Sub
End Module
Program.vb(5,1): error BC30059: Constant expression is required.
Program.vb(6,20): error BC30205: End of statement expected.
Program.vb(7,20): error BC30205: End of statement expected.
Program.vb(8,18): error BC31427: Syntax error in conditional compilation expression.
Program.vb(12,1): error BC30060: Conversion from 'String' to 'Boolean' cannot occur in a constant expression.
Program.vb(15,1): error BC30060: Conversion from 'String' to 'Double' cannot occur in a constant expression.

BC30205 for Len(Edition) and Level.ToString() and BC31427 for Like show the limit: there are no function or method calls and no Like, TypeOf or Is in a conditional-compilation expression. BC30059 ("Constant expression is required") is what the compiler says when an operand cannot be folded to a constant, as with CStr(Level) or a string joined with an integer. The two BC30060 messages are the type-mismatch rejections: a String cannot act as a Boolean condition and cannot be compared with a number (the rule belongs to the directive language, not to Option Strict). If a decision needs real code, put it in an ordinary method and select the method call with #If.

A #Const belongs to one file

Learn: "Conditional compiler constants are always private to the file in which they appear." The two-file program shows it. Part1.vb defines OnlyHere; Part2.vb tests the same name:

Option Strict On

#Const OnlyHere = True

Friend Module Part1
    Friend Sub Report()
#If OnlyHere Then
        Console.WriteLine("Part1: OnlyHere is True")
#Else
        Console.WriteLine("Part1: OnlyHere is False")
#End If
    End Sub
End Module
Option Strict On

Module Part2
    Sub Main()
        Part1.Report()
#If OnlyHere Then
        Console.WriteLine("Part2: OnlyHere is True")
#Else
        Console.WriteLine("Part2: OnlyHere is False")
#End If
    End Sub
End Module
Part1: OnlyHere is True
Part2: OnlyHere is False

Source: Const directive

This is a design feature rather than a limitation: a file-level constant cannot affect other files by accident. Constants that must apply to the whole project belong in the project file, below.

A file-level constant overrides a project-level one

A constant defined with #Const replaces a constant of the same name from the project, but only for the rest of that file. The project below sets FEATURE_EXPORT=True in DefineConstants; the file turns it off, and also turns off DEBUG in a Debug build:

<DefineConstants>FEATURE_EXPORT=True</DefineConstants>
Option Strict On

#Const FEATURE_EXPORT = False
#Const DEBUG = False

Module Program
    Sub Main()
#If FEATURE_EXPORT Then
        Console.WriteLine("FEATURE_EXPORT is on in this file")
#Else
        Console.WriteLine("FEATURE_EXPORT is off in this file")
#End If
#If DEBUG Then
        Console.WriteLine("DEBUG is on in this file")
#Else
        Console.WriteLine("DEBUG is off in this file, although this is a Debug build")
#End If
    End Sub
End Module
FEATURE_EXPORT is off in this file
DEBUG is off in this file, although this is a Debug build

Source: Const directive

Redefining a standard constant this way works, and it has a wider effect than the #If blocks of the file: the Conditional attribute (below) uses the same constants at the call site. A file with #Const DEBUG = False that calls a <Conditional("DEBUG")> method in a Debug build had the call removed (the program printed only "Done"). Avoid redefining the standard constants; it makes one file disagree with the rest of the project.

The constants the SDK defines

Without writing a line, a project built with the .NET SDK has a set of constants. Some come from Visual Basic’s own targets file, some from the .NET SDK.

Constant Value in the tested projects Where it comes from and what it means

DEBUG

-1 (True) in a Debug build, undefined in Release

the SDK defines the upper-cased name of the build configuration (Configuration), so it is also RELEASE in Release. A custom configuration Staging defines STAGING. Switched off by DisableImplicitConfigurationDefines

TRACE

-1 in the Debug and Release configurations, absent in a custom configuration

the DefineTrace property; set by the SDK for the two standard configurations only

CONFIG

"Debug", "Release" or the custom name

the name of the configuration, as written (case preserved)

PLATFORM

"AnyCPU"

the Platform property, AnyCPU when empty

TARGET

"exe"

the kind of output; exe for the console project tested (the compiler’s -target values winexe, library and module are the other possible values; not tested here)

VBC_VER

17.13

the version of the Visual Basic compiler language, matching "Visual Basic 17.13"

_MyType

"Empty"

the MyType property; this is how the generated My code decides what to include (see The My Namespace)

NET, NETCOREAPP

True on .NET 5 and later (NETCOREAPP also on .NET Core 1.0 to 3.1)

target-framework symbols, see the next table

NET10_0, NET10_0_OR_GREATER

True on .NET 10

one exact-version symbol and one "this version or newer" symbol for every supported target

The DEBUG and TRACE constants are integers, not Booleans: -1 is the value of True when converted to a number. This is why #If DEBUG = -1 Then and #If DEBUG Then both work. The next program asks the compiler about the others. It cannot print the values directly (a conditional constant is not readable from code), so each #If sets an ordinary variable. It was run in both configurations:

Option Strict On

Module Program
    Private Sub Report(name As String, isDefined As Boolean)
        Console.WriteLine(name.PadRight(26) & If(isDefined, "yes", "no"))
    End Sub

    Sub Main()
        Dim vbcVer As Boolean = False
        Dim target As String = "?"
        Dim config As String = "?"
        Dim platform As String = "?"
        Dim myType As String = "?"
#If VBC_VER = 17.13 Then
        vbcVer = True
#End If
#If TARGET = "exe" Then
        target = "exe"
#End If
#If CONFIG = "Debug" Then
        config = "Debug"
#ElseIf CONFIG = "Release" Then
        config = "Release"
#End If
#If PLATFORM = "AnyCPU" Then
        platform = "AnyCPU"
#End If
#If _MyType = "Empty" Then
        myType = "Empty"
#End If
        Console.WriteLine("VBC_VER = 17.13           " & vbcVer)
        Console.WriteLine("TARGET                    " & target)
        Console.WriteLine("CONFIG                    " & config)
        Console.WriteLine("PLATFORM                  " & platform)
        Console.WriteLine("_MyType                   " & myType)
        Console.WriteLine()

        Dim isDebug As Boolean = False
        Dim isRelease As Boolean = False
        Dim isTrace As Boolean = False
        Dim isNet As Boolean = False
        Dim isCoreApp As Boolean = False
        Dim isNet10 As Boolean = False
        Dim isNet10Up As Boolean = False
        Dim isNet11Up As Boolean = False
        Dim isFramework As Boolean = False
#If DEBUG Then
        isDebug = True
#End If
#If RELEASE Then
        isRelease = True
#End If
#If TRACE Then
        isTrace = True
#End If
#If NET Then
        isNet = True
#End If
#If NETCOREAPP Then
        isCoreApp = True
#End If
#If NET10_0 Then
        isNet10 = True
#End If
#If NET10_0_OR_GREATER Then
        isNet10Up = True
#End If
#If NET11_0_OR_GREATER Then
        isNet11Up = True
#End If
#If NETFRAMEWORK Then
        isFramework = True
#End If
        Report("DEBUG", isDebug)
        Report("RELEASE", isRelease)
        Report("TRACE", isTrace)
        Report("NET", isNet)
        Report("NETCOREAPP", isCoreApp)
        Report("NET10_0", isNet10)
        Report("NET10_0_OR_GREATER", isNet10Up)
        Report("NET11_0_OR_GREATER", isNet11Up)
        Report("NETFRAMEWORK", isFramework)
    End Sub
End Module

Debug configuration:

VBC_VER = 17.13           True
TARGET                    exe
CONFIG                    Debug
PLATFORM                  AnyCPU
_MyType                   Empty

DEBUG                     yes
RELEASE                   no
TRACE                     yes
NET                       yes
NETCOREAPP                yes
NET10_0                   yes
NET10_0_OR_GREATER        yes
NET11_0_OR_GREATER        no
NETFRAMEWORK              no

Release configuration (-c Release):

VBC_VER = 17.13           True
TARGET                    exe
CONFIG                    Release
PLATFORM                  AnyCPU
_MyType                   Empty

DEBUG                     no
RELEASE                   yes
TRACE                     yes
NET                       yes
NETCOREAPP                yes
NET10_0                   yes
NET10_0_OR_GREATER        yes
NET11_0_OR_GREATER        no
NETFRAMEWORK              no

Three details are worth keeping from that output. CONFIG comes through with the configuration’s own spelling; RELEASE exists because the SDK defines it from the configuration name, as it does DEBUG; and TRACE is defined in Release as well, which is why Trace.WriteLine still works there and Debug.WriteLine does not.

Custom configurations

Any configuration name works with the .NET SDK, not just Debug and Release. Building the program below with dotnet build -c Staging defines STAGING and sets CONFIG to "Staging", but not DEBUG and not TRACE:

Option Strict On

Module Program
    Sub Main()
#If DEBUG Then
        Console.WriteLine("DEBUG")
#End If
#If DEBUG = -1 Then
        Console.WriteLine("DEBUG = -1")
#End If
#If STAGING Then
        Console.WriteLine("STAGING is defined")
#End If
#If CONFIG = "Staging" Then
        Console.WriteLine("CONFIG is Staging")
#End If
#If TRACE Then
        Console.WriteLine("TRACE")
#End If
    End Sub
End Module
STAGING is defined
CONFIG is Staging

Names with a dash, a period or a space are rewritten with an underscore before they become constants (Pre-Release gives PRE_RELEASE), according to the SDK targets. Setting DisableImplicitConfigurationDefines to true stops the SDK from adding the configuration constant at all.

Target-framework symbols

The SDK adds the symbols for the target framework of each build; with several target frameworks (Multi-targeting: net481 and net10.0) each target is compiled with its own set. I built a library once per target and listed which of a set of symbols were defined (the program in each case was a series of #If blocks that declare an unused variable, and the compiler warning BC42024 named the blocks it had compiled):

Target framework Symbols that were defined (from the list tested)

net481

NETFRAMEWORK, NET481, NET481_OR_GREATER, NET48_OR_GREATER, NET462_OR_GREATER, NET20_OR_GREATER

netstandard2.0

NETSTANDARD, NETSTANDARD2_0, NETSTANDARD2_0_OR_GREATER, NETSTANDARD1_0_OR_GREATER

net8.0

NET, NETCOREAPP, NET8_0, NET8_0_OR_GREATER, NET6_0_OR_GREATER, NET5_0_OR_GREATER, NETCOREAPP3_1_OR_GREATER, NETCOREAPP2_0_OR_GREATER, NETCOREAPP1_0_OR_GREATER

net10.0

NET, NETCOREAPP, NET10_0, NET10_0_OR_GREATER, NET8_0_OR_GREATER, NET6_0_OR_GREATER, NET5_0_OR_GREATER, NETCOREAPP3_1_OR_GREATER, NETCOREAPP2_0_OR_GREATER, NETCOREAPP1_0_OR_GREATER

net10.0-windows

as net10.0, plus WINDOWS, WINDOWS7_0 and WINDOWS7_0_OR_GREATER (built with EnableWindowsTargeting; compiled, not run)

Two consequences follow. A NET symbol is not a .NET Framework symbol: it means ".NET 5 or later" (NETFRAMEWORK is the old one). And a symbol such as NET8_0 is true only on exactly that target, which is why code should use the _OR_GREATER form for "this API exists". The symbols named WINDOWS and LINUX do not come from the operating system the build runs on: WINDOWS was defined for net10.0-windows and not for net10.0, net8.0, netstandard2.0 or net481 built on this Linux machine, and LINUX was not defined for any of them. Asking "what operating system is the program running on?" is a run-time question (OperatingSystem.IsWindows()), answered at the end of this page.

Defining constants in the project

Constants for every file in the project go in the project file with DefineConstants. The property’s format is the one place where the Visual Basic compiler differs most from the C# one, so it is worth stating exactly what I tested:

Item Visual Basic compiler Result tested

Separator

a comma

FEATURE_EXPORT=True,MAX_ITEMS=50 builds. With ; the build fails with BC31030 (...is not valid: End of statement expected for FEATURE_EXPORT=True;MAX_ITEMS=50)

Value

name=value where the value is a Visual Basic literal

MAX_ITEMS=50, FEATURE_EXPORT=True. A name alone, with no =, is taken to mean True

String value

the value must carry double quotes; in an MSBuild file write them as "

EDITION="Community" works. EDITION=\"Community\" (the command-line-compiler spelling) fails with BC31030 inside MSBuild

Appending to the SDK’s value

$(DefineConstants),MORE=1

works; the SDK does not seed DefineConstants with DEBUG or TRACE (the Visual Basic targets add them separately, see below), so the property can start empty

A semicolon in DefineConstants is wrong for Visual Basic although it is the C# separator: the value reaches the compiler as one string, which must be comma separated. The failing build prints:

vbc : error BC31030: Conditional compilation constant 'FEATURE_EXPORT=True ^^ ^^ ;MAX_ITEMS=50' is not valid: End of statement expected.

The working form, with three constants of three types, keeping everything the SDK adds:

<PropertyGroup>
  <DefineConstants>FEATURE_EXPORT=True,MAX_ITEMS=50,EDITION=&quot;Community&quot;</DefineConstants>
</PropertyGroup>
Option Strict On

Module Program
    Sub Main()
#If FEATURE_EXPORT Then
        Console.WriteLine("FEATURE_EXPORT is on")
#End If
#If MAX_ITEMS > 10 Then
        Console.WriteLine("MAX_ITEMS is greater than 10")
#End If
#If EDITION = "Community" Then
        Console.WriteLine("EDITION is Community")
#End If
#If DEBUG Then
        Console.WriteLine("DEBUG is defined")
#End If
#If TRACE Then
        Console.WriteLine("TRACE is defined")
#End If
#If NET10_0 Then
        Console.WriteLine("NET10_0 is defined")
#End If
    End Sub
End Module
FEATURE_EXPORT is on
MAX_ITEMS is greater than 10
EDITION is Community
DEBUG is defined
TRACE is defined
NET10_0 is defined

The output also shows that the constants the SDK defines (DEBUG, TRACE, NET10_0) are still there. They are not part of the DefineConstants property: the Visual Basic targets append CONFIG, DEBUG, TRACE, _MyType and PLATFORM to the value the compiler finally receives.

Constants that depend on the configuration

Because DefineConstants is an ordinary MSBuild property, a condition on Configuration (or on the target framework) selects the constants. The project below defines LEVEL=1 always and adds FAST_PATH and LEVEL=2 only in Release:

<PropertyGroup>
  <DefineConstants>$(DefineConstants),LEVEL=1</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <DefineConstants>$(DefineConstants),FAST_PATH=True,LEVEL=2</DefineConstants>
</PropertyGroup>
Option Strict On

Module Program
    Sub Main()
#If FAST_PATH Then
        Console.WriteLine("FAST_PATH is defined")
#Else
        Console.WriteLine("FAST_PATH is not defined")
#End If
#If LEVEL = 2 Then
        Console.WriteLine("LEVEL is 2")
#End If
    End Sub
End Module

Debug (dotnet run):

FAST_PATH is not defined

Release (dotnet run -c Release):

FAST_PATH is defined
LEVEL is 2

(The leading comma that appears when $(DefineConstants) is empty is harmless.) The same program can be used to test any constant you add.

DefineDebug, DefineTrace and the implicit configuration constant

DEBUG and TRACE are controlled by two Boolean properties, DefineDebug and DefineTrace. Turning DefineTrace off removes TRACE, but turning DefineDebug off does not remove DEBUG in a Debug build: the SDK then defines the upper-cased configuration name instead (DEBUG), so the symbol is back. To remove it you also have to disable the implicit configuration constant:

<PropertyGroup>
  <DefineDebug>false</DefineDebug>
  <DefineTrace>false</DefineTrace>
  <DisableImplicitConfigurationDefines>true</DisableImplicitConfigurationDefines>
</PropertyGroup>

I built the program that tests DEBUG, TRACE and NET10_0 three ways, with the properties above and different values:

Properties Constants defined (of DEBUG, TRACE, NET10_0)

DefineDebug=false and DefineTrace=false

DEBUG, NET10_0 (TRACE is gone; DEBUG returns through the configuration name)

the three properties above

NET10_0 only

DisableImplicitFrameworkDefines=true

DEBUG, TRACE (NET10_0 is gone; the other implicit framework symbols come from the same property)

Disabling the framework or configuration constants is rarely a good idea, because the libraries you reference are built against the same assumptions; it exists for build systems that supply their own.

From the command line

An MSBuild property can be set on the command line, and a property given there replaces the project’s. A comma in the value separates the constants, so the form tested here writes it as %2c:

dotnet run -p:DefineConstants=FEATURE_EXPORT=True%2cMAX_ITEMS=50

Run on the program above, the output was:

FEATURE_EXPORT is on
MAX_ITEMS is greater than 10
DEBUG is defined
TRACE is defined
NET10_0 is defined

The DEBUG, TRACE and NET10_0 constants survive, because they do not live in DefineConstants. The Visual Basic command-line compiler vbc takes the same list with -define (short form -d), comma separated, with strings quoted as \"text\"; since dotnet build is the normal route, that is rarely needed.

Scope summary (Learn’s table, restated with what the SDK adds):

How the constant is set Scope

DefineConstants in the project, or -p:DefineConstants=

every file in the project

SDK and targets (DEBUG, CONFIG, NET10_0_OR_GREATER, …​)

every file in the project

#Const in a source file

that file only, from that line on; replaces a project constant of the same name

Multi-targeting: net481 and net10.0

A library that must serve both .NET Framework and modern .NET is built for several target frameworks, and conditional compilation covers the places where the APIs differ. The project below targets net481 and net10.0; the folder Legacy is compiled only for net481, by removing it from the other targets:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net481;net10.0</TargetFrameworks>
    <RootNamespace>ShelfApp</RootNamespace>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>

  <ItemGroup Condition="'$(TargetFramework)' != 'net481'">
    <Compile Remove="Legacy\**" />
  </ItemGroup>
</Project>

The source selects an implementation by symbol. Convert.ToHexString exists from .NET 5; the older framework needs BitConverter. The NETFRAMEWORK branch also calls into the Legacy file that only the net481 build compiles:

Option Strict On

Namespace Legacy
    Friend Module Net481Support
        Friend Function Describe() As String
            Return "compiled from the Legacy folder (net481 only)"
        End Function
    End Module
End Namespace
Option Strict On

Module Program
    Function ToHex(data As Byte()) As String
#If NET5_0_OR_GREATER Then
        Return Convert.ToHexString(data)
#Else
        Return BitConverter.ToString(data).Replace("-", "")
#End If
    End Function

    Function RuntimeKind() As String
#If NETFRAMEWORK Then
        Return ".NET Framework (NETFRAMEWORK, " & NetVersionSymbol() & ")"
#ElseIf NET10_0_OR_GREATER Then
        Return ".NET 10 or later (NET10_0_OR_GREATER)"
#ElseIf NETCOREAPP Then
        Return "an older .NET (NETCOREAPP)"
#Else
        Return "something else"
#End If
    End Function

#If NETFRAMEWORK Then
    Function NetVersionSymbol() As String
#If NET481 Then
        Return "NET481"
#Else
        Return "another .NET Framework version"
#End If
    End Function
#End If

    Sub Main()
        Console.WriteLine("Running on " & RuntimeKind())
        Console.WriteLine("Hex: " & ToHex(New Byte() {1, 171, 255}))
#If NETFRAMEWORK Then
        Console.WriteLine(Legacy.Net481Support.Describe())
#End If
    End Sub
End Module

Run with dotnet run -f net10.0:

Running on .NET 10 or later (NET10_0_OR_GREATER)
Hex: 01ABFF

dotnet build compiled both targets with no warning, so the net481 assembly exists and is warning-free under TreatWarningsAsErrors; I could not run that assembly on Linux. To prove that the net481 build did take the NETFRAMEWORK and NET481 branches (and the .NET 10 build the NET10_0_OR_GREATER one), I added a deliberately unused local variable to each branch and built with warnings as non-fatal; the compiler reported BC42024 for the variable in the NET481 branch when building net481 and for the one in the NET10_0_OR_GREATER branch when building net10.0, and for no other branch. The Linux build of net481 works because the SDK restores the Microsoft.NETFramework.ReferenceAssemblies package, which needs one NuGet restore with network access the first time.

Guidelines that follow from those tests:

  • Test for the capability, not for the framework you remember. NET5_0_OR_GREATER for Convert.ToHexString says why the branch exists. A #If NETFRAMEWORK says who is running, which is less precise, and NET is true on .NET 5 and later only.

  • Keep the #If blocks small and put the difference behind a function. A public API that changes shape between targets is much harder to document and to test than one that is identical everywhere and calls a private ToHex.

  • Prefer separate files for large differences. The Compile Remove item in the project keeps the file out of the compilation for other targets, so neither the compiler nor the analyzers see it, and a refactoring tool still sees the whole file when it is open. A file-per-target convention (Legacy, Modern) is easier to review than interleaved blocks.

  • Build every target in CI. A branch that a developer’s usual target never compiles can rot. dotnet build with no -f builds all targets.

  • Reference packages conditionally the same way: an ItemGroup with Condition="'$(TargetFramework)' == 'net481'" adds a package or a reference for one target only.

#Region and #End Region

A region names a block of source that an editor can collapse. It has no effect on the compiled code or on name scope: a procedure declared inside a region is just a member of the type, and a variable declared in a region inside a method is an ordinary local of the method.

#Region "Name"
    ' any complete or partial declarations or statements
#End Region

The earlier program with the skipped block already contained regions at file level, in a module, inside a method and nested. A region can also cross an #If block, as in the next program, whose #Region starts inside a conditional block and ends outside it; the two directive kinds are processed independently:

Option Strict On

Module Program
    Sub Main()
#If DEBUG Then
#Region "Debug helpers"
        Console.WriteLine("Region opened inside an #If block")
#End If
        Console.WriteLine("Region still open after #End If")
#End Region
    End Sub
End Module
Region opened inside an #If block
Region still open after #End If

Crossing blocks like this is legal, but not a good idea: when the #If condition is False (the same program built with -c Release) the #Region line is skipped and the #End Region is left without a match, and the build fails with BC30680: '#End Region' must be preceded by a matching '#Region'. Keep each region entirely inside or entirely outside a conditional block. The error messages for broken regions:

Program.vb(6,8): error BC30217: String constant expected.
Program.vb(5,1): error BC30681: '#Region' statement must end with a matching '#End Region'.
Program.vb(6,1): error BC30680: '#End Region' must be preceded by a matching '#Region'.

(The first line is #Region without a name, the second a region that is never closed, the third an #End Region without a start.) Regions are a matter of taste: many teams treat a region as a sign that a class is too big, and prefer smaller classes, partial classes or extension methods (Extension Methods and Partial Methods) over hiding code in folds.

#ExternalSource and #ExternalChecksum

Tools that generate Visual Basic from another file (a template, a markup page, an XML description) need errors, debug locations and caller information to point at the original file rather than at the generated one. The directive does it:

#ExternalSource("filename", lineNumber)
    ' statements reported as coming from filename starting at lineNumber
#End ExternalSource

The program below puts statements in Generated under #ExternalSource("ShelfPage.template", 100). A CallerFilePath and CallerLineNumber helper (see Attributes, Caller Information and Reflection) reports the file and line the compiler recorded, and the stack trace of an exception thrown inside the block names the template:

Option Strict On

Imports System.IO
Imports System.Runtime.CompilerServices

Module Program
    Function Here(<CallerFilePath> Optional callerFile As String = "",
                  <CallerLineNumber> Optional callerLine As Integer = 0) As String
        Return Path.GetFileName(callerFile) & ":" & callerLine
    End Function

    Sub Generated()
#ExternalSource("ShelfPage.template", 100)
        Console.WriteLine("Mapped code      " & Here())
        Console.WriteLine("Mapped next line " & Here())
        Throw New InvalidOperationException("Failed in generated code")
#End ExternalSource
    End Sub

    Sub Main()
        Console.WriteLine("Normal code      " & Here())
        Try
            Generated()
        Catch ex As InvalidOperationException
            Console.WriteLine(ex.Message)
            Dim top As String = ex.StackTrace.Split(ControlChars.Lf)(0)
            Console.WriteLine(Path.GetFileName(top.Substring(top.IndexOf(" in ", StringComparison.Ordinal) + 4)))
        End Try
        Console.WriteLine("After the block  " & Here())
    End Sub
End Module
Normal code      Program.vb:21
Mapped code      ShelfPage.template:100
Mapped next line ShelfPage.template:101
Failed in generated code
ShelfPage.template:line 102
After the block  Program.vb:29

Line numbers inside the block count from the number in the directive: the first statement is line 100, the next is 101, the Throw is 102. Outside the block the numbers are the real ones of Program.vb. Debuggers and exception stack traces show the template and its line, as the line ShelfPage.template:line 102 in the output shows.

The block is not optional once opened: leaving out the #End ExternalSource gives

Program.vb(3,1): error BC30579: '#ExternalSource' statement must end with a matching '#End ExternalSource'.

#ExternalChecksum is the companion: it associates a name, a GUID that identifies the checksum algorithm, and a hex checksum with the external file so the debugger can verify that the source it finds matches. It is a generator’s directive, written once near the top of a generated file:

Option Strict On

#ExternalChecksum("Template.tt", "{406EA660-64CF-4C82-B6F0-42D48172A799}", "0123456789ABCDEF")

Module Program
    Sub Main()
#ExternalSource("Template.tt", 7)
        Console.WriteLine("Mapped to Template.tt")
#End ExternalSource
    End Sub
End Module
Mapped to Template.tt

Hand-written code does not need either directive; ASP.NET (Web, HTTP and Services) and code generators are the typical users.

#Disable Warning and #Enable Warning

Visual Basic 14 (Visual Studio 2015) added directives that switch a warning off for a stretch of source and back on. They are the Visual Basic equivalent of C#'s pragma warning. The syntax takes a comma-separated list of diagnostic IDs, or no ID, which means all warnings:

#Disable Warning BC42024           ' one warning
#Disable Warning BC42024, BC42105  ' several
#Disable Warning                   ' all warnings
#Enable Warning BC42024
#Enable Warning                    ' re-enable all

A test with eight variables and different directive forms was run with warnings not treated as errors. These were the results that matter:

  • #Disable Warning BC42024 suppresses the unused-variable warning on the lines that follow, up to the matching #Enable Warning BC42024 (or the end of the file).

  • A list (BC42024, BC42105) works. A bare #Disable Warning disabled the warning too; a bare #Enable Warning switched it back on.

  • The ID is not case sensitive (bc42024 was accepted), but it must start with the prefix: a bare number (#Disable Warning 42024) fails with BC30203 "Identifier expected".

  • An unknown ID (BC99999) was accepted without complaint, so a typo silently suppresses nothing.

  • The directives work for analyzer IDs too (CA2200 below).

A realistic use is to leave one deliberate construct in place and keep the rest of the build strict. The program below has TreatWarningsAsErrors on. Without the two pairs of directives it would fail to build, with BC42024 for spare and CA2200 for Throw ex:

Option Strict On

Module Program
    Sub Rethrow()
        Try
            Throw New InvalidOperationException("inner")
        Catch ex As InvalidOperationException
#Disable Warning CA2200 ' Rethrow to preserve stack details
            Throw ex
#Enable Warning CA2200
        End Try
    End Sub

    Sub Main()
#Disable Warning BC42024 ' Unused local variable
        Dim spare As Integer
#Enable Warning BC42024

        Try
            Rethrow()
        Catch ex As InvalidOperationException
            Console.WriteLine("Caught: " & ex.Message)
        End Try
    End Sub
End Module
Caught: inner

When a warning is not suppressed and warnings are errors, the compiler reports it as an error with the same ID. This program leaves one unused variable unsuppressed:

Option Strict On

Module Program
    Sub Main()
#Disable Warning BC42024
        Dim hidden As Integer
#Enable Warning BC42024
        Dim shown As Integer
        Console.WriteLine("Built")
    End Sub
End Module
Program.vb(8,13): error BC42024: Unused local variable: 'shown'.

There are four tools for the same job, with different reach:

Tool Reach Tested result

#Disable Warning / #Enable Warning

a region of one file

suppresses the lines in between (above)

<NoWarn>$(NoWarn);BC42024</NoWarn> in the project

the whole project

the same program without any directive builds and runs (note: the ID goes in the property with the prefix; the compiler accepted BC42024)

dotnet_diagnostic.BC42024.severity = none in .editorconfig

every file below the .editorconfig

built; also lets you raise a diagnostic: = error made BC42024 an error for both variables

<WarningsNotAsErrors>BC42024</WarningsNotAsErrors> with TreatWarningsAsErrors on

the project

the warning stays visible but does not fail the build: the program with a plain unused variable built and printed Built with two BC42024 warnings

Raising the severity in .editorconfig is the useful direction in a team: it turns a warning you care about into a build error, while the directives are for local exceptions. The .editorconfig and analyzer options are covered in XML Documentation, Analyzers and Code Style.

With dotnet_diagnostic.BC42024.severity = error and warnings not treated as errors, the program with one suppressed and one unsuppressed variable failed with:

Program.vb(6,13): error BC42024: Unused local variable: 'hidden'.
Program.vb(8,13): error BC42024: Unused local variable: 'shown'.

Both variables are reported. When the directives are present (the program with hidden and shown), only shown is reported, so a #Disable Warning directive still wins over the severity from .editorconfig:

Program.vb(8,13): error BC42024: Unused local variable: 'shown'.

The Conditional attribute and the Debug class

A directive removes source text. A Conditional attribute removes calls: a call to a method marked <Conditional("NAME")> is compiled only when NAME is defined at the call site. The method itself is always in the assembly, and the arguments of the call are not evaluated when the call is dropped. System.Diagnostics.Debug and Trace use the same mechanism, so Debug.WriteLine and Debug.Assert disappear from a Release build together with the cost of their arguments.

In the example, Tracing.vb declares two conditional methods, one on DEBUG and one on the custom constant AUDIT. Program.vb defines AUDIT with #Const, so its calls to Audit compile, and it also calls Debug.Assert with an argument that has a side effect:

Option Strict On

Imports System.Diagnostics

Module Tracing
    Friend Calls As Integer

    Friend Function Describe() As String
        Calls += 1
        Return "description #" & Calls
    End Function

    <Conditional("DEBUG")>
    Friend Sub DebugNote(message As String)
        Console.WriteLine("[debug] " & message)
    End Sub

    <Conditional("AUDIT")>
    Friend Sub Audit(message As String)
        Console.WriteLine("[audit] " & message)
    End Sub
End Module
Option Strict On

#Const AUDIT = True

Imports System.Diagnostics

Module Program
    Sub Main()
        DebugNote(Describe())
        Audit(Describe())
        Debug.Assert(Describe().Length > 0)
        Console.WriteLine("Describe was evaluated " & Calls & " time(s)")
    End Sub
End Module

Debug configuration, AUDIT defined in Program.vb:

[debug] description #1
[audit] description #2
Describe was evaluated 3 time(s)

Release configuration (-c Release), same source:

[audit] description #1
Describe was evaluated 1 time(s)

Debug configuration with the #Const AUDIT line removed from Program.vb:

[debug] description #1
Describe was evaluated 2 time(s)

Read the three counts: in Debug, Describe ran three times (the call to DebugNote, the call to Audit and the Debug.Assert argument); in Release only the Audit call survived, because the AUDIT constant is defined in Program.vb regardless of the configuration; and without AUDIT in Debug, Audit and its argument vanished. The rule has consequences:

  • The decision belongs to the file that makes the call, not the file that declares the method. The declaring file is irrelevant.

  • Arguments of a dropped call are not evaluated. Never put a side effect that matters (an increment, a write, an I/O call) in the argument of a conditional call or of Debug.Assert.

  • A conditional method must be a Sub. On a Function the attribute is ignored with the warning BC41007: Attribute 'Conditional' is only valid on 'Sub' declarations. (an error under TreatWarningsAsErrors), and the call is always made: a test with <Conditional("DEBUG")> Function Check() As Boolean printed True.

  • Debug members are conditional on DEBUG; Trace members on TRACE, which is defined in Release too.

Compile time or run time

An If statement and an #If directive look alike, but they run at different times and the second one changes what the compiler checks. With an ordinary constant, both branches must compile:

Option Strict On

Module Program
    Private Const AuditEnabled As Boolean = False

    Sub Main()
        If AuditEnabled Then
            Console.WriteLine(MissingAuditTrail)
        End If
#If AUDIT_ENABLED Then
        Console.WriteLine(MissingAuditTrail)
#End If
        Console.WriteLine("Done")
    End Sub
End Module
Program.vb(8,31): error BC30451: 'MissingAuditTrail' is not declared. It may be inaccessible due to its protection level.

The If branch is dead code, but it is type checked, so it fails; the excluded #If block with the same text did not. That is the main difference, and also the main danger of directives: code in an excluded block can rot unseen. The reverse choice also exists: for a condition that is only known at run time, a directive is the wrong tool, because the decision is already made when the program starts.

Option Strict On

Imports System.Diagnostics
Imports System.Runtime.InteropServices

Module Program
    Sub Main()
        ' Compile-time decisions: one branch is compiled into the assembly.
#If DEBUG Then
        Console.WriteLine("Compiled as a Debug build")
#Else
        Console.WriteLine("Compiled as a Release build")
#End If

        ' Run-time decisions: both branches are always compiled.
        Console.WriteLine("Debugger attached: " & Debugger.IsAttached)
        Console.WriteLine("Running on Linux: " & OperatingSystem.IsLinux())
        Console.WriteLine("Running on Windows: " & OperatingSystem.IsWindows())
        Console.WriteLine("Framework: " & RuntimeInformation.FrameworkDescription)
        Console.WriteLine("Is the assembly built for Debug? " & IsDebugAssembly())
    End Sub

    Private Function IsDebugAssembly() As Boolean
        Dim attribute As DebuggableAttribute =
            CType(Reflection.Assembly.GetExecutingAssembly().GetCustomAttributes(GetType(DebuggableAttribute), False)(0), DebuggableAttribute)
        Return attribute.IsJITOptimizerDisabled
    End Function
End Module

Debug build:

Compiled as a Debug build
Debugger attached: False
Running on Linux: True
Running on Windows: False
Framework: .NET 10.0.13
Is the assembly built for Debug? True

Release build (-c Release):

Compiled as a Release build
Debugger attached: False
Running on Linux: True
Running on Windows: False
Framework: .NET 10.0.13
Is the assembly built for Debug? False

The first line of each run is a compile-time decision, the other lines are run-time ones. Debugger.IsAttached and OperatingSystem.IsLinux() are evaluated by the running program, so a single assembly can ask them; the last line reads the DebuggableAttribute that the compiler wrote and shows the effect of the Debug configuration on the assembly, a separate setting (Optimize, DebugType) from the DEBUG constant. Platform differences that are only known at run time belong to those APIs; the [SupportedOSPlatform] attribute and the CA1416 analyzer help to keep them honest (see Native Interop: Declare and P/Invoke).

Choose by this rule of thumb:

The difference is…​ Use

a different API or type exists in different target frameworks

#If with NET..._OR_GREATER, small and behind a function

code that must not exist at all in the product (test hooks, debugging dumps)

#If DEBUG or <Conditional("DEBUG")>

a feature switch chosen at build time

a constant in DefineConstants, or a Condition on an item

something the user or the machine decides

an ordinary If and configuration

expensive diagnostics that should be switchable in production

Debug/Trace with listeners, or logging with levels (ILogger)

The C# directives that Visual Basic does not have

C# has the same idea with a bigger vocabulary. What Visual Basic lacks, and what it does instead:

C# directive Visual Basic What to do instead

#define, #undef

no directive

#Const for a file-level constant; DefineConstants for the project. A constant can be redefined but not "undefined", except by giving it False or Nothing

#pragma warning

#Disable Warning / #Enable Warning

almost the same; the IDs have the compiler prefix (BC)

#pragma checksum

#ExternalChecksum

generator use only

#nullable

none

Visual Basic does not have nullable reference types; see Nothing and Nullable Value Types

#line

#ExternalSource

maps lines to another file in a block; no #line hidden

#warning, #error

none

a compile-time message needs an analyzer or a build step (Message/Error tasks in MSBuild)

#region

#Region

same

#if A && B, ||, !

#If A And B, Or, Not (also AndAlso, OrElse)

C# symbols only have two states; Visual Basic constants carry values, so #If LEVEL >= 2 is legal

DefineConstants with ;

a comma

see the table above; strings need " in a project file

The directive keywords are not case sensitive (a test with #if debug then, #else and #end if indented under the code compiled and selected the Debug branch), unlike C#'s lower-case-only directives, and a directive may be indented. The compiler message when it meets a C# directive in a Visual Basic file lists the legal ones. A file containing #define FEATURE, #pragma warning disable, #nullable enable, #error stop, #warning careful, #line 5 and #undef FEATURE, one per line, gave seven times:

Program.vb(3,1): error BC30248: 'If', 'ElseIf', 'Else', 'Const', 'Region', 'ExternalSource', 'ExternalChecksum', 'Enable', 'Disable', 'End' or 'R' expected.

(one message for each of lines 3 to 9, all with the same text.)

Error reference

Every message below was printed by SDK 10.0.402 for one of the failing programs on this page (all in Option Strict On, warnings as errors off for the failing programs).

Code Message Cause

BC30012

'#If' block must end with a matching '#End If'.

missing #End If, or a one-line #If

BC30013

'#ElseIf', '#Else', or '#End If' must be preceded by a matching '#If'.

a stray #End If

BC30028

'#Else' must be preceded by a matching '#If' or '#ElseIf'.

a stray #Else

BC32030

'#ElseIf' cannot follow '#Else' as part of a '#If' block.

wrong order

BC30059

Constant expression is required.

an operand of #Const is not a constant (CStr(Level), Edition & Level)

BC30060

Conversion from 'String' to 'Boolean' cannot occur in a constant expression.

a String used as a condition or compared with a number

BC31427

Syntax error in conditional compilation expression.

Like, TypeOf and similar operators

BC30205

End of statement expected.

a function or method call in a constant expression

BC31030

Conditional compilation constant '...' is not valid: ...

a bad DefineConstants value: ; as separator, \" for a string

BC30217

String constant expected.

#Region with no name

BC30681

'#Region' statement must end with a matching '#End Region'.

unclosed region

BC30680

'#End Region' must be preceded by a matching '#Region'.

stray #End Region

BC30579

'#ExternalSource' statement must end with a matching '#End ExternalSource'.

unclosed external source

BC30248

'If', 'ElseIf', 'Else', 'Const', 'Region', 'ExternalSource', 'ExternalChecksum', 'Enable', 'Disable', 'End' or 'R' expected.

a C# directive such as #define

BC42024

Unused local variable: 'name'.

warning; an error with TreatWarningsAsErrors

BC30203

Identifier expected.

#Disable Warning 42024 (ID without the prefix)

BC30451

'MissingAuditTrail' is not declared. It may be inaccessible due to its protection level.

code in a branch that If keeps but a directive would have dropped

Pitfalls checklist

  • A misspelled constant is Nothing, so its #If block silently vanishes. After adding a constant, build once with it on and check that the code is in the assembly (a deliberate unused variable in the block gives a warning that proves it).

  • ; in DefineConstants fails in Visual Basic; use ,. A string constant needs " in the project file.

  • #Const is per file; two files that disagree about a constant compile different code. Put project-wide constants in the project.

  • DefineDebug=false does not remove DEBUG; NET does not mean .NET Framework; WINDOWS is a target-framework constant, not a build-machine one.

  • Arguments to Conditional methods and Debug.Assert are not evaluated when the call is dropped.

  • Excluded code is not compiled: build every configuration and target in CI so it cannot rot.

  • Keep #If blocks small; wrap the difference in a function instead of interleaving it with logic.

  • A region is documentation for an editor, not structure for the compiler. Do not let one cross an #If block.

  • Do not use #Disable Warning without an ID, and add a comment saying why. An unknown ID is accepted silently.

  • No conditional constant can be read at run time. If a program must report its build configuration, expose it through a function that is itself compiled conditionally, or read DebuggableAttribute as above.