How to migrate VB.NET to C# - syntax conversion, project structure changes, COM dependencies, testing strategy, and tooling. A practical step-by-step guide.
How to migrate VB.NET to C# - syntax conversion, project structure changes, COM dependencies, testing strategy, and tooling. A practical step-by-step guide.
If you're running a VB.NET application and considering a migration to C#, the short answer is this: VB.NET and C# compile to the same .NET runtime, so migration is a code conversion and refactoring exercise rather than a platform change - but it's not trivial, and the complexity depends almost entirely on what your codebase does rather than how large it is. A 50,000-line VB.NET application with clean, straightforward business logic can migrate faster than a 15,000-line application with heavy use of late binding, optional parameters, and VB-specific runtime behaviors.
This guide walks through the assessment, the conversion process, the patterns that need manual attention, and the testing strategy that makes migration safe.
VB.NET is not dead - Microsoft still maintains it as part of .NET, and existing VB.NET applications will continue to run. The practical reasons to migrate are narrower but real:
Ecosystem convergence. The .NET ecosystem - tutorials, Stack Overflow answers, NuGet packages, AI coding tools, Microsoft documentation - has converged almost entirely on C#. New features like Span<T>, record types, pattern matching, and source generators are documented and exemplified in C#. VB.NET supports most of these but the documentation, examples, and community support are increasingly C#-first.
Hiring difficulty. Finding VB.NET developers is meaningfully harder than finding C# developers and has been getting harder for several years. Migrating to C# expands the available developer pool and makes onboarding new developers faster.
Tooling support. Some modern .NET tooling - .NET Aspire, Blazor, some code analysis tools - is C#-first or C#-only. A C# codebase has access to the full tooling ecosystem without workarounds.
The migration is contained. Unlike migrating from VB.NET to Python or Java (which changes the runtime), VB.NET to C# stays on the same .NET platform. The same Entity Framework Core models work, the same NuGet packages work, the same Azure services work. You're changing syntax and idioms, not infrastructure.
The assessment phase determines the migration's scope and complexity. Skipping it produces scope surprises that cost more time than the assessment would have.
What to measure:
Lines of code by file type. VB.NET projects typically contain .vb files (application code), .vbproj files (project configuration), and sometimes .aspx or .ascx files if the project includes ASP.NET Web Forms. Count separately - Web Forms UI files require different handling from pure VB.NET business logic.
VB.NET-specific feature usage. Some VB.NET features have no direct C# equivalent and require pattern changes rather than syntax substitution:
| VB.NET Feature | Migration Complexity | C# Approach |
|---|---|---|
| On Error GoTo / On Error Resume Next | Medium | Rewrite as try/catch blocks |
| Late binding (Dim x As Object) | High | Introduce proper types or dynamic (avoid where possible) |
| Optional parameters with Optional/IsMissing | Low | C# optional parameters or overloads |
| My namespace (My.Application, My.Settings) | Medium | Replace with .NET BCL equivalents |
| WithEvents / event handlers | Low | C# event syntax |
| IsNothing() / IsNumeric() / IsDate() | Low | is null / int.TryParse() / DateTime.TryParse() |
| String comparison with = operator | Low | .Equals() or == with explicit culture |
| Dim with implicit typing | Low | var in C# |
| Module (static class equivalent) | Low | static class |
| Integer / Long / String types | Low | int / long / string |
| Default properties (indexers) | Medium | Explicit indexer implementation |
COM dependency inventory. If the VB.NET application calls COM objects - early or late binding - these need individual assessment. COM dependencies are the most common cause of VB.NET migration complexity and timeline overrun.
Third-party package compatibility. Any VB.NET-specific libraries (Microsoft.VisualBasic namespace usage) need replacing. Most functionality in Microsoft.VisualBasic has a direct .NET BCL equivalent, but it needs mapping explicitly.
Output from the assessment:
There are two realistic approaches for most VB.NET-to-C# migrations. The right choice depends on codebase size and complexity.
Approach A: Automated Conversion + Manual Review
Automated VB.NET-to-C# converters translate syntax mechanically. The output is rarely production-quality but provides a starting point that's faster than writing C# from scratch.
Tools:
The converter workflow:
What automated converters handle well: Basic syntax (variable declarations, loops, conditionals, method signatures, class structure), simple string operations, arithmetic, access modifiers.
What they handle poorly: Late binding, On Error Resume Next patterns, Microsoft.VisualBasic runtime function usage, implicit type conversions VB.NET allows but C# doesn't, and context-dependent operator behaviour differences.
Approach B: Manual Rewrite by Module
For applications where the business logic is complex, the VB.NET code uses heavy late binding, or the conversion output from automated tools requires more review effort than a direct rewrite, a module-by-module manual rewrite is more reliable.
When to choose this approach:
VB.NET and C# project files have the same structure in modern SDK-style .csproj format, making the project conversion straightforward:
VB.NET project file (.vbproj):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>MyApp</RootNamespace>
</PropertyGroup>
</Project>Converted C# project file (.csproj):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>MyApp</RootNamespace>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>The only meaningful changes: the file extension (.vbproj → .csproj), the language version (implicit in the SDK), and the addition of <Nullable>enable</Nullable> - C#'s nullable reference types, which have no VB.NET equivalent but are a significant safety improvement worth enabling from the start.
Variable Declarations and Types
' VB.NET Dim name As String = "Facile" Dim count As Integer = 0 Dim price As Decimal = 9.99D Dim isActive As Boolean = True Dim items As New List(Of String)() Dim result As Object ' late binding - needs specific type in C#
// C# string name = "Facile"; // or: var name = "Facile"; int count = 0; decimal price = 9.99m; bool isActive = true; var items = new List<string>(); object result; // or introduce the proper type
Control Flow
' VB.NET
If x > 0 Then
DoSomething()
ElseIf x < 0 Then
DoSomethingElse()
Else
DoDefault()
End If
For i As Integer = 0 To 9
Console.WriteLine(i)
Next
For Each item As String In items
Console.WriteLine(item)
Next
Select Case status
Case "Active"
ActivateAccount()
Case "Inactive"
DeactivateAccount()
Case Else
HandleUnknown()
End Select// C#
if (x > 0)
{
DoSomething();
}
else if (x < 0)
{
DoSomethingElse();
}
else
{
DoDefault();
}
for (int i = 0; i <= 9; i++)
{
Console.WriteLine(i);
}
foreach (string item in items)
{
Console.WriteLine(item);
}
switch (status)
{
case "Active":
ActivateAccount();
break;
case "Inactive":
DeactivateAccount();
break;
default:
HandleUnknown();
break;
}
// Or C# pattern matching (cleaner for modern code):
switch (status)
{
case "Active" => ActivateAccount(),
case "Inactive" => DeactivateAccount(),
_ => HandleUnknown()
};Error Handling
This is the most important conversion in most VB.NET codebases. On Error Resume Next - which silently ignores all errors - has no C# equivalent and should not be emulated. Each instance needs individual analysis:
' VB.NET - silently ignores errors
On Error Resume Next
Dim result = SomeOperation()
If Err.Number <> 0 Then
LogError(Err.Description)
Err.Clear()
End If
' VB.NET - structured error handling
Try
result = SomeOperation()
Catch ex As Exception
LogError(ex.Message)
End Try// C# - always use try/catch
try
{
var result = SomeOperation();
}
catch (SpecificException ex) // catch specific exceptions where possible
{
_logger.LogError(ex, "Operation failed");
// handle or rethrow
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error in SomeOperation");
throw; // rethrow unexpected exceptions - don't swallow them
}The On Error Resume Next problem: When converting a VB.NET codebase that uses On Error Resume Next pervasively, each instance requires understanding what would happen if the operation fails. Some are genuinely intentional (file existence checks where "file not found" is expected) - these should become explicit checks with File.Exists() or similar. Others are accidental error suppression - these should become proper try/catch blocks with logging.
String Operations
' VB.NET Dim result As String = Left(name, 5) Dim found As Integer = InStr(name, "abc") Dim upper As String = UCase(name) Dim trimmed As String = Trim(name) Dim combined As String = name & " " & surname
// C#
string result = name[..5]; // or name.Substring(0, 5)
int found = name.IndexOf("abc") + 1; // InStr is 1-based; IndexOf is 0-based
string upper = name.ToUpper();
string trimmed = name.Trim();
string combined = $"{name} {surname}"; // string interpolationThe Microsoft.VisualBasic Namespace
VB.NET code often uses functions from the Microsoft.VisualBasic namespace - IsNumeric(), IsDate(), Format(), DateDiff(), Left(), Right(), Mid(). Replace these with .NET BCL equivalents:
| VB.NET | C# Equivalent |
|---|---|
| IsNumeric(x) | int.TryParse(x, out _) or double.TryParse(x, out _) |
| IsDate(x) | DateTime.TryParse(x, out _) |
| IsNothing(x) | x is null |
| Format(d, "dd/MM/yyyy") | d.ToString("dd/MM/yyyy") |
| DateDiff(DateInterval.Day, d1, d2) | (d2 - d1).Days |
| Left(s, n) | s[..n] or s.Substring(0, n) |
| Right(s, n) | s[^n..] or s.Substring(s.Length - n) |
| Mid(s, start, len) | s.Substring(start - 1, len) (InStr is 1-based) |
| StrConv(s, vbProperCase) | CultureInfo.CurrentCulture.TextInfo.ToTitleCase(s) |
| Now | DateTime.Now |
| Today | DateTime.Today |
Late binding - calling methods on Object-typed variables without compile-time type checking - is common in older VB.NET code and has no safe direct equivalent in C#.
' VB.NET late binding
Dim obj As Object = CreateObject("Excel.Application")
obj.Visible = True
obj.Workbooks.Add()Three conversion options:
Option 1: Introduce the proper type (best choice when possible)
// Add reference to Microsoft.Office.Interop.Excel using Excel = Microsoft.Office.Interop.Excel; var excel = new Excel.Application(); excel.Visible = true; excel.Workbooks.Add();
Option 2: Use dynamic (temporary bridge - avoids compiler errors but loses type safety)
dynamic obj = Activator.CreateInstance(
Type.GetTypeFromProgID("Excel.Application")!);
obj.Visible = true;
obj.Workbooks.Add();Option 3: Replace the COM dependency entirely (best long-term choice for non-Office COM)
// Replace Excel COM automation with EPPlus or ClosedXML
using var package = new ExcelPackage();
var sheet = package.Workbook.Worksheets.Add("Sheet1");
// No COM required - pure .NETFor most production migrations, Option 3 is the correct long-term approach. COM interop in .NET works but creates deployment complexity (requires COM components installed on the server) and has no benefit over modern .NET libraries for most use cases.
Testing is what makes a VB.NET to C# migration safe. Without tests, you have no reliable way to confirm that the C# code produces the same outputs as the VB.NET code for the same inputs.
Establish a baseline before converting:
The most effective approach is black-box testing at the API or function level before migration begins - capturing what the VB.NET application does for a representative set of inputs. These become the acceptance tests for the migrated C# code.
Three testing levels for migration:
Unit tests on converted business logic:
[Fact]
public void CalculateDiscount_ReturnsCorrectAmount_ForPremiumCustomer()
{
// Same test, now running against C# implementation
var service = new PricingService();
var result = service.CalculateDiscount(100m, CustomerTier.Premium);
Assert.Equal(15m, result); // matches known VB.NET output
}Integration tests on data access:
After conversion, run the full data access layer against a test database with known data - confirm that EF Core (or Dapper) queries return the same results as the original ADO.NET queries in the VB.NET code.
Side-by-side comparison testing:
For applications where automated tests are impractical to write before the migration (common in legacy codebases), run the VB.NET and C# versions simultaneously against the same inputs and compare outputs. This requires both versions to be runnable at the same time - possible during a phased migration.
The migration to C# creates a natural opportunity to modernize code patterns - but it's important to separate migration (VB.NET → C#, same behaviour) from modernization (improving the design), because mixing them makes validation harder.
Patterns worth modernizing during migration:
Patterns to defer until after migration is validated:
The rule: if the change makes the C# code behave differently from the VB.NET code, it's modernization, not migration. Do migration first, validate it against the VB.NET baseline, then modernize in a separate phase.
How long does a VB.NET to C# migration take?
For a clean VB.NET codebase (minimal late binding, structured error handling, no COM dependencies) of around 20,000 lines, an experienced C# developer can convert approximately 500–1,000 lines per day using automated conversion plus manual review. A 20,000-line project might take 4–6 weeks. COM-heavy or late-binding-heavy codebases take significantly longer - the assessment phase is what determines the real estimate, not the line count alone.
Should I migrate to C# and .NET 8 at the same time?
These are separable concerns and are best separated. VB.NET targeting .NET 6 can be migrated to C# still targeting .NET 6, then separately upgraded to .NET 8. Combining both changes makes validation harder - a failing test could be caused by the language change or the framework version change, and knowing which matters for fixing it. Migrate language first, validate, then upgrade the target framework.
Can I have VB.NET and C# in the same solution?
Yes - Visual Studio supports solutions containing both VB.NET and C# projects, and they can reference each other freely since both compile to the same .NET intermediate language. This enables a phased migration where some projects are converted while others remain VB.NET, useful for large solutions where converting everything at once isn't practical.
What are the biggest risks in a VB.NET to C# migration?
The three most common risk areas are: On Error Resume Next patterns that silently suppress errors in VB.NET code - when these are properly converted to C# exception handling, previously hidden errors may surface for the first time; late binding code that the VB.NET compiler allowed but that doesn't translate cleanly to strongly-typed C#; and VB.NET-specific string and date functions behaving slightly differently from their C# BCL equivalents (particularly around culture-sensitive string comparisons and date formatting). All three are manageable with thorough testing.
Is there a tool that fully automates VB.NET to C# conversion?
Partial automation only. Tools like Telerik Code Converter handle syntax translation well but cannot handle semantic differences - late binding, error handling patterns, implicit type conversions, and VB.NET runtime behavior. Every automated conversion output needs review before being considered production-ready. For complex codebases, manual review of converter output often takes as long as a direct rewrite.
After migrating to C#, should we also move from .NET Framework to .NET 8?
Usually yes, but separately and after the language migration is validated. .NET 8 provides better performance, current security patches, and access to the full modern .NET ecosystem. The migration path from .NET Framework to .NET 8 is well-documented and supported by Microsoft's .NET Upgrade Assistant tooling. Running both migrations sequentially - VB.NET to C# first, then .NET Framework to .NET 8 - is lower risk than attempting both simultaneously.
Migrating from VB.NET to C# is one of the more contained legacy modernization projects available to .NET teams - the platform stays the same, the tooling stays the same, and the migration is primarily a syntax and idiom conversion. The complexity is real but bounded, and the outcome is a codebase with significantly broader hiring availability, better tooling support, and access to the full modern .NET ecosystem.
If you're planning a VB.NET to C# migration and want an experienced team to assess scope and lead the work, our VB.NET development and migration team has delivered migrations across a range of codebase sizes and complexity levels