Libraries, Packaging and Deployment

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. Examples marked "built and run" are complete projects that were built with warnings treated as errors (except where the text says otherwise) and run in the .NET 10 SDK container (Linux, arm64); the output shown is real output. The class library is called Shelf.Lib, it is the reusable part of the shelf library-catalogue sample, and the C#-compatibility checks use a C# console program that consumes it. Sizes quoted for publish modes are du -sk kilobytes on that container, for linux-arm64, and only the ratios are meaningful. Anything that needs Windows (Windows Forms publishing, ClickOnce, MSIX, running a Windows service) is labelled "built, not run" or "not run".

A library is the unit of reuse in .NET, and for Visual Basic it is also the safest way to work: the language has no ASP.NET Core, MAUI or Blazor templates, but a Visual Basic class library can be referenced by projects in any .NET language (see Web, HTTP and Services for a C# web host over a Visual Basic library). This page covers how to write a library that is pleasant to consume from Visual Basic and C#, how to target several frameworks, how to package it for NuGet, and how to ship an application with the right publish mode, including what Native AOT and trimming do and do not mean for a language with late binding. The build system itself (properties, the dotnet command line, vbc options) is on Projects, MSBuild and the dotnet CLI.

A class library

dotnet new classlib -lang VB creates the smallest library: an SDK-style project and a Class1.vb.

Restore succeeded.


Shelf.Core/Class1.vb
Shelf.Core/Shelf.Core.vbproj
Shelf.Core/obj/Shelf.Core.vbproj.nuget.dgspec.json
Shelf.Core/obj/Shelf.Core.vbproj.nuget.g.props
Shelf.Core/obj/Shelf.Core.vbproj.nuget.g.targets
Shelf.Core/obj/project.assets.json
Shelf.Core/obj/project.nuget.cache
---
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <RootNamespace>Shelf.Core</RootNamespace>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>

</Project>
---
Public Class Class1

End Class

(The listing shows the files the template wrote, the project and the class; obj/ is the restore output.) The project has no OutputType, which means Library, and a RootNamespace taken from the project name. Three names are easy to confuse:

Property Controls Default

RootNamespace

The namespace the compiler prefixes to every Namespace block and to types declared outside one. Visual Basic only; C# has no such concept.

The project name.

AssemblyName

The file name of the .dll and the assembly identity.

The project name.

PackageId

The NuGet package id. It is independent of the other two.

The assembly name.

The root namespace is the Visual Basic surprise for C# developers: with <RootNamespace>Shelf</RootNamespace>, the code Namespace Catalog ... End Namespace declares Shelf.Catalog, and a type outside any Namespace block is in Shelf. Writing the full name in the Namespace statement as well (Namespace Shelf.Catalog) declares Shelf.Shelf.Catalog. A class called Shelf inside Shelf.Catalog would read confusingly as Shelf.Catalog.Shelf (and Visual Basic names are case-insensitive), which is why the sample class is called Bookshelf.

Designing the public API for Visual Basic and C# consumers

A library written in Visual Basic is consumed by C# as ordinary IL, but several Visual Basic constructs show up differently. The sample library has three files that exercise them. The ISBN value type:

Built and run: Isbn.vb, an immutable value object with TryParse
Option Strict On

Imports System.Runtime.InteropServices

Namespace Catalog

    ''' <summary>A validated 13-digit ISBN.</summary>
    Public NotInheritable Class Isbn
        Implements IEquatable(Of Isbn)

        Private ReadOnly _digits As String

        Private Sub New(digits As String)
            _digits = digits
        End Sub

        ''' <summary>The 13 digits without separators.</summary>
        Public ReadOnly Property Digits As String
            Get
                Return _digits
            End Get
        End Property

        ''' <summary>Parses an ISBN-13, ignoring hyphens and spaces.</summary>
        ''' <param name="text">The text to parse.</param>
        ''' <param name="result">The ISBN when the text is valid; otherwise Nothing.</param>
        ''' <returns><c>True</c> when the text is a valid ISBN-13.</returns>
        Public Shared Function TryParse(text As String, <Out> ByRef result As Isbn) As Boolean
            result = Nothing
            If text Is Nothing Then Return False
            Dim digits As String = text.Replace("-", "").Replace(" ", "")
            If digits.Length <> 13 Then Return False
            Dim sum As Integer = 0
            For i As Integer = 0 To 12
                If Not Char.IsDigit(digits(i)) Then Return False
                sum += (AscW(digits(i)) - AscW("0"c)) * If(i Mod 2 = 0, 1, 3)
            Next
            If sum Mod 10 <> 0 Then Return False
            result = New Isbn(digits)
            Return True
        End Function

        Public Overloads Function Equals(other As Isbn) As Boolean Implements IEquatable(Of Isbn).Equals
            Return other IsNot Nothing AndAlso _digits = other._digits
        End Function

        Public Overrides Function Equals(obj As Object) As Boolean
            Return Equals(TryCast(obj, Isbn))
        End Function

        Public Overrides Function GetHashCode() As Integer
            Return _digits.GetHashCode()
        End Function

        Public Overrides Function ToString() As String
            Return _digits
        End Function
    End Class

End Namespace

The helpers module and a class with a default property and an event:

Built and run: Api.vb, a Module, ParamArray, Optional, a Default property and an event
Option Strict On

Namespace Catalog

    ''' <summary>Functions that C# callers see as <c>Helpers.Slugify(...)</c>.</summary>
    Public Module Helpers

        ''' <summary>Makes a URL-friendly slug.</summary>
        ''' <param name="title">The title.</param>
        ''' <param name="separator">The separator; a hyphen by default.</param>
        <System.Runtime.CompilerServices.Extension>
        Public Function Slugify(title As String, Optional separator As String = "-") As String
            Return String.Join(separator, title.ToLowerInvariant().Split(New Char() {" "c}, StringSplitOptions.RemoveEmptyEntries))
        End Function

        ''' <summary>Joins any number of tags.</summary>
        ''' <param name="tags">The tags.</param>
        Public Function JoinTags(ParamArray tags As String()) As String
            Return String.Join(",", tags)
        End Function

    End Module

    ''' <summary>A shelf holding titles, with a default indexer and an event.</summary>
    Public Class Bookshelf
        Private ReadOnly _titles As New List(Of String)

        ''' <summary>Raised after a title is added.</summary>
        Public Event TitleAdded As EventHandler(Of TitleAddedEventArgs)

        ''' <summary>Gets a title by position; C# sees this as the indexer.</summary>
        Default Public ReadOnly Property Item(index As Integer) As String
            Get
                Return _titles(index)
            End Get
        End Property

        ''' <summary>The number of titles.</summary>
        Public ReadOnly Property Count As Integer
            Get
                Return _titles.Count
            End Get
        End Property

        ''' <summary>Adds a title.</summary>
        ''' <param name="title">The title.</param>
        Public Sub Add(title As String)
            _titles.Add(title)
            RaiseEvent TitleAdded(Me, New TitleAddedEventArgs(title))
        End Sub

        Friend Function InternalCount() As Integer
            Return _titles.Count
        End Function
    End Class

    ''' <summary>Data for <see cref="Bookshelf.TitleAdded"/>.</summary>
    Public Class TitleAddedEventArgs
        Inherits EventArgs

        Public Sub New(title As String)
            Me.Title = title
        End Sub

        ''' <summary>The title that was added.</summary>
        Public ReadOnly Property Title As String
    End Class

End Namespace

and a build-info class that uses conditional compilation (explained under multi-targeting below):

Built and run: Runtime.vb
Option Strict On

Namespace Catalog

    ''' <summary>Reports which build of the library is running.</summary>
    Public NotInheritable Class BuildInfo
        Private Sub New()
        End Sub

        ''' <summary>The target framework this assembly was compiled for.</summary>
        Public Shared ReadOnly Property Target As String
            Get
#If NET10_0_OR_GREATER Then
                Return "net10.0 build"
#ElseIf NETFRAMEWORK Then
                Return ".NET Framework build"
#Else
                Return "other build"
#End If
            End Get
        End Property
    End Class

End Namespace

The library project, with the packaging metadata used later in this page:

The library project (Shelf.Lib.vbproj)
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>net481;net10.0</TargetFrameworks>
    <RootNamespace>Shelf</RootNamespace>
    <AssemblyName>Shelf.Lib</AssemblyName>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <NuGetAudit>false</NuGetAudit>

    <!-- NuGet metadata -->
    <PackageId>Irurueta.Shelf.Lib</PackageId>
    <VersionPrefix>1.2.0</VersionPrefix>
    <Authors>Shelf Team</Authors>
    <Description>ISBN validation and catalogue helpers for the shelf sample, written in Visual Basic.</Description>
    <PackageTags>isbn;catalogue;visual-basic</PackageTags>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageReadmeFile>README.md</PackageReadmeFile>
    <RepositoryUrl>https://example.invalid/shelf.git</RepositoryUrl>
    <RepositoryType>git</RepositoryType>
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  </PropertyGroup>

  <ItemGroup>
    <None Include="README.md" Pack="true" PackagePath="\" />
  </ItemGroup>

</Project>

A Visual Basic consumer and a C# consumer of the same library (installed from a local package source, see below) print the same things; the C# comments show what each Visual Basic construct became.

Built and run: the Visual Basic consumer
Option Strict On

Imports System.Reflection
Imports Shelf.Catalog

Module Program

    Sub Main()
        Dim isbn As Isbn = Nothing
        Console.WriteLine("valid:   " & Isbn.TryParse("978-0-306-40615-7", isbn).ToString() & " -> " & isbn.Digits)
        Console.WriteLine("invalid: " & Isbn.TryParse("978-0-306-40615-8", isbn).ToString() & " -> " & If(isbn Is Nothing, "Nothing", isbn.Digits))

        Dim shelf As New Bookshelf()
        AddHandler shelf.TitleAdded, Sub(sender, e) Console.WriteLine("event:   added " & e.Title)
        shelf.Add("Types and Tables")
        Console.WriteLine("indexer: " & shelf(0) & ", count " & shelf.Count)

        Console.WriteLine("slug:    " & "Types and Tables".Slugify() & " / " & "Types and Tables".Slugify("_"))
        Console.WriteLine("tags:    " & Helpers.JoinTags("vb", "dotnet", "nuget"))
        Console.WriteLine("build:   " & BuildInfo.Target)

        Dim library As Assembly = GetType(Isbn).Assembly
        Console.WriteLine("assembly:" & library.GetName().Version.ToString() & " | file " & library.GetCustomAttribute(Of AssemblyFileVersionAttribute)().Version & " | informational " & library.GetCustomAttribute(Of AssemblyInformationalVersionAttribute)().InformationalVersion)
    End Sub

End Module
valid:   True -> 9780306406157
invalid: False -> Nothing
event:   added Types and Tables
indexer: Types and Tables, count 1
slug:    types-and-tables / types_and_tables
tags:    vb,dotnet,nuget
build:   net10.0 build
assembly:1.2.0.0 | file 1.2.0.0 | informational 1.2.0
Built and run: the C# consumer of the same package
using Shelf.Catalog;

if (Isbn.TryParse("978-0-306-40615-7", out Isbn? isbn))   // the VB ByRef + <Out> parameter is an out parameter
    Console.WriteLine("valid:   " + isbn.Digits);

var shelf = new Bookshelf();
shelf.TitleAdded += (sender, e) => Console.WriteLine("event:   added " + e.Title);
shelf.Add("Types and Tables");
Console.WriteLine("indexer: " + shelf[0] + ", count " + shelf.Count);   // the VB Default property is the indexer

Console.WriteLine("slug:    " + "Types and Tables".Slugify() + " / " + Helpers.Slugify("Types and Tables", "_"));  // the Module is a static class
Console.WriteLine("tags:    " + Helpers.JoinTags("vb", "dotnet", "nuget"));   // ParamArray is params
Console.WriteLine("build:   " + BuildInfo.Target);
valid:   9780306406157
event:   added Types and Tables
indexer: Types and Tables, count 1
slug:    types-and-tables / types_and_tables
tags:    vb,dotnet,nuget
build:   net10.0 build

How each Visual Basic construct looks from C#:

In Visual Basic C# sees Design advice

Module Helpers with `Public Function`s

A static class Helpers (and, with <Extension>, extension methods)

A Module is fine for a library, but its members are also visible without the module name inside Visual Basic callers that import the namespace. Prefer a NotInheritable Class with a Private Sub New() and Shared members when the API is mainly for C# callers; the type is then an ordinary static-like class.

Default Public ReadOnly Property Item(index As Integer)

An indexer this[int]

A default property must have a parameter and there is at most one per type; the compiler records its name in DefaultMemberAttribute, which is what C# reads.

ParamArray tags As String()

params string[] tags

Fine. Only the last parameter can be a ParamArray.

Optional separator As String = "-"

An optional parameter with a default

The default value is compiled into the caller (see below): changing it later does not change callers that were not recompiled. The same rule holds in C#.

ByRef result As Isbn with <Out>

out Isbn? result; without <Out> it would be ref

Use <Out> from System.Runtime.InteropServices for the "Try" pattern so C# callers write out.

Event TitleAdded As EventHandler(Of TitleAddedEventArgs)

An event of that type

Use EventHandler(Of TArgs) and an EventArgs subclass; a RaiseEvent is the only way to raise it.

Friend

internal

Share internals with a test project through InternalsVisibleTo (below).

NotInheritable Class Isbn

A sealed class

Seal types that are not designed for inheritance; a small immutable value is a sealed class or a Structure.

Reference types and Nothing

Reference types without nullable annotations ("oblivious")

Visual Basic does not emit C# nullable-reference-type attributes, so C# callers get no warnings and no guarantees; document nullability in the XML comments.

Optional parameter defaults are baked into callers

A consumer compiled against version 1.2.0 of the library (default separator -) was run twice: once with that library, then with a rebuilt Shelf.Lib.dll whose default had become _, copied over the first without recompiling the consumer. The call "Types and Tables".Slugify() passes the separator the compiler saw:

Build succeeded.
Build succeeded.
--- consumer built against the old library (default "-"):
slug:    types-and-tables / types_and_tables
--- same consumer binary, library replaced by one whose default is "_":
slug:    types-and-tables / types_and_tables

(Both runs printed types-and-tables for the call that omitted the argument.) The consequence for library authors: treat a changed default as a breaking change, or give the parameter no default and add an overload. Overloads are also more reliable than Optional for C# callers and for reflection.

CLS compliance

Visual Basic is a CLS consumer and producer; <Assembly: CLSCompliant(True)> makes the compiler report public API that another .NET language might not be able to use, such as unsigned types:

Option Strict On

<Assembly: CLSCompliant(True)>

Public Class Totals
    Public Function Total() As Integer
        Return 1
    End Function

    Public Function total(value As Integer) As Integer
        Return value
    End Function

    Public Function Count(value As UInteger) As UInteger
        Return value
    End Function
End Class
Code.vb(14,21): warning BC40027: Return type of function 'Count' is not CLS-compliant.
Code.vb(14,27): warning BC40028: Type of parameter 'value' is not CLS-compliant.
Build succeeded.

The two warnings (BC40027 and BC40028) name the unsigned return and parameter types. Visual Basic has UInteger, ULong and UShort, but Visual Basic programs seldom use them in public signatures; a library meant for every .NET language should not either.

Friend assemblies

Friend members are invisible to other assemblies. To let a test project see them, add InternalsVisibleTo as an MSBuild item; the SDK writes the attribute into the generated assembly info for Visual Basic too:

<ItemGroup>
  <InternalsVisibleTo Include="Tests" />
</ItemGroup>

With the item present, a console program in the Tests assembly calls New Engine().Secret() on a Friend method and prints:

42

Without the item the same call fails to compile:

Program.vb(7,27): error BC30456: 'Secret' is not a member of 'Engine'.

Multi-targeting

A library that must serve both .NET Framework and modern .NET lists several target frameworks:

<TargetFrameworks>net481;net10.0</TargetFrameworks>

dotnet build then compiles the same sources once per framework and writes one output folder per target, and dotnet pack puts each into its own lib/<tfm>/ folder. The test library above builds both on Linux, because the SDK downloads the .NET Framework reference assemblies for net481 (it can compile for Framework anywhere; it cannot run the result there):

  Shelf.Lib -> Shelf.Lib/bin/Release/net481/Shelf.Lib.dll
  Shelf.Lib -> Shelf.Lib/bin/Release/net10.0/Shelf.Lib.dll
  Successfully created package 'nupkgs/Irurueta.Shelf.Lib.1.2.0.nupkg'.
  Successfully created package 'nupkgs/Irurueta.Shelf.Lib.1.2.0.snupkg'.

Code that differs per framework uses the preprocessor constants the SDK defines for each target (NET10_0_OR_GREATER, NETFRAMEWORK, NET481, …​): the BuildInfo.Target property above is "net10.0 build" when the net10.0 assembly is loaded and ".NET Framework build" for the other. The #If ... #ElseIf ... #Else ... #End If forms are the Visual Basic conditional compilation directives (see Conditional Compilation and Directives). Package references can also be conditional on the target:

<ItemGroup Condition="'$(TargetFramework)' == 'net481'">
  <PackageReference Include="System.Text.Json" Version="10.*" />
</ItemGroup>

netstandard2.0 and the Visual Basic runtime

netstandard2.0 is the target of choice for a library consumed by old and new runtimes, and it works for Visual Basic with one important exception: the functions of the Visual Basic runtime library (Left, Mid, Format, CInt conversions that call into the library, Strings, Interaction) are not available to it. A library that calls Left fails on the netstandard2.0 target:

Option Strict On

Public Module Text
    Public Function FirstThree(value As String) As String
        Return Left(value, 3)
    End Function
End Module
Code.vb(5,16): error BC30451: 'Left' is not declared. It may be inaccessible due to its protection level.

Embedding the runtime (<VBRuntime>Embed</VBRuntime>) did not help (the same BC30451), and adding the Microsoft.VisualBasic 10.3.0 package turned it into 'Left' is not a member of 'Strings':

Code.vb(5,16): error BC30456: 'Left' is not a member of 'Strings'.

The robust choice is to avoid the old functions in a netstandard2.0 library and use the .NET equivalents (value.Substring, String.Format, Convert), or to multi-target net481;net10.0 instead as above. Both netstandard2.0 and net10.0 outputs build when the library uses only .NET Base Class Library types.

XML documentation

<GenerateDocumentationFile>true</GenerateDocumentationFile> makes the compiler write Shelf.Lib.xml next to the assembly. Visual Basic doc comments start with three apostrophes ('''); typing them above a member in Visual Studio inserts the skeleton. The XML file is packed with the assembly into lib/<tfm>/, so consumers get IntelliSense (see the listing of the package below). The compiler checks the comments it can: a param name that does not exist and a cref that does not resolve are warnings.

Option Strict On

''' <summary>Documented type.</summary>
Public Class Documented
    Public Property Undocumented As Integer

    ''' <summary>Has a bad param.</summary>
    ''' <param name="wrong">Not a parameter.</param>
    Public Sub Run(value As Integer)
    End Sub

    ''' <summary>See <see cref="Missing"/>.</summary>
    Public Sub Other()
    End Sub
End Class
Code.vb(12,27): warning BC42309: XML comment has a tag with a 'cref' attribute 'Missing' that could not be resolved.
Code.vb(8,9): warning BC42307: XML comment parameter 'wrong' does not match a parameter on the corresponding 'sub' statement.
Build succeeded.

Two things follow. BC42307 and BC42309 are warnings, and TreatWarningsAsErrors makes them errors, which is what you want on a library. And the compiler did not warn that the public property Undocumented has no comment: unlike C# (warning CS1591), the Visual Basic compiler printed no "missing XML comment" warning in this test, so missing documentation needs an analyzer or a review rule. The conventions for comments and the analyzers that enforce style are on XML Documentation, Analyzers and Code Style.

Assembly attributes and versions

Old Visual Basic projects keep <Assembly: AssemblyVersion("1.0.0.0")> in My Project\AssemblyInfo.vb. SDK-style projects generate those attributes from project properties (Version, Authors, Description, Company, Product) into obj/.../L.AssemblyInfo.vb, so a migrated AssemblyInfo.vb produces duplicates:

Option Strict On

Imports System.Reflection

<Assembly: AssemblyVersion("1.0.0.0")>
<Assembly: AssemblyTitle("Shelf")>

Public Class Placeholder
End Class
obj/Debug/net10.0/L.AssemblyInfo.vb(20,2): error BC30663: Attribute 'AssemblyTitleAttribute' cannot be applied multiple times.
obj/Debug/net10.0/L.AssemblyInfo.vb(21,2): error BC30663: Attribute 'AssemblyVersionAttribute' cannot be applied multiple times.

The two fixes: delete the attributes from the file and set the properties in the project, or switch the generation off with <GenerateAssemblyInfo>false</GenerateAssemblyInfo> (the same file then builds: Build succeeded.). Prefer properties. The version properties map to three different values; the consumer printed them for the 1.2.0 package:

Value Property Printed

Assembly (identity, binding)

AssemblyVersion (default: Version with a fourth part 0)

1.2.0.0

File version (Windows Explorer)

FileVersion (default: same as assembly)

1.2.0.0

Informational (shown by NuGet, can carry a pre-release label and a commit hash)

InformationalVersion (default: Version, plus the source revision when Source Link provides it)

1.2.0

Use VersionPrefix for the number and VersionSuffix for the pre-release label so that the command line can add the label: dotnet pack --version-suffix beta.1 produced Irurueta.Shelf.Lib.1.2.0-beta.1.nupkg:

  Successfully created package 'nupkgs-beta/Irurueta.Shelf.Lib.1.2.0-beta.1.nupkg'.
  Successfully created package 'nupkgs-beta/Irurueta.Shelf.Lib.1.2.0-beta.1.snupkg'.

Strong-name signing is optional and rarely needed outside .NET Framework scenarios: the project properties are SignAssembly and AssemblyOriginatorKeyFile. Never commit the key.

NuGet packaging

dotnet pack builds the project and creates a .nupkg. The metadata is MSBuild properties in the project file, shown in the Shelf.Lib.vbproj above: PackageId, VersionPrefix, Authors, Description, PackageTags, PackageLicenseExpression (an SPDX identifier), PackageReadmeFile with the file added as a None item with Pack="true", RepositoryUrl, and IncludeSymbols plus SymbolPackageFormat for a .snupkg of portable PDBs. The package contents, listed with unzip -l (sizes in bytes):

481 _rels/.rels
751 Irurueta.Shelf.Lib.nuspec
26 README.md
6656 lib/net10.0/Shelf.Lib.dll
2344 lib/net10.0/Shelf.Lib.xml
9216 lib/net481/Shelf.Lib.dll
2344 lib/net481/Shelf.Lib.xml
582 [Content_Types].xml
708 package/services/metadata/core-properties/nuget.psmdcp

The symbols package:

481 _rels/.rels
664 Irurueta.Shelf.Lib.nuspec
12084 lib/net10.0/Shelf.Lib.pdb
3888 lib/net481/Shelf.Lib.pdb
459 [Content_Types].xml
708 package/services/metadata/core-properties/nuget.psmdcp

and the .nuspec that the SDK wrote:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
  <metadata>
    <id>Irurueta.Shelf.Lib</id>
    <version>1.2.0</version>
    <authors>Shelf Team</authors>
    <license type="expression">MIT</license>
    <licenseUrl>https://licenses.nuget.org/MIT</licenseUrl>
    <readme>README.md</readme>
    <description>ISBN validation and catalogue helpers for the shelf sample, written in Visual Basic.</description>
    <tags>isbn catalogue visual-basic</tags>
    <repository type="git" url="https://example.invalid/shelf.git" />
    <dependencies>
      <group targetFramework="net10.0" />
      <group targetFramework=".NETFramework4.8.1" />
    </dependencies>
  </metadata>
</package>

Observations: each target has its own lib/<tfm>/ folder holding the assembly and its XML documentation; the <dependencies> groups are empty because the library has no package references (the Visual Basic runtime for net10.0 is part of the shared framework, and for net481 part of the .NET Framework: a Visual Basic library does not add a dependency for it); the license is an expression, and licenseUrl is the compatibility link NuGet adds. The README.md is shown on the package page of nuget.org. Publishing is dotnet nuget push Irurueta.Shelf.Lib.1.2.0.nupkg --source <feed> --api-key <key> against a feed; it was not run here (it needs a feed and a key, and keys are never written into project files or scripts).

Using a package: a local feed, PackageReference and ProjectReference

The consumers above used a folder as the package source. A nuget.config next to the project (the single source /m/nupkgs was the mounted package folder):

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="local" value="/m/nupkgs" />
  </packageSources>
</configuration>

and <PackageReference Include="Irurueta.Shelf.Lib" Version="1.2.*" /> in the consumer project (or dotnet add package Irurueta.Shelf.Lib --version 1.2.0). The version 1.2.* is a floating range that resolved to 1.2.0. Two cautions: <clear /> removes nuget.org, so a restore that also needs public packages must add it back; and a nuget.config in a parent folder applies to every project below it, which cost the scratch build one failure (the clear-ed source made a runtime-pack restore fail) until the file was moved into the consumer folders.

Inside a solution the library is referenced as a project, not as a package:

dotnet add Shelf.Consumer reference Shelf.Lib
dotnet add Shelf.Consumer package Microsoft.Extensions.Logging

A ProjectReference rebuilds the library with the consumer and flows its target frameworks (the consumer picks the best match from net481;net10.0); a PackageReference consumes a binary. Version numbers for many projects are best kept in one place with central package management (Directory.Packages.props with ManagePackageVersionsCentrally set to true, and PackageReference items without a Version).

A command-line program as a dotnet tool

A console application can be packed as a .NET tool, installed with dotnet tool install and run by name. Two project properties do it: PackAsTool and ToolCommandName.

Built and run: a Visual Basic program packed as a dotnet tool
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>ShelfTool</RootNamespace>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <NuGetAudit>false</NuGetAudit>
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>shelf</ToolCommandName>
    <PackageId>Irurueta.Shelf.Tool</PackageId>
    <VersionPrefix>1.0.0</VersionPrefix>
    <Description>Command-line front end of the shelf sample.</Description>
  </PropertyGroup>
</Project>
Option Strict On

Module Program

    Function Main(args As String()) As Integer
        If args.Length = 0 Then
            Console.Error.WriteLine("usage: shelf <isbn>")
            Return 2
        End If
        Console.WriteLine("looking up " & args(0))
        Return 0
    End Function

End Module

Function Main(args As String()) As Integer is the entry point that returns the exit code. The tool was packed and installed to a local folder (dotnet tool install --tool-path toolbin --add-source toolpkg Irurueta.Shelf.Tool) and run:

  Successfully created package '/m/toolpkg/Irurueta.Shelf.Tool.1.0.0.nupkg'.
You can invoke the tool using the following command: shelf
Tool 'irurueta.shelf.tool' (version '1.0.0') was successfully installed.
looking up 9780306406157
exit 0
usage: shelf <isbn>
exit 2

Deploying applications

dotnet publish produces the files to give to users. The first decision is whether the .NET runtime travels with the app.

flowchart TD A["dotnet publish"] --> B{"Is the runtime installed on the target?"} B -->|"Yes, and shared"| C["Framework-dependent
small, needs the runtime"] B -->|"No or not guaranteed"| D["Self-contained
-r RID --self-contained"] D --> E{"One file wanted?"} E -->|"Yes"| F["PublishSingleFile
optional compression"] E -->|"No"| G["Folder of files"] D --> H{"Smaller or faster start?"} H -->|"Faster start"| I["PublishReadyToRun"] H -->|"Smaller"| J["PublishTrimmed
late binding at risk"] J --> K{"No late binding, no reflection by name?"} K -->|"Yes"| L["PublishAot
one native executable"] K -->|"No"| M["Stay with trimming or self-contained"]

The measurements below come from one small Visual Basic console program (Deploy), published eight ways. It prints the runtime description, then finds a class by name and calls a method through CallByName, which is Visual Basic late binding:

Built and run: the Deploy program (late-bound call by name)
Option Strict On

Imports System.Runtime.InteropServices

Public Class Magazine
    Public Property Title As String = "Shelf Monthly"
    Public Function Describe() As String
        Return "Magazine: " & Title
    End Function
End Class

Module Program

    Sub Main()
        Console.WriteLine("runtime:  " & RuntimeInformation.FrameworkDescription)
        Console.WriteLine("dynamic code supported: " & System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported.ToString())

        ' Late-bound call: the type is found by name and the member by string, so the
        ' trimmer and the Native AOT compiler cannot see that Magazine is used.
        Dim type As Type = System.Type.GetType("Deploy.Magazine")
        If type Is Nothing Then
            Console.WriteLine("late binding: type Deploy.Magazine was not found")
            Return
        End If
        Dim instance As Object = Activator.CreateInstance(type)
        Console.WriteLine("late binding: " & CStr(CallByName(instance, "Describe", CallType.Method)))
    End Sub

End Module
Mode (command line) Size (KB) Files Result when run

Framework-dependent: dotnet publish -c Release

104

5

Runs on the installed .NET 10 runtime: late binding: Magazine: Shelf Monthly.

Self-contained: -r linux-arm64 --self-contained

86,992

192

Runs without a runtime installed; same output.

Single file: ... -p:PublishSingleFile=true

92,844

2 (the executable and its .pdb)

Same output; larger because the files are packed uncompressed.

Single file with compression: ... -p:EnableCompressionInSingleFile=true

36,288

2

Same output; slower to start because the file is decompressed.

ReadyToRun: ... -p:PublishReadyToRun=true

86,992

192

Same output. Deploy.dll grew from 5,120 to 7,680 bytes (precompiled code); the framework was already ReadyToRun, so the folder total did not change for this tiny program.

Trimmed: ... -p:PublishTrimmed=true

22,632

34

Warning IL2026, and the program crashes: MissingMemberException.

Trimmed and single file

14,744

2

Same crash; the smallest of the self-contained forms.

Native AOT: -r linux-arm64 -p:PublishAot=true

a 3,963,480-byte executable (and a 6,360,496-byte .dbg file)

2

Warning IL2026 and IL3053; the same crash at run time.

Everything in the table can be reproduced with one project: the mode is chosen by dotnet publish arguments or by the same properties in the project file. PublishSingleFile, PublishReadyToRun, PublishTrimmed and PublishAot need a runtime identifier (-r), because they produce platform-specific output; a framework-dependent publish needs none and runs anywhere the runtime is installed.

For the first row, the published folder and the run were:

Deploy
Deploy.deps.json
Deploy.dll
Deploy.pdb
Deploy.runtimeconfig.json
runtime:  .NET 10.0.13
dynamic code supported: True
late binding: Magazine: Shelf Monthly

Trimming and late binding

The trimmer removes code that it cannot prove is used. A call written as a string (CallByName(instance, "Describe", CallType.Method), Type.GetType("Deploy.Magazine"), Activator.CreateInstance(type)) hides the target from it, and Visual Basic’s late binding is built on the same mechanism: late-bound member access on Object (possible only when Option Strict is switched off) is compiled into calls of the NewLateBinding runtime helpers, which the stack trace below shows underneath CallByName. Publishing with -p:PublishTrimmed=true reports it as a warning:

Program.vb(26,9): Trim analysis warning IL2026: Deploy.Program.Main(): Using member 'Microsoft.VisualBasic.CompilerServices.Versioned.CallByName(Object, String, CallType, Object[])' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The method name cannot and type cannot be statically analyzed so it may be trimmed.

With TreatWarningsAsErrors set, which the project file in this page does, the warning is an error and the publish fails:

Program.vb(26,9): Trim analysis error IL2026: Deploy.Program.Main(): Using member 'Microsoft.VisualBasic.CompilerServices.Versioned.CallByName(Object, String, CallType, Object[])' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The method name cannot and type cannot be statically analyzed so it may be trimmed.
/root/.nuget/packages/microsoft.net.illink.tasks/10.0.13/build/Microsoft.NET.ILLink.targets(103,5): error NETSDK1144: Optimizing assemblies for size failed. [Deploy.vbproj]

Setting -p:TreatWarningsAsErrors=false lets the publish finish, and the program then fails where the trimmer removed the method:

runtime:  .NET 10.0.13
dynamic code supported: True
Unhandled exception. System.MissingMemberException: Public member 'Describe' on type 'Magazine' not found.
   at Microsoft.VisualBasic.CompilerServices.Symbols.Container.GetMembers(String& memberName, Boolean reportErrors)
   at Microsoft.VisualBasic.CompilerServices.NewLateBinding.CallMethod(Container baseReference, String methodName, Object[] arguments, String[] argumentNames, Type[] typeArguments, Boolean[] copyBack, BindingFlags invocationFlags, Boolean reportErrors, ResolutionFailure& failure)
   at Microsoft.VisualBasic.CompilerServices.NewLateBinding.ObjectLateCall(Object instance, Type type, String memberName, Object[] arguments, String[] argumentNames, Type[] typeArguments, Boolean[] copyBack, Boolean ignoreReturn)
   at Microsoft.VisualBasic.CompilerServices.NewLateBinding.LateCall(Object Instance, Type Type, String MemberName, Object[] Arguments, String[] ArgumentNames, Type[] TypeArguments, Boolean[] CopyBack, Boolean IgnoreReturn)
   at Microsoft.VisualBasic.CompilerServices.Versioned.CallByName(Object Instance, String MethodName, CallType UseCallType, Object[] Arguments)
   at Deploy.Program.Main() in /m/Deploy/Program.vb:line 26

Two repairs are shown. The first replaces the string call with a static one: the type is named in code (GetType(Magazine)) and the method is reached through an interface, so the trimmer sees every use and the program publishes without warnings.

Built and run: DeployFix, the trim-safe version
Option Strict On

Imports System.Runtime.InteropServices

Public Interface IDescribable
    Function Describe() As String
End Interface

Public Class Magazine
    Implements IDescribable

    Public Property Title As String = "Shelf Monthly"

    Public Function Describe() As String Implements IDescribable.Describe
        Return "Magazine: " & Title
    End Function
End Class

Module Program

    Sub Main()
        Console.WriteLine("runtime:  " & RuntimeInformation.FrameworkDescription)
        ' The type is named in the code, so the trimmer keeps it; the call goes through an interface.
        Dim item As IDescribable = CType(Activator.CreateInstance(GetType(Magazine)), IDescribable)
        Console.WriteLine("static:   " & item.Describe())
    End Sub

End Module
  DeployFix -> bin/Release/net10.0/linux-arm64/Deploy.dll
  DeployFix -> /m/out/DeployFix/
size 21852
runtime:  .NET 10.0.13
static:   Magazine: Shelf Monthly

The second keeps the late call and tells the trimmer what it needs with DynamicDependency, suppressing the analysis warning on purpose with the justification written down:

Built and run: DeployDyn, late binding preserved with DynamicDependency
Option Strict On

Imports System.Diagnostics.CodeAnalysis
Imports System.Runtime.InteropServices

Public Class Magazine
    Public Property Title As String = "Shelf Monthly"
    Public Function Describe() As String
        Return "Magazine: " & Title
    End Function
End Class

Module Program

    ' Tells the trimmer to keep the public methods of Magazine because strings reach them at run time.
    <DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods Or DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, GetType(Magazine))>
    <UnconditionalSuppressMessage("Trimming", "IL2026", Justification:="Magazine members are preserved by DynamicDependency.")>
    Sub Main()
        Console.WriteLine("runtime:  " & RuntimeInformation.FrameworkDescription)
        Dim instance As Object = Activator.CreateInstance(GetType(Magazine))
        Console.WriteLine("late binding: " & CStr(CallByName(instance, "Describe", CallType.Method)))
    End Sub

End Module
  DeployDyn -> bin/Release/net10.0/linux-arm64/Deploy.dll
  DeployDyn -> /m/out/DeployDyn/
size 22632
runtime:  .NET 10.0.13
late binding: Magazine: Shelf Monthly

Trimming is not available to every Visual Basic application. The documentation lists WPF and Windows Forms as not trim-compatible, and the SDK enforces it for Windows Forms: publishing the Visual Basic Windows Forms project below with -p:PublishTrimmed=true ended with

error NETSDK1175: Windows Forms is not supported or recommended with trimming enabled. Please go to https://aka.ms/dotnet-illink/windows-forms for more details.

The same project published as framework-dependent and as self-contained single-file for win-x64 (cross-published on Linux, so built, not run; the single file is about 116 MB self-contained because the Windows Forms runtime is large):

Built, not run: a Windows Forms project published for win-x64
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <RootNamespace>Desk</RootNamespace>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <EnableWindowsTargeting>true</EnableWindowsTargeting>
    <NuGetAudit>false</NuGetAudit>
    <MyType>WindowsFormsWithCustomSubMain</MyType>
    <StartupObject>Sub Main</StartupObject>
  </PropertyGroup>
</Project>
Option Strict On

Imports System.Windows.Forms

Module Program

    <STAThread>
    Sub Main()
        Application.EnableVisualStyles()
        Application.Run(New Form With {.Text = "Shelf"})
    End Sub

End Module
116110624 Desk.exe
13780 Desk.pdb

Native AOT

Native AOT compiles the program to a native executable with no JIT and no runtime installed. The documented limitations are: no dynamic loading (Assembly.LoadFile), no runtime code generation (System.Reflection.Emit), no C/CLI, no built-in COM on Windows, and the app must be trimmed and, effectively, single-file. Compiling needs a native toolchain (a C/C linker and zlib headers), so the build used the sdk:10.0-aot container image, which has it.

What this means for Visual Basic was tested rather than assumed. A program that uses the typical static language features, the runtime library (Format, UCase, Left, Mid, CInt, CDbl, IsNumeric, Like), LINQ query syntax, string interpolation, Select Case and For Each, published with PublishAot=true and TreatWarningsAsErrors=true without a single warning and ran:

Built and run (Native AOT): ordinary Visual Basic with the runtime library
Option Strict On

Imports System.Collections.Generic
Imports System.Linq
Imports Microsoft.VisualBasic

Public Class Book
    Public Property Title As String = ""
    Public Property Year As Integer
End Class

Module Program

    Sub Main()
        ' Typical Visual Basic code: runtime-library functions, Like, LINQ, interpolation, For Each.
        Dim books As New List(Of Book) From {
            New Book With {.Title = "Types and Tables", .Year = 1988},
            New Book With {.Title = "Shelf Basics", .Year = 2020}
        }
        Dim recent = From b In books Where b.Year > 2000 Select b.Title
        Console.WriteLine("LINQ:      " & String.Join(", ", recent))
        Console.WriteLine("Format:    " & Format(1234.5, "N2") & " | " & Strings.UCase("shelf") & " | " & Left("catalogue", 3) & " | " & Mid("catalogue", 4, 3))
        Console.WriteLine("Conversion: " & CInt("42") + 1 & " | " & CDbl("3.5") & " | " & IsNumeric("12x").ToString())
        Console.WriteLine("Like:      " & ("ISBN-0306" Like "ISBN-####").ToString())
        Console.WriteLine("Select:    " & Describe("vb"))
        Console.WriteLine($"Interp:    {books.Count} books, newest {books.Max(Function(b) b.Year)}")
        Dim items As Object = books
        For Each item As Object In CType(items, IEnumerable(Of Book))
            Console.WriteLine("For Each:  " & CType(item, Book).Title)
        Next
    End Sub

    Function Describe(language As String) As String
        Select Case language
            Case "vb" : Return "Visual Basic"
            Case "cs" : Return "C#"
            Case Else : Return "unknown"
        End Select
    End Function

End Module
2178040 Deploy
3458888 Deploy.dbg
LINQ:      Shelf Basics
Format:    1,234.50 | SHELF | cat | alo
Conversion: 43 | 3.5 | False
Like:      True
Select:    Visual Basic
Interp:    2 books, newest 2020
For Each:  Types and Tables
For Each:  Shelf Basics

The first block lists the sizes of the native executable and its debug file in bytes (2.1 MB for the program). The DeployFix program (an interface call and Activator.CreateInstance(GetType(Magazine))) also published warning-free to a 1,980,776-byte executable and printed static: Magazine: Shelf Monthly.

The late-bound Deploy program is where AOT bites. The publish reports the call-site warning and a warning on the Visual Basic runtime assembly itself, and the executable fails at run time exactly like the trimmed one:

Program.vb(26): Trim analysis warning IL2026: Deploy.Program.Main(): Using member 'Microsoft.VisualBasic.CompilerServices.Versioned.CallByName(Object,String,CallType,Object[])' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The method name cannot and type cannot be statically analyzed so it may be trimmed.
/root/.nuget/packages/microsoft.netcore.app.runtime.nativeaot.linux-arm64/10.0.13/runtimes/linux-arm64/lib/net10.0/Microsoft.VisualBasic.Core.dll : warning IL3053: Assembly 'Microsoft.VisualBasic.Core' produced AOT analysis warnings. [Deploy.vbproj]
runtime:  .NET 10.0.13
dynamic code supported: False
Unhandled exception. System.MissingMemberException: Public member 'Describe' on type 'Magazine' not found.
   at Microsoft.VisualBasic.CompilerServices.Symbols.Container.GetMembers(String&, Boolean) + 0x168
   at Microsoft.VisualBasic.CompilerServices.NewLateBinding.CallMethod(Symbols.Container, String, Object[], String[], Type[], Boolean[], BindingFlags, Boolean, OverloadResolution.ResolutionFailure&) + 0x9c
   at Microsoft.VisualBasic.CompilerServices.NewLateBinding.ObjectLateCall(Object, Type, String, Object[], String[], Type[], Boolean[], Boolean) + 0xb8
   at Microsoft.VisualBasic.CompilerServices.NewLateBinding.LateCall(Object, Type, String, Object[], String[], Type[], Boolean[], Boolean) + 0x16c
   at Microsoft.VisualBasic.CompilerServices.Versioned.CallByName(Object, String, CallType, Object[]) + 0x90
   at Deploy.Program.Main() + 0xa4

(dynamic code supported: False is the program’s own check of RuntimeFeature.IsDynamicCodeSupported.) The practical rule: Native AOT is possible for a Visual Basic program that avoids late binding, CallByName, CreateObject and reflection by name; Option Strict On (which every example in this reference uses) already forbids implicit late binding, so the remaining sources are the explicit ones above. JSON through System.Text.Json is reflection-based unless it uses the source generator, which needs a C# helper project (see Serialization, JSON and XML). Whether any particular Visual Basic library is AOT-safe is not documented by Microsoft; test it by publishing, as above.

Windows services

A Windows service is a worker (BackgroundService) hosted with Microsoft.Extensions.Hosting.WindowsServices. There is no Visual Basic worker template, so the project is written by hand; the service name is set with AddWindowsService, which does nothing when the program is started from a console. That makes the same program testable on any operating system:

Built and run (as a console program): a worker that can also run as a Windows service
<Project Sdk="Microsoft.NET.Sdk.Worker">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>Svc</RootNamespace>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.*" />
  </ItemGroup>
</Project>
Option Strict On

Imports System.Threading
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

' The shelf's nightly overdue check, as a worker that can run as a console program or a Windows service.
Public NotInheritable Class OverdueWorker
    Inherits BackgroundService

    Private ReadOnly _lifetime As IHostApplicationLifetime

    Public Sub New(lifetime As IHostApplicationLifetime)
        _lifetime = lifetime
    End Sub

    Protected Overrides Async Function ExecuteAsync(stoppingToken As CancellationToken) As Task
        For tick As Integer = 1 To 2
            Console.WriteLine("overdue check " & tick)
            Await Task.Delay(100, stoppingToken)
        Next
        _lifetime.StopApplication()
    End Function
End Class

Module Program

    Sub Main(args As String())
        Dim builder = Host.CreateApplicationBuilder(args)
        builder.Configuration("Logging:LogLevel:Default") = "None"
        ' A no-op outside the Service Control Manager; under sc.exe it switches the host lifetime.
        builder.Services.AddWindowsService(Sub(options) options.ServiceName = "ShelfOverdue")
        builder.Services.AddHostedService(Of OverdueWorker)()
        Using app As IHost = builder.Build()
            app.Run()
        End Using
        Console.WriteLine("stopped")
    End Sub

End Module
overdue check 1
overdue check 2
stopped

Installing it as a service is a Windows-only step with sc.exe from an elevated prompt (not run here):

dotnet publish -c Release -r win-x64 --self-contained -o C:\Services\ShelfOverdue
sc.exe create ShelfOverdue binPath= "C:\Services\ShelfOverdue\Svc.exe" start= auto
sc.exe start ShelfOverdue

The old route (a ServiceBase subclass registered with InstallUtil) belongs to .NET Framework.

ClickOnce, MSIX and installers

Desktop applications (Windows Forms and WPF, covered in Windows Forms Applications and WPF Applications) have installer options beyond copying the publish folder (an "xcopy" deployment, which works for self-contained builds):

  • ClickOnce. Visual Studio’s Publish tool can publish a .NET (not only .NET Framework) Windows desktop application with ClickOnce: the documentation says this has been possible since Visual Studio 2019 version 16.8 for .NET Core 3.1 and .NET 5 or newer, through Publish, Folder and the ClickOnce target; for a .NET Framework application the separate "Deploy a desktop app using ClickOnce (C# or Visual Basic)" article applies. ClickOnce needs Windows and Visual Studio, so it was not run here.

  • MSIX. MSIX is the modern Windows package format, with a package identity that unlocks Windows platform APIs, Store distribution and enterprise management. For desktop applications that are not WinUI 3 the Learn article describes the Windows Application Packaging Project in Visual Studio; it was not built here.

  • MSI and other installers. Third-party installer authoring tools (WiX is the well known one) are outside this reference.

Where the books are outdated

What the books show What is current

Deployment of a desktop app means "ensure the .NET Framework is installed", or a Store (UWP) package.

Framework-dependent publishing with the .NET Desktop Runtime installed, or self-contained and single-file publishing. MSIX for Windows Forms and WPF replaces UWP packaging; ClickOnce and MSI remain options.

Windows Services need a ServiceBase subclass, a project installer component and InstallUtil.

A BackgroundService worker with AddWindowsService, published and registered with sc.exe create. There is no Visual Basic worker template; the project is hand-written.

System.IO.Ports, System.Runtime.Caching and similar are presented as built in.

On .NET 5 and later they are NuGet packages (System.IO.Ports) or replaced (Microsoft.Extensions.Caching.Memory).

My Project\AssemblyInfo.vb holds the version attributes.

SDK-style projects generate them from Version and related properties; keep the file only with GenerateAssemblyInfo set to false.

Class libraries are .dll projects you copy.

Libraries are packed as NuGet packages (dotnet pack), multi-targeted, and referenced by PackageReference or ProjectReference.

Differences from C#

Topic C# Visual Basic

Namespace of a type

The namespace declaration only.

RootNamespace is prefixed to every Namespace block.

netstandard2.0 libraries

Full language and library.

Compiles, but the runtime-library functions (Left, Mid, …​) are unavailable (BC30451).

Static helper class

static class.

Module (a static class with StandardModule) or a NotInheritable class with a private constructor.

Missing XML comments

CS1591 warning.

No compiler warning (an analyzer is needed).

Assembly info

The SDK generates it; a hand-written duplicate gives CS0579.

My Project\AssemblyInfo.vb in old projects; a duplicate gives BC30663.

Tools that generate code

Add C# files to the project.

dotnet-svcutil and Grpc.Tools write C#: compile it in a C# project and reference that.

Nullable annotations

Emitted and enforced.

Not emitted; C# consumers see oblivious types.

Late binding

The dynamic type, opt-in.

Implicit on Object when Option Strict is off, or via CallByName: both go through the NewLateBinding helpers, which trimming and Native AOT cannot analyse.

Trimming and AOT

Trim-annotated libraries and source generators.

Works for statically bound code (tested); late binding and reflection by name fail.

Pitfalls

  • A nuget.config with <clear /> in a parent folder applies to every project beneath it, including projects that need nuget.org for runtime packs: put it next to the consumers, or add the public source back.

  • Changing Optional defaults is a breaking change for callers that were not recompiled.

  • Duplicate assembly attributes (BC30663) after migrating an old project: delete them or turn off generation.

  • netstandard2.0 plus the Visual Basic runtime functions: use net481;net10.0 or avoid Left, Mid and Format.

  • The .pdb is counted in the single-file rows above: set DebugType to none to leave symbols out of the publish folder.

  • Trimming with TreatWarningsAsErrors turns every trim warning into a failed publish: that is the desired behaviour for libraries.

  • Single-file publish is not trimming: the code inside is the same, only packed; it does not shrink the app unless compression or trimming is added (see the table).

  • Late binding survives a normal run and fails only after trimming or AOT: add a publish-and-run step to the build for apps that use CallByName, CreateObject or a project with Option Strict switched off.

  • The Lib identifier is a keyword (Declare ... Lib): a project or namespace called Lib fails with BC30183, so name it EngineLib or ShelfLib.

  • Cross-publishing for Windows from Linux compiles but does not run the result: test on Windows.

Compiler and SDK messages seen on this page

Code Message (first line, as printed by the build) Cause

BC30663

Attribute 'AssemblyVersionAttribute' cannot be applied multiple times.

The generated assembly info and a hand-written AssemblyInfo.vb.

BC30451

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

A runtime-library function in a netstandard2.0 target.

BC30456

'Left' is not a member of 'Strings'.

The same, after adding the Microsoft.VisualBasic 10.3.0 package.

BC40027 and BC40028

Return type of function 'Count' is not CLS-compliant. and Type of parameter 'value' is not CLS-compliant.

<Assembly: CLSCompliant(True)> with UInteger in a public signature.

BC42307

XML comment parameter 'wrong' does not match a parameter on the corresponding 'sub' statement.

A param tag naming a parameter that does not exist.

BC42309

XML comment has a tag with a 'cref' attribute 'Missing' that could not be resolved.

A cref to a name that does not exist.

BC30456

'Secret' is not a member of 'Engine'.

A Friend member used from another assembly without InternalsVisibleTo.

IL2026

Using member 'Microsoft.VisualBasic.CompilerServices.Versioned.CallByName(...)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code.

Late binding under trimming or AOT.

IL3053

Assembly 'Microsoft.VisualBasic.Core' produced AOT analysis warnings.

Native AOT publish of a program that reaches the late-binding helpers.

NETSDK1175

Windows Forms is not supported or recommended with trimming enabled.

PublishTrimmed on a Windows Forms project.

NETSDK1144

Optimizing assemblies for size failed.

The trimmer failing because an analysis warning is an error.

Version notes

  • .NET tools arrived in .NET Core 2.1; single-file publishing and ReadyToRun in .NET Core 3.0; Native AOT for console apps in .NET 7.

  • The InternalsVisibleTo MSBuild item and the generated assembly info work for Visual Basic in the SDK used here (10.0.402).

  • Visual Basic 17.13 (the compiler in SDK 10.0.402) is the version tested here; no newer language version exists.