Migrating Legacy Visual Basic
|
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. It is the practical companion to The VB Family: VB6, VBA and VBScript, which explains how the dialects differ. Here the question is what to do with code that already exists: which path to take, what each step changes, which tools help and which do not, and what to replace each old construct with.
Two kinds of code blocks appear:
-
Legacy "before" snippets (VB6, VBA, VBScript, and Visual Basic written for .NET Framework). Each has a block title that starts with "Legacy" or "Before" and says that it was not built and not run. They are original illustrations of the old shapes you will meet; they are not recommended code.
-
Modern examples: complete Visual Basic console programs that start with
Option Strict On, were built with warnings treated as errors and were run in the .NET 10 SDK container (Linux, arm64, root namespaceShelfApp). The output is the real output. Every replacement in the table at the end that is Visual Basic .NET code was verified this way, and the example that proves it is named in the table.
Migration paths
Where you start decides the path, and no path leads straight from VB6 to .NET 10 by a tool. The flowchart shows the options that this page covers.
Read it as a checklist:
-
Name what you have. A VB6 program, a VBA macro, a VBScript file, a Classic ASP site, a .NET Framework Visual Basic application, or a Web Forms site each have different tools and different risks.
-
Decide the destination. For desktop, console and library code the destination is Visual Basic on .NET 10, which is a supported, stable language. For web code the destination is usually a C# ASP.NET Core host, because ASP.NET Core has no Visual Basic templates and no Razor syntax in Visual Basic.
-
Choose big bang or incremental. A rewrite of a large VB6 application in one step is the highest-risk option. Incremental paths keep the old program running and replace pieces behind a boundary (a COM component, a service, a file format).
-
Decide whether C# belongs in the plan. It is optional, and it is a separate decision (see the last sections).
VB6 to Visual Basic .NET
Visual Basic .NET was never source compatible with VB6. The names are similar and a large part of the syntax is, but forms, controls, the type system, error handling, the object model and the runtime differ. The
family page lists the removed syntax with the real compiler messages (Set, Variant, GoSub, Wend, Type, Property Get, Option Base, fixed-length strings). The consequences for a migration are these:
-
No tool converts a real VB6 application completely. Neither the .NET Upgrade Assistant nor its successor GitHub Copilot upgrade and modernization agent is a VB6 converter: their documented scope is C# and Visual Basic .NET projects. Microsoft’s current upgrade tooling documents Visual Basic .NET projects only.
-
The compatibility library is not an option on modern .NET.
Microsoft.VisualBasic.Compatibility.VB6(helpers such as control arrays and font conversion) exists only for .NET Framework. Example 1 shows that the type cannot be found on .NET 10. -
Third-party converters exist. Some are commercial, some are free. This section neither evaluates nor endorses them. If you try one, run it on a copy of the code, treat the output as a first draft that still has to be reviewed construct by construct, and keep the test results from the original program for comparison.
-
The forms do not move automatically. A VB6 form becomes a Windows Forms form or a WPF window (see Windows Forms Applications and WPF Applications); the
HandlesandAddHandlerevent model replaces the name-basedControl_Eventhandlers (see Events and Handlers); default properties (Text1 = "x") need the property name; control arrays become collections of controls with one shared handler.
Option Strict On
Module M09Compatibility
Sub Main()
Dim compat = Type.GetType("Microsoft.VisualBasic.Compatibility.VB6.Support, Microsoft.VisualBasic.Compatibility", False)
Console.WriteLine($"Compatibility.VB6.Support found: {compat IsNot Nothing}")
Console.WriteLine($"Strings.Left lives in: {GetType(Microsoft.VisualBasic.Strings).Assembly.GetName().Name}")
Console.WriteLine($"Runtime: {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}")
End Sub
End Module
Output:
Compatibility.VB6.Support found: False
Strings.Left lives in: Microsoft.VisualBasic.Core
Runtime: .NET 10.0.13
Three strategies
| Strategy | What it is | Strengths | Risks |
|---|---|---|---|
Rewrite |
Build a new Visual Basic .NET (or C#) application feature by feature, using the old program as the specification |
Clean design, no old types, tests from the start |
Long time without new features; behaviour that nobody documented is lost; needs access to the people who know the old program |
Incremental behind a COM bridge |
Keep the VB6 program, expose or consume COM components, and replace one component at a time with a .NET class library registered for COM |
Production keeps working; each step is small and testable |
Two runtimes and two deployments; COM registration is Windows-specific; debugging crosses a boundary |
Contain and wait |
Leave the VB6 program as it is, wrap it (for example behind a service), and make no further investment |
Cheapest now |
The IDE is unsupported, the runtime is supported only with Windows, and the pool of people who know it shrinks |
The COM bridge is described in detail, with tested build steps and platform limits, on COM Interop and Office Automation; native calls into Windows DLLs are on Native Interop: Declare and P/Invoke. Whatever the strategy, order the work by what can be tested: pure logic and calculations first, data access next, user interface last.
A migration vocabulary, row by row
The following sections walk through the main constructs. Each shows the old shape, then a tested Visual Basic 17.13 equivalent. The complete table is at the end of the page.
Types, records, assignment and parameters (rows 1 to 6)
Private Type BookRecord
Title As String
Copies As Integer ' 16 bits
End Type
Private Sub AddCopy(copies As Long) ' ByRef, because nothing was written
copies = copies + 1
End Sub
Private Sub Demo()
Dim anything As Variant
Dim shelf1 As Book, shelf2 As Book
anything = 42
Set shelf1 = New Book
Set shelf2 = shelf1
shelf2.Title = "Emma"
End Sub
Option Strict On
Module M01TypesAndAssignment
Public Structure BookRecord
Public Title As String
Public Copies As Short
Public Price As Decimal
End Structure
Public Class Book
Public Property Title As String = ""
End Class
Sub AddCopyByDefault(copies As Integer)
copies += 1
End Sub
Sub AddCopyByRef(ByRef copies As Integer)
copies += 1
End Sub
Sub Main()
Console.WriteLine($"Short is System.{GetType(Short).Name}, {Short.MinValue} to {Short.MaxValue}")
Console.WriteLine($"Integer is System.{GetType(Integer).Name}, {Integer.MinValue} to {Integer.MaxValue}")
Console.WriteLine($"Long is System.{GetType(Long).Name}, {Long.MinValue} to {Long.MaxValue}")
Dim anything As Object = 42
Console.WriteLine($"Object holds {anything.GetType().Name}; CInt(anything) + 1 = {CInt(anything) + 1}")
anything = "forty-two"
Console.WriteLine($"Object now holds {anything.GetType().Name}")
Dim first As BookRecord
first.Title = "Dune"
first.Copies = 3S
first.Price = 9.99D
Dim second = first
second.Title = "Emma"
Console.WriteLine($"Structure copy: first={first.Title}, second={second.Title}")
Dim shelf1 As New Book With {.Title = "Dune"}
Dim shelf2 = shelf1
shelf2.Title = "Emma"
Console.WriteLine($"Class reference, no Set: shelf1={shelf1.Title}, shelf2={shelf2.Title}")
Dim copies = 3
AddCopyByDefault(copies)
Console.WriteLine($"After the default (ByVal) Sub: {copies}")
AddCopyByRef(copies)
Console.WriteLine($"After the ByRef Sub: {copies}")
End Sub
End Module
Output:
Short is System.Int16, -32768 to 32767
Integer is System.Int32, -2147483648 to 2147483647
Long is System.Int64, -9223372036854775808 to 9223372036854775807
Object holds Int32; CInt(anything) + 1 = 43
Object now holds String
Structure copy: first=Dune, second=Emma
Class reference, no Set: shelf1=Emma, shelf2=Emma
After the default (ByVal) Sub: 3
After the ByRef Sub: 4
What the output proves, row by row:
-
Integer and Long.
Shortis the 16-bit type,Integerthe 32-bit one,Longthe 64-bit one. Review every oldIntegerthat is stored in a file or sent over a wire: its width may have to stay 16 bits, which meansShort. -
Variant to Object.
Objectholds anIntegerand then aString, butCInt(anything) + 1needs an explicit conversion. Without it,Option Strict Onstops the program at build time with BC30038. The better migration is to give the variable a real type or a generic parameter, and keepObjectfor the rare heterogeneous value. -
Type to Structure. A
Structureis a value type, sosecond = firstcopies it and changingsecondleavesfirstalone. AClassis a reference type: aftershelf2 = shelf1both names refer to one object, which is whatSet shelf2 = shelf1meant in VB6. TheSetkeyword is gone; plain assignment does both jobs. -
ByRef by default to ByVal. The
Subwithout a modifier did not change the caller’s variable; theByRefone did.
Error handling and GoSub (rows 4 and 7)
Private Function ParseCopies(text As String) As Long
On Error GoTo Fail
ParseCopies = CLng(text)
Exit Function
Fail:
Debug.Print "bad number: " & text
ParseCopies = 0
End Function
Private Sub PrintReport()
GoSub WriteHeader
Debug.Print "body"
Exit Sub
WriteHeader:
Debug.Print "== Report =="
Return
End Sub
Option Strict On
Imports System.Globalization
Module M02ErrorsAndSubroutines
Function ParseCopies(text As String) As Integer
Try
Return Integer.Parse(text, CultureInfo.InvariantCulture)
Catch ex As FormatException
Console.WriteLine($" FormatException for '{text}'")
Return 0
Catch ex As OverflowException
Console.WriteLine($" OverflowException for '{text}'")
Return Integer.MaxValue
Finally
Console.WriteLine($" checked '{text}'")
End Try
End Function
Function FormatLine(title As String, copies As Integer) As String
Return $"{title,-8}{copies,3}"
End Function
Sub PrintReport(titles As String(), copies As Integer())
For i = 0 To titles.Length - 1
Console.WriteLine(FormatLine(titles(i), copies(i)))
Next
End Sub
Sub Main()
Console.WriteLine(ParseCopies("4"))
Console.WriteLine(ParseCopies("four"))
Console.WriteLine(ParseCopies("99999999999"))
Dim value As Integer
If Integer.TryParse("12x", NumberStyles.Integer, CultureInfo.InvariantCulture, value) Then
Console.WriteLine($"parsed {value}")
Else
Console.WriteLine("TryParse says no, and nothing was thrown")
End If
PrintReport({"Dune", "Emma"}, {3, 12})
End Sub
End Module
Output:
checked '4'
4
FormatException for 'four'
checked 'four'
0
OverflowException for '99999999999'
checked '99999999999'
2147483647
TryParse says no, and nothing was thrown
Dune 3
Emma 12
Source: Try…Catch…Finally statement
On Error still compiles, which makes a line-by-line translation tempting, but structured handling is the migration target: a Catch names the exception it handles, a Finally always runs and TryParse avoids
the exception for expected bad input. GoSub does not compile (BC30814). A GoSub target that used the caller’s local variables becomes a procedure that receives them as parameters, as FormatLine does here.
The complete rules, including the Err.Number mapping, are on
Structured Exception Handling and
Unstructured Error Handling: On Error.
Files and the FileSystemObject (rows 8 and 9)
Dim n As Integer
n = FreeFile
Open "C:\temp\catalog.txt" For Output As #n
Print #n, "Dune,3"
Print #n, "Emma,12"
Close #n
Dim fso As Object
Set fso = CreateObject("Scripting.FileSystemObject")
If fso.FileExists("C:\temp\catalog.txt") Then
Debug.Print fso.GetFile("C:\temp\catalog.txt").Size
End If
Option Strict On
Imports System.IO
Imports System.Text
Module M03Files
Sub Main()
Dim folder = Path.Combine(Path.GetTempPath(), "shelf-g6-" & Guid.NewGuid().ToString("N"))
Directory.CreateDirectory(folder)
Try
Dim catalog = Path.Combine(folder, "catalog.txt")
Dim utf8 As New UTF8Encoding(False)
Using writer As New StreamWriter(catalog, False, utf8)
writer.WriteLine("Dune,3")
writer.WriteLine("Emma,12")
End Using
File.AppendAllText(catalog, "Ulysses,1" & vbLf, utf8)
Using reader As New StreamReader(catalog, utf8)
Do While Not reader.EndOfStream
Dim line = reader.ReadLine()
Console.WriteLine($"read: {line}")
Loop
End Using
File.Copy(catalog, Path.Combine(folder, "backup.txt"))
File.WriteAllText(Path.Combine(folder, "notes.md"), "# notes", utf8)
For Each file In Directory.EnumerateFiles(folder, "*.txt").OrderBy(Function(p) p)
Dim info As New FileInfo(file)
Console.WriteLine($"{info.Name,-12}{info.Length,4} bytes")
Next
Console.WriteLine($"catalog.txt exists: {File.Exists(catalog)}, folder exists: {Directory.Exists(folder)}")
Console.WriteLine($"extension: {Path.GetExtension(catalog)}, name without extension: {Path.GetFileNameWithoutExtension(catalog)}")
Finally
Directory.Delete(folder, True)
End Try
Console.WriteLine($"folder exists after cleanup: {Directory.Exists(folder)}")
End Sub
End Module
Output:
read: Dune,3
read: Emma,12
read: Ulysses,1
backup.txt 25 bytes
catalog.txt 25 bytes
catalog.txt exists: True, folder exists: True
extension: .txt, name without extension: catalog
folder exists after cleanup: False
Source: File and stream I/O, StreamWriter class
Every Open ... As #n statement becomes a StreamWriter, StreamReader or one of the File helper methods, and every Close becomes the end of a Using block. The FileSystemObject becomes File, Directory,
Path and FileInfo, which need no COM registration and run on every platform that .NET runs on. Two details of the example matter in a migration: it chooses the encoding explicitly (UTF8Encoding(False) writes no byte
order mark, so the file is 25 bytes on Linux; WriteLine uses the platform newline, so on Windows it is 28), and it uses Path.Combine, never a hand-built backslash path. The My.Computer.FileSystem shortcuts of Visual Basic exist only in Windows Forms projects on .NET (see
The My Namespace); System.IO works in every project type. See also Files, Directories and Streams.
Dictionaries, If and line continuation (rows 10 to 12)
Dim stock
Set stock = CreateObject("Scripting.Dictionary")
stock.CompareMode = 1 ' text comparison: keys ignore case
stock("Dune") = 3 ' assignment adds the key
Dim copies
copies = stock("Ulysses") ' reading a missing key ADDS it, with an empty value
Dim label
label = IIf(copies > 0, "in stock", "out of stock") ' evaluates both branches
Dim summary
summary = "Dune: " & stock("Dune") & _
", copies: " & stock.Count & _
", label: " & label
Option Strict On
Module M04CollectionsAndExpressions
Function Describe(copies As Integer) As String
Return If(copies > 0, "in stock", "out of stock")
End Function
Sub Main()
Dim stock As New Dictionary(Of String, Integer)(StringComparer.OrdinalIgnoreCase)
stock("Dune") = 3
stock.Add("Emma", 0)
stock("DUNE") += 1
For Each pair In stock
Console.WriteLine($"{pair.Key,-6}{pair.Value,2} {Describe(pair.Value)}")
Next
Dim copies As Integer
If stock.TryGetValue("Ulysses", copies) Then
Console.WriteLine(copies)
Else
Console.WriteLine("Ulysses is not in the dictionary")
End If
Try
Console.WriteLine(stock("Ulysses"))
Catch ex As KeyNotFoundException
Console.WriteLine("Reading a missing key throws KeyNotFoundException")
End Try
Dim legacy As New Microsoft.VisualBasic.Collection()
legacy.Add("Dune", "k1")
legacy.Add("Emma", "k2")
Console.WriteLine($"Collection(1)={legacy(1)}, Collection(""k2"")={legacy("k2")}, Count={legacy.Count}")
Dim modern As New List(Of String) From {"Dune", "Emma"}
Console.WriteLine($"List(0)={modern(0)}, Count={modern.Count}")
Dim nickname As String = Nothing
Console.WriteLine(If(nickname, "(no nickname)"))
Dim summary = stock.
Where(Function(p) p.Value > 0).
Select(Function(p) p.Key.ToUpperInvariant()).
ToList()
Dim total = stock.Values.Sum() +
modern.Count
Console.WriteLine(String.Join(", ", summary) & " / total " & total.ToString())
End Sub
End Module
Output:
Dune 4 in stock
Emma 0 out of stock
Ulysses is not in the dictionary
Reading a missing key throws KeyNotFoundException
Collection(1)=Dune, Collection("k2")=Emma, Count=2
List(0)=Dune, Count=2
(no nickname)
DUNE / total 6
Three behaviours differ from the legacy shape:
-
Dictionary. The comparer (
StringComparer.OrdinalIgnoreCase) replacesCompareMode. Reading a missing key throwsKeyNotFoundExceptioninstead of silently adding it;TryGetValueis the way to ask. Both outputs above are from the real run. The oldMicrosoft.VisualBasic.Collectionstill works for old code (1-based, keys case-insensitive) but has no place in new code. -
If versus IIf.
If(condition, a, b)evaluates only the branch that it returns, andIf(value, fallback)with two arguments is the null-coalescing form (VB has no??operator). The oldIIffunction evaluates both arguments; this was verified in a scratch program in which the false branch had a side effect andIIfran it whileIfdid not.IIfreturnsObject, so underOption Strict Onit also needs a conversion. -
Line continuation. Since Visual Basic 10 (2010) a statement continues onto the next line after a comma, an opening parenthesis, an operator, a leading dot of a member access (
stock.at the end of the line, as in the example) and in several other places, so the_continuation character is rarely needed. The example has no_. The explicit character remains valid where implicit continuation does not apply.
Disposal instead of Class_Terminate
The family page showed a VB6 class that closed its file in Class_Terminate, which ran when the last reference went away. The .NET form is IDisposable and Using:
Option Strict On
Imports System.IO
Public NotInheritable Class LogFile
Implements IDisposable
Private ReadOnly _writer As StreamWriter
Private _disposed As Boolean
Public Sub New(path As String)
_writer = New StreamWriter(path, True)
End Sub
Public Sub WriteLine(text As String)
ObjectDisposedException.ThrowIf(_disposed, Me)
_writer.WriteLine(text)
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
If Not _disposed Then
_disposed = True
_writer.Dispose()
Console.WriteLine("LogFile closed")
End If
End Sub
End Class
Module M10Disposal
Sub Main()
Dim path = IO.Path.Combine(IO.Path.GetTempPath(), "shelf-g6-log-" & Guid.NewGuid().ToString("N") & ".txt")
Dim kept As LogFile
Using log As New LogFile(path)
kept = log
log.WriteLine("first line")
Console.WriteLine("inside Using")
End Using
Console.WriteLine($"after Using, file holds: {File.ReadAllText(path).Trim()}")
Try
kept.WriteLine("too late")
Catch ex As ObjectDisposedException
Console.WriteLine("writing after the block throws ObjectDisposedException")
End Try
File.Delete(path)
End Sub
End Module
Output:
inside Using
LogFile closed
after Using, file holds: first line
writing after the block throws ObjectDisposedException
Source: Using statement
The cleanup happens at End Using, not when the last reference disappears, and using the object afterwards fails with ObjectDisposedException. See
Object Lifetime and Disposal.
VBA
VBA code lives inside a document and is bound to the host’s object model, so the migration question is where the logic should live:
-
Leave it in VBA if it is small, owned by the person who uses the workbook and supported with Office. This is a legitimate decision, and it costs nothing.
-
Move the logic to a Visual Basic class library when it is business logic that several documents need, that needs tests, or that must run without Office. Leave the macro as a thin caller, or call the library from a COM-visible wrapper (see COM Interop and Office Automation).
-
Replace Office automation with a file-format library when the code only produces or reads documents: Microsoft does not support running Excel or Word on a server, and a library that reads and writes the Open XML formats needs no Office at all (see file formats).
-
Rewrite the types.
Variantarrays,Longthat meant 32 bits andDeclarestatements withoutPtrSafeandLongPtrare the usual traps.
Visual Basic .NET cannot run VBA macros, and a VBA project cannot call a .NET library directly unless the library is exposed through COM or a host add-in model. For the language differences see the VBA section of The VB Family.
VBScript and Classic ASP
VBScript has two destinations, depending on what the script really is.
-
An administration or automation script (copy files, read a log, change a setting) belongs in PowerShell, which is the supported scripting language on Windows and also runs on Linux and macOS. The script below is the PowerShell version of the VBScript on the family page (file test, line-by-line read, and a dictionary whose keys ignore case, as PowerShell hashtables do by default). It is not Visual Basic, so it is marked as run with PowerShell and not built; it was run in a PowerShell container and printed what is shown.
-
A script that grew into an application belongs in a Visual Basic (or C#) console program; Example 4 and Example 5 are the building blocks.
Set-StrictMode -Version Latest
$catalog = Join-Path ([System.IO.Path]::GetTempPath()) 'catalog.txt'
Set-Content -Path $catalog -Value @('Dune,3', 'Emma,12', 'Ulysses,1')
if (Test-Path -LiteralPath $catalog) {
foreach ($line in Get-Content -LiteralPath $catalog) {
Write-Output "read: $line"
}
}
$stock = @{}
foreach ($line in Get-Content -LiteralPath $catalog) {
$title, $copies = $line -split ','
$stock[$title] = [int]$copies
}
$stock.GetEnumerator() | Sort-Object Name | ForEach-Object { '{0} ({1})' -f $_.Name, $_.Value }
Remove-Item -LiteralPath $catalog
Output:
read: Dune,3
read: Emma,12
read: Ulysses,1
Dune (3)
Emma (12)
Ulysses (1)
Source: PowerShell documentation
VBScript is deprecated: it is a feature on demand that is being phased out, so a script that must outlive the current Windows releases should be migrated on a schedule, not left. The details are on
The VB Family. A Classic ASP site is migrated as a web application (next section), not as a script: the pages, the global.asa handlers and the Session and
Application state each need a modern counterpart, and every Response.Write of user input must be encoded in the new code.
.NET Framework to .NET 10
Visual Basic written for .NET Framework (version 4.8 or earlier) is already Visual Basic .NET, so most of the language carries over unchanged. What changes is the platform underneath: the project format, the libraries, the configuration system, and the technologies that do not exist on modern .NET.
Plan and tools
Microsoft’s porting guide for .NET Framework projects covers the sequence: assess the dependencies, convert the projects to SDK style, retarget, and fix what breaks. The tooling has changed recently:
-
The .NET Upgrade Assistant is officially deprecated. Its documentation page now points to the GitHub Copilot modernization chat agent, which analyses projects and dependencies, produces a step-by-step plan, applies targeted fixes and commits each change so that it can be reviewed or rolled back. The upgrade agent’s page names it GitHub Copilot upgrade and lists C# and Visual Basic projects of many types (class libraries, console apps, Windows Forms, WPF, ASP.NET Core, ASP.NET Web Forms and others) with .NET Framework (any version) as a source, and scenarios such as SDK-style conversion and the SqlClient upgrade. The documents do not name a release in which Visual Basic support began, and Visual Basic support for any particular scenario should be tried on a copy. Neither tool converts VB6.
-
Windows Forms and WPF have their own migration guides, which cover the designer, the project properties, the Application Framework (
Application.myapp) and theMynamespace. Visual Basic Windows Forms applications keep the Application Framework on .NET, as Windows Forms Applications shows. -
Pre-migration changes in the .NET Framework project reduce the work later: move from
packages.configtoPackageReference, remove unused references, update NuGet packages and turn on the analyzers.
Multi-target during the transition
A class library can target .NET Framework and .NET 10 at the same time, so that the old application keeps using it while the new one starts to. Option Strict On and a conditional-compilation constant let you isolate the few
differences. The SDK defines a constant for each target, such as NETFRAMEWORK and NET10_0_OR_GREATER used below (see
Conditional Compilation and Directives).
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net10.0</TargetFrameworks>
<RootNamespace>Shelf.Compat</RootNamespace>
<OptionStrict>On</OptionStrict>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
</Project>
Option Strict On
Public Module RuntimeInfo
Public Function Describe() As String
#If NETFRAMEWORK Then
Return "built for .NET Framework"
#ElseIf NET10_0_OR_GREATER Then
Return "built for .NET 10 or later"
#Else
Return "built for another target"
#End If
End Function
Public Function Join(values As IEnumerable(Of String)) As String
Return String.Join(", ", values)
End Function
End Module
Option Strict On
Imports Shelf.Compat
Module Program
Sub Main()
Console.WriteLine(RuntimeInfo.Describe())
Console.WriteLine(RuntimeInfo.Join({"Dune", "Emma"}))
End Sub
End Module
The library was built for both targets on Linux with the SDK’s reference assemblies (0 warnings, 0 errors). The net10.0 build was then run by a console project that references it, and printed:
built for .NET 10 or later
Dune, Emma
The net48 assembly was built, not run (running it needs .NET Framework or Mono, which the container does not have). It was inspected instead: the string table of the net48 assembly contains "built for .NET Framework" and
the net10.0 one contains "built for .NET 10 or later", which proves that each target compiled its own branch.
What is not available
Some .NET Framework technologies have no equivalent in .NET 6 and later. The table is taken from Microsoft’s list of unavailable technologies and tells you where Visual Basic code will stop compiling or working.
| Technology | On .NET 6 and later |
|---|---|
Application domains ( |
not supported; use separate processes or containers, and |
.NET Remoting |
not supported; use inter-process communication such as pipes, or network protocols such as HTTP |
Code access security and security transparency |
not supported as a boundary; use operating-system boundaries (containers, accounts) |
|
not supported |
Windows Workflow Foundation |
not supported; a community port exists |
WCF server |
not part of .NET; the community CoreWCF packages provide it (the WCF client libraries are available) |
ASP.NET Web Forms and |
not available on ASP.NET Core: see the next section |
Delegate |
throw |
|
not available (Example 1) |
Web Forms and web applications
ASP.NET Web Forms is a .NET Framework technology; it does not run on modern .NET, and ASP.NET Core has no Visual Basic project templates and no Razor syntax for Visual Basic. The options are:
-
Keep it on .NET Framework while it is supported (it is maintained with Windows). Visual Studio 2026 still offers the .NET Framework web application templates that Visual Basic can use.
-
Replace the pages with a C# ASP.NET Core application and keep the business logic in Visual Basic class libraries that the C# host references. This is the path the other pages of this section recommend because it needs no Visual Basic web templates; see Web, HTTP and Services for a tested C# host over a Visual Basic library.
-
Move to Blazor. The GitHub Copilot upgrade agent lists a Web Forms to Blazor scenario; Blazor components are written in C# and Razor.
The existing web sections of this site explain the framework side: Migrating to Modern ASP.NET, Migrating to ASP.NET Core and ASP.NET evolution.
Data access, serialization and HTTP
Several APIs that every .NET Framework Visual Basic application used have moved or have been withdrawn. These are the rows 13 to 16 of the table.
Imports System.Data.SqlClient
Dim cs As String = "Server=.;Database=Shelf;Integrated Security=SSPI"
Using conn As New SqlConnection(cs)
Dim cmd As New SqlCommand("SELECT Title FROM Books WHERE Isbn = '" & isbn & "'", conn)
...
End Using
The legacy shape above also has a security problem (string-built SQL). The tested replacement uses the Microsoft.Data.SqlClient package and a parameter. The example needs no database: it builds the connection string and the command, and
prints them. Note that Microsoft.Data.SqlClient encrypts the connection by default.
Option Strict On
Imports System.Data
Imports Microsoft.Data.SqlClient
Module M05SqlClient
Sub Main()
Dim builder As New SqlConnectionStringBuilder() With {
.DataSource = "db.example.test,1433",
.InitialCatalog = "Shelf",
.IntegratedSecurity = True
}
Console.WriteLine(builder.ConnectionString)
Console.WriteLine($"Encrypt defaults to {builder.Encrypt}")
Console.WriteLine($"Assembly: {GetType(SqlConnection).Assembly.GetName().Name}")
Using command As New SqlCommand("SELECT Title FROM Books WHERE Isbn = @isbn")
command.Parameters.Add("@isbn", SqlDbType.NVarChar, 17).Value = "978-0-306-40615-7"
For Each parameter As SqlParameter In command.Parameters
Console.WriteLine($"{parameter.ParameterName} {parameter.SqlDbType}({parameter.Size}) = {parameter.Value}")
Next
End Using
End Sub
End Module
Output:
Data Source=db.example.test,1433;Initial Catalog=Shelf;Integrated Security=True
Encrypt defaults to True
Assembly: Microsoft.Data.SqlClient
@isbn NVarChar(17) = 978-0-306-40615-7
The project file of that example references Microsoft.Data.SqlClient 6.1.2. For a real connection, the changes that surprise people are the default Encrypt=True (a server with a self-signed certificate needs a trusted certificate or an explicit,
deliberate setting) and the different package name in every Imports.
EF6 and LINQ to SQL are replaced by EF Core. EF6 (version 6.3 and later) can run on modern .NET, but new code uses EF Core, whose Visual Basic story has two limits: the designer and dotnet ef migrations add do not support Visual Basic projects, so
migrations live in a C# project, and some Visual Basic operators (such as Like) cannot be translated to SQL. The example below uses an in-memory SQLite database so that it can run anywhere.
Imports System.Data.Entity
Public Class ShelfContext
Inherits DbContext
Public Property Books As DbSet(Of BookRow)
End Class
Using db As New ShelfContext()
Dim available = From b In db.Books Where b.Copies > 0 Order By b.Title Select b
End Using
Option Strict On
Imports Microsoft.Data.Sqlite
Imports Microsoft.EntityFrameworkCore
Public Class BookRow
Public Property Id As Integer
Public Property Title As String = ""
Public Property Copies As Integer
End Class
Public Class ShelfContext
Inherits DbContext
Public Sub New(options As DbContextOptions(Of ShelfContext))
MyBase.New(options)
End Sub
Public Property Books As DbSet(Of BookRow)
End Class
Module M06EfCore
Sub Main()
Using connection As New SqliteConnection("Data Source=:memory:")
connection.Open()
Dim options = New DbContextOptionsBuilder(Of ShelfContext)().UseSqlite(connection).Options
Using context As New ShelfContext(options)
context.Database.EnsureCreated()
context.Books.Add(New BookRow With {.Title = "Dune", .Copies = 3})
context.Books.Add(New BookRow With {.Title = "Emma", .Copies = 0})
context.Books.Add(New BookRow With {.Title = "Ulysses", .Copies = 5})
Console.WriteLine($"saved {context.SaveChanges()} rows")
End Using
Using context As New ShelfContext(options)
Dim available = context.Books.
Where(Function(b) b.Copies > 0).
OrderBy(Function(b) b.Title).
ToList()
For Each row In available
Console.WriteLine($"{row.Id}: {row.Title} ({row.Copies})")
Next
End Using
End Using
End Sub
End Module
Output:
saved 3 rows
1: Dune (3)
3: Ulysses (5)
BinaryFormatter is removed: since .NET 9 its implementation is gone and it throws PlatformNotSupportedException, because the format cannot be made secure. Data that was written with it must be read once with a tool that still runs on .NET Framework, then
written again in an open format. The replacement for object graphs is System.Text.Json (or XmlSerializer or DataContractSerializer):
<Serializable>
Public Class BookDto
Public Title As String
End Class
Dim formatter As New BinaryFormatter()
formatter.Serialize(stream, book)
Option Strict On
Imports System.Text.Json
Public Class BookDto
Public Property Title As String = ""
Public Property Copies As Integer
Public Property Tags As List(Of String) = New List(Of String)()
End Class
Module M07Json
Sub Main()
Dim book As New BookDto With {.Title = "Dune", .Copies = 3}
book.Tags.Add("sci-fi")
Dim json = JsonSerializer.Serialize(book)
Console.WriteLine(json)
Dim back = JsonSerializer.Deserialize(Of BookDto)(json)
If back IsNot Nothing Then
Console.WriteLine($"{back.Title} / {back.Copies} / {String.Join("|", back.Tags)}")
End If
Dim pretty As New JsonSerializerOptions With {.WriteIndented = True, .PropertyNamingPolicy = JsonNamingPolicy.CamelCase}
Console.WriteLine(JsonSerializer.Serialize(book, pretty))
End Sub
End Module
Output:
{"Title":"Dune","Copies":3,"Tags":["sci-fi"]}
Dune / 3 / sci-fi
{
"title": "Dune",
"copies": 3,
"tags": [
"sci-fi"
]
}
Visual Basic has a limit here that C# does not: custom JsonConverter classes and the source-generator are awkward or impossible from Visual Basic, because Utf8JsonReader is a ref struct. When a model needs a custom converter, put it in a small C# project.
The reasons are on Serialization, JSON and XML.
HttpWebRequest (and WebClient) are obsolete (SYSLIB0014); the replacement is one long-lived HttpClient. The example runs with a stub message handler instead of a real server, so it needs no network:
Dim request As HttpWebRequest = CType(WebRequest.Create("https://catalog.example.test/books/1"), HttpWebRequest)
Using response As WebResponse = request.GetResponse()
Using reader As New StreamReader(response.GetResponseStream())
Console.WriteLine(reader.ReadToEnd())
End Using
End Using
Option Strict On
Imports System.Net
Imports System.Net.Http
Imports System.Threading
Public Class CannedHandler
Inherits HttpMessageHandler
Protected Overrides Function SendAsync(request As HttpRequestMessage, cancellationToken As CancellationToken) As Task(Of HttpResponseMessage)
Dim response As New HttpResponseMessage(HttpStatusCode.OK) With {
.Content = New StringContent("{""title"":""Dune""}")
}
Console.WriteLine($"handler saw {request.Method} {request.RequestUri}")
Return Task.FromResult(response)
End Function
End Class
Module M08HttpClientDemo
Async Function FetchAsync() As Task
Using client As New HttpClient(New CannedHandler())
client.BaseAddress = New Uri("https://catalog.example.test/")
Dim body = Await client.GetStringAsync("books/1")
Console.WriteLine($"body: {body}")
End Using
End Function
Sub Main()
FetchAsync().GetAwaiter().GetResult()
End Sub
End Module
Output:
handler saw GET https://catalog.example.test/books/1
body: {"title":"Dune"}
Visual Basic has no Async Main, so the program starts the asynchronous method from Sub Main and blocks on it once, at the entry point. See
Async and Await and Web, HTTP and Services.
Considering a move from Visual Basic to C#
Some teams decide to leave Visual Basic. That is a business decision, and nothing on this page argues for or against it: Visual Basic 17.13 is supported and stable. The decision is worth a structured look because it is expensive.
When it tends to pay off.
-
The application needs a technology that has no Visual Basic support: ASP.NET Core with Razor, Blazor, .NET MAUI, source generators and analyzers that you write,
Span<T>-heavy performance code, static abstract interface members, or the new language features that Visual Basic only consumes. -
Hiring and training: the team’s future developers know C#, and the cost of two languages is higher than the cost of conversion.
-
The code is going to be heavily reworked anyway, so a conversion is a smaller extra cost.
When it tends not to.
-
The code is stable libraries and Windows Forms or WPF screens that work and rarely change. Visual Basic on .NET 10 runs it, and the cost of conversion buys nothing the business notices.
-
The code uses features that have no direct C# form:
HandlesandWithEvents, XML literals, late binding (which C# has no counterpart for outsidedynamic), theMynamespace,Like,Select Caseranges, parameterized properties, case-insensitive identifiers, optional parameters that bake defaults into callers, andOn Error. Each of these needs a manual decision.
Mixed-language intermediate steps. You do not have to convert everything at once, because Visual Basic and C# projects reference each other in one solution (see Interop with C# and Other .NET Languages).
-
Put new code in C# in a new project and let it reference the Visual Basic libraries.
-
Convert leaf libraries with no dependants first, with the tests running before and after.
-
Convert the applications (the projects with the user interface) last, or never.
-
Use automated converters only for a first draft, review every file, and keep the tests; converters of this kind differ in quality, and this section does not endorse any.
The C# side of the comparison is in C# and .NET and in Visual Basic for Programmers of Other Languages.
The legacy to modern table
The table below is the same list that the cheat sheet prints as its "legacy to modern" strip, with the version in which the modern form became available, and a note of how it was verified on this page. "Example n" means that the Visual Basic code was built and run on this page. Rows with no Visual Basic code to run name the documentation that was read instead.
| # | Legacy | Modern (Visual Basic 17.13 on .NET 10) | Since | Verified by |
|---|---|---|---|---|
1 |
VB6 |
|
VB.NET 7.0 (2002) |
Example 2 |
2 |
|
|
VB.NET 7.0 |
Example 2 |
3 |
|
|
VB.NET 7.0 |
Example 2 |
4 |
|
|
VB.NET 7.0 |
Example 3 |
5 |
|
plain assignment |
VB.NET 7.0 |
Example 2 |
6 |
ByRef by default |
ByVal by default; write |
VB.NET 7.0 |
Example 2 |
7 |
|
|
VB.NET 7.0 |
Example 3 |
8 |
|
|
VB.NET 7.0 ( |
Example 4 |
9 |
FileSystemObject (FSO) |
|
VB.NET 7.0 |
Example 4 |
10 |
|
|
VB.NET 8.0 (2005, generics) |
Example 5 |
11 |
|
|
VB.NET 9.0 (2008) |
Example 5 |
12 |
|
implicit line continuation |
VB.NET 10.0 (2010) |
Example 5 |
13 |
|
|
modern .NET; 6.1.2 on .NET 10 here |
Example 9 |
14 |
EF6 / LINQ to SQL |
Entity Framework Core |
EF Core 10 on .NET 10 |
Example 10 |
15 |
|
|
|
Example 11 |
16 |
|
|
|
Example 12 |
17 |
Web Forms |
C# ASP.NET Core host with Visual Basic libraries |
ASP.NET Core; no Visual Basic web templates |
documentation: Web Forms and web applications |
18 |
Upgrade Assistant |
GitHub Copilot modernization |
Upgrade Assistant deprecated; the agent is included with Visual Studio 2026 and 2022 17.14.16 or later |
documentation: .NET Framework to .NET 10 |
19 |
VBScript |
PowerShell |
any supported PowerShell (7 here) |
Example 7 (run with PowerShell) |
Where the books are outdated
| Book habit | Today | Row or section |
|---|---|---|
Visual Basic .NET’s "backward compatibility" makes the move from VB6 easy |
It never was source compatible, and the VB6 compatibility library exists only on .NET Framework |
VB6 to Visual Basic .NET; Example 1 |
COM interop "does not work with .NET Core or .NET 5 and later" |
It works on Windows, in both directions; it is Windows-only |
|
|
Microsoft.Data.SqlClient and EF Core ( |
Rows 13 and 14; Examples 9 and 10 |
|
Removed in .NET 9; use System.Text.Json |
Row 15; Example 11 |
|
Obsolete (SYSLIB0014); use |
Row 16; Example 12 |
Web Forms, ASP.NET Core and SignalR "in VB.NET", cross-platform Windows Forms |
Web Forms is .NET Framework only; ASP.NET Core has no Visual Basic templates; Windows Forms and WPF are Windows only |
Web Forms and web applications |
Installing the .NET Framework on the target machine, UWP and Store deployment |
Framework-dependent publishing with the Desktop Runtime, self-contained or single-file publishing, MSIX |
|
The Upgrade Assistant as the tool for upgrading |
It is deprecated; GitHub Copilot modernization replaced it, and neither converts VB6 |
.NET Framework to .NET 10 |
Common mistakes
| Mistake | Correct form |
|---|---|
Expecting a tool to convert a VB6 project completely |
No tool does; plan a rewrite or an incremental, test-led replacement. |
Translating |
Use |
Carrying |
Give each variable a real type, and use |
Leaving parameters without a modifier and assuming |
The default is |
Relying on finalizers for cleanup |
Implement |
Keeping |
Move to Microsoft.Data.SqlClient, System.Text.Json and |
Assuming Web Forms can be ported with a tool and stay in Visual Basic |
Plan a new C# ASP.NET Core host with Visual Basic libraries, or keep the site on .NET Framework. |
Converting to C# to "modernize" without a reason |
Visual Basic 17.13 on .NET 10 is current and supported; convert only for a concrete technology, hiring or maintenance reason. |
Migrating the user interface first |
Migrate tested logic first, then data access, and the user interface last. |
References
-
Overview of upgrading .NET apps and .NET Framework technologies unavailable on .NET 6+
-
.NET Upgrade Assistant (deprecated) and GitHub Copilot upgrade and modernization
-
Blazor for Web Forms developers and Migrate from ASP.NET to ASP.NET Core
-
BinaryFormatter migration guide and BinaryFormatter removal in .NET 9
-
SYSLIB0014 and HttpClient
-
Support Statement for Visual Basic 6.0 on Windows and Windows deprecated features (VBScript)