Interoperating with C# and Other .NET Languages

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, with the C# 14 and F# compilers of that SDK) and was checked on 2026-10-19. Every program on it is part of one scratch solution, Shelf.slnx, that was built in the .NET 10 SDK container (Linux, arm64) with warnings treated as errors, and the console programs were run there. The programs that are meant to fail show the compiler messages the SDK printed; in those, warnings are not errors. The root namespaces are Shelf.Core, Shelf.Pricing, Shelf.Cli, Shelf.CsClient and Shelf.Fs. The version numbers in the second column of the feature table (15, 16.9, 17.13) are the Visual Basic versions recorded in this section’s research notes for when support was added; they were not re-measured here. The behaviours are what the 17.13 compiler in the container actually did, and they are the part to rely on. Microsoft’s release notes for the language are linked from What’s new for Visual Basic.

A .NET assembly does not remember the language it was written in. A C# class library, a Visual Basic one and an F# one all compile to the same metadata and the same IL, so a project in any of them can reference a project or package in any other. What does not cross freely is language features: a feature that the C# compiler encodes in metadata (a modifier, an attribute, a special method name) can be used from Visual Basic only if the Visual Basic compiler knows how to read that encoding. The Visual Basic language is in a stable, consumption-only mode (see History, Versions and Language Strategy): new C# and runtime features are added to Visual Basic only when they are needed to consume them. This page is the map of where that boundary lies in version 17.13, in both directions.

The Shelf solution

The solution mixes three languages: a Visual Basic domain library, a C# library that builds on it, an F# library, a Visual Basic console application that uses all three, and a C# console application that uses the Visual Basic and C# libraries.

flowchart LR subgraph VBP["Visual Basic projects"] Core["Shelf.Core (VB class library)"] Cli["Shelf.Cli (VB console app)"] end subgraph CSP["C# projects"] Pricing["Shelf.Pricing (C# class library)"] CsClient["Shelf.CsClient (C# console app)"] end subgraph FSP["F# project"] Fs["Shelf.Fs (F# class library)"] end Pricing -->|"ProjectReference"| Core Cli -->|"ProjectReference"| Core Cli -->|"ProjectReference"| Pricing Cli -->|"ProjectReference"| Fs CsClient -->|"ProjectReference"| Core CsClient -->|"ProjectReference"| Pricing

Each arrow is a ProjectReference from the project at the tail to the project at the head. Note that the C# library Shelf.Pricing references the Visual Basic Shelf.Core, so the dependency runs both ways in terms of languages even though it is acyclic in terms of projects. The solution file uses the .slnx format that the .NET 10 SDK understands; the language of a project is chosen by its project file’s extension and nothing else.

<Solution>
  <Project Path="Shelf.Core/Shelf.Core.vbproj" />
  <Project Path="Shelf.Pricing/Shelf.Pricing.csproj" />
  <Project Path="Shelf.Fs/Shelf.Fs.fsproj" />
  <Project Path="Shelf.Cli/Shelf.Cli.vbproj" />
  <Project Path="Shelf.CsClient/Shelf.CsClient.csproj" />
</Solution>

All five projects share one Directory.Build.props, which sets warnings as errors, and options that apply only to the language they are meant for:

<Project>
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
  <PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.vbproj'">
    <OptionExplicit>On</OptionExplicit>
    <OptionStrict>On</OptionStrict>
    <OptionInfer>On</OptionInfer>
  </PropertyGroup>
  <PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

The three class libraries and the two applications differ only in their references:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <RootNamespace>Shelf.Core</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <InternalsVisibleTo Include="Shelf.CsClient" />
  </ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <RootNamespace>Shelf.Pricing</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\Shelf.Core\Shelf.Core.vbproj" />
  </ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <Compile Include="Library.fs" />
  </ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <RootNamespace>Shelf.Cli</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\Shelf.Core\Shelf.Core.vbproj" />
    <ProjectReference Include="..\Shelf.Pricing\Shelf.Pricing.csproj" />
    <ProjectReference Include="..\Shelf.Fs\Shelf.Fs.fsproj" />
  </ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <RootNamespace>Shelf.CsClient</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\Shelf.Core\Shelf.Core.vbproj" />
    <ProjectReference Include="..\Shelf.Pricing\Shelf.Pricing.csproj" />
  </ItemGroup>
</Project>

Build it from the solution directory with dotnet build Shelf.slnx. MSBuild orders the projects by their references and compiles each with its own compiler: vbc, csc and the F# compiler. Shelf.Core.vbproj also contains InternalsVisibleTo Include="Shelf.CsClient", which the section on C# consuming Visual Basic uses.

Referencing another language

  • Project references are the normal case in one repository. ProjectReference copies the output assembly and flows its dependencies (the F# project brings FSharp.Core with it into Shelf.Cli).

  • Package references work the same: a NuGet package written in C# is just an assembly. The next program is a separate Visual Basic project that references the Humanizer.Core package (written in C#) and calls its extension methods.

  • Assembly references (Reference with a HintPath) are the fallback for a binary that is not in a package. Prefer a package or project.

  • Target frameworks should match or be compatible: a net10.0 project can reference net10.0 and netstandard2.0 libraries, not the other way round. See Projects, MSBuild and the .NET CLI and Namespaces, Assemblies and Projects (C#) for the shared platform detail.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>NugetApp</RootNamespace>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Humanizer.Core" Version="2.14.1" />
  </ItemGroup>
</Project>
Option Strict On

Imports Humanizer

Module Program
    Sub Main()
        Dim count As Integer = 2
        Console.WriteLine("PascalCaseTitle".Humanize())
        Console.WriteLine("book".ToQuantity(count))
        Console.WriteLine(TimeSpan.FromMinutes(90).Humanize())
    End Sub
End Module
Pascal case title
2 books
1 hour

What Visual Basic can consume from modern C#

The C# library is split into short files, each shown with the Visual Basic that uses it. The rows of the table below are all tested, and the compiler messages for the failures follow the code.

C# feature Visual Basic version Result in Visual Basic 17.13 on .NET 10

Records (C# 9)

n/a

usable as a class: constructor, properties, value Equals, =, the generated ToString, an ordinary Deconstruct method; no With expression, no positional deconstruction

Init-only setters (C# 9)

16.9

assignable in an object initializer or in a constructor of the class; elsewhere BC37311

required members (C# 11)

enforced

BC37321 when not set in the initializer; SetsRequiredMembers constructors are honoured; Visual Basic cannot declare required or init

Default interface methods (C# 8)

consumed

callable through an interface variable; a Visual Basic class that implements the interface must still write the body (BC30149); not callable on a class variable (BC30456)

ref returns (C# 7)

15

a ref return can be read into a value and also assigned through; ref readonly returns are rejected (BC30657)

Span(Of T), ReadOnlySpan(Of T) (C# 7.2)

local use

a call may pass an array where the parameter is a span; a returned span can be held by an inferred local and its Length and ToArray() used; its indexer and enumerator cannot (BC30643) and the type cannot be named (BC30668)

params Integer()

ParamArray

works like a Visual Basic ParamArray

params ReadOnlySpan (C# 13)

not supported

the values cannot be passed separately (BC30311, BC30057); an array can be passed

INumber(Of T) and other generic-math constraints (C# 11)

consumed

a generic method with the constraint can be called with Integer or Double; Visual Basic cannot implement static abstract interface members (BC37315)

unmanaged constraint (C# 7.3)

17.13

accepted for blittable value types; String is rejected (BC37332 and BC32105)

OverloadResolutionPriority (C# 13)

17.13

honoured when choosing between overloads

Extension methods and extension blocks (C# 14)

consumed

methods in an extension block are callable as extension methods; extension properties are only reachable as static accessor methods

Nullable reference annotations (C# 8)

ignored

no warning; passing Nothing compiles and fails at run time inside the callee

Named tuples (C# 7)

15

element names are honoured

async, Task, IAsyncEnumerable

consumed

Await works; there is no Await For Each (BC30201 and BC30092 on the For), and Await is not allowed in Finally (BC36943)

Events, delegates, indexers, optional parameters

consumed

all work; indexers appear as default properties

dynamic (C# 4)

Object

the attribute is ignored; use a late-bound call site, see Early and Late Binding

F# unions and options

consumed

factory methods and Tag; None is Nothing; FSharp.Core must be referenced

The authoritative list of what the C# compiler itself supports in each version is Roslyn’s Language Feature Status page, Language Feature Status; the C# column above corresponds to the versions in What’s new in C#. A feature that is not in the table has simply not been tested for this page.

Records, init-only setters and required members

The C# side defines a positional record, a class with an init-only property and a required member, a default interface method, and an abstract base class:

using System.Diagnostics.CodeAnalysis;

namespace Shelf.Pricing;

// A positional record: Visual Basic sees a class with properties, a constructor and value equality.
public record Price(decimal Amount, string Currency);

// An init-only setter and a required member.
public sealed class Discount
{
    public required string Code { get; init; }
    public decimal Percent { get; init; }

    public Discount() { }

    [SetsRequiredMembers]
    public Discount(string code, decimal percent)
    {
        Code = code;
        Percent = percent;
    }
}

// A default interface method: the body lives in the interface.
public interface ILabel
{
    string Text { get; }
    string Upper() => Text.ToUpperInvariant();
}

public sealed class ShelfLabel : ILabel
{
    public string Text { get; init; } = "";
}

// An abstract base class that Visual Basic classes can derive from.
public abstract class Pricer
{
    public virtual string Name => "base pricer";
    public abstract decimal Compute(decimal basePrice);
}

The Visual Basic side builds and compares them:

Option Strict On

Imports Shelf.Pricing

Module RecordsAndInit
    Sub Run()
        Dim first = New Price(9.99D, "EUR")
        Dim second = New Price(9.99D, "EUR")
        Console.WriteLine(first.ToString())
        Console.WriteLine(first.Equals(second))
        Console.WriteLine(first = second)
        Console.WriteLine(ReferenceEquals(first, second))

        Dim amount As Decimal
        Dim currency As String = Nothing
        first.Deconstruct(amount, currency)
        Console.WriteLine(amount.ToString() & " " & currency)

        Dim spring = New Discount With {.Code = "SPRING", .Percent = 15D}
        Console.WriteLine(spring.Code & " " & spring.Percent.ToString())

        Dim autumn = New Discount("AUTUMN", 5D)
        Console.WriteLine(autumn.Code & " " & autumn.Percent.ToString())
    End Sub
End Module
Price { Amount = 9.99, Currency = EUR }
True
True
False
9.99 EUR
SPRING 15
AUTUMN 5

Three details are worth reading out of the output. first = second compiled and returned True, because a record defines op_Equality and Visual Basic resolves = through it; ReferenceEquals is False, so the equality is by value. Deconstruct is an ordinary method with ByRef parameters (there is no Dim (a, b) = price in Visual Basic; see Tuples and Anonymous Types). And the initializer syntax New Discount With {.Code = "SPRING", .Percent = 15D} is the only way to set an init-only property of an object you did not construct inside the class.

The failures. Setting an init-only property after construction:

Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing

Module Program
    Sub Main()
        Dim d = New Discount With {.Code = "A", .Percent = 1D}
        d.Code = "B"
        d.Percent = 2D
    End Sub
End Module
Program.vb(9,9): error BC37311: Init-only property 'Code' can only be assigned by an object member initializer, or on 'Me', 'MyClass` or 'MyBase' in an instance constructor.
Program.vb(10,9): error BC37311: Init-only property 'Percent' can only be assigned by an object member initializer, or on 'Me', 'MyClass` or 'MyBase' in an instance constructor.

Source: IsExternalInit

Leaving a required member out of the initializer (and constructing with the parameterless constructor, which does not have SetsRequiredMembers):

Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing

Module Program
    Sub Main()
        Dim d = New Discount()
        Dim e = New Discount With {.Percent = 1D}
    End Sub
End Module
Program.vb(8,21): error BC37321: Required member 'Public Overloads Property Code As String' must be set in the object initializer or attribute arguments.
Program.vb(9,21): error BC37321: Required member 'Public Overloads Property Code As String' must be set in the object initializer or attribute arguments.

A record’s with expression has no Visual Basic equivalent; the line below is read as an unexpected word after the expression:

Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing

Module Program
    Sub Main()
        Dim price = New Price(9.99D, "EUR")
        Dim cheaper = price With {.Amount = 1D}
    End Sub
End Module
Program.vb(9,29): error BC30205: End of statement expected.

The practical way to copy a record in Visual Basic is to construct a new one from the old one’s properties, or to add a WithAmount method on the C# side.

Default interface members and inheritance

A C# interface can carry a method body (Upper in ILabel). Visual Basic can call it through an interface variable, but a Visual Basic class that implements the interface must write the body itself. Visual Basic can derive from the C# abstract class Pricer and override its virtual members.

Option Strict On

Imports Shelf.Pricing

Public Class SeasonalPricer
    Inherits Pricer

    Public Overrides ReadOnly Property Name As String
        Get
            Return "seasonal (" & MyBase.Name & ")"
        End Get
    End Property

    Public Overrides Function Compute(basePrice As Decimal) As Decimal
        Return Math.Round(basePrice * 0.9D, 2)
    End Function
End Class

Public Class PlainLabel
    Implements ILabel

    Public ReadOnly Property Text As String Implements ILabel.Text
        Get
            Return "plain"
        End Get
    End Property

    ' Visual Basic classes must write the body of a default interface method themselves.
    Public Function Upper() As String Implements ILabel.Upper
        Return Text.ToUpperInvariant()
    End Function
End Class

Module InterfacesAndInheritance
    Sub Run()
        Dim label As ILabel = New ShelfLabel With {.Text = "abc"}
        Console.WriteLine(label.Upper())

        ' A Visual Basic implementation of the interface, with its own Upper.
        Dim plain As ILabel = New PlainLabel()
        Console.WriteLine(plain.Upper())

        Dim pricer As Pricer = New SeasonalPricer()
        Console.WriteLine(pricer.Name)
        Console.WriteLine(pricer.Compute(20D))
    End Sub
End Module
ABC
PLAIN
seasonal (base pricer)
18.0

Source: interface (C#)

If the class does not write Upper, the compiler refuses:

Option Strict On

Imports Shelf.Pricing

Public Class PlainLabel
    Implements ILabel

    Public ReadOnly Property Text As String Implements ILabel.Text
        Get
            Return "plain"
        End Get
    End Property
End Class

Module Program
    Sub Main()
    End Sub
End Module
Program.vb(6,16): error BC30149: Class 'PlainLabel' must implement 'Function Upper() As String' for interface 'ILabel'.

And a default method is reachable only through the interface type, not through the class that inherits it (the same rule as in C#):

Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing

Module Program
    Sub Main()
        Dim label As New ShelfLabel With {.Text = "abc"}
        Console.WriteLine(label.Upper())
    End Sub
End Module
Program.vb(9,27): error BC30456: 'Upper' is not a member of 'ShelfLabel'.

Ref returns, spans and params

using System.Numerics;
using System.Runtime.CompilerServices;
using Shelf.Core;

namespace Shelf.Pricing;

public static class PriceMath
{
    // Ref returns, spans and params.
    public static ref int RefTo(int[] values, int index) => ref values[index];

    public static ref readonly int RefReadOnlyTo(int[] values, int index) => ref values[index];

    public static int SumSpan(ReadOnlySpan<int> values)
    {
        int sum = 0;
        foreach (int v in values) sum += v;
        return sum;
    }

    public static ReadOnlySpan<int> Head(int[] values) => values.AsSpan(0, 2);

    public static int Total(params int[] values) => values.Sum();

    public static int TotalSpan(params ReadOnlySpan<int> values) => SumSpan(values);

    // Generic constraints that Visual Basic cannot write itself.
    public static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
    {
        T total = T.Zero;
        foreach (T v in values) total += v;
        return total;
    }

    public static int SizeOf<T>() where T : unmanaged => Unsafe.SizeOf<T>();

    // Overload resolution priority (C# 13): the object overload wins for a string argument.
    [OverloadResolutionPriority(1)]
    public static string Prefer(object value) => "object (priority 1)";

    public static string Prefer(string value) => "string";

    // ByRef and Optional.
    public static void Swap<T>(ref T a, ref T b) => (a, b) = (b, a);

    public static bool TryRate(string key, out decimal rate)
    {
        rate = key == "EUR" ? 1.0m : 1.1m;
        return key.Length == 3;
    }

    public static int Optional(int a, int b = 10, string label = "n") => a + b + label.Length;

    // Tuples, nullable annotations and non-null parameters.
    public static (string Name, int Count) Stats(Catalog catalog) => ("catalog", catalog.Search("").Count());

    public static string? Find(string key) => key == "x" ? "found" : null;

    public static int LengthOf(string text) => text.Length;

    // Async.
    public static Task<Price> LoadAsync(decimal amount) => Task.FromResult(new Price(amount, "EUR"));

    public static async IAsyncEnumerable<int> CountAsync(int n)
    {
        for (int i = 1; i <= n; i++)
        {
            await Task.Yield();
            yield return i;
        }
    }

    public static dynamic MakeDynamic() => new System.Dynamic.ExpandoObject();
}

The Visual Basic side uses the first group:

Option Strict On

Imports Shelf.Pricing

Module RefsSpansAndParams
    Sub Run()
        Dim numbers = New Integer() {1, 2, 3}

        ' A ref return can be read, and also written through.
        Dim copy As Integer = PriceMath.RefTo(numbers, 1)
        Console.WriteLine(copy)
        PriceMath.RefTo(numbers, 1) = 99
        Console.WriteLine(numbers(1))

        ' Spans: an array converts to ReadOnlySpan(Of Integer) at the call.
        Console.WriteLine(PriceMath.SumSpan(numbers))

        ' A returned span can be held in an inferred local; its indexer cannot be used.
        Dim head = PriceMath.Head(numbers)
        Console.WriteLine(head.Length)
        Console.WriteLine(String.Join(",", head.ToArray()))

        ' A params ReadOnlySpan parameter accepts an array, not a list of values.
        Console.WriteLine(PriceMath.TotalSpan(numbers))

        ' params Integer() is a ParamArray.
        Console.WriteLine(PriceMath.Total(1, 2, 3))
        Console.WriteLine(PriceMath.Total())
    End Sub
End Module
2
99
103
2
1,99
103
6
0

PriceMath.RefTo(numbers, 1) = 99 changed the array element: Visual Basic treats a call that returns a reference as an assignable expression. That is a useful low-level tool, and also a way to change data from a place that looks like a read, so keep it out of ordinary application code. SumSpan(numbers) passed an array where the parameter is a ReadOnlySpan(Of Integer), because the span type has an implicit conversion from arrays and Visual Basic honours user-defined conversions on method arguments.

What does not work is any use of a ref struct (such as a span) beyond holding it in an inferred local:

Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing

Module Program
    Sub Main()
        Dim s As Span(Of Integer)
        Dim r As ReadOnlySpan(Of Integer)
    End Sub
End Module
Program.vb(8,18): error BC30668: 'Span(Of Integer)' is obsolete: 'Types with embedded references are not supported in this version of your compiler.'.
Program.vb(9,18): error BC30668: 'ReadOnlySpan(Of Integer)' is obsolete: 'Types with embedded references are not supported in this version of your compiler.'.
Program.vb(8,13): warning BC42024: Unused local variable: 's'.
Program.vb(9,13): warning BC42024: Unused local variable: 'r'.

The text of BC30668 is the compiler saying that it does not know the "embedded reference" rules for ref structs. The same applies to the indexer and the enumerator of a span you did get from a call:

Option Strict On

Imports Shelf.Pricing

Module Program
    Sub Main()
        Dim numbers = New Integer() {1, 2, 3}
        Dim head = PriceMath.Head(numbers)
        Console.WriteLine(head(1))
        For Each n In head
            Console.WriteLine(n)
        Next
    End Sub
End Module
Program.vb(9,27): error BC30643: Property 'System.ReadOnlySpan(Of T).Item(index As Integer)' is of an unsupported type.
Program.vb(10,23): error BC30643: Property 'System.ReadOnlySpan(Of T).Enumerator.Current' is of an unsupported type.

Source: BC30643 and BC30668

A ref readonly return is not supported at all:

Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing

Module Program
    Sub Main()
        Dim numbers = New Integer() {1, 2, 3}
        Dim v As Integer = PriceMath.RefReadOnlyTo(numbers, 0)
        Console.WriteLine(v)
    End Sub
End Module
Program.vb(9,38): error BC30657: 'RefReadOnlyTo' has a return type that is not supported or parameter types that are not supported.

Source: BC30657

params with a ReadOnlySpan (C# 13) compiles to a span parameter with an attribute. For Visual Basic it is just a span parameter, so the individual values cannot be passed:

Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing

Module Program
    Sub Main()
        Console.WriteLine(PriceMath.TotalSpan(1, 2, 3))
    End Sub
End Module
Program.vb(8,50): error BC30057: Too many arguments to 'Public Shared Overloads Function TotalSpan(values As ReadOnlySpan(Of Integer)) As Integer'.
Program.vb(8,47): error BC30311: Value of type 'Integer' cannot be converted to 'ReadOnlySpan(Of Integer)'.

Source: BC30311 and BC30057

Remember that Visual Basic also cannot declare span-typed locals, fields or parameters: do not write code that needs them in Visual Basic, and put the span-heavy code in a C# project that exposes arrays or Memory(Of T). The rest of the language page for this is Unsafe Code, Spans and Performance (C#).

Generic constraints, overload priority and static abstract members

The generic-math constraint INumber<T>, the unmanaged constraint, OverloadResolutionPriority and static abstract interface members are all in PriceMath.cs and StaticAbstract.cs:

namespace Shelf.Pricing;

public interface IParse<T> where T : IParse<T>
{
    static abstract T Parse(string text);
}

public readonly struct Cents : IParse<Cents>
{
    public int Value { get; }
    public Cents(int value) => Value = value;
    public static Cents Parse(string text) => new(int.Parse(text));
    public override string ToString() => Value + "c";
}

public static class Parser
{
    public static T ParseOne<T>(string text) where T : IParse<T> => T.Parse(text);
}
Option Strict On

Imports Shelf.Pricing

Module GenericsAndOverloads
    Sub Run()
        ' INumber(Of T) constraint, satisfied by Integer and Double.
        Console.WriteLine(PriceMath.Sum(New Integer() {1, 2, 3}))
        Console.WriteLine(PriceMath.Sum(New Double() {1.5, 2.25}))

        ' The unmanaged constraint, satisfied by value types without references.
        Console.WriteLine(PriceMath.SizeOf(Of Integer)())
        Console.WriteLine(PriceMath.SizeOf(Of Double)())

        ' Overload resolution priority is honoured since Visual Basic 17.13.
        Console.WriteLine(PriceMath.Prefer("text"))

        ' A static abstract interface member, called through a generic method.
        Dim cents = Parser.ParseOne(Of Cents)("250")
        Console.WriteLine(cents)
        Console.WriteLine(cents.Value)

        Dim a = 1
        Dim b = 2
        PriceMath.Swap(a, b)
        Console.WriteLine(a.ToString() & " " & b.ToString())

        Dim rate As Decimal
        Console.WriteLine(PriceMath.TryRate("EUR", rate).ToString() & " " & rate.ToString())
    End Sub
End Module
6
3.75
4
8
object (priority 1)
250c
250
2 1
True 1.0

Sum was called with an Integer() and a Double() and returned 6 and 3.75; SizeOf(Of Integer) and SizeOf(Of Double) returned 4 and 8. Prefer("text") printed object (priority 1): both overloads are applicable, the String one is the better conversion, and the attribute still made the Object overload win, which is what C# 13 does. A compiler that ignored the attribute would have printed string. And Parser.ParseOne(Of Cents)("250") called a static abstract interface member through a type argument, which Visual Basic can do although it cannot declare such a member.

Visual Basic cannot implement the interface, and the unmanaged constraint is checked:

Option Strict On

Imports Shelf.Pricing

Public Structure Euros
    Implements IParse(Of Euros)

    Public Shared Function Parse(text As String) As Euros Implements IParse(Of Euros).Parse
        Return New Euros()
    End Function
End Structure

Module Program
    Sub Main()
    End Sub
End Module
Program.vb(8,12): error BC30505: Methods or events that implement interface members cannot be declared 'Shared'.
Program.vb(6,16): error BC37315: Structure 'Euros' cannot implement interface 'IParse(Of Euros)' because it contains shared abstract or virtual 'Function Parse(text As String) As Euros'.
Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing

Module Program
    Sub Main()
        Console.WriteLine(PriceMath.SizeOf(Of String)())
        Console.WriteLine(PriceMath.SizeOf(Of Date)())
    End Sub
End Module
Program.vb(8,37): error BC37332: Type argument 'String' must be a non-nullable value type, along with all fields at any level of nesting, in order to use it as type parameter 'T'.
Program.vb(8,37): error BC32105: Type argument 'String' does not satisfy the 'Structure' constraint for type parameter 'T'.

Source: BC32105

Extension methods, extension blocks, names and nullability

C# 14 added extension blocks. A method in a block is also emitted as an ordinary static method that carries the Extension attribute, so Visual Basic can call it as an extension method; a property in a block is emitted as a pair of static accessor methods, which are not extension methods.

namespace Shelf.Pricing;

public static class StringExtensions
{
    // A classic extension method.
    public static string Shout(this string text) => text.ToUpperInvariant() + "!";

    // C# 14 extension block: the new syntax for extension members.
    extension(string text)
    {
        public bool IsBlank => string.IsNullOrWhiteSpace(text);
        public string Twice() => text + text;
    }
}
namespace Shelf.Pricing;

// Names that are keywords in Visual Basic.
public class KeywordNames
{
    public int End { get; set; } = 3;
    public string Error { get; set; } = "e";
    public string Class() => "class-method";
    public static string Select(string text) => "select:" + text;
}

public class Counter
{
    public event EventHandler<int>? Changed;
    public void Raise(int value) => Changed?.Invoke(this, value);

    // Indexers.
    public int this[int i] => i * 2;
    public int this[string s] => s.Length;
}

public delegate int Combine(int a, int b);
Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing

Module ExtensionsAndNames
    Sub Run()
        ' A classic extension method and a method from a C# 14 extension block.
        Console.WriteLine("abc".Shout())
        Console.WriteLine("ab".Twice())
        ' The extension property is a static accessor method for Visual Basic.
        Console.WriteLine(StringExtensions.get_IsBlank("  "))

        ' Members named like Visual Basic keywords are fine after a dot.
        Dim names As New KeywordNames()
        Console.WriteLine(names.End)
        Console.WriteLine(names.Error)
        Console.WriteLine(names.Class())
        Console.WriteLine(KeywordNames.Select("x"))

        ' Optional parameters keep their defaults.
        Console.WriteLine(PriceMath.Optional(1))
        Console.WriteLine(PriceMath.Optional(1, label:="long"))

        ' A tuple with names, and a string? result.
        Dim catalog = New Catalog(New MemoryStore())
        Dim stats = PriceMath.Stats(catalog)
        Console.WriteLine(stats.Name & " " & stats.Count.ToString())
        Console.WriteLine(PriceMath.Find("x"))
        Console.WriteLine(PriceMath.Find("y") Is Nothing)

        ' Nullable reference annotations are not enforced: Nothing compiles.
        Try
            Console.WriteLine(PriceMath.LengthOf(Nothing))
        Catch ex As NullReferenceException
            Console.WriteLine("NullReferenceException")
        End Try
    End Sub
End Module

Public Class MemoryStore
    Implements ICatalogStore

    Private ReadOnly _items As New List(Of CatalogItem)

    Public Function GetAll() As IReadOnlyList(Of CatalogItem) Implements ICatalogStore.GetAll
        Return _items
    End Function

    Public Sub Add(item As CatalogItem) Implements ICatalogStore.Add
        _items.Add(item)
    End Sub

    Public Function Find(id As Integer) As CatalogItem Implements ICatalogStore.Find
        Return _items.First(Function(i) i.Id = id)
    End Function
End Class
ABC!
abab
True
3
e
class-method
select:x
12
15
catalog 0
found
True
NullReferenceException

The output shows "ab".Twice() working as an extension method and StringExtensions.get_IsBlank(" ") working as an ordinary static call. The natural property syntax is not available:

Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing

Module Program
    Sub Main()
        Console.WriteLine("ab".Twice())
        Console.WriteLine("ab".IsBlank)
        Console.WriteLine(StringExtensions.Twice("ab"))
        Console.WriteLine(StringExtensions.get_IsBlank("  "))
    End Sub
End Module
Program.vb(9,27): error BC30456: 'IsBlank' is not a member of 'String'.

Other lines of the output: names.End, names.Error, names.Class() and KeywordNames.Select("x") compile because Visual Basic allows a reserved word after the dot; optional C# parameters keep their defaults (Optional(1) returned 12, which is 1 + 10 + 1); the C# tuple (string Name, int Count) has element names; Find("y") Is Nothing is true for a C# string? result. Nullable annotations are not enforced: LengthOf(Nothing) compiled without a warning and threw NullReferenceException inside the C# method, which is the contract the annotation was documenting. Because Visual Basic is blind to the annotation, a C# library that you consume should still validate its arguments (ArgumentNullException.ThrowIfNull).

Events, indexers, async and F#

Option Strict On

Imports Microsoft.FSharp.Collections
Imports Shelf.Fs
Imports Shelf.Pricing

Module EventsAsyncAndFSharp
    Private Sub OnChanged(sender As Object, e As Integer)
        Console.WriteLine("Changed " & e.ToString())
    End Sub

    Private Async Function CountAsync() As Task
        Dim price = Await PriceMath.LoadAsync(5D)
        Console.WriteLine(price)

        ' There is no Await For Each: drive the async enumerator by hand.
        Dim iterator = PriceMath.CountAsync(3).GetAsyncEnumerator()
        Try
            While Await iterator.MoveNextAsync()
                Console.WriteLine(iterator.Current)
            End While
        Finally
            iterator.DisposeAsync().AsTask().GetAwaiter().GetResult()
        End Try
    End Function

    Sub Run()
        Dim counter As New Counter()
        AddHandler counter.Changed, AddressOf OnChanged
        counter.Raise(42)
        Console.WriteLine(counter(21))
        Console.WriteLine(counter("abcd"))

        Dim combine As New Combine(Function(a, b) a * b)
        Console.WriteLine(combine(6, 7))

        CountAsync().GetAwaiter().GetResult()

        Dim review = Rating.NewReview(5, "great")
        Console.WriteLine(Ratings.describe(review))
        Console.WriteLine(Ratings.describe(Rating.Unrated))
        Console.WriteLine(review.Tag)
        Console.WriteLine(Ratings.average(ListModule.OfSeq(New Integer() {4, 5, 3})))
        Console.WriteLine(Ratings.parse("12").Value)
        Console.WriteLine(Ratings.parse("x") Is Nothing)
    End Sub
End Module
Changed 42
42
4
42
Price { Amount = 5, Currency = EUR }
1
2
3
5 stars: great
unrated
2
4
12
True

The F# library:

namespace Shelf.Fs

type Rating =
    | Unrated
    | Stars of int
    | Review of stars: int * text: string

module Ratings =
    let describe (r: Rating) =
        match r with
        | Unrated -> "unrated"
        | Stars n -> sprintf "%d stars" n
        | Review (n, t) -> sprintf "%d stars: %s" n t

    let average (xs: int list) : float = List.averageBy float xs

    let parse (s: string) : int option =
        match System.Int32.TryParse s with
        | true, v -> Some v
        | _ -> None

Read the F# part of the Visual Basic side as follows. A union case with fields is created by a static New factory (Rating.NewReview(5, "great")), a case without fields is a static property (Rating.Unrated), and Tag is an integer that identifies the case. An F# module is a static class, so Ratings.describe(...) is an ordinary call; F# lowercase names are legal in Visual Basic. An F# option is FSharpOption(Of T), and None is a null reference, which Visual Basic sees as Nothing: Ratings.parse("x") Is Nothing printed True, and Ratings.parse("12").Value read the value of a Some. An F# list is an FSharpList(Of T) from Microsoft.FSharp.Collections, built here by ListModule.OfSeq. All of this requires a reference to FSharp.Core, which comes with the project reference.

The async enumerator is driven by hand because there is no Await For Each in Visual Basic:

Option Strict On

Imports Shelf.Pricing

Module Program
    Async Function RunAsync() As Task
        Await For Each n In PriceMath.CountAsync(3)
            Console.WriteLine(n)
        Next
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(7,15): error BC30201: Expression expected.
Program.vb(9,9): error BC30092: 'Next' must be preceded by a matching 'For'.

What C# sees of Visual Basic

The C# application uses the types of Shelf.Core, which is Visual Basic. The Visual Basic side defines a module, optional and ParamArray parameters, a default property, a parameterised property, a Friend method, keyword-named members and overloaded operators:

Option Strict On

Public Module ShelfInfo
    Public Const Name As String = "Shelf"

    Public Function Greet(person As String, Optional greeting As String = "Hello") As String
        Return greeting & ", " & person
    End Function

    Public Function Join(separator As String, ParamArray parts As String()) As String
        Return String.Join(separator, parts)
    End Function

    Public Sub Swap(Of T)(ByRef first As T, ByRef second As T)
        Dim temp As T = first
        first = second
        second = temp
    End Sub
End Module

Public Class Shelf
    Private ReadOnly _slots As New List(Of String)

    Public Event Filled As EventHandler

    Public Sub New()
    End Sub

    ' A parameterised property and a default property.
    Default Public Property Slot(index As Integer) As String
        Get
            Return _slots(index)
        End Get
        Set(value As String)
            _slots(index) = value
        End Set
    End Property

    Public ReadOnly Property Count As Integer
        Get
            Return _slots.Count
        End Get
    End Property

    Public Property Label(position As Integer, upper As Boolean) As String
        Get
            Dim text As String = _slots(position)
            Return If(upper, text.ToUpperInvariant(), text)
        End Get
        Set(value As String)
            _slots(position) = value
        End Set
    End Property

    Public Sub Add(title As String)
        _slots.Add(title)
        RaiseEvent Filled(Me, EventArgs.Empty)
    End Sub

    Friend Function Secret() As String
        Return "friend member"
    End Function

    Public Shared Function Total(ParamArray values As Integer()) As Integer
        Return values.Sum()
    End Function
End Class

Public Class Reading
    Public ReadOnly Property Pages As Integer
    Public Sub New(pages As Integer)
        Me.Pages = pages
    End Sub

    Public Shared Operator +(left As Reading, right As Reading) As Reading
        Return New Reading(left.Pages + right.Pages)
    End Operator
End Class

Public Class Keywords
    Public Property [Error] As String = "error-property"

    Public Function [Select]() As String
        Return "select-method"
    End Function

    Public Function [class]() As String
        Return "class-method"
    End Function

    Public Function [event]() As String
        Return "event-method"
    End Function
End Class

Public Class Money
    Public ReadOnly Property Cents As Integer

    Public Sub New(cents As Integer)
        Me.Cents = cents
    End Sub

    Public Shared Operator +(left As Money, right As Money) As Money
        Return New Money(left.Cents + right.Cents)
    End Operator

    Public Shared Operator Mod(left As Money, right As Integer) As Money
        Return New Money(left.Cents Mod right)
    End Operator

    Public Shared Operator \(left As Money, right As Integer) As Money
        Return New Money(left.Cents \ right)
    End Operator

    Public Shared Operator ^(left As Money, right As Integer) As Money
        Return New Money(CInt(Math.Pow(left.Cents, right)))
    End Operator

    Public Shared Operator &(left As Money, right As Money) As String
        Return left.Cents.ToString() & "|" & right.Cents.ToString()
    End Operator

    Public Shared Operator Like(left As Money, pattern As String) As Boolean
        Return left.Cents.ToString() Like pattern
    End Operator

    Public Shared Operator IsTrue(value As Money) As Boolean
        Return value.Cents <> 0
    End Operator

    Public Shared Operator IsFalse(value As Money) As Boolean
        Return value.Cents = 0
    End Operator

    Public Shared Widening Operator CType(value As Integer) As Money
        Return New Money(value)
    End Operator
End Class
using Shelf.Core;
using Shelf.Pricing;

Isbn isbn = Isbn.Parse("9780306406157");
var book = new Book("Interop", 2026, isbn, new Author("Ada"), new Author("Grace"));
Console.WriteLine(book.Describe());

// A VB Module is a static-like class; Optional and ParamArray are honoured.
Console.WriteLine(ShelfInfo.Greet("Ada"));
Console.WriteLine(ShelfInfo.Greet("Ada", "Welcome"));
Console.WriteLine(ShelfInfo.Join("-", "a", "b", "c"));
Console.WriteLine(ShelfInfo.Name);

// A VB Default Property is an indexer; a parameterised property is a method pair.
var shelf = new Shelf.Core.Shelf();
shelf.Filled += (s, e) => Console.WriteLine("Filled raised");
shelf.Add("Dune");
shelf.Add("Emma");
Console.WriteLine(shelf[1]);
shelf[1] = "Persuasion";
Console.WriteLine(shelf.get_Label(1, true));
shelf.set_Label(0, false, "Dune II");
Console.WriteLine(shelf[0]);
Console.WriteLine(Shelf.Core.Shelf.Total(1, 2, 3));

// An InternalsVisibleTo friend sees Friend members.
Console.WriteLine(shelf.Secret());

// VB operator overloads.
var r = new Reading(10) + new Reading(5);
Console.WriteLine(r.Pages);

// A Visual Basic Widening CType operator is op_Implicit for C#.
Money converted = 5;
Console.WriteLine(converted.Cents);

// VB [Return] method.
var loan = new Loan(book, "Ada", new DateTime(2026, 10, 19));
loan.Returned += (s, e) => Console.WriteLine("Returned: " + e.Loan.Item.Title);
loan.Return();

// Keyword-named members: C# uses @ for its own keywords only.
var kw = new Keywords();
Console.WriteLine(kw.Select() + " " + kw.@class() + " " + kw.@event() + " " + kw.Error);

// VB operators: + and Mod map to C# operators, \\ ^ & Like have no C# syntax.
var money = new Money(250) + new Money(50);
Console.WriteLine((money % 7).Cents);
Console.WriteLine(Money.op_IntegerDivision(money, 4).Cents);
Console.WriteLine(Money.op_Concatenate(money, new Money(1)));

// The C# helper project can sit on the VB model.
var catalog = new Catalog(new InMemory());
catalog.Add(book);
Console.WriteLine(PriceMath.Stats(catalog));

class InMemory : ICatalogStore
{
    private readonly List<CatalogItem> _items = new();
    public IReadOnlyList<CatalogItem> GetAll() => _items;
    public void Add(CatalogItem item) => _items.Add(item);
    public CatalogItem Find(int id) => _items.First(i => i.Id == id);
}
Book: Interop (2026) by Ada, Grace
Hello, Ada
Welcome, Ada
a-b-c
Shelf
Filled raised
Filled raised
Emma
PERSUASION
Dune II
6
friend member
15
5
Returned: Interop
select-method class-method event-method error-property
6
75
300|1
(catalog, 1)

What the C# program shows:

  • A Visual Basic Module is a class whose members C# calls through the module’s name (ShelfInfo.Greet). The Const is a constant.

  • Optional greeting As String = "Hello" appears to C# as an optional parameter with a default (ShelfInfo.Greet("Ada") printed Hello, Ada), and ParamArray is params.

  • A Default Property is an indexer (shelf[1]).

  • A parameterised property that is not the default has no C# syntax: C# calls its accessor methods, get_Label(1, true) and set_Label(0, false, "Dune II"). A Visual Basic library meant for C# callers should therefore prefer methods or a default property.

  • Friend is internal. The assembly attribute InternalsVisibleTo Include="Shelf.CsClient" in Shelf.Core.vbproj is what lets shelf.Secret() compile in the client.

  • Events are ordinary .NET events; Handles and WithEvents are Visual Basic compile-time features that leave no trace in the public surface.

  • A member named with brackets, such as Public Function [class](), is emitted as class, so C# needs @class. Names that are reserved in Visual Basic but not in C# ([Select], [Error], [event]) are only the concern of the Visual Basic author.

  • A Visual Basic Widening conversion becomes op_Implicit, which C# applies automatically: Money converted = 5; printed 5.

The failures:

Program.cs(4,25): error CS1061: 'Shelf' does not contain a definition for 'Secret' and no accessible extension method 'Secret' accepting a first argument of type 'Shelf' could be found (are you missing a using directive or an assembly reference?)

(from shelf.Secret() in a project whose assembly name is not on the InternalsVisibleTo list),

Program.cs(5,25): error CS1061: 'Shelf' does not contain a definition for 'Slot' and no accessible extension method 'Slot' accepting a first argument of type 'Shelf' could be found (are you missing a using directive or an assembly reference?)

(from shelf.Slot[0]: a default property is an indexer for C#, written shelf[0], and is not reached by its name), and

Program.cs(4,29): error CS7036: There is no argument given that corresponds to the required parameter 'person' of 'ShelfInfo.Greet(string, string)'

(from ShelfInfo.Greet() with no argument: the first parameter is required, only greeting is optional).

Operators

Visual Basic has operators that C# does not. They are emitted as op_ methods with the names listed below, which C# can call by name but cannot write as operators. The list comes from reflection over Money, the class that declares them:

Option Strict On

Imports System.Reflection
Imports Shelf.Core

Module OperatorNames
    Sub Run()
        ' A Visual Basic class that overloads operators: the metadata names C# sees.
        Dim flags = BindingFlags.Public Or BindingFlags.Static Or BindingFlags.DeclaredOnly
        For Each method In GetType(Money).GetMethods(flags).OrderBy(Function(m) m.Name)
            Console.WriteLine(method.Name)
        Next
    End Sub
End Module
op_Addition
op_Concatenate
op_Exponent
op_False
op_Implicit
op_IntegerDivision
op_Like
op_Modulus
op_True
Visual Basic operator Metadata name C# view

+

op_Addition

+

Mod

op_Modulus

%

\ (integer division)

op_IntegerDivision

no operator; call Money.op_IntegerDivision(money, 4) (it returned 75 for 300 and 4)

^ (exponent)

op_Exponent

no operator; C# ^ is exclusive or, so money ^ 2 fails

& (concatenate)

op_Concatenate

no operator; Money.op_Concatenate(money, new Money(1)) printed 300|1

Like

op_Like

no operator

IsTrue, IsFalse

op_True, op_False

C# true and false operators

Widening CType

op_Implicit

implicit conversion

Writing the Visual Basic exponent in C#, Console.WriteLine((money ^ 2).Cents);, compiles ^ as exclusive or, which Money does not define:

Program.cs(4,20): error CS0019: Operator '^' cannot be applied to operands of type 'Money' and 'int'

Names, case and CLS compliance

Visual Basic identifiers are case-insensitive, C# ones are not, and the Common Language Specification (CLS) is the set of rules for a public API that every language can use. A C# library that declares Value and value in the same class is legal C#, and not CLS-compliant. A project that marks its assembly CLSCompliant(True) asks its own compiler to report these problems:

[assembly: System.CLSCompliant(true)]

namespace Shelf.Cls;

public class Clash
{
    public int Value { get; set; } = 1;
    public int value { get; set; } = 2;
    public string Name() => "method";
    public string name() => "other";
    public uint Counter() => 7;
}

public class Ok
{
    public int Count { get; set; } = 3;
}

The C# compiler reports, for that file:

Cls/Cls.cs(8,16): warning CS3005: Identifier 'Clash.value' differing only in case is not CLS-compliant
Cls/Cls.cs(8,24): warning CS3005: Identifier 'Clash.value.get' differing only in case is not CLS-compliant
Cls/Cls.cs(8,29): warning CS3005: Identifier 'Clash.value.set' differing only in case is not CLS-compliant
Cls/Cls.cs(10,19): warning CS3005: Identifier 'Clash.name()' differing only in case is not CLS-compliant
Cls/Cls.cs(11,17): warning CS3002: Return type of 'Clash.Counter()' is not CLS-compliant

Visual Basic cannot decide which member a case-insensitive name means. Two members that differ only in case, or a method and a property with the same name, are ambiguous at the call site:

Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing
Imports Shelf.Cls

Module Program
    Sub Main()
        Dim item As New Clash()
        Console.WriteLine(item.Value)
    End Sub
End Module
Program.vb(10,32): error BC31429: 'Value' is ambiguous because multiple kinds of members with this name exist in class 'Clash'.

Source: BC31429

Option Strict On

Imports Shelf.Core
Imports Shelf.Pricing

Module Program
    Sub Main()
        Dim cls As New Shelf.Cls.Clash()
        Console.WriteLine(cls.Name())
    End Sub
End Module
Program.vb(9,31): error BC31429: 'Name' is ambiguous because multiple kinds of members with this name exist in class 'Clash'.

The same attribute on the Visual Basic side makes the Visual Basic compiler report a public member that other languages could not use, such as an unsigned type:

Option Strict On

<Assembly: CLSCompliant(True)>

Public Class Reading
    Public Function Counter() As UInteger
        Return 7UI
    End Function

    Public Sub Scale(factor As ULong)
    End Sub

    Public Sub Print()
    End Sub

    Public Sub print(value As Integer)
    End Sub
End Class

Module Program
    Sub Main()
    End Sub
End Module
Program.vb(6,21): warning BC40027: Return type of function 'Counter' is not CLS-compliant.
Program.vb(10,22): warning BC40028: Type of parameter 'factor' is not CLS-compliant.

For a mixed-language solution the rules reduce to a short list:

Rule Reason

Do not expose members that differ only in case, in either language

Visual Basic cannot bind them (BC31429); C# callers cannot tell which is meant when a Visual Basic library is reached by case-folding tools

Prefer Integer, Long, Double, Decimal, String, Boolean, Date in public signatures; avoid unsigned types and ref structs

not CLS-compliant (CS3002 in C#, BC40027 and BC40028 in Visual Basic) and ref structs cannot be named in Visual Basic

Avoid public members named like keywords of either language

[End], @class work but make every caller type brackets

Avoid parameterised properties and Module-only designs in libraries that C# callers use

C# sees accessor methods; a module is a static class

Avoid ref readonly, params spans, static abstract implementations and required or init in an API that a Visual Basic project must implement

Visual Basic cannot implement or express them

Put <Assembly: CLSCompliant(True)> (or [assembly: CLSCompliant(true)]) on the library and keep warnings visible

the compiler of the library’s own language then lists the problems for you

Other .NET languages

  • F# is shown above. The things to remember are FSharp.Core (a runtime dependency of any assembly that exposes F# types), unions as classes with New factories and Tag, and option being a nullable reference.

  • C++/CLI (Windows only) produces ordinary managed assemblies too; they are referenced like any other, and the same CLS rules apply. This was not tested for this page.

  • Visual Basic from C# works through the same mechanisms in reverse. If the public API of a Visual Basic library exposes Visual Basic runtime types such as Collection, the C# project needs Microsoft.VisualBasic.Core, which is part of the shared framework, so no package is needed. C# and .NET has the C# view of the same options.

  • Java is not a .NET language and has no built-in interoperability with Visual Basic; bridges such as IKVM or JNI exist as third-party and native options and are not covered here.

When a library needs a feature Visual Basic cannot express

Visual Basic is in a stable, consumption-only state, so the right move for a feature it lacks is to put that part in C# in the same solution, as Shelf.Pricing does here. The pattern that works:

  1. Isolate the unsupported feature in a small C# class library (generic math, ref structs, spans, source generators, static abstract interface implementations, required/init, ref readonly).

  2. Expose a CLS-friendly public API from it: arrays, IReadOnlyList(Of T), Task, plain classes and records, delegates. Avoid spans and ref in the signatures Visual Basic must call.

  3. Reference it from the Visual Basic project with a ProjectReference (as Shelf.Cli does) and put both under one solution so that a single dotnet build and dotnet test cover them.

  4. Test the boundary from Visual Basic. The example above is the template: one Visual Basic file per C# feature, built with Option Strict On.

  5. Use InternalsVisibleTo for test projects only; do not use it to give a Visual Basic project access to C# internals.

Source generators are an example that crosses the boundary badly: the generated code is C#, and in the experiments for Serialization: JSON and XML a generator attached to a Visual Basic project was not usable (BC30610), so put types that need a generator in a C# project and reference it.

Where older material is outdated

  • Older guides describe interoperating by adding a reference to a .dll built for .NET Framework, and registering assemblies in the GAC. For current .NET use project and package references; the GAC is a .NET Framework concept.

  • Advice that Visual Basic "cannot use" modern C# is too strong and too weak: it can consume records, init, required, default interface members, generic-math methods and overload priority; it cannot implement static abstract members, use spans, or declare the newer modifiers. The table above is the current state.

  • Statements that a C# project cannot reference a Visual Basic project, or the reverse, are wrong for SDK-style projects: any language mix is fine.

  • Material that treats the Microsoft.VisualBasic namespace as a .NET Framework component to avoid in C# is outdated for .NET: Microsoft.VisualBasic.Core is part of the shared framework.