Early and Late Binding

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. Every program on it was built (with warnings treated as errors, except the ones that are meant to fail) and run in the .NET 10 SDK container (Linux, arm64); the programs that are meant to fail show the compiler messages the SDK printed. The one place where a program was not run in full is the Excel automation branch of the late-bound file, which needs Windows: it is compiled on every build, guarded with OperatingSystem.IsWindows(), and labelled "built, not run" where it appears. The root namespace of the scratch project is LateApp, because the CLR and compiler messages include it.

Binding is the step that connects the name in item.Title to a real member. When the compiler does it, the call is early bound; when the decision is deferred to run time, the call is late bound. Visual Basic is unusual among .NET languages because late binding is an implicit, per-file mode of the ordinary dot operator: with Option Strict Off, a member access on an Object variable compiles and is resolved by the run-time binder, with no dynamic keyword. That makes it the most visible difference between a Visual Basic file that is safe and one that merely looks safe. This section of the site uses Option Strict On everywhere (see Option Statements); this is the only page that shows Option Strict Off, in a single labelled file (LateBinding.vb, below), so that you can see what the switch really does and why you keep it out of the rest of the project.

Early and late binding at a glance

Form How the member is found What it costs and what it gives

Early binding

The compiler looks the member up in the static type of the expression, checks arguments, and emits a direct call (or a virtual or interface call)

Fastest; every mistake is a compile-time error; refactoring tools, IntelliSense and trimming see the call

Late binding (Option Strict Off, Object receiver)

The compiler emits a call to the run-time helper NewLateBinding; the member name is a string, matched at run time by reflection and the Visual Basic rules (case-insensitive, narrowing conversions allowed)

Slow (see the measurements below); a typo is a MissingMemberException in production; invisible to the trimmer; the only way to talk to a COM object that has no type library

Name-based calls (CallByName, Type.InvokeMember, MethodInfo.Invoke)

The same run-time lookup, written as an explicit function call

Allowed under Option Strict On, so the risk is visible in the source; the same performance and trimming problems

Dynamic objects (ExpandoObject, DynamicObject, C# dynamic providers)

The receiver itself decides, through IDynamicMetaObjectProvider

Needs a late-bound call site on the Visual Basic side; the receiver chooses semantics

The comparison with C# is short. In C#, a member access on object is always an error; late binding is requested per variable with the dynamic type, and the run-time binder is the C# one. In Visual Basic there is no dynamic keyword: the Object type plays that part in files compiled with Option Strict Off. A C# member declared dynamic appears to Visual Basic as plain Object. Visual Basic 6 and VBA had the same implicit behaviour for Object and for Variant, and no Option Strict at all, which is why so much old Visual Basic code is late bound without its authors knowing.

The scratch project has one .vbproj and the files below. Only LateBinding.vb has Option Strict Off at the top; the project itself sets OptionStrict to On, so a file that says nothing is strict.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>LateApp</RootNamespace>
    <OptionExplicit>On</OptionExplicit>
    <OptionStrict>On</OptionStrict>
    <OptionInfer>On</OptionInfer>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
</Project>
File Role

Domain.vb

the Shelf domain (Isbn, Book, Magazine, …​), copied from the reference solution; Option Strict On

Program.vb

calls each demonstration in turn; Option Strict On

EarlyBinding.vb

the strict, early-bound way to do each thing; Option Strict On

CallByNameDemo.vb, ReflectionDemo.vb

name-based access under Option Strict On

Settings.vb

dynamic helper classes and a structure used by the late-bound file; Option Strict On

LateBinding.vb

the only file with Option Strict Off

Performance.vb

times five ways of making the same call; Option Strict On

Program.vb is the entry point. Its output, section by section, is quoted through the page.

Option Strict On

Module Program
    Sub Main()
        Console.WriteLine("-- early binding")
        EarlyBinding.Run()
        Console.WriteLine("-- CallByName under Option Strict On")
        CallByNameDemo.Run()
        Console.WriteLine("-- reflection under Option Strict On")
        ReflectionDemo.Run()
        Console.WriteLine("-- late binding (LateBinding.vb)")
        LateBinding.Run()
        Console.WriteLine("-- relative cost, one million calls")
        Performance.Run()
        Console.WriteLine("-- a typed facade over the late-bound file")
        Console.WriteLine(LateBinding.DescribeAny(New Book("Emma", 1815, Isbn.Parse("9780306406157"), New Author("Jane Austen"))))
        Console.WriteLine(LateBinding.DescribeAny(New Magazine("Byte", 1982, 7)))
    End Sub
End Module

What Option Strict On forbids

Option Strict controls three separate things, and the compiler message tells you which one you hit.

Rule under Option Strict On Error Why the rule exists

No late binding: a member access on Object is an error

BC30574

the compiler cannot check that the member exists

No implicit narrowing conversions: Dim n As Integer = 3.7, number to or from String, Object to anything else

BC30512

a narrowing conversion can lose data or throw; you must write CInt, CType, DirectCast or a parse call

Every declaration needs an As clause (when Option Infer is off) and every Function, Property and Operator needs a return type

BC30209, BC30210

a missing type silently means Object

No Object operands for most operators, and = on Object is refused

BC30038, BC32013

the operator would be late bound

For Each over an Object expression

BC32023

the collection type is unknown

Each error below comes from a short program that was built on its own (the whole file is shown, so the line and column numbers match). The first has three late-bound uses of a String held in an Object variable: a property read, a method call, and an index.

Option Strict On

Module Program
    Sub Main()
        Dim item As Object = "text"
        Console.WriteLine(item.Length)
        item.Trim()
        Dim n As Object = item(0)
    End Sub
End Module
Program.vb(6,27): error BC30574: Option Strict On disallows late binding.
Program.vb(7,9): error BC30574: Option Strict On disallows late binding.
Program.vb(8,27): error BC30574: Option Strict On disallows late binding.

Source: BC30574

Narrowing is the second rule. Each of the four lines below compiles under Option Strict Off and does something surprising; under Option Strict On each is an error that names the two types:

Option Strict On

Module Program
    Sub Main()
        Dim rounded As Integer = 3.7
        Dim parsed As Double = "3.5"
        Dim shown As String = 42
        Dim number As Integer = "12"
    End Sub
End Module
Program.vb(5,34): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Program.vb(6,32): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Double'.
Program.vb(7,31): error BC30512: Option Strict On disallows implicit conversions from 'Integer' to 'String'.
Program.vb(8,33): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Integer'.

Source: BC30512

(The surprises: 3.7 becomes 4 by rounding, not truncation; "3.5" is parsed with the current culture; 42 becomes "42"; and "12" is parsed.) The third rule applies when Option Infer is off, as it is in this file (Option Infer is on by default in SDK projects, so Dim x = 1 normally infers Integer). The compiler also reports the unused variable, because warnings are not errors in this scratch project:

Option Strict On
Option Infer Off

Module Program
    Sub Main()
        Dim x = 1
        Dim y
    End Sub
End Module
Program.vb(6,13): error BC30209: Option Strict On requires all variable declarations to have an 'As' clause.
Program.vb(7,13): error BC30209: Option Strict On requires all variable declarations to have an 'As' clause.
Program.vb(7,13): warning BC42024: Unused local variable: 'y'.

Source: BC30209

The same rule applies to results: a method or property with no As clause returns Object, which is then late bound at every call.

Option Strict On

Module Program
    Function Twice(n As Integer)
        Return n * 2
    End Function

    ReadOnly Property Label
        Get
            Return "x"
        End Get
    End Property

    Sub Main()
    End Sub
End Module
Program.vb(4,14): error BC30210: Option Strict On requires all Function, Property, and Operator declarations to have an 'As' clause.
Program.vb(8,23): error BC30210: Option Strict On requires all Function, Property, and Operator declarations to have an 'As' clause.

Source: BC30210

Operators on Object operands are the fourth rule. Object + Object would run Operators.AddObject at run time, which looks at the actual types and may add numbers, concatenate strings or throw. Object = Object is rejected with a hint about reference identity (Is):

Option Strict On

Module Program
    Sub Main()
        Dim a As Object = 1
        Dim b As Object = 2
        Console.WriteLine(a + b)
        Console.WriteLine(a = b)
        Console.WriteLine(a < b)
        Console.WriteLine(a And b)
    End Sub
End Module
Program.vb(7,27): error BC30038: Option Strict On prohibits operands of type Object for operator '+'.
Program.vb(7,31): error BC30038: Option Strict On prohibits operands of type Object for operator '+'.
Program.vb(8,27): error BC32013: Option Strict On disallows operands of type Object for operator '='. Use the 'Is' operator to test for object identity.
Program.vb(8,31): error BC32013: Option Strict On disallows operands of type Object for operator '='. Use the 'Is' operator to test for object identity.
Program.vb(9,27): error BC30038: Option Strict On prohibits operands of type Object for operator '<'.
Program.vb(9,31): error BC30038: Option Strict On prohibits operands of type Object for operator '<'.
Program.vb(10,27): error BC30038: Option Strict On prohibits operands of type Object for operator 'And'.
Program.vb(10,33): error BC30038: Option Strict On prohibits operands of type Object for operator 'And'.

Source: BC30038 and BC32013

For Each needs a known collection type, so an Object variable cannot be enumerated even when it holds an array, with or without an element type:

Option Strict On

Module Program
    Sub Main()
        Dim items As Object = New Integer() {1, 2}
        For Each n In items
            Console.WriteLine(n)
        Next
        For Each n As Integer In items
            Console.WriteLine(n)
        Next
    End Sub
End Module
Program.vb(6,23): error BC32023: Expression is of type 'Object', which is not a collection type.
Program.vb(9,34): error BC32023: Expression is of type 'Object', which is not a collection type.

Source: BC32023

Two last cases. A method call on a variable that is declared Object is late bound even when the object is a StringBuilder, and the indexer of a Hashtable returns Object, which needs a conversion before it can become an Integer:

Option Strict On

Module Program
    Sub Main()
        Dim book As Object = New System.Text.StringBuilder()
        book.Append("x")
        Dim d As New System.Collections.Hashtable()
        Dim v As Integer = d("k")
    End Sub
End Module
Program.vb(6,9): error BC30574: Option Strict On disallows late binding.
Program.vb(8,28): error BC30512: Option Strict On disallows implicit conversions from 'Object' to 'Integer'.

Early-bound code gets back what these errors take away by a cast. The fix is almost always one of the forms below, and EarlyBinding.vb shows them on the Shelf domain:

Option Strict On

Module EarlyBinding
    Sub Run()
        Dim isbn As Isbn = Isbn.Parse("9780306406157")
        Dim book As New Book("Dune", 1965, isbn, New Author("Frank Herbert"))

        ' Every member is resolved by the compiler against the static type.
        Console.WriteLine(book.Title)
        book.Title = "Dune Messiah"
        Console.WriteLine(book.Describe())

        ' Through the base class: the call is still bound at compile time (a virtual call).
        Dim item As CatalogItem = book
        Console.WriteLine(item.Describe())

        ' An Object variable needs a cast to get early binding back.
        Dim anything As Object = book
        Dim asBook As Book = DirectCast(anything, Book)
        Console.WriteLine(asBook.Kind)
        Dim asMagazine As Magazine = TryCast(anything, Magazine)
        Console.WriteLine(asMagazine Is Nothing)
        Try
            Dim wrong As Magazine = DirectCast(anything, Magazine)
            Console.WriteLine(wrong.IssueNumber)
        Catch ex As InvalidCastException
            Console.WriteLine(ex.Message)
        End Try
        If TypeOf anything Is CatalogItem Then
            Console.WriteLine(DirectCast(anything, CatalogItem).Year)
        End If
    End Sub
End Module

The output (the early binding section of the run):

Dune
Book: Dune Messiah (1965) by Frank Herbert
Book: Dune Messiah (1965) by Frank Herbert
Book
True
Unable to cast object of type 'LateApp.Book' to type 'LateApp.Magazine'.
1965
Form Result type and failure When to use it

DirectCast(x, T)

T; InvalidCastException if the run-time type is not T or derived from it; no user-defined or numeric conversions

you know the type and want a cast that never converts

TryCast(x, T)

T or Nothing; never throws; T must be a reference or nullable type

you are not sure; test the result with Is Nothing

CType(x, T)

T; also runs conversion operators, numeric and string conversions

you want a conversion rather than a cast

TypeOf x Is T

Boolean; true for the type or any derived type; no variable is introduced

a guard before DirectCast

x.GetType() = GetType(T)

Boolean; exact type only

you need exactness

Visual Basic has no pattern-matching is with a declared variable, so the guard and the cast are two steps (C# folds them: x is Book b). The message of a failed DirectCast names both types, including the root namespace, as the output above shows.

Option Strict Custom and the warnings the SDK hides

The project property OptionStrict is Boolean in the SDK’s MSBuild task: it accepts On or Off, and an attempt to set it to Custom stops with error MSB4030: "Custom" is an invalid value for the "OptionStrict" parameter of the "Vbc" task. The "OptionStrict" parameter is of type "System.Boolean". The compiler itself has a third mode, -optionstrict:custom, in which the same problems are warnings instead of errors (BC42016 to BC42022). It is reached through AdditionalOptions. There is a trap, though, that I checked in the SDK’s Microsoft.NET.Sdk.VisualBasic.props: when the project’s NoWarn is empty, the SDK sets it to 41999,42016,42017,42018,42019,42020,42021,42022,42032,42036, so these warnings are silently suppressed. A project that wants to see them must replace NoWarn. The project used for the next program is:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>ErrorsApp</RootNamespace>
    <OptionStrict>Off</OptionStrict>
    <AdditionalOptions>-optionstrict:custom</AdditionalOptions>
    <NoWarn>1701</NoWarn>
    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

This is a project-level mode, not an Option Strict Off file: the file below says only Option Infer Off, and the project decides the rest.

Option Infer Off

Module Program
    Function Twice(n As Integer)
        Return n * 2
    End Function

    Sub Main()
        Dim x = 1
        Dim rounded As Integer = 3.7
        Dim item As Object = "text"
        Console.WriteLine(item.Length)
        Dim a As Object = 1
        Dim b As Object = 2
        Console.WriteLine(a + b)
        Console.WriteLine(a = b)
        Console.WriteLine(a < b)
        Console.WriteLine(a And b)
    End Sub
End Module
Program.vb(4,14): warning BC42021: Function without an 'As' clause; return type of Object assumed.
Program.vb(9,13): warning BC42020: Variable declaration without an 'As' clause; type of Object assumed.
Program.vb(10,34): warning BC42016: Implicit conversion from 'Double' to 'Integer'.
Program.vb(12,27): warning BC42017: Late bound resolution; runtime errors could occur.
Program.vb(15,27): warning BC42019: Operands of type Object used for operator '+'; runtime errors could occur.
Program.vb(15,31): warning BC42019: Operands of type Object used for operator '+'; runtime errors could occur.
Program.vb(16,27): warning BC42018: Operands of type Object used for operator '='; use the 'Is' operator to test object identity.
Program.vb(16,31): warning BC42018: Operands of type Object used for operator '='; use the 'Is' operator to test object identity.
Program.vb(17,27): warning BC42019: Operands of type Object used for operator '<'; runtime errors could occur.
Program.vb(17,31): warning BC42019: Operands of type Object used for operator '<'; runtime errors could occur.
Program.vb(18,27): warning BC42019: Operands of type Object used for operator 'And'; runtime errors could occur.
Program.vb(18,33): warning BC42019: Operands of type Object used for operator 'And'; runtime errors could occur.
Warning Meaning

BC42016

implicit conversion (narrowing) between two types

BC42017

late-bound resolution; run-time errors could occur

BC42018

Object operands for = or <>; use Is

BC42019

Object operands for another operator

BC42020

a variable declared with no As clause is Object

BC42021, BC42022

a function or property with no As clause returns Object

Source: -optionstrict

The same codes exist as errors in the Option Strict On table above. Custom is useful for migrating old code: you turn the specific warnings into errors (WarningsAsErrors) one family at a time while the rest still compiles. For new code, On.

CallByName under Option Strict On

CallByName (in Microsoft.VisualBasic.Interaction) takes an object, the member’s name and a CallType, and returns Object. It performs the same run-time lookup as a late-bound call, but it is an ordinary function call, so Option Strict On allows it; the risk is visible in the source and greppable. It is the right tool when the member name comes from data (a configuration file, a column header, a plug-in contract) rather than from the program text.

Option Strict On

Imports Microsoft.VisualBasic

Public Class Printer
    Public Property Prefix As String = ">"

    Public Function Show(value As Integer) As String
        Return Prefix & " Integer " & value.ToString()
    End Function

    Public Sub Increment(ByRef value As Integer)
        value += 1
    End Sub

    Private Function Hidden() As String
        Return "hidden"
    End Function

    Public Sub Fail()
        Throw New InvalidOperationException("boom")
    End Sub

    Public Function Pad(value As String, width As Integer) As String
        Return value.PadLeft(width, "."c)
    End Function
End Class

Module CallByNameDemo
    Sub Run()
        Dim book As New Book("Dune", 1965, Isbn.Parse("9780306406157"), New Author("Frank Herbert"))

        ' CallType.Get reads a property, CallType.Let / Set writes it, CallType.Method calls a method.
        Console.WriteLine(CallByName(book, "Title", CallType.Get))
        CallByName(book, "Title", CallType.Let, "Dune Messiah")
        Console.WriteLine(CallByName(book, "Describe", CallType.Method))

        ' Names can come from data; the result type is Object, so convert it.
        Dim printer As New Printer()
        Dim shown As String = CStr(CallByName(printer, "Show", CallType.Method, 7))
        Console.WriteLine(shown)
        Console.WriteLine(CallByName(printer, "Pad", CallType.Method, "7", 4))

        ProbeCreateObject()

        ' Member names are matched without regard to case, and non-public members are not found.
        Console.WriteLine(CallByName(book, "TITLE", CallType.Get))
        Try
            Console.WriteLine(CallByName(printer, "Hidden", CallType.Method))
        Catch ex As MissingMemberException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        ' An exception thrown by the target arrives as itself, not wrapped.
        Try
            CallByName(printer, "Fail", CallType.Method)
        Catch ex As InvalidOperationException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        Dim memberNames As String() = {"Kind", "Year", "Missing"}
        For Each memberName In memberNames
            Try
                Console.WriteLine(memberName & " = " & CStr(CallByName(book, memberName, CallType.Get)))
            Catch ex As MissingMemberException
                Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
            End Try
        Next
    End Sub

#Disable Warning CA1416 ' CreateObject is Windows-only; this probe exists to show the failure elsewhere
    Sub ProbeCreateObject()
        Try
            Dim fso As Object = CreateObject("Scripting.FileSystemObject")
            Console.WriteLine(CStr(CallByName(fso, "GetFileName", CallType.Method, "C:\data\books.txt")))
        Catch ex As Exception
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try
    End Sub
#Enable Warning CA1416
End Module

The output (the CallByName under Option Strict On section; the line Exception: Cannot create ActiveX component. is the Linux behaviour of CreateObject, whose ProgID lookup needs Windows):

Dune
Book: Dune Messiah (1965) by Frank Herbert
> Integer 7
...7
Exception: Cannot create ActiveX component.
Dune Messiah
MissingMemberException: Public member 'Hidden' on type 'Printer' not found.
InvalidOperationException: boom
Kind = Book
Year = 1965
MissingMemberException: Public member 'Missing' on type 'Book' not found.

What the output shows, and what to remember:

  • CallType.Get reads a property (or calls a method that takes no arguments), CallType.Let and CallType.Set assign (Set for object references, Let for values), and CallType.Method calls a method, with the arguments as the ParamArray. The value comes back as Object; convert it with CStr, CInt or DirectCast.

  • Names are matched without regard to case ("TITLE" found Title) and only public members are found: the private Hidden method raised MissingMemberException: Public member 'Hidden' on type 'Printer' not found. The message is the same for a misspelling and for a non-public member.

  • An exception thrown by the target arrives as itself (InvalidOperationException: boom), not wrapped in a TargetInvocationException. Reflection does wrap it (see below).

  • CreateObject compiles under Option Strict On (it returns Object) but raises a plain System.Exception with the text Cannot create ActiveX component. when the ProgID cannot be created, which is every ProgID on Linux. The analyzer CA1416 flags the call as Windows-only, which is why the program disables that warning around the probe.

Reflection under Option Strict On

Reflection is the other name-based tool. It is stricter in one way (it returns Nothing for an unknown member rather than throwing at the call site) and looser in another (a MemberInfo can reach non-public members when you ask).

Option Strict On

Imports System.Reflection

Module ReflectionDemo
    Sub Run()
        Dim book As New Book("Dune", 1965, Isbn.Parse("9780306406157"), New Author("Frank Herbert"))
        Dim bookType As Type = book.GetType()

        ' The same lookups, written with reflection. Nothing here is late bound.
        Dim title As PropertyInfo = bookType.GetProperty("Title")
        Console.WriteLine(title.GetValue(book))
        title.SetValue(book, "Dune Messiah")
        Console.WriteLine(bookType.GetMethod("Describe").Invoke(book, Nothing))
        Console.WriteLine(bookType.InvokeMember("Year", BindingFlags.GetProperty, Nothing, book, Nothing))

        ' A missing member returns Nothing from GetMethod instead of throwing at the call.
        Console.WriteLine(bookType.GetMethod("Missing") Is Nothing)

        ' Reflection is case sensitive unless you ask otherwise, and it wraps exceptions.
        Console.WriteLine(bookType.GetProperty("title") Is Nothing)
        Console.WriteLine(bookType.GetProperty("title", BindingFlags.Public Or BindingFlags.Instance Or BindingFlags.IgnoreCase) Is Nothing)
        Dim printer As New Printer()
        Try
            GetType(Printer).GetMethod("Fail").Invoke(printer, Nothing)
        Catch ex As TargetInvocationException
            Console.WriteLine(ex.GetType().Name & " -> " & ex.InnerException.GetType().Name)
        End Try

        ' Look up once, then call through a typed delegate: the fast form of the same idea.
        Dim getter As Func(Of Book, String) = DirectCast(
            System.Delegate.CreateDelegate(GetType(Func(Of Book, String)), bookType.GetProperty("Title").GetGetMethod()),
            Func(Of Book, String))
        Console.WriteLine(getter(book))
    End Sub
End Module

The output (the reflection under Option Strict On section):

Dune
Book: Dune Messiah (1965) by Frank Herbert
1965
True
True
False
TargetInvocationException -> InvalidOperationException
Dune Messiah

The differences from CallByName are worth a table, because converting between the two without noticing them is a common source of bugs.

Aspect CallByName and late binding Type.GetMethod and Invoke

Name matching

case-insensitive (TITLE found Title)

case-sensitive; GetProperty("title") returned Nothing until BindingFlags.IgnoreCase was added

Exceptions from the target

arrive as themselves

wrapped in TargetInvocationException

Overload selection

Visual Basic rules at run time, narrowing conversions allowed

you pick the MethodInfo; Invoke converts only simple cases

Unknown member

MissingMemberException at the call

Nothing from GetMethod; NullReferenceException if you use it

Typed delegate cache

not available

Delegate.CreateDelegate once, then call at nearly the speed of a normal call

The last row is the practical advice: when names must be dynamic and speed matters, look the member up once, turn it into a typed delegate (Func(Of Book, String) above) and keep the delegate.

The labelled Option Strict Off file

Everything above was written under Option Strict On. The file below is the only one in the project that is not. Its first line says so, the project’s other files reach into it only through DescribeAny and LateAddLoop, whose signatures are fully typed, and the module is Friend so that nothing outside the assembly sees it. Read the comments in the file: each block is one behaviour of the Visual Basic run-time binder, and the output follows.

' LateBinding.vb: the ONLY file in this project with Option Strict Off.
' Everything else, including the project itself, uses Option Strict On.
Option Strict Off

Imports System.Dynamic
Imports Microsoft.VisualBasic

Friend Module LateBinding
    ' The typed facade: callers see strict, early-bound signatures.
    Public Function DescribeAny(item As Object) As String
        Return item.Kind & " -> " & item.Describe()
    End Function

    Public Function LateAddLoop(target As Object, count As Integer) As Integer
        Dim last As Integer
        For i = 1 To count
            last = target.Bump()
        Next
        Return last
    End Function

    Public Sub Run()
        Dim item As Object = New Book("Dune", 1965, Isbn.Parse("9780306406157"), New Author("Frank Herbert"))

        ' Property get, property set and method call on an Object variable.
        Console.WriteLine(item.Title)
        item.Title = "Dune Messiah"
        Console.WriteLine(item.Describe())
        Console.WriteLine(item.Authors.Count)
        Console.WriteLine(item.Authors(0).Name)
        Console.WriteLine(item.TITLE)

        ' An exception thrown by the target arrives as itself.
        Try
            Dim failing As Object = New Printer()
            failing.Fail()
        Catch ex As InvalidOperationException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        ' The compiler cannot see the members, so the run-time binder looks them up.
        Try
            Console.WriteLine(item.Pages)
        Catch ex As MissingMemberException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        Try
            item.Rewind(3)
        Catch ex As MissingMemberException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        ' A Nothing receiver fails at run time too.
        Dim nobody As Object = Nothing
        Try
            Console.WriteLine(nobody.ToString())
        Catch ex As NullReferenceException
            Console.WriteLine(ex.GetType().Name)
        End Try

        ' Overloads are chosen from the run-time types of the arguments.
        Dim printer As Object = New Printer()
        Dim small As Object = CShort(7)
        Console.WriteLine(printer.Show(small))
        Dim text As Object = "8"
        Console.WriteLine(printer.Show(text))
        Try
            Console.WriteLine(printer.Show("nine"))
        Catch ex As InvalidCastException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        ' Named arguments work too.
        Console.WriteLine(printer.Pad(width:=6, value:="x"))

        ' ByRef arguments are copied back into the caller's variable after the late call.
        Dim counter As Integer = 1
        printer.Increment(counter)
        Console.WriteLine(counter)

        ' Setting a field of a boxed structure through an Object variable.
        Dim corner As Object = New Point2D()
        corner.X = 10
        corner.Y = 20
        Console.WriteLine(corner.X & "," & corner.Y)

        ' Assigning to a field of a structure that is itself the result of a late-bound read is refused.
        Dim holder As Object = New Corner()
        Try
            holder.Position.X = 5
        Catch ex As Exception
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        ' Operators on Object operands are resolved at run time as well.
        Dim a As Object = 10
        Dim b As Object = "2.5"
        Console.WriteLine(a + b)
        Console.WriteLine(a & b)
        Console.WriteLine(a > b)
        Console.WriteLine((a + b).GetType().Name)

        ' Turning the switch off also allows implicit narrowing and string-to-number conversions.
        Dim rounded As Integer = 3.7
        Dim parsed As Double = "3.5"
        Dim shown As String = 42
        Console.WriteLine(rounded & " " & parsed & " " & shown)

        ' Dynamic objects: IDynamicMetaObjectProvider is honoured by the Visual Basic late binder.
        Dim bag As Object = New ExpandoObject()
        bag.Title = "Emma"
        bag.Pages = 474
        Console.WriteLine(bag.Title & " " & bag.Pages)

        Dim settings As Object = New SettingsBag()
        settings.Theme = "dark"
        Console.WriteLine(settings.THEME)

        ' DynamicObject.TryInvokeMember answers calls; a member read without parentheses tries the
        ' call first, so it reaches TryInvokeMember when the class overrides it.
        Dim recorder As Object = New Recorder()
        Console.WriteLine(recorder.Open("a", 2))
        Console.WriteLine(recorder.Status)

        ' A type parameter must be boxed to an Object variable first to be used late-bound.
        Console.WriteLine(LengthOf("hello"))
        Console.WriteLine(LengthOf(New Integer() {1, 2, 3}))

        ' COM automation is late bound through CreateObject; the Windows branch is not run here.
        If OperatingSystem.IsWindows() Then
            Dim excel As Object = CreateObject("Excel.Application")
            excel.Visible = False
            Dim workbook As Object = excel.Workbooks.Add()
            workbook.Worksheets(1).Cells(1, 1).Value = "Shelf"
            workbook.Close(False)
            excel.Quit()
        Else
            Console.WriteLine("COM automation needs Windows; skipped on " & Runtime.InteropServices.RuntimeInformation.OSDescription.Split(" "c)(0))
        End If
    End Sub

    Private Function LengthOf(Of T)(value As T) As Integer
        Dim boxed As Object = value
        Return boxed.Length
    End Function
End Module

The output (the late binding section of the run):

Dune
Book: Dune Messiah (1965) by Frank Herbert
1
Frank Herbert
Dune Messiah
InvalidOperationException: boom
MissingMemberException: Public member 'Pages' on type 'Book' not found.
MissingMemberException: Public member 'Rewind' on type 'Book' not found.
NullReferenceException
> Integer 7
> Integer 8
InvalidCastException: Conversion from string "nine" to type 'Integer' is not valid.
.....x
2
10,20
Exception: Late-bound assignment to a field of value type 'Point2D' is not valid when 'Point2D' is the result of a late-bound expression.
12.5
102.5
True
Double
4 3.5 42
Emma 474
dark
Open(a, 2)
Status()
5
3
COM automation needs Windows; skipped on Ubuntu

Program.vb then calls the typed facade, with a Book and a Magazine, and gets strings back:

Book -> Book: Emma (1815) by Jane Austen
Magazine -> Magazine: Byte (1982)

The helper classes that the file uses live in Settings.vb, which is Option Strict On:

Option Strict On

Imports System.Dynamic

' A dynamic property bag written under Option Strict On. Late-bound callers can read and write
' its members by name because it implements IDynamicMetaObjectProvider through DynamicObject.
Public Class SettingsBag
    Inherits DynamicObject

    Private ReadOnly _values As New Dictionary(Of String, Object)(StringComparer.OrdinalIgnoreCase)

    Public Overrides Function TryGetMember(binder As GetMemberBinder, ByRef result As Object) As Boolean
        Return _values.TryGetValue(binder.Name, result)
    End Function

    Public Overrides Function TrySetMember(binder As SetMemberBinder, value As Object) As Boolean
        _values(binder.Name) = value
        Return True
    End Function
End Class

' A bag that answers every method call; it also answers a plain member read, as the call
' to a method with no arguments.
Public Class Recorder
    Inherits DynamicObject

    Public Overrides Function TryInvokeMember(binder As InvokeMemberBinder, args() As Object, ByRef result As Object) As Boolean
        result = binder.Name & "(" & String.Join(", ", args) & ")"
        Return True
    End Function

    Public Overrides Function TryGetMember(binder As GetMemberBinder, ByRef result As Object) As Boolean
        result = "get " & binder.Name
        Return True
    End Function
End Class


Public Class Cell
    Public Property Value As Integer
    Public Function Bump() As Integer
        Value += 1
        Return Value
    End Function
End Class

Public Structure Point2D
    Public X As Integer
    Public Y As Integer
End Structure

Public Class Corner
    Public Property Position As Point2D
End Class

The behaviours, in the order of the file:

  • Property get, set and method call. item.Title, item.Title = "...", item.Describe(), item.Authors.Count and item.Authors(0).Name are all resolved at run time. The compiler emits calls into Microsoft.VisualBasic.CompilerServices.NewLateBinding (LateGet, LateSet, LateCall); the names of those helpers are visible in the trim warnings further down. Chained accesses are late bound at every link.

  • Case-insensitive lookup. item.TITLE found Title, as CallByName did.

  • Exceptions from the target arrive as themselves (InvalidOperationException: boom).

  • Missing members raise MissingMemberException with the text Public member 'Pages' on type 'Book' not found., for properties and methods alike; the type name is the run-time type, not the variable’s.

  • Nothing receivers. A late-bound call on Nothing is a NullReferenceException, as in early-bound code.

  • Overloads are resolved from the run-time argument types. Show(Integer) is the only overload of Printer.Show, but the call is made with a Short (widened), a String that holds a number (converted, so "8" printed > Integer 8) and a String that does not (InvalidCastException: Conversion from string "nine" to type 'Integer' is not valid.). Early binding would have refused two of those three calls at compile time.

  • Named arguments work (Pad(width:=6, value:="x")).

  • ByRef arguments are copied back. printer.Increment(counter) changed the caller’s counter from 1 to 2, because the binder copies ByRef values back after the call.

  • Boxed structures. corner.X = 10 on an Object that holds a Point2D works, because the binder modifies the box. Assigning to a field of a structure that is itself the result of a late-bound read is refused, with a plain System.Exception: Late-bound assignment to a field of value type 'Point2D' is not valid when 'Point2D' is the result of a late-bound expression. (the Learn page for this message advises to make the assignment early bound).

  • Operators on Object operands follow the Visual Basic operator rules at run time: 10 + "2.5" is the Double 12.5 (a string that looks like a number is converted for + when the other operand is numeric), & concatenates (102.5), and a > b compares numerically. C# dynamic gives different answers for the same operands, as the comparison section shows.

  • Narrowing conversions are silent: 3.7 became 4, "3.5" became 3.5, and 42 became "42".

  • Dynamic objects. The binder honours IDynamicMetaObjectProvider: an ExpandoObject takes bag.Title = "Emma", and a DynamicObject subclass sees settings.THEME through its TryGetMember. The trap is in the last two lines: recorder.Status printed Status(), not get Status. A member read without parentheses first tries the invoke form, so a class that overrides TryInvokeMember answers property reads through it. If you write a DynamicObject, override only the hooks you want.

  • Type parameters. A value of an unconstrained type parameter T cannot be used late bound even in an Option Strict Off file: value.Length on a T stopped with error BC30456: 'Length' is not a member of 'T'. The file assigns the value to an Object variable first.

  • COM automation. CreateObject("Excel.Application") returns a COM object; every .Workbooks.Add(), .Worksheets(1).Cells(1, 1).Value after it is late bound through IDispatch. That block is compiled but built, not run here, since it needs Windows and Excel; the Else branch prints a note on Linux.

flowchart TD A(["item.Title on an Object variable"]) --> B{"Option Strict Off in this file?"} B -->|"no"| C["BC30574: compile-time error"] B -->|"yes"| D["Compiler emits NewLateBinding.LateGet with the name as a string"] D --> E{"IDynamicMetaObjectProvider?"} E -->|"yes"| F["The receiver binds: ExpandoObject, DynamicObject, C# dynamic provider"] E -->|"no"| G{"COM object?"} G -->|"yes"| H["The COM binder dispatches the call, Windows only"] G -->|"no"| I["Reflection: public members, case-insensitive, choose the overload by run-time types"] F --> J(["Result as Object"]) H --> J I --> J I -->|"not found"| K(["MissingMemberException"])

Containing late binding in one file

The structure of the project is the recommendation:

  • The project sets <OptionStrict>On</OptionStrict>; a file that wants late binding says Option Strict Off on its first line and says why in a comment above it.

  • The late-bound code lives in one Friend module (here LateBinding). Everything else calls typed functions on it: DescribeAny(item As Object) As String takes the untyped value and returns a String; LateAddLoop returns an Integer. Strictness then stops at the facade, and a code search for Option Strict Off lists every place that needs review.

  • Prefer to remove the need: if the types are yours, give them a common interface or base class (CatalogItem has Kind and Describe, so DescribeAny could take a CatalogItem); if the names come from data, use CallByName under Option Strict On; if the types come from a plug-in, load them through an interface defined in a shared assembly.

  • Do not use Option Strict Off to avoid writing a conversion. CInt, CStr, Integer.Parse and DirectCast say what happens; the implicit versions hide a rounding or a culture dependency.

What a late call costs

Performance.vb makes the same call one million times five ways: a direct call, CallByName, MethodInfo.Invoke, a delegate created once with Delegate.CreateDelegate, and a late-bound call on an Object from the Option Strict Off file. Each variant is run once to warm up and once timed.

Option Strict On

Imports System.Diagnostics
Imports System.Reflection
Imports Microsoft.VisualBasic

Module Performance
    Private Const Iterations As Integer = 1000000

    Private Function Measure(label As String, work As Func(Of Integer)) As Double
        work() ' warm up
        Dim watch = Stopwatch.StartNew()
        Dim result = work()
        watch.Stop()
        Console.WriteLine(label.PadRight(28) & result.ToString().PadLeft(8) & "  " & (watch.Elapsed.TotalMilliseconds).ToString("F1").PadLeft(8) & " ms")
        Return watch.Elapsed.TotalMilliseconds
    End Function

    Sub Run()
        Dim cell As New Cell()
        Dim bump As MethodInfo = GetType(Cell).GetMethod("Bump")
        Dim typed As Func(Of Integer) = Function() As Integer
                                           cell.Value = 0
                                           Dim last As Integer
                                           For i = 1 To Iterations
                                               last = cell.Bump()
                                           Next
                                           Return last
                                       End Function
        Dim viaCallByName As Func(Of Integer) = Function() As Integer
                                                    cell.Value = 0
                                                    Dim last As Integer
                                                    For i = 1 To Iterations
                                                        last = CInt(CallByName(cell, "Bump", CallType.Method))
                                                    Next
                                                    Return last
                                                End Function
        Dim viaReflection As Func(Of Integer) = Function() As Integer
                                                    cell.Value = 0
                                                    Dim last As Integer
                                                    For i = 1 To Iterations
                                                        last = CInt(bump.Invoke(cell, Nothing))
                                                    Next
                                                    Return last
                                                End Function
        Dim viaDelegate As Func(Of Integer) = Function() As Integer
                                                  cell.Value = 0
                                                  Dim call1 = DirectCast(System.Delegate.CreateDelegate(GetType(Func(Of Integer)), cell, bump), Func(Of Integer))
                                                  Dim last As Integer
                                                  For i = 1 To Iterations
                                                      last = call1()
                                                  Next
                                                  Return last
                                              End Function
        Dim viaLate As Func(Of Integer) = Function() As Integer
                                              cell.Value = 0
                                              Return LateBinding.LateAddLoop(cell, Iterations)
                                          End Function

        Dim baseline = Measure("early bound call", typed)
        Dim t1 = Measure("CallByName", viaCallByName)
        Dim t2 = Measure("MethodInfo.Invoke", viaReflection)
        Dim t3 = Measure("cached typed delegate", viaDelegate)
        Dim t4 = Measure("late bound (Object)", viaLate)
        Console.WriteLine("late / early ratio: " & (t4 / baseline).ToString("F0") & "x (varies by machine)")
    End Sub
End Module

The output (the relative cost section; absolute times vary by machine and run, the ranking does not):

early bound call             1000000       6.9 ms
CallByName                   1000000     329.2 ms
MethodInfo.Invoke            1000000      28.4 ms
cached typed delegate        1000000       9.4 ms
late bound (Object)          1000000     328.2 ms
late / early ratio: 48x (varies by machine)

The late-bound call and CallByName cost about the same, because both run through NewLateBinding, and both were between 34 and 48 times slower than the direct call in the runs made for this page; MethodInfo.Invoke was in the middle, and a cached delegate was as fast as the direct call. These are the numbers of one Linux arm64 container run, not a benchmark suite: use them for the shape, and measure your own code with a proper harness if it matters. The binder does extra work on each call that a cache cannot remove for you: it must find the member by name, apply the overload rules to the actual arguments, convert the arguments, and copy ByRef values back.

Trimming and Native AOT

Late binding looks members up by name at run time, and the .NET trimmer removes members that nothing refers to by name at build time. The Visual Basic runtime marks the late-binding helpers RequiresUnreferencedCode, so publishing with trimming enabled reports them. This is a real publish of the project with PublishTrimmed:

dotnet publish -r linux-arm64 -p:PublishTrimmed=true -p:TreatWarningsAsErrors=false -o out

Representative warnings (the publish printed dozens of IL2026 warnings, most of them in LateBinding.vb, and more for CallByName and CreateObject in CallByNameDemo.vb):

LateBinding.vb(26,9): Trim analysis warning IL2026: LateApp.LateBinding.Run(): Using member 'Microsoft.VisualBasic.CompilerServices.NewLateBinding.LateGet(Object, Type, String, Object[], String[], Type[], Boolean[])' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Late binding is dynamic and cannot be statically analyzed. The referenced types and members may be trimmed.
LateBinding.vb(27,9): Trim analysis warning IL2026: LateApp.LateBinding.Run(): Using member 'Microsoft.VisualBasic.CompilerServices.NewLateBinding.LateSet(Object, Type, String, Object[], String[], Type[])' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Late binding is dynamic and cannot be statically analyzed. The referenced types and members may be trimmed.
LateBinding.vb(28,9): Trim analysis warning IL2026: LateApp.LateBinding.Run(): Using member 'Microsoft.VisualBasic.CompilerServices.NewLateBinding.LateGet(Object, Type, String, Object[], String[], Type[], Boolean[])' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Late binding is dynamic and cannot be statically analyzed. The referenced types and members may be trimmed.
LateBinding.vb(99,9): Trim analysis warning IL2026: LateApp.LateBinding.Run(): Using member 'Microsoft.VisualBasic.CompilerServices.Operators.AddObject(Object, Object)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The object types cannot be statically analyzed so their operators may be trimmed.
CallByNameDemo.vb(74,17): Trim analysis warning IL2026: LateApp.CallByNameDemo.ProbeCreateObject(): Using member 'Microsoft.VisualBasic.Interaction.CreateObject(String, String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The COM object to be created cannot be statically analyzed and may be trimmed.
ReflectionDemo.vb(11,13): Trim analysis warning IL2075: LateApp.ReflectionDemo.Run(): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The return value of method 'System.Object.GetType()' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

The warnings are not hypothetical. Running the trimmed program (the first section is early bound and works; Printer.Show is reached only by name, so the trimmer removed it):

-- early binding
Dune
Book: Dune Messiah (1965) by Frank Herbert
Book: Dune Messiah (1965) by Frank Herbert
Book
True
Unable to cast object of type 'LateApp.Book' to type 'LateApp.Magazine'.
1965
-- CallByName under Option Strict On
Dune
Book: Dune Messiah (1965) by Frank Herbert
Unhandled exception. System.MissingMemberException: Public member 'Show' on type 'Printer' not found.

The same program passes when it is not trimmed, as the earlier output shows. The remedies, in order of preference:

  • Replace late binding by interfaces, generics or delegates, so that the trimmer sees the call.

  • Keep reflection but tell the trimmer what it needs: DynamicDependency or DynamicallyAccessedMembers annotations on the types you reflect on, or a root descriptor file (TrimmerRootDescriptor) for plug-in assemblies.

  • Do not trim the application (PublishTrimmed is off by default).

The same RequiresUnreferencedCode annotations apply to Native AOT, which also rejects code that must be generated at run time. A Native AOT publish was not run for this page (it needs a native toolchain that the SDK container does not have), so treat late binding in a Native AOT application as unsupported until you have tested the exact calls. Source: Native AOT deployment

C# dynamic compared

C# dynamic solves the same problem with a different default: it is opt-in per variable, and it uses the C# binder (Microsoft.CSharp.RuntimeBinder, whose assembly is part of the shared framework) rather than the Visual Basic one. The same scenario written in C# against the same Visual Basic domain (Shelf.Core):

using System.Dynamic;
using Microsoft.CSharp.RuntimeBinder;
using Shelf.Core;

// C# dynamic: the same late binding idea, resolved by the C# run-time binder.
dynamic item = new Book("Dune", 1965, Isbn.Parse("9780306406157"), new Author("Frank Herbert"));
Console.WriteLine(item.Title);
item.Title = "Dune Messiah";
Console.WriteLine(item.Describe());
Console.WriteLine(item.Authors[0].Name);

try
{
    Console.WriteLine(item.Pages);
}
catch (RuntimeBinderException ex)
{
    Console.WriteLine(ex.GetType().Name + ": " + ex.Message);
}

dynamic bag = new ExpandoObject();
bag.Title = "Emma";
bag.Pages = 474;
Console.WriteLine(bag.Title + " " + bag.Pages);

dynamic a = 10;
dynamic b = "2.5";
try
{
    Console.WriteLine(a + b);
}
catch (RuntimeBinderException ex)
{
    Console.WriteLine(ex.GetType().Name);
}
Console.WriteLine(a + 2.5);
Console.WriteLine((a + 2.5).GetType().Name);
Dune
Book: Dune Messiah (1965) by Frank Herbert
Frank Herbert
RuntimeBinderException: 'Shelf.Core.Book' does not contain a definition for 'Pages'
Emma 474
102.5
12.5
Double
Behaviour Visual Basic with Option Strict Off C# dynamic

Switch

per file, by the Option Strict Off statement, applies to every Object

per variable, by the type dynamic; object stays early bound

Binder

NewLateBinding and Operators in Microsoft.VisualBasic.Core

Microsoft.CSharp.RuntimeBinder in Microsoft.CSharp

Missing member

MissingMemberException: Public member 'Pages' on type 'Book' not found.

RuntimeBinderException: 'Shelf.Core.Book' does not contain a definition for 'Pages'

Name matching

case-insensitive

case-sensitive

10 plus "2.5"

12.5 (numeric +)

102.5 (string concatenation, as in C#)

Exceptions from the target

unwrapped (shown above)

see the C# page; not measured here

When a C# library hands you a dynamic value, Visual Basic sees Object (the [Dynamic] attribute is ignored), so you need a late-bound call site to use it. That is one more reason to isolate the call: see Interoperating with C# and Other .NET Languages, and for the C# side of the same feature Expression Trees and dynamic and Native Interop.

When late binding is still the right tool

  • COM automation without a type library or with several versions of one. Excel, Word and Outlook are the classic case: a late-bound CreateObject("Excel.Application") works with whatever version is installed, where an early-bound project references one version’s interop assembly. The cost is no IntelliSense and no compile-time check of member names. Details, and the Windows-only build settings, are on COM Interop and Office Automation; for server-side document generation use the libraries described in Apache POI: introduction and architecture and exporting reports from a server on this site’s backend pages, or the Open XML SDK, not Office automation.

  • Dynamic data with ExpandoObject, DynamicObject or a C# library that returns dynamic.

  • Plug-ins and scripting hosts that expose objects by name, when you control neither the types nor a shared interface.

  • Test and tooling code where the cost does not matter and the type is genuinely unknown.

For everything else, the early-bound version is shorter to read, faster, trimmable and checked by the compiler.

Where older material is outdated

  • Older Visual Basic books and articles often recommend Option Strict Off for convenience, or leave it off by default. The .NET SDK still defaults it to Off (OptionStrict is Off unless the project sets it, as I confirmed from the SDK properties), so the recommendation of this section is to set On in the project or in a Directory.Build.props.

  • Material written for Visual Basic 6 and VBA treats Variant and Object as freely late bound, and has no equivalent of Option Strict. In Visual Basic .NET the difference is a switch, and the default for new code should be strict.

  • Advice that Office automation needs Option Strict Off is out of date: early binding to the interop assemblies is the norm when the Office version is fixed, and CallByName under Option Strict On covers the occasional late call.

  • Older guidance about "late-bound `CreateObject`" does not mention trimming: late-bound code is not trim-safe, and the SDK now tells you so when you publish with trimming.

  • Windows-only advice such as CreateObject and GetObject is not available on Linux and macOS, where the call fails with Cannot create ActiveX component.

Rules of thumb

  1. Set OptionStrict to On in the project; treat Option Strict Off as a per-file exception that needs a comment.

  2. Use DirectCast and TryCast for cast needs, CType and Parse methods for conversions.

  3. Use CallByName or a cached delegate when the member name is data.

  4. Keep late-bound code in a Friend module with typed signatures.

  5. Remember that a late bind is case-insensitive, allows narrowing, copies ByRef values back and can silently convert strings.

  6. Do not publish a late-bound application trimmed or with Native AOT unless you have tested every late call after publishing.

  7. Prefer interfaces: the compiler can check a call to an interface and the trimmer can see it.