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 namespace ShelfApp). 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.

flowchart TD vb6["VB6 application with forms and COM components"] vba["VBA macros inside Office documents"] vbs["VBScript in Windows Script Host or Classic ASP"] fx["Visual Basic .NET on .NET Framework"] web["Web Forms application on .NET Framework and IIS"] keep["Keep running while the runtime is supported, isolated behind a COM boundary"] stayvba["Stay in VBA, supported with Office"] ps["PowerShell script or a .NET console tool"] modern["Visual Basic on .NET 10: console, libraries, Windows Forms, WPF"] host["C# ASP.NET Core host with Visual Basic libraries"] cs["Optional: C# in a mixed solution, then conversion"] vb6 -->|"manual rewrite, screen by screen"| modern vb6 -->|"incremental, behind a COM bridge"| keep keep -->|"replace one component at a time"| modern vba -->|"move logic to a library and drive Office from .NET"| modern vba -->|"or leave it"| stayvba vbs -->|"administration scripts"| ps vbs -->|"scripts that became applications"| modern fx -->|"porting guide and GitHub Copilot upgrade"| modern web -->|"replace the pages, keep the logic"| host host -->|"libraries stay Visual Basic"| modern modern -->|"only if the team decides to"| cs host -->|"or move the host and the libraries"| cs

Read it as a checklist:

  1. 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.

  2. 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.

  3. 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).

  4. 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 Handles and AddHandler event model replaces the name-based Control_Event handlers (see Events and Handlers); default properties (Text1 = "x") need the property name; control arrays become collections of controls with one shared handler.

Example 1: the compatibility library is not on .NET 10 (built and run)
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)

Legacy VB6, not built and not run
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
Example 2: Short and Integer, Object, Structure, assignment and ByVal (built and run)
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. Short is the 16-bit type, Integer the 32-bit one, Long the 64-bit one. Review every old Integer that is stored in a file or sent over a wire: its width may have to stay 16 bits, which means Short.

  • Variant to Object. Object holds an Integer and then a String, but CInt(anything) + 1 needs an explicit conversion. Without it, Option Strict On stops the program at build time with BC30038. The better migration is to give the variable a real type or a generic parameter, and keep Object for the rare heterogeneous value.

  • Type to Structure. A Structure is a value type, so second = first copies it and changing second leaves first alone. A Class is a reference type: after shelf2 = shelf1 both names refer to one object, which is what Set shelf2 = shelf1 meant in VB6. The Set keyword is gone; plain assignment does both jobs.

  • ByRef by default to ByVal. The Sub without a modifier did not change the caller’s variable; the ByRef one did.

Error handling and GoSub (rows 4 and 7)

Legacy VB6, not built and not run
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
Example 3: Try/Catch/Finally, TryParse and a plain Sub (built and run)
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

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)

Legacy VB6, not built and not run
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
Example 4: StreamWriter, StreamReader and System.IO (built and run)
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

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)

Legacy VBScript and VB6, not built and not run
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
Example 5: Dictionary, Collection, If() and implicit line continuation (built and run)
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) replaces CompareMode. Reading a missing key throws KeyNotFoundException instead of silently adding it; TryGetValue is the way to ask. Both outputs above are from the real run. The old Microsoft.VisualBasic.Collection still 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, and If(value, fallback) with two arguments is the null-coalescing form (VB has no ?? operator). The old IIf function evaluates both arguments; this was verified in a scratch program in which the false branch had a side effect and IIf ran it while If did not. IIf returns Object, so under Option Strict On it 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:

Example 6: deterministic cleanup with IDisposable and Using (built and run)
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. Variant arrays, Long that meant 32 bits and Declare statements without PtrSafe and LongPtr are 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.

Example 7: PowerShell version of the VBScript file reader (not Visual Basic; run with PowerShell)
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)

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 the My namespace. 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.config to PackageReference, 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).

Shelf.Compat.vbproj: one library, two targets
<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>
Example 8: RuntimeInfo.vb, one source file with a conditional section (built for both targets)
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
The console project’s Program.vb
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 (CreateDomain)

not supported; use separate processes or containers, and AssemblyLoadContext to load assemblies dynamically

.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)

System.EnterpriseServices (COM+)

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 System.Web

not available on ASP.NET Core: see the next section

Delegate BeginInvoke and EndInvoke

throw PlatformNotSupportedException; use Task and Async/Await

Microsoft.VisualBasic.Compatibility.VB6

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.

Before (.NET Framework era), not built and not run
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.

Example 9: Microsoft.Data.SqlClient with a parameter (built and run; no server is contacted)
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.

Before (.NET Framework era), not built and not run
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
Example 10: EF Core with SQLite (built and run)
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):

Before (.NET Framework era), not built and not run
<Serializable>
Public Class BookDto
    Public Title As String
End Class

Dim formatter As New BinaryFormatter()
formatter.Serialize(stream, book)
Example 11: System.Text.Json (built and run)
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:

Before (.NET Framework era), not built and not run
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
Example 12: HttpClient with a stub handler (built and run)
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: Handles and WithEvents, XML literals, late binding (which C# has no counterpart for outside dynamic), the My namespace, Like, Select Case ranges, parameterized properties, case-insensitive identifiers, optional parameters that bake defaults into callers, and On 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).

  1. Put new code in C# in a new project and let it reference the Visual Basic libraries.

  2. Convert leaf libraries with no dependants first, with the tests running before and after.

  3. Convert the applications (the projects with the user interface) last, or never.

  4. 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 Integer/Long (16/32-bit)

Short/Integer (Long is 64-bit)

VB.NET 7.0 (2002)

Example 2

2

Variant

Object plus Option Strict On (better: a real type or a generic)

VB.NET 7.0

Example 2

3

Type ... End Type

Structure ... End Structure

VB.NET 7.0

Example 2

4

On Error GoTo / Resume Next

Try/Catch/Finally

VB.NET 7.0

Example 3

5

Set obj = ...

plain assignment obj = ...

VB.NET 7.0

Example 2

6

ByRef by default

ByVal by default; write ByRef when needed

VB.NET 7.0

Example 2

7

GoSub and Return

Sub and Function procedures with parameters

VB.NET 7.0

Example 3

8

Open ... For Output As #1

StreamWriter or File.WriteAllText (My.Computer.FileSystem only in Windows Forms projects)

VB.NET 7.0 (My: 8.0)

Example 4

9

FileSystemObject (FSO)

System.IO (File, Directory, Path)

VB.NET 7.0

Example 4

10

Scripting.Dictionary

Dictionary(Of TKey, TValue)

VB.NET 8.0 (2005, generics)

Example 5

11

IIf(c, a, b)

If(c, a, b), which evaluates one branch

VB.NET 9.0 (2008)

Example 5

12

_ at the end of every continued line

implicit line continuation

VB.NET 10.0 (2010)

Example 5

13

System.Data.SqlClient

Microsoft.Data.SqlClient (NuGet package)

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

BinaryFormatter

System.Text.Json (or XmlSerializer, DataContractSerializer)

BinaryFormatter removed in .NET 9; System.Text.Json since .NET Core 3.0

Example 11

16

HttpWebRequest

HttpClient

HttpWebRequest obsolete (SYSLIB0014) since .NET 6

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

COM Interop and Office Automation

System.Data.SqlClient, LINQ to SQL and EF6 Enable-Migrations

Microsoft.Data.SqlClient and EF Core (dotnet ef migrations add, with a C# migrations project for Visual Basic)

Rows 13 and 14; Examples 9 and 10

BinaryFormatter for saving objects

Removed in .NET 9; use System.Text.Json

Row 15; Example 11

HttpWebRequest and WebRequest.Create

Obsolete (SYSLIB0014); use HttpClient

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

Libraries, Packaging and Deployment

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 On Error line by line

Use Try/Catch/Finally, TryParse and Using; reserve On Error for code that has to stay as it is.

Carrying Variant over as Object everywhere

Give each variable a real type, and use Option Strict On so that conversions are visible.

Leaving parameters without a modifier and assuming ByRef

The default is ByVal; write ByRef where the callee must change the argument.

Relying on finalizers for cleanup

Implement IDisposable and use Using.

Keeping System.Data.SqlClient, BinaryFormatter or HttpWebRequest

Move to Microsoft.Data.SqlClient, System.Text.Json and HttpClient.

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.