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.
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.
Source: dotnet sln and MSBuild properties for Microsoft.NET.Sdk
Referencing another language
-
Project references are the normal case in one repository.
ProjectReferencecopies the output assembly and flows its dependencies (the F# project bringsFSharp.Corewith it intoShelf.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.Corepackage (written in C#) and calls its extension methods. -
Assembly references (
Referencewith aHintPath) 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.0project can referencenet10.0andnetstandard2.0libraries, 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 |
Init-only setters (C# 9) |
16.9 |
assignable in an object initializer or in a constructor of the class; elsewhere BC37311 |
|
enforced |
BC37321 when not set in the initializer; |
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) |
|
15 |
a ref return can be read into a value and also assigned through; |
|
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 |
|
ParamArray |
works like a Visual Basic |
|
not supported |
the values cannot be passed separately (BC30311, BC30057); an array can be passed |
|
consumed |
a generic method with the constraint can be called with |
|
17.13 |
accepted for blittable value types; |
|
17.13 |
honoured when choosing between overloads |
Extension methods and extension blocks (C# 14) |
consumed |
methods in an |
Nullable reference annotations (C# 8) |
ignored |
no warning; passing |
Named tuples (C# 7) |
15 |
element names are honoured |
|
consumed |
|
Events, delegates, indexers, optional parameters |
consumed |
all work; indexers appear as default properties |
|
|
the attribute is ignored; use a late-bound call site, see Early and Late Binding |
F# unions and options |
consumed |
factory methods and |
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
Source: Records (C#) and required modifier
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.
Source: RequiredMemberAttribute
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
Source: ref struct types (C#)
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.
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)'.
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
Source: Unmanaged types and OverloadResolutionPriorityAttribute
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
Source: ExtensionAttribute and ExtensionMarkerAttribute
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
Source: Discriminated unions (F#) and Options (F#)
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)
Source: Optional parameters and InternalsVisibleTo
What the C# program shows:
-
A Visual Basic
Moduleis a class whose members C# calls through the module’s name (ShelfInfo.Greet). TheConstis a constant. -
Optional greeting As String = "Hello"appears to C# as an optional parameter with a default (ShelfInfo.Greet("Ada")printedHello, Ada), andParamArrayisparams. -
A
Default Propertyis 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)andset_Label(0, false, "Dune II"). A Visual Basic library meant for C# callers should therefore prefer methods or a default property. -
Friendisinternal. The assembly attributeInternalsVisibleTo Include="Shelf.CsClient"inShelf.Core.vbprojis what letsshelf.Secret()compile in the client. -
Events are ordinary .NET events;
HandlesandWithEventsare 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 asclass, 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
Wideningconversion becomesop_Implicit, which C# applies automatically:Money converted = 5;printed5.
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 |
|---|---|---|
|
|
|
|
|
|
|
|
no operator; call |
|
|
no operator; C# |
|
|
no operator; |
|
|
no operator |
|
|
C# |
|
|
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'
Source: Operator procedures (see also Operator Overloading and Equality)
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'.
Source: Language independence and CLSCompliantAttribute
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 |
not CLS-compliant (CS3002 in C#, BC40027 and BC40028 in Visual Basic) and |
Avoid public members named like keywords of either language |
|
Avoid parameterised properties and |
C# sees accessor methods; a module is a static class |
Avoid |
Visual Basic cannot implement or express them |
Put |
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 withNewfactories andTag, andoptionbeing 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 needsMicrosoft.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:
-
Isolate the unsupported feature in a small C# class library (generic math, ref structs, spans, source generators,
static abstractinterface implementations,required/init,ref readonly). -
Expose a CLS-friendly public API from it: arrays,
IReadOnlyList(Of T),Task, plain classes and records, delegates. Avoid spans andrefin the signatures Visual Basic must call. -
Reference it from the Visual Basic project with a
ProjectReference(asShelf.Clidoes) and put both under one solution so that a singledotnet buildanddotnet testcover them. -
Test the boundary from Visual Basic. The example above is the template: one Visual Basic file per C# feature, built with
Option Strict On. -
Use
InternalsVisibleTofor 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
.dllbuilt 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 implementstatic abstractmembers, 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.VisualBasicnamespace as a .NET Framework component to avoid in C# is outdated for .NET:Microsoft.VisualBasic.Coreis part of the shared framework.
References
-
Language independence and language-independent components and CLSCompliantAttribute
-
dotnet sln, MSBuild properties for Microsoft.NET.Sdk and Package references in project files
-
Records, required, unmanaged types and What’s new in C# 14 (C#)
-
Visual Basic and .NET, Early and Late Binding, Native Interop: Declare and P/Invoke, Libraries, Packaging and Deployment and Projects, MSBuild and the .NET CLI in this section