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 |
|---|---|---|
|
The namespace the compiler prefixes to every |
The project name. |
|
The file name of the |
The project name. |
|
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:
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:
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):
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:
<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.
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
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
Source: Library guidance, Optional, ParamArray
How each Visual Basic construct looks from C#:
| In Visual Basic | C# sees | Design advice |
|---|---|---|
|
A |
A |
|
An indexer |
A default property must have a parameter and there is at most one per type; the compiler records its name in |
|
|
Fine. Only the last parameter can be a |
|
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#. |
|
|
Use |
|
An event of that type |
Use |
|
|
Share internals with a test project through |
|
A |
Seal types that are not designed for inheritance; a small immutable value is a sealed class or a |
Reference types and |
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'.
Source: Friend assemblies
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.
Source: .NET Standard, Cross-platform targeting
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.
Source: Documenting your code with XML
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) |
|
|
File version (Windows Explorer) |
|
|
Informational (shown by NuGet, can carry a pre-release label and a commit hash) |
|
|
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.
Source: Assembly versioning, Versioning guidance
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.
<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
Source: Create a .NET tool
Deploying applications
dotnet publish produces the files to give to users. The first decision is whether the .NET runtime travels with the app.
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:
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: |
104 |
5 |
Runs on the installed .NET 10 runtime: |
Self-contained: |
86,992 |
192 |
Runs without a runtime installed; same output. |
Single file: |
92,844 |
2 (the executable and its |
Same output; larger because the files are packed uncompressed. |
Single file with compression: |
36,288 |
2 |
Same output; slower to start because the file is decompressed. |
ReadyToRun: |
86,992 |
192 |
Same output. |
Trimmed: |
22,632 |
34 |
Warning IL2026, and the program crashes: |
Trimmed and single file |
14,744 |
2 |
Same crash; the smallest of the self-contained forms. |
Native AOT: |
a 3,963,480-byte executable (and a 6,360,496-byte |
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.
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:
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):
<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:
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:
<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 |
A |
|
On .NET 5 and later they are NuGet packages ( |
|
SDK-style projects generate them from |
Class libraries are |
Libraries are packed as NuGet packages ( |
Differences from C#
| Topic | C# | Visual Basic |
|---|---|---|
Namespace of a type |
The |
|
|
Full language and library. |
Compiles, but the runtime-library functions ( |
Static helper class |
|
|
Missing XML comments |
CS1591 warning. |
No compiler warning (an analyzer is needed). |
Assembly info |
The SDK generates it; a hand-written duplicate gives CS0579. |
|
Tools that generate code |
Add C# files to the project. |
|
Nullable annotations |
Emitted and enforced. |
Not emitted; C# consumers see oblivious types. |
Late binding |
The |
Implicit on |
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.configwith<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
Optionaldefaults 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.0plus the Visual Basic runtime functions: usenet481;net10.0or avoidLeft,MidandFormat. -
The
.pdbis counted in the single-file rows above: setDebugTypetononeto leave symbols out of the publish folder. -
Trimming with
TreatWarningsAsErrorsturns 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,CreateObjector a project withOption Strictswitched off. -
The
Libidentifier is a keyword (Declare ... Lib): a project or namespace calledLibfails with BC30183, so name itEngineLiborShelfLib. -
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 |
|
The generated assembly info and a hand-written |
BC30451 |
|
A runtime-library function in a |
BC30456 |
|
The same, after adding the |
BC40027 and BC40028 |
|
|
BC42307 |
|
A |
BC42309 |
|
A |
BC30456 |
|
A |
IL2026 |
|
Late binding under trimming or AOT. |
IL3053 |
|
Native AOT publish of a program that reaches the late-binding helpers. |
NETSDK1175 |
|
|
NETSDK1144 |
|
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
InternalsVisibleToMSBuild 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.
References
-
Library guidance, Cross-platform targeting, Include NuGet packages in libraries and .NET Standard
-
Friend assemblies, Assembly versioning and Strong-named assemblies
-
Create a NuGet package with the dotnet CLI, NuGet pack and restore as MSBuild targets, Install and use a package with the dotnet CLI, Central Package Management, Symbol packages, Package versioning and Local feeds
-
dotnet pack, dotnet publish, Create a .NET tool and MSBuild properties for the .NET SDK
-
.NET application publishing overview, Single-file deployment, ReadyToRun, Trim self-contained deployments, Trimming options, Known trimming incompatibilities, Fix trim warnings, Native AOT and Runtime identifier catalog
-
ClickOnce for .NET desktop apps, ClickOnce security and deployment, MSIX overview and MSIX packaging in Visual Studio