What VB6-to-.NET 10 Modernization Requires

by DeeDee Walsh, on Sep 5, 2026, 8:06:40 PM

Visual Basic 6 survived Y2K, the dot-com crash, several generations of Windows, the cloud migration wave, and the end of Windows 10 support. Plenty of VB6 applications still run important parts of real businesses every day.

That longevity creates a false sense of security.

Microsoft continues to support the core VB6 runtime on Windows 11, but only as a 32-bit technology running under WOW64. The VB6 development environment is no longer supported, and the third-party ActiveX controls surrounding many applications have disappeared along with the companies that built them.

The application still runs. That doesn't make it easy to maintain, secure, integrate, or deploy.

Moving from VB6 to .NET 10 isn't a syntax problem. A translator can convert language constructs. It can't decide how to separate business logic from the user interface, replace obsolete components, preserve undocumented behavior, or restructure code around modern deployment and concurrency requirements.

You can't regex your way out of 25 years of architectural decisions.

COM Interop Is a Bridge, Not a Destination

A common migration strategy is to replace the application in pieces while allowing new .NET code to call existing VB6 COM components. That's super useful, but it comes with constraints that quickly shape the new architecture.

The first is process architecture. A 64-bit .NET process can't load a 32-bit VB6 in-process DLL. The .NET application has to either continue running as a 32-bit process or isolate the VB6 component in a separate 32-bit process and communicate across that boundary.

Either option preserves part of the old operating model.

Threading presents another complication. VB6 COM components use the Single-Threaded Apartment model. Calls from another COM apartment need to be marshaled back to the apartment that owns the object. COM handles most of this automatically, but the calls are probably serialized and then become a bottleneck when the application is under load or the owning thread is busy.

Then there is object lifetime.

VB6 COM objects use reference counting. .NET uses garbage collection and exposes COM objects through Runtime Callable Wrappers. The two lifetime models do not always release resources on the same schedule. Explicitly calling Marshal.ReleaseComObject can sometimes help when resources must be released in a precise order, but it's not a cleanup strategy to scatter throughout the code. Used incorrectly, it invalidates objects that other code's still using and cause failures that are really difficult to diagnose.

Variants and late binding add another layer. A VB6 Variant represents several different values and states, while IDispatch defers member resolution until runtime. Moving that behavior into strongly typed .NET code requires more than substituting object. The migration has to account for conversions, default properties, missing values, error handling, and the assumptions built into the original code.

Interop keeps a phased migration moving. It shouldn't quietly become the permanent architecture.

Rebuilding the Data Layer Without Changing Its Behavior

Most VB6 applications mix SQL, business rules, and interface behavior inside the same event handler. A button click might validate a field, build a SQL string, open an ADODB.Recordset, update several controls, and handle errors with On Error Resume Next.

That code's ugly, but it also contains years of accumulated business behavior.

The modernization work begins by separating those responsibilities. SQL belongs in a defined data-access layer. Business rules need explicit boundaries. User-interface code should coordinate interaction rather than own the entire transaction.

The replacement technology depends on the application.

Entity Framework Core works well when the team wants a richer domain model, change tracking, and LINQ-based queries. Dapper is often a better fit when control over SQL and predictable query behavior matter more. Microsoft.Data.SqlClient provides direct access when neither abstraction is appropriate.

The important changes are more fundamental than selecting a library:

  • Parameterize queries instead of concatenating SQL.
  • Introduce typed models in place of loosely structured rows and arrays.
  • Make database I/O asynchronous where it improves responsiveness or server scalability.
  • Limit result sets and stream large reads when the use case warrants it.
  • Preserve transaction boundaries, locking expectations, rounding rules, and null behavior.

Replacing an ADODB.Recordset with an IQueryable<T> doesn't automatically improve the application. It simply moves poorly understood behavior into a newer framework.

The job is to understand the old behavior first and then choose the modern pattern that fits it.

DoEvents Isn't an Async Strategy

VB6 was designed around a synchronous, event-driven desktop model. Developers used timers, callbacks, ActiveX EXEs, and other techniques to keep applications responsive, but the language had nothing equivalent to modern async and await.

That's why DoEvents appears in so many older applications. It lets Windows process pending messages while a long-running operation continues. It keeps the screen from appearing frozen, but it also allows another event to enter code that wasn't designed to be re-entered.

A direct translation preserves the problem. The resulting C# might compile, but it still behaves like VB6.

Modernization requires distinguishing between two different kinds of work.

Database calls, file operations, and network requests are usually I/O-bound. These are candidates for async and await, which allow the thread to perform other work while the application waits.

CPU-intensive calculations are different. They benefit from parallel execution, but only when the operations are independent and the surrounding state is thread-safe. Wrapping a legacy loop in Task.Run doesn't make it well designed. It might only move the blocking work and introduce race conditions.

Global variables, shared mutable state, hidden control dependencies, and assumptions about event order all have to be addressed before concurrency can be introduced safely.

Compilation's Only the First Test

A successful build proves that the compiler understands the new code. It doesn't prove the application still does the same work.

VB6 migrations regularly expose behavioral differences involving:

  • Null, Empty, Nothing, and zero-length strings
  • Numeric conversion, overflow, and rounding
  • Locale-dependent dates and decimal values
  • Default properties and implicit type coercion
  • On Error Resume Next
  • Control initialization and event ordering
  • Database cursor and transaction behavior
  • ActiveX control properties that have no direct modern equivalent

These details are where apparently successful migrations fail. A report still opens, but calculates a total differently. A screen loads, but fires validation in the wrong order. A method was translated, but nothing calls it anymore.

Modernization therefore needs several levels of validation: dependency coverage, compilation, automated tests, data reconciliation, user-interface behavior, and comparison against the original application.

If those checks happen only at the end, the team has already lost.

Moving Beyond Translation

This is the problem we built VELO to address.

At GAPVelocity AI, we use agentic AI to analyze the application as a connected system: its source code, call relationships, dependencies, data access, user-interface events, and legacy components. VELO then helps transform related parts of the application together and validate the resulting .NET code.

The goal is to preserve the business behavior people rely on while removing the constraints that make the application expensive and risky to change.

That still requires engineering judgment. No credible platform should pretend otherwise. Teams have to make decisions about architecture, user experience, data access, security, deployment, and which legacy behaviors should survive.

AI changes how much of the analysis, transformation, and validation can be automated. It doesn't eliminate the need to understand the application.

VB6 earned its reputation for survival. But survival is no longer the standard.

The real objective is to leave behind the 32-bit runtime dependencies, unsupported ActiveX inventory, apartment-threading constraints, and tightly coupled code without losing the business logic that made the application valuable in the first place.

Let the ActiveX controls rest. Keep the behavior. Modernize everything around it.

Topics:VB6.NET.NET 10

Comments

Subscribe to GAPVelocity AI Modernization Blog

FREE CODE ASSESSMENT TOOL