Web, HTTP and Services

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Examples marked "built and run" are complete programs that start with Option Strict On, were built with warnings treated as errors and were run in the .NET 10 SDK container (Linux, arm64); the output shown is real output. No example needs the internet: every web server in this page listens on the loopback address (port 0, so the operating system chooses a free port) or runs in memory, and the client code talks to it inside the same process. The scratch projects had the root namespace App. Code that needs .NET Framework or Windows is labelled "built, not run" and was compiled against reference assemblies on Linux; nothing more is claimed for it.

"The web" means several different things to a Visual Basic programmer, and the answer to "can I do it in VB?" changes with each. The server side has a hard edge: ASP.NET Web Forms and ASP.NET MVC 5 are fully supported with Visual Basic, but they run only on .NET Framework and IIS, and ASP.NET Core can be written in Visual Basic by hand, but Microsoft ships no Visual Basic template for it and the Razor view engine has no Visual Basic syntax. The client side has no edge at all: HttpClient, System.Net.Http.Json, the SignalR client and ClientWebSocket are ordinary .NET libraries that Visual Basic calls like any other. This page covers both sides, using the shelf library catalogue (an ISBN lookup service and its callers) as the running example. The existing sections on ASP.NET Web Forms, ASP.NET MVC and ASP.NET Core document those frameworks in depth, in C# for ASP.NET Core and in markup plus C# or VB code-behind for the Framework stacks; this page does not repeat them, it says what is different when the language is Visual Basic.

What the web means for Visual Basic

Workload Runs on Visual Basic status (checked 2026-10-19) What to do

ASP.NET Web Forms (.aspx, .ascx, code-behind .aspx.vb)

.NET Framework 4.8.1, IIS

Fully supported: Visual Studio 2026 still offers "ASP.NET Web Application (.NET Framework)" with Visual Basic. No .NET 10 version exists.

Maintain it; plan the move described in Migrating to Modern ASP.NET.

ASP.NET MVC 5 and Web API 2

.NET Framework 4.8.1, IIS

Fully supported, views are .vbhtml Razor files (the Razor dialect with Visual Basic syntax exists only on this stack).

Maintain it; see Migrating to ASP.NET Core.

ASP.NET Core minimal APIs and Web API controllers

.NET 10, Kestrel

Not a documented or templated VB workload, but the libraries are ordinary .NET and the code compiles and runs (this page proves it). Handwritten.

Possible for small services; keep to the rules in the section on pitfalls below.

ASP.NET Core Razor Pages, MVC views, Blazor

.NET 10

Not available: .cshtml files are silently ignored in a VB project (tested below); .razor components go through the same C#-only Razor compiler.

Write the UI in C#, keep the domain in a VB class library.

A VB class library used by a C# ASP.NET Core host

.NET 10

Supported and ordinary: a project reference or a package.

The recommended design when the host is ASP.NET Core.

HTTP, JSON, SignalR and WebSocket clients

.NET 10 and .NET Framework

Fully supported.

HttpClient, IHttpClientFactory, System.Net.Http.Json, the SignalR .NET client.

SOAP and WCF clients

.NET 10

The WCF client libraries work; the dotnet-svcutil generator writes C# only.

Generate the proxy into a small C# library, or write the contract by hand.

ASMX web services, WCF servers, .svc hosting

.NET Framework

Framework only; nothing to port directly.

CoreWCF (a server-side open-source port) or a REST API.

MyType=Web and the web My objects

.NET Framework only

Does not compile on .NET 10 (BC30002, shown below).

Use HttpContext and dependency injection.

The matrix is the page in one table. The sections that follow take the rows in order: the Framework stacks, ASP.NET Core written in Visual Basic, the class-library pattern, and then the client side.

flowchart TD A["I want to do web work in Visual Basic"] --> B{"Server or client?"} B -->|"Client: call an HTTP API"| C["HttpClient, IHttpClientFactory,
System.Net.Http.Json (any .NET)"] B -->|"Client: real-time"| D["SignalR .NET client
or ClientWebSocket"] B -->|"Client: SOAP"| E["WCF client libraries,
proxy generated as C#"] B -->|"Server"| F{"Existing application?"} F -->|"Yes: Web Forms or MVC 5"| G["Stay on .NET Framework,
VB code-behind is supported"] F -->|"No: new service"| H{"Needs views or Blazor?"} H -->|"Yes"| I["C# host for the UI,
VB class library for the domain"] H -->|"No: JSON API"| J{"Comfortable without templates?"} J -->|"Yes"| K["ASP.NET Core minimal API or controllers
written by hand in VB"] J -->|"No"| I

Web Forms and MVC 5 with Visual Basic

Web Forms and MVC 5 are the stacks where Visual Basic is a first-class language on the server. A Web Forms page is markup in an .aspx file with a Visual Basic code-behind file (Default.aspx.vb); an MVC 5 application has controllers in .vb files and views in .vbhtml. Both run on .NET Framework 4.8.1 under IIS, which is why they have no net10.0 form and why the rest of this page cannot show them running on Linux. The listing below is the code-behind of a Web Forms search page and an MVC 5 controller for the shelf, compiled against the .NET Framework 4.8 reference assemblies (Microsoft.NETFramework.ReferenceAssemblies, which the SDK adds on its own for a net48 target) with the Microsoft.AspNet.Mvc 5.3.0 package. It is built, not run: compiling proves the language and API shapes, not that a page renders.

Built, not run: Web Forms code-behind and an MVC 5 controller in Visual Basic (net48)
Option Strict On

Imports System.Web
Imports System.Web.Mvc
Imports System.Web.UI
Imports System.Web.UI.WebControls

' Web Forms code-behind: the .aspx markup declares the controls, this class handles the events.
Public Partial Class BookSearch
    Inherits Page

    Protected SearchBox As TextBox
    Protected ResultLabel As Label

    Protected Sub SearchButton_Click(sender As Object, e As EventArgs)
        ResultLabel.Text = "Searching for " & HttpUtility.HtmlEncode(SearchBox.Text)
        Session("LastSearch") = SearchBox.Text
    End Sub
End Class

' ASP.NET MVC 5 controller.
Public Class BooksController
    Inherits Controller

    Function Index() As ActionResult
        ViewData("Title") = "Shelf"
        Return View()
    End Function

    <HttpPost>
    <ValidateAntiForgeryToken>
    Function Lend(isbn As String) As ActionResult
        Return RedirectToAction("Index")
    End Function
End Class

The project file is an SDK-style .vbproj targeting net48, with <Reference Include="System.Web" /> and the two NuGet packages. Visual Studio’s "ASP.NET Web Application (.NET Framework)" template builds the same code.

ShelfWeb -> bin/Debug/net48/ShelfWeb.dll
Build succeeded.

Three things are specific to Visual Basic on these stacks. First, Handles clauses wire events in code-behind (Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load), exactly as in Windows Forms; the markup can also name the handler (OnClick="SearchButton_Click") as above, in which case no Handles is written. Second, Public Partial Class is how the designer-generated half of a page (the .aspx.designer.vb file declaring the control fields) joins the half you edit; in a hand-written class the fields are plain Protected members. Third, Razor views are .vbhtml files with Visual Basic expressions (@If model IsNot Nothing Then ... End If); that dialect exists only in MVC 5 and Web Pages, and was never carried to ASP.NET Core.

The web My objects belong to the same world. MyType=Web makes the compiler generate My.User, My.Request, My.Response, My.Computer (as ServerComputer) and My.Log (as AspLog), all backed by System.Web. On .NET Framework that works; on .NET 10 it does not. The same project property that builds on net48:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Library</OutputType>
    <TargetFramework>net48</TargetFramework>
    <MyType>Web</MyType>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System.Web" />
  </ItemGroup>
</Project>

builds there (a class using My.User.Name compiled, not run), while <MyType>Web</MyType> in a Microsoft.NET.Sdk.Web project that targets net10.0 fails with five BC30002 errors:

vbc : error BC30002: Type 'Global.Microsoft.VisualBasic.ApplicationServices.WebUser' is not defined.
vbc : error BC30002: Type 'Global.Microsoft.VisualBasic.Devices.ServerComputer' is not defined.
vbc : error BC30002: Type 'Global.Microsoft.VisualBasic.Logging.AspLog' is not defined.
vbc : error BC30002: Type 'Global.System.Web.HttpRequest' is not defined.
vbc : error BC30002: Type 'Global.System.Web.HttpResponse' is not defined.

The generated My code names System.Web.HttpRequest and friends, which do not exist on .NET 10. The other MyType values (WindowsForms, WindowsFormsWithCustomSubMain, Console, Empty) are described in The My Namespace.

Session and application state are the two ideas that Classic ASP, Web Forms and ASP.NET Core share, with different mechanics: Session("visits") and Application("hits") are dictionaries of Object in Classic ASP and Web Forms. ASP.NET Core splits them: per-visitor state is the opt-in session middleware (typed accessors such as SetInt32), application-wide state is an ordinary singleton or a field. The ASP.NET Core version, written in Visual Basic, is shown further down in the section on sessions.

ASP.NET Core written in Visual Basic

ASP.NET Core is a set of ordinary .NET libraries (the Microsoft.AspNetCore.App shared framework), so nothing stops Visual Basic from calling them. What is missing is everything around the libraries: dotnet new webapi -lang VB does not exist, and the template engine says so:

No templates found matching: 'webapi', -lang='VB'.
Allowed values for '-lang' option are: 'C#', 'F#'.

Microsoft’s ASP.NET Core documentation shows C# only, and the Razor compiler produces C# and skipped the Visual Basic project in the test below. The language strategy (see History, Versions and Language Strategy) says that new workloads are not planned for Visual Basic, so treat what follows as supported-by-the-compiler, not supported-by-the-product.

The project file is the C# one with a .vbproj extension: the Web SDK works for Visual Basic. The difference that matters is that there is no implicit using list. C# web projects get ImplicitUsings, a generated file of global usings for Microsoft.AspNetCore.Builder, Microsoft.AspNetCore.Http, Microsoft.Extensions.DependencyInjection and others; a Visual Basic project gets none of them, so every file imports what it needs, and a missing import shows up as BC30456 on an extension method:

error BC30456: 'UseUrls' is not a member of 'ConfigureWebHostBuilder'.
error BC30456: 'ClearProviders' is not a member of 'ILoggingBuilder'.

Both are extension methods whose namespaces (Microsoft.AspNetCore.Hosting and Microsoft.Extensions.Logging) were not imported. The demonstration project used in this page has one .vbproj and several modules; StartupObject selects the program, and each listing is complete except for the shared types, shown first:

The scratch project used by the ASP.NET Core examples (App.vbproj)
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>App</RootNamespace>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <StartupObject>App.MinimalApiDemo</StartupObject>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.13" />
    <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.13" />
    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.13" />
    <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.11.0" />
  </ItemGroup>
</Project>

The three Microsoft.AspNetCore.* packages and the resilience package are needed only by the later examples; the minimal API itself needs none. (When switching StartupObject on the command line with -p:StartupObject=App.X, build with --no-incremental: MSBuild does not see a changed property as a reason to recompile, and the previous program runs again.)

Built and run: the shared types (Shared.vb), used by every later ASP.NET Core listing
Option Strict On

Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting.Server
Imports Microsoft.AspNetCore.Hosting.Server.Features
Imports Microsoft.Extensions.DependencyInjection

Public Class BookDto
    Public Property Isbn As String = ""
    Public Property Title As String = ""
    Public Property Year As Integer
End Class

Public Class BookStore
    Private ReadOnly _books As New Dictionary(Of String, BookDto) From {
        {"9780306406157", New BookDto With {.Isbn = "9780306406157", .Title = "Shelf Basics", .Year = 2020}}
    }

    Public Function Find(isbn As String) As BookDto
        Dim book As BookDto = Nothing
        Return If(_books.TryGetValue(isbn, book), book, Nothing)
    End Function

    Public Sub Add(book As BookDto)
        _books(book.Isbn) = book
    End Sub

    Public Function All() As IReadOnlyList(Of BookDto)
        Return _books.Values.ToList()
    End Function
End Class

Friend Module Hosting

    ' The loopback address Kestrel picked when asked for port 0.
    Friend Function ServerAddress(app As WebApplication) As String
        Return app.Services.GetRequiredService(Of IServer)().Features.Get(Of IServerAddressesFeature)().Addresses.First()
    End Function

End Module

BookDto is a plain class with public get/set properties and a parameterless constructor, which is what the JSON binder needs; Visual Basic has no record, and a Structure or a class with only a parameterised constructor would not bind the way a C# record does. Hosting.ServerAddress reads the address Kestrel chose for port 0 from IServerAddressesFeature.

A minimal API

A minimal API maps a URL pattern and a delegate to the request pipeline. Visual Basic lambdas are the delegates. Two things differ from the C# examples in the ASP.NET Core documentation: a multi-statement lambda is written as a Function block ending in End Function, and the As IResult is worth keeping, because when the branches return different result types (here Results.NotFound() and Results.Ok(book)) an explicit return type avoids relying on type inference.

Built and run: a minimal API with GET and POST, called through HttpClient in the same process
Option Strict On

Imports System.Net.Http
Imports System.Net.Http.Json
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports Microsoft.AspNetCore.Http
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

Module MinimalApiDemo

    Sub Main(args As String())
        RunAsync(args).GetAwaiter().GetResult()
    End Sub

    Async Function RunAsync(args As String()) As Task
        Dim builder = WebApplication.CreateBuilder(args)
        builder.WebHost.UseUrls("http://127.0.0.1:0")
        builder.Configuration("Logging:LogLevel:Default") = "None"
        builder.Services.AddSingleton(Of BookStore)()
        Dim app = builder.Build()

        app.MapGet("/books", Function(store As BookStore) store.All())
        app.MapGet("/books/{isbn}",
                   Function(isbn As String, store As BookStore) As IResult
                       Dim book As BookDto = store.Find(isbn)
                       Return If(book Is Nothing, Results.NotFound(), Results.Ok(book))
                   End Function)
        app.MapPost("/books",
                    Function(book As BookDto, store As BookStore) As IResult
                        store.Add(book)
                        Return Results.Created("/books/" & book.Isbn, book)
                    End Function)

        Await app.StartAsync()
        Dim address As String = Hosting.ServerAddress(app)
        Console.WriteLine("listening on loopback: " & address.StartsWith("http://127.0.0.1:").ToString())

        Using client As New HttpClient With {.BaseAddress = New Uri(address)}
            Dim all = Await client.GetFromJsonAsync(Of List(Of BookDto))("/books")
            Console.WriteLine("GET /books -> " & all.Count & " book(s), first = " & all(0).Title)

            Dim created = Await client.PostAsJsonAsync("/books", New BookDto With {.Isbn = "9780131103627", .Title = "Types and Tables", .Year = 1988})
            Console.WriteLine("POST /books -> " & CInt(created.StatusCode) & " " & created.StatusCode.ToString() & ", Location = " & created.Headers.Location.ToString())

            Dim found = Await client.GetAsync("/books/9780131103627")
            Console.WriteLine("GET known -> " & CInt(found.StatusCode) & " " & Await found.Content.ReadAsStringAsync())

            Dim missing = Await client.GetAsync("/books/0000000000000")
            Console.WriteLine("GET unknown -> " & CInt(missing.StatusCode) & " " & missing.StatusCode.ToString())
        End Using

        Await app.StopAsync()
    End Function

End Module
listening on loopback: True
GET /books -> 1 book(s), first = Shelf Basics
POST /books -> 201 Created, Location = /books/9780131103627
GET known -> 200 {"isbn":"9780131103627","title":"Types and Tables","year":1988}
GET unknown -> 404 NotFound

Notes on the listing. WebApplication.CreateBuilder(args) and builder.Build() are the same calls as in C#. app.StartAsync() (rather than app.Run()) starts Kestrel without blocking so that the program can drive it; a real service ends Main with app.Run(). The entry point is a synchronous Sub Main that waits on an Async Function, because the language has no Async Main (declaring one fails with BC30737, see the pitfalls below). Configuration("Logging:LogLevel:Default") = "None" silences the request log so that the output stays readable; ClearProviders() would need Imports Microsoft.Extensions.Logging, and that import makes the name Console bind to the namespace Microsoft.Extensions.Logging.Console (BC30456 on every Console.WriteLine), which is why the listing uses configuration instead. If you do need the logging import, put it in a file that does not print.

Controllers

Web API controllers are classes decorated with attributes, which Visual Basic writes with angle brackets and line continuation inside the brackets as usual. The action methods are Function`s; `ActionResult(Of BookDto) is ActionResult<BookDto>, and returning a BookDto where an ActionResult(Of BookDto) is expected works through the implicit conversion that the type defines (Visual Basic accepts the widening conversion without a cast). Validation attributes on the model class give the automatic 400 application/problem+json response of [ApiController].

Built and run: a controller with model validation, called in the same process
Option Strict On

Imports System.ComponentModel.DataAnnotations
Imports System.Net.Http
Imports System.Net.Http.Json
Imports System.Text.Json
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports Microsoft.AspNetCore.Http
Imports Microsoft.AspNetCore.Mvc
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

Public Class NewBook
    <Required, StringLength(13, MinimumLength:=10)>
    Public Property Isbn As String = ""

    <Required>
    Public Property Title As String = ""

    <Range(1450, 2100)>
    Public Property Year As Integer
End Class

<ApiController>
<Route("api/[controller]")>
Public Class BooksController
    Inherits ControllerBase

    Private ReadOnly _store As BookStore

    Public Sub New(store As BookStore)
        _store = store
    End Sub

    <HttpGet>
    Public Function GetAll() As IEnumerable(Of BookDto)
        Return _store.All()
    End Function

    <HttpGet("{isbn}")>
    Public Function GetOne(isbn As String) As ActionResult(Of BookDto)
        Dim book As BookDto = _store.Find(isbn)
        If book Is Nothing Then
            Return NotFound()
        End If
        Return book
    End Function

    <HttpPost>
    Public Function Create(newBook As NewBook) As ActionResult(Of BookDto)
        Dim book As New BookDto With {.Isbn = newBook.Isbn, .Title = newBook.Title, .Year = newBook.Year}
        _store.Add(book)
        Return CreatedAtAction(NameOf(GetOne), New With {.isbn = book.Isbn}, book)
    End Function
End Class

Module ControllersDemo

    Sub Main(args As String())
        RunAsync(args).GetAwaiter().GetResult()
    End Sub

    Async Function RunAsync(args As String()) As Task
        Dim builder = WebApplication.CreateBuilder(args)
        builder.WebHost.UseUrls("http://127.0.0.1:0")
        builder.Configuration("Logging:LogLevel:Default") = "None"
        builder.Services.AddSingleton(Of BookStore)()
        builder.Services.AddControllers()
        Dim app = builder.Build()
        app.MapControllers()
        Await app.StartAsync()

        Using client As New HttpClient With {.BaseAddress = New Uri(Hosting.ServerAddress(app))}
            Dim all = Await client.GetFromJsonAsync(Of List(Of BookDto))("/api/books")
            Console.WriteLine("GET /api/books -> " & all.Count & " book(s)")

            Dim ok = Await client.PostAsJsonAsync("/api/books", New NewBook With {.Isbn = "9780131103627", .Title = "Types and Tables", .Year = 1988})
            Console.WriteLine("POST valid -> " & CInt(ok.StatusCode) & ", Location = " & ok.Headers.Location.AbsolutePath)

            Dim bad = Await client.PostAsJsonAsync("/api/books", New NewBook With {.Isbn = "123", .Title = "", .Year = 3000})
            Console.WriteLine("POST invalid -> " & CInt(bad.StatusCode) & " " & bad.Content.Headers.ContentType.MediaType)
            Using problem As JsonDocument = JsonDocument.Parse(Await bad.Content.ReadAsStringAsync())
                Console.WriteLine(problem.RootElement.GetProperty("title").GetString())
                For Each field In problem.RootElement.GetProperty("errors").EnumerateObject().OrderBy(Function(p) p.Name)
                    Console.WriteLine("  " & field.Name & ": " & field.Value(0).GetString())
                Next
            End Using

            Dim missing = Await client.GetAsync("/api/books/0000000000000")
            Console.WriteLine("GET unknown -> " & CInt(missing.StatusCode))
        End Using
        Await app.StopAsync()
    End Function

End Module
GET /api/books -> 1 book(s)
POST valid -> 201, Location = /api/Books/9780131103627
POST invalid -> 400 application/problem+json
One or more validation errors occurred.
  Isbn: The field Isbn must be a string with a minimum length of 10 and a maximum length of 13.
  Title: The Title field is required.
  Year: The field Year must be between 1450 and 2100.
GET unknown -> 404

Visual Basic specifics visible here. The attribute argument "api/[controller]" is just a string; NameOf(GetOne) works as in C#; the anonymous type New With {.isbn = book.Isbn} supplies route values to CreatedAtAction; and the controller is discovered because it is Public and declared at namespace level in the entry assembly (a Public Class nested inside a default Friend Module is not visible outside the assembly, because its container is Friend; the same rule makes XmlSerializer fail on such a class). The Location header reads /api/Books/... with a capital B: the [controller] token takes the class name without the Controller suffix as written, so the casing of the class name appears in the URL, and URLs are case-insensitive in ASP.NET Core routing.

Handlers, attributes and the places where Visual Basic differs

The delegate-based API is where the language shows. These are the four things a C# example in the documentation does that a Visual Basic program cannot, with the real compiler messages and the working replacement for each.

1. A method group does not convert to Delegate. MapGet takes a Delegate (and a RequestDelegate). C# converts a method group to a delegate type it infers; Visual Basic’s AddressOf needs a concrete delegate type to convert to:

app.MapGet("/hello/{name}", AddressOf Hello)
Program.vb(14,13): error BC30518: Overload resolution failed because no accessible 'MapGet' can be called with these arguments:

Wrap the method in the delegate type that matches it: New Func(Of String, IResult)(AddressOf Hello).

2. No attributes on lambda parameters. C# writes ([FromQuery(Name = "q")] string term) => term. Visual Basic forbids attributes on lambda parameters:

app.MapGet("/search", Function(<FromQuery(Name:="q")> term As String) term)
Program.vb(10,40): error BC36634: Attributes cannot be applied to parameters of lambda expressions.

Put the attributes on a named method’s parameters (allowed) and pass the method in a Func, as the listing below does, or bind a whole parameter object with <AsParameters>.

3. No Optional lambda parameters. C# lambdas can have default values ((int page = 1) => ...); Visual Basic cannot:

app.MapGet("/page", Function(Optional page As Integer = 1) page.ToString())
Program.vb(9,38): error BC33010: 'Lambda' parameters cannot be declared 'Optional'.

Use a named method with Optional, a nullable parameter (page As Integer?) or a property with a default in an <AsParameters> class.

4. An untyped Async lambda handed to MapGet returns an empty body. This one has no compiler message. A lambda converted straight to Delegate becomes an instance of a compiler-generated type named VB$AnonymousDelegate_n, not of Func(Of ...):

Option Strict On

Module DelegateProbe
    Sub Main()
        Dim a As [Delegate] = Async Function() As Task(Of String)
                                  Await Task.Delay(1)
                                  Return "x"
                              End Function
        Dim s As [Delegate] = Function() "x"
        Dim t As Func(Of Task(Of String)) = Async Function() As Task(Of String)
                                                Await Task.Delay(1)
                                                Return "x"
                                            End Function
        For Each d As [Delegate] In {a, s, t}
            Console.WriteLine(d.GetType().Name & " | Method.ReturnType = " & d.Method.ReturnType.Name & " | Invoke = " & d.GetType().GetMethod("Invoke").ReturnType.Name & " | IsPublic = " & d.GetType().IsPublic.ToString())
        Next
    End Sub
End Module
VB$AnonymousDelegate_2`1 | Method.ReturnType = Task`1 | Invoke = Task`1 | IsPublic = False
VB$AnonymousDelegate_2`1 | Method.ReturnType = String | Invoke = String | IsPublic = False
Func`1 | Method.ReturnType = Task`1 | Invoke = Task`1 | IsPublic = True

(The number in the type name depends on how many anonymous delegates the assembly already contains.) With a synchronous lambda this is harmless; with an Async Function returning Task(Of String) the endpoint answered 200 with an empty body in the listing below, while the same lambda stored in a Func(Of Task(Of String)) variable first produced its text. I did not trace the cause inside ASP.NET Core; the safe rule is to give an asynchronous handler a real delegate type (Func(Of Task(Of String))) before passing it to MapGet, and to test async endpoints for their body, not only their status code.

The next program shows all four working: named methods with binding attributes wrapped in Func, an <AsParameters> class, TypedResults with a Results(Of ..., ...) union, a route group, the async pitfall, and an endpoint filter. Filters are classes: IEndpointFilter.InvokeAsync returns ValueTask(Of Object), and a Visual Basic Async Function cannot return ValueTask (BC36945), so the interface method is a plain Function that wraps the Task of a private Async helper in a New ValueTask(Of Object)(...).

Built and run: handlers with attributes, typed results, groups, the async-lambda pitfall and an endpoint filter
Option Strict On

Imports System.Net.Http
Imports System.Net.Http.Json
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports Microsoft.AspNetCore.Http
Imports Microsoft.AspNetCore.Http.HttpResults
Imports Microsoft.AspNetCore.Mvc
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

Public Class SearchOptions
    <FromQuery(Name:="q")>
    Public Property Term As String = ""

    <FromQuery>
    Public Property Page As Integer = 1
End Class

Public NotInheritable Class Handlers
    Private Sub New()
    End Sub

    ' A named method can carry binding attributes on its parameters.
    Public Shared Function Search(<FromQuery(Name:="q")> term As String,
                                  <FromQuery> Optional limit As Integer = 10) As String
        Return "term=" & term & " limit=" & limit
    End Function

    Public Shared Function Paged(<AsParameters> options As SearchOptions) As String
        Return "term=" & options.Term & " page=" & options.Page
    End Function

    Public Shared Function Lookup(isbn As String, store As BookStore) As Results(Of Ok(Of BookDto), NotFound)
        Dim book As BookDto = store.Find(isbn)
        If book Is Nothing Then
            Return TypedResults.NotFound()
        End If
        Return TypedResults.Ok(book)
    End Function
End Class

Public NotInheritable Class LoggingFilter
    Implements IEndpointFilter

    Public Function InvokeAsync(context As EndpointFilterInvocationContext,
                                [next] As EndpointFilterDelegate) As ValueTask(Of Object) Implements IEndpointFilter.InvokeAsync
        Return New ValueTask(Of Object)(RunAsync(context, [next]))
    End Function

    Private Shared Async Function RunAsync(context As EndpointFilterInvocationContext,
                                           [next] As EndpointFilterDelegate) As Task(Of Object)
        Console.WriteLine("  filter: before")
        Dim result As Object = Await [next](context)
        Console.WriteLine("  filter: after, result = " & result.ToString())
        Return result
    End Function
End Class

Module HandlersDemo

    Sub Main(args As String())
        RunAsync(args).GetAwaiter().GetResult()
    End Sub

    Async Function RunAsync(args As String()) As Task
        Dim builder = WebApplication.CreateBuilder(args)
        builder.WebHost.UseUrls("http://127.0.0.1:0")
        builder.Configuration("Logging:LogLevel:Default") = "None"
        builder.Services.AddSingleton(Of BookStore)()
        Dim app = builder.Build()

        ' AddressOf alone does not convert to the Delegate parameter of MapGet: wrap it.
        app.MapGet("/search", New Func(Of String, Integer, String)(AddressOf Handlers.Search))
        app.MapGet("/paged", New Func(Of SearchOptions, String)(AddressOf Handlers.Paged))
        app.MapGet("/typed/{isbn}", New Func(Of String, BookStore, Results(Of Ok(Of BookDto), NotFound))(AddressOf Handlers.Lookup))

        ' A route group shares a prefix.
        Dim api = app.MapGroup("/api/v1")
        api.MapGet("/ping", Function() "pong")

        ' The same asynchronous handler twice: handed over as a bare lambda, and held in a typed Func first.
        api.MapGet("/lambda",
                   Async Function() As Task(Of String)
                       Await Task.Delay(1)
                       Return "async lambda"
                   End Function)
        Dim typed As Func(Of Task(Of String)) =
            Async Function() As Task(Of String)
                Await Task.Delay(1)
                Return "typed Func"
            End Function
        api.MapGet("/typed-func", typed)

        ' An endpoint filter is a class: IEndpointFilter returns ValueTask(Of Object), which an Async method cannot return.
        api.MapGet("/filtered", Function() "body").AddEndpointFilter(New LoggingFilter())

        Await app.StartAsync()
        Using client As New HttpClient With {.BaseAddress = New Uri(Hosting.ServerAddress(app))}
            For Each path In {"/search?q=vb", "/search?q=vb&limit=3", "/paged?q=dotnet&page=2", "/typed/9780306406157", "/typed/1", "/api/v1/ping", "/api/v1/lambda", "/api/v1/typed-func", "/api/v1/filtered"}
                Dim response = Await client.GetAsync(path)
                Console.WriteLine(path & " -> " & CInt(response.StatusCode) & " " & Await response.Content.ReadAsStringAsync())
            Next
        End Using
        Await app.StopAsync()
    End Function

End Module
/search?q=vb -> 200 term=vb limit=10
/search?q=vb&limit=3 -> 200 term=vb limit=3
/paged?q=dotnet&page=2 -> 200 term=dotnet page=2
/typed/9780306406157 -> 200 {"isbn":"9780306406157","title":"Shelf Basics","year":2020}
/typed/1 -> 404
/api/v1/ping -> 200 pong
/api/v1/lambda -> 200
/api/v1/typed-func -> 200 typed Func
  filter: before
  filter: after, result = body
/api/v1/filtered -> 200 body

Reading the output: /search shows <FromQuery(Name:="q")> binding q to term and the Optional limit taking its default; /paged shows <AsParameters> filling Term (from q) and Page from one class; /typed/... returns 404 with an empty body from TypedResults.NotFound(); /api/v1/lambda is the pitfall (200 and nothing after it) next to /api/v1/typed-func; and the filter printed before and after the endpoint ran.

Middleware, dependency injection and options

The pipeline is configured with app.Use.... An inline middleware is a lambda taking HttpContext and the next step. Spelling out the parameter types, as below, selects the Func(Of Task) form unambiguously and reads better than untyped parameters. A convention-based middleware class (a constructor taking RequestDelegate and an InvokeAsync method whose other parameters come from dependency injection) needs [next] in brackets because Next is a Visual Basic keyword. Options bind configuration sections to a class with Configure(Of T); note that the property initialisers give the defaults.

Built and run: inline and class middleware, an injected service and bound options
Option Strict On

Imports System.Net.Http
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports Microsoft.AspNetCore.Http
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting
Imports Microsoft.Extensions.Options

Public Class ShelfOptions
    Public Const SectionName As String = "Shelf"
    Public Property LibraryName As String = "Unnamed"
    Public Property MaxLoans As Integer = 3
End Class

Public Interface IClock
    Function UtcNow() As DateTimeOffset
End Interface

Public NotInheritable Class FixedClock
    Implements IClock

    Public Function UtcNow() As DateTimeOffset Implements IClock.UtcNow
        Return New DateTimeOffset(2026, 10, 19, 12, 0, 0, TimeSpan.Zero)
    End Function
End Class

' Convention-based middleware: a constructor taking RequestDelegate and an InvokeAsync method.
Public NotInheritable Class StampMiddleware
    Private ReadOnly _next As RequestDelegate

    Public Sub New([next] As RequestDelegate)
        _next = [next]
    End Sub

    Public Async Function InvokeAsync(context As HttpContext, clock As IClock) As Task
        context.Response.Headers("X-Stamp") = clock.UtcNow().ToString("yyyy-MM-dd")
        Await _next(context)
    End Function
End Class

Module PipelineDemo

    Sub Main(args As String())
        RunAsync(args).GetAwaiter().GetResult()
    End Sub

    Async Function RunAsync(args As String()) As Task
        Dim builder = WebApplication.CreateBuilder(args)
        builder.WebHost.UseUrls("http://127.0.0.1:0")
        builder.Configuration("Logging:LogLevel:Default") = "None"
        builder.Configuration("Shelf:LibraryName") = "Central Library"
        builder.Services.Configure(Of ShelfOptions)(builder.Configuration.GetSection(ShelfOptions.SectionName))
        builder.Services.AddSingleton(Of IClock, FixedClock)()
        Dim app = builder.Build()

        ' Inline middleware: spell out the parameter types so the Func(Of Task) overload is chosen.
        app.Use(Async Function(context As HttpContext, [next] As Func(Of Task)) As Task
                    Console.WriteLine("  inline middleware: before " & context.Request.Path.Value)
                    Await [next]()
                    Console.WriteLine("  inline middleware: after, status " & context.Response.StatusCode)
                End Function)
        app.UseMiddleware(Of StampMiddleware)()

        app.MapGet("/info",
                   Function(options As IOptions(Of ShelfOptions), clock As IClock) As String
                       Return options.Value.LibraryName & " (max " & options.Value.MaxLoans & " loans) at " & clock.UtcNow().ToString("u")
                   End Function)

        Await app.StartAsync()
        Using client As New HttpClient With {.BaseAddress = New Uri(Hosting.ServerAddress(app))}
            Dim response = Await client.GetAsync("/info")
            Console.WriteLine("body:    " & Await response.Content.ReadAsStringAsync())
            Console.WriteLine("X-Stamp: " & response.Headers.GetValues("X-Stamp").First())
        End Using
        Await app.StopAsync()
    End Function

End Module
  inline middleware: before /info
  inline middleware: after, status 200
body:    Central Library (max 3 loans) at 2026-10-19 12:00:00Z
X-Stamp: 2026-10-19

Public Const SectionName As String = "Shelf" is the idiom for the section key. Interface IClock with FixedClock is the usual dependency-injection example; builder.Services.AddSingleton(Of IClock, FixedClock)() is the generic registration, with Of where C# has angle brackets. Visual Basic has no primary constructors, so each injected service is a field set in an explicit New.

Sessions and application state

The Classic ASP and Web Forms idioms Session("visits") and Application("hits") map to the session middleware and to a singleton (or, as below, a captured variable). The session needs AddDistributedMemoryCache() and AddSession() registered and UseSession() in the pipeline; the accessors GetInt32 and SetInt32 are extension methods in Microsoft.AspNetCore.Http. If(a, b) is the Visual Basic replacement for C# ?? (the language has no ??): GetInt32 returns Integer?, and If(x, 0) supplies the default.

Built and run: per-visitor session state and application-wide state
Option Strict On

Imports System.Net.Http
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports Microsoft.AspNetCore.Http
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

Module SessionDemo

    Sub Main(args As String())
        RunAsync(args).GetAwaiter().GetResult()
    End Sub

    Async Function RunAsync(args As String()) As Task
        Dim builder = WebApplication.CreateBuilder(args)
        builder.WebHost.UseUrls("http://127.0.0.1:0")
        builder.Configuration("Logging:LogLevel:Default") = "None"
        builder.Services.AddDistributedMemoryCache()
        builder.Services.AddSession()
        Dim app = builder.Build()
        app.UseSession()

        Dim hits As Integer = 0   ' application-wide state (Application("hits") in classic ASP)
        app.MapGet("/visit",
                   Function(context As HttpContext) As String
                       Dim own As Integer = If(context.Session.GetInt32("visits"), 0) + 1   ' per-visitor state (Session("visits"))
                       context.Session.SetInt32("visits", own)
                       hits += 1
                       Return "you: " & own & ", everyone: " & hits
                   End Function)
        Await app.StartAsync()

        Dim address As New Uri(Hosting.ServerAddress(app))
        Using ada As New HttpClient With {.BaseAddress = address}, grace As New HttpClient With {.BaseAddress = address}
            Console.WriteLine("ada:   " & Await ada.GetStringAsync("/visit"))
            Console.WriteLine("ada:   " & Await ada.GetStringAsync("/visit"))
            Console.WriteLine("grace: " & Await grace.GetStringAsync("/visit"))
            Console.WriteLine("ada:   " & Await ada.GetStringAsync("/visit"))
        End Using
        Await app.StopAsync()
    End Function

End Module
ada:   you: 1, everyone: 1
ada:   you: 2, everyone: 2
grace: you: 1, everyone: 3
ada:   you: 3, everyone: 4

Two clients (two HttpClient instances, so two cookie jars) get separate session counters (ada counts 1, 2, 3; grace counts 1) while the application-wide counter increases for everybody. The in-memory session store is for demonstrations and single servers; a real deployment uses a distributed cache. The lambda captures the local hits, which is shared state mutated from several requests: use Interlocked or a lock in real code.

OpenAPI

AddOpenApi() and MapOpenApi() (from the Microsoft.AspNetCore.OpenApi package) generate an OpenAPI 3.1 document at runtime from the endpoint metadata. They work from Visual Basic. The output lists the two routes and the BookDto schema with its camelCase property names. Two limits apply: lambda parameters carry no attributes in Visual Basic (see above), so richer metadata comes from .WithName(...) and the Produces methods on the endpoint, not from attributes; and the package’s integration with XML documentation comments is implemented as a source generator, which was not tested from Visual Basic.

Built and run: the generated OpenAPI document
Option Strict On

Imports System.Net.Http
Imports System.Text.Json
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports Microsoft.AspNetCore.Http
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

Module OpenApiDemo

    Sub Main(args As String())
        RunAsync(args).GetAwaiter().GetResult()
    End Sub

    Async Function RunAsync(args As String()) As Task
        Dim builder = WebApplication.CreateBuilder(args)
        builder.WebHost.UseUrls("http://127.0.0.1:0")
        builder.Configuration("Logging:LogLevel:Default") = "None"
        builder.Services.AddOpenApi()
        builder.Services.AddSingleton(Of BookStore)()
        Dim app = builder.Build()
        app.MapOpenApi()
        app.MapGet("/books/{isbn}",
                   Function(isbn As String, store As BookStore) As IResult
                       Dim book As BookDto = store.Find(isbn)
                       Return If(book Is Nothing, Results.NotFound(), Results.Ok(book))
                   End Function).WithName("GetBook")
        app.MapPost("/books", Function(book As BookDto, store As BookStore) As IResult
                                  store.Add(book)
                                  Return Results.Created("/books/" & book.Isbn, book)
                              End Function)
        Await app.StartAsync()

        Using client As New HttpClient With {.BaseAddress = New Uri(Hosting.ServerAddress(app))}
            Dim json As String = Await client.GetStringAsync("/openapi/v1.json")
            Using doc As JsonDocument = JsonDocument.Parse(json)
                Console.WriteLine("openapi = " & doc.RootElement.GetProperty("openapi").GetString())
                For Each path In doc.RootElement.GetProperty("paths").EnumerateObject()
                    For Each op In path.Value.EnumerateObject()
                        Console.WriteLine(op.Name.ToUpperInvariant() & " " & path.Name)
                    Next
                Next
                For Each entry In doc.RootElement.GetProperty("components").GetProperty("schemas").EnumerateObject()
                    Console.WriteLine("schema " & entry.Name & ": " & String.Join(", ", entry.Value.GetProperty("properties").EnumerateObject().Select(Function(p) p.Name)))
                Next
            End Using
        End Using
        Await app.StopAsync()
    End Function

End Module
openapi = 3.1.1
GET /books/{isbn}
POST /books
schema BookDto: isbn, title, year

The loop variable is called entry, not schema: with Imports System.Text.Json in scope, the child namespace System.Text.Json.Schema is visible as Schema, and because names are case-insensitive a local named schema is read as that namespace (BC30112 "is a namespace and cannot be used as an expression"). The same trap applies to any local named after a child namespace of an imported one.

What does not work: Razor and Blazor

A Razor Pages application, MVC views and Blazor components are authored in .cshtml and .razor files and compiled by a Razor source generator that emits C#. A Visual Basic project accepts the files, builds without a warning, and ignores them. This program calls AddRazorPages() and MapRazorPages() with a Pages/Index.cshtml file next to it:

Option Strict On

Imports System.Net.Http
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

Module Program
    Sub Main(args As String())
        Dim builder = WebApplication.CreateBuilder(args)
        builder.WebHost.UseUrls("http://127.0.0.1:0")
        builder.Configuration("Logging:LogLevel:Default") = "None"
        builder.Services.AddRazorPages()
        Dim app = builder.Build()
        app.MapRazorPages()
        app.Start()
        Dim address = app.Urls.First()
        Using client As New HttpClient With {.BaseAddress = New Uri(address)}
            Dim r = client.GetAsync("/").GetAwaiter().GetResult()
            Console.WriteLine(CInt(r.StatusCode))
        End Using
        app.StopAsync().GetAwaiter().GetResult()
    End Sub
End Module

with a file Pages/Index.cshtml next to it that contains @page and a heading. The output is the HTTP status of GET /:

404

The status is 404: no page was compiled. This is the reason the matrix says "not available". The supported design for UI work is a C# host (next section). Source generators written for C# (the ASP.NET Core request-delegate generator and similar) were not tested from Visual Basic, and publishing an ASP.NET Core host as Native AOT is not something this page tested; the examples here rely on reflection.

SignalR and WebSockets on the server

A SignalR hub is a class that inherits Hub; clients call its public methods by name and the server pushes to clients with Clients.All.SendAsync("Name", arg1, arg2), an extension method with overloads for up to ten arguments that Visual Basic calls directly. The same project hosts a raw WebSocket endpoint. The program in the section on the client side below contains the hub, the endpoint and both clients.

In-process testing

Microsoft.AspNetCore.TestHost runs the whole pipeline in memory: UseTestServer() replaces Kestrel and GetTestClient() returns an HttpClient that dispatches straight to the app. No socket is opened, which makes it the right default for tests of a Visual Basic service (see Testing for the test frameworks).

Built and run: a minimal API tested in memory with TestServer
Option Strict On

Imports System.Net.Http
Imports System.Net.Http.Json
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Http
Imports Microsoft.AspNetCore.TestHost
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

Module TestServerDemo

    Sub Main(args As String())
        RunAsync(args).GetAwaiter().GetResult()
    End Sub

    Async Function RunAsync(args As String()) As Task
        Dim builder = WebApplication.CreateBuilder(args)
        builder.WebHost.UseTestServer()          ' no socket is opened
        builder.Configuration("Logging:LogLevel:Default") = "None"
        builder.Services.AddSingleton(Of BookStore)()
        Dim app = builder.Build()
        app.MapGet("/books/{isbn}",
                   Function(isbn As String, store As BookStore) As IResult
                       Dim book As BookDto = store.Find(isbn)
                       Return If(book Is Nothing, Results.NotFound(), Results.Ok(book))
                   End Function)
        Await app.StartAsync()

        Using client As HttpClient = app.GetTestClient()
            Dim book = Await client.GetFromJsonAsync(Of BookDto)("/books/9780306406157")
            Console.WriteLine("in-process GET -> " & book.Title)
            Dim response = Await client.GetAsync("/books/0000000000000")
            Console.WriteLine("in-process GET unknown -> " & CInt(response.StatusCode))
            Console.WriteLine("BaseAddress = " & client.BaseAddress.ToString())
        End Using
        Await app.StopAsync()
    End Function

End Module
in-process GET -> Shelf Basics
in-process GET unknown -> 404
BaseAddress = http://localhost/

The helper WebApplicationFactory<TEntryPoint> from Microsoft.AspNetCore.Mvc.Testing was not tried with a Visual Basic entry point; building the WebApplication yourself as above needs no factory.

A Visual Basic library behind a C# ASP.NET Core host

This is the pattern to prefer when the host needs Razor, Blazor, OpenAPI source generation or Native AOT. The domain (the catalogue rules, validation, calculations) lives in a Visual Basic class library, and a C# project references it and exposes it. Nothing about the library knows it is behind a web host. The C# host below was run with dotnet run and calls its own endpoints:

Built and run: the VB library
Option Strict On

Public Enum LoanOutcome
    Lent
    AlreadyOnLoan
    UnknownBook
End Enum

Public Class Book
    Public Property Isbn As String = ""
    Public Property Title As String = ""
    Public Property OnLoan As Boolean
End Class

Public Class CatalogService
    Private ReadOnly _books As New Dictionary(Of String, Book)

    Public Sub New()
        _books("9780306406157") = New Book With {.Isbn = "9780306406157", .Title = "Shelf Basics"}
        _books("9780131103627") = New Book With {.Isbn = "9780131103627", .Title = "Types and Tables", .OnLoan = True}
    End Sub

    Public Function Find(isbn As String) As Book
        Dim book As Book = Nothing
        Return If(_books.TryGetValue(isbn, book), book, Nothing)
    End Function

    Public Function Lend(isbn As String) As LoanOutcome
        Dim book As Book = Find(isbn)
        If book Is Nothing Then Return LoanOutcome.UnknownBook
        If book.OnLoan Then Return LoanOutcome.AlreadyOnLoan
        book.OnLoan = True
        Return LoanOutcome.Lent
    End Function
End Class
Built and run: the C# host (Program.cs) referencing the VB project
using ShelfLib;

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://127.0.0.1:0");
builder.Configuration["Logging:LogLevel:Default"] = "None";
builder.Services.AddSingleton<CatalogService>();
var app = builder.Build();

app.MapGet("/books/{isbn}", (string isbn, CatalogService catalog) =>
    catalog.Find(isbn) is { } book ? Results.Ok(book) : Results.NotFound());
app.MapPost("/loans/{isbn}", (string isbn, CatalogService catalog) => catalog.Lend(isbn) switch
{
    LoanOutcome.Lent => Results.Ok(new { outcome = "lent" }),
    LoanOutcome.AlreadyOnLoan => Results.Conflict(new { outcome = "already on loan" }),
    _ => Results.NotFound(),
});

await app.StartAsync();
var address = app.Urls.First();
using var client = new HttpClient { BaseAddress = new Uri(address) };
Console.WriteLine("GET known    : " + await client.GetStringAsync("/books/9780306406157"));
foreach (var isbn in new[] { "9780306406157", "9780306406157", "9780131103627", "0000000000000" })
{
    var r = await client.PostAsync("/loans/" + isbn, null);
    Console.WriteLine($"POST /loans/{isbn} -> {(int)r.StatusCode} {await r.Content.ReadAsStringAsync()}");
}
await app.StopAsync();
GET known    : {"isbn":"9780306406157","title":"Shelf Basics","onLoan":false}
POST /loans/9780306406157 -> 200 {"outcome":"lent"}
POST /loans/9780306406157 -> 409 {"outcome":"already on loan"}
POST /loans/9780131103627 -> 409 {"outcome":"already on loan"}
POST /loans/0000000000000 -> 404

The ShelfHost.csproj contains a <ProjectReference Include="..\ShelfLib\ShelfLib.vbproj" />. A Visual Basic Enum becomes a C# enum that switch expressions match, a Class with auto-properties serialises to JSON with the same camelCase rule as any other, and a Dictionary(Of ...) field stays an implementation detail. The details of exposing a Visual Basic library to C# (case-insensitivity, Module`s, `ByRef parameters) are in Libraries, Packaging and Deployment and Interop with C# and Other .NET Languages.

HTTP clients

An HTTP client is the part of the web story with no Visual Basic caveats. HttpClient is the only class to use; WebClient, HttpWebRequest and WebRequest are obsolete (the diagnostic is SYSLIB0014, shown below). The shelf’s ISBN lookup is the running example: IsbnClient wraps one HttpClient, converts a 404 into Nothing, turns other failures into exceptions, and passes a CancellationToken through.

Built and run: a typed ISBN client, errors, cancellation and timeout (the service is a local Kestrel)
Option Strict On

Imports System.Net
Imports System.Net.Http
Imports System.Net.Http.Json
Imports System.Text.Json
Imports System.Threading
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports Microsoft.AspNetCore.Http
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

' The shelf's ISBN lookup client: one HttpClient, JSON in and out, errors turned into results.
Public NotInheritable Class IsbnClient
    Private ReadOnly _http As HttpClient

    Public Sub New(http As HttpClient)
        _http = http
    End Sub

    Public Async Function FindAsync(isbn As String, cancellationToken As CancellationToken) As Task(Of BookDto)
        Using response As HttpResponseMessage = Await _http.GetAsync("isbn/" & isbn, cancellationToken)
            If response.StatusCode = HttpStatusCode.NotFound Then
                Return Nothing
            End If
            response.EnsureSuccessStatusCode()
            Return Await response.Content.ReadFromJsonAsync(Of BookDto)(cancellationToken)
        End Using
    End Function

    Public Async Function AddAsync(book As BookDto, cancellationToken As CancellationToken) As Task(Of String)
        Using response As HttpResponseMessage = Await _http.PostAsJsonAsync("isbn", book, cancellationToken)
            response.EnsureSuccessStatusCode()
            Return response.Headers.Location.ToString()
        End Using
    End Function
End Class

Module ClientDemo

    Sub Main(args As String())
        RunAsync(args).GetAwaiter().GetResult()
    End Sub

    Async Function RunAsync(args As String()) As Task
        Dim builder = WebApplication.CreateBuilder(args)
        builder.WebHost.UseUrls("http://127.0.0.1:0")
        builder.Configuration("Logging:LogLevel:Default") = "None"
        Dim app = builder.Build()
        app.MapGet("/isbn/{isbn}",
                   Function(isbn As String) As IResult
                       Return If(isbn = "9780306406157",
                                 Results.Ok(New BookDto With {.Isbn = isbn, .Title = "Shelf Basics", .Year = 2020}),
                                 Results.NotFound())
                   End Function)
        app.MapPost("/isbn", Function(book As BookDto) Results.Created("/isbn/" & book.Isbn, book))
        app.MapGet("/boom", Function() Results.StatusCode(503))
        app.MapGet("/slow",
                   New Func(Of HttpContext, Task)(
                       Async Function(context As HttpContext) As Task
                           Try
                               Await Task.Delay(TimeSpan.FromSeconds(5), context.RequestAborted)
                           Catch ex As OperationCanceledException
                               ' The client gave up; nothing to send.
                           End Try
                       End Function))
        Await app.StartAsync()

        ' One HttpClient for the life of the program; BaseAddress needs the trailing slash for relative paths.
        Using http As New HttpClient With {.BaseAddress = New Uri(Hosting.ServerAddress(app) & "/")}
            Dim client As New IsbnClient(http)

            Dim found As BookDto = Await client.FindAsync("9780306406157", CancellationToken.None)
            Console.WriteLine("known ISBN:   " & found.Title & " (" & found.Year & ")")

            Dim missing As BookDto = Await client.FindAsync("0000000000000", CancellationToken.None)
            Console.WriteLine("unknown ISBN: " & If(missing Is Nothing, "Nothing", missing.Title))

            Console.WriteLine("added:        " & Await client.AddAsync(New BookDto With {.Isbn = "9780131103627", .Title = "Types and Tables", .Year = 1988}, CancellationToken.None))

            Try
                Dim response = Await http.GetAsync("boom")
                response.EnsureSuccessStatusCode()
            Catch ex As HttpRequestException
                Console.WriteLine("EnsureSuccessStatusCode: " & ex.StatusCode.ToString() & " / " & ex.Message)
            End Try

            Using cts As New CancellationTokenSource(TimeSpan.FromMilliseconds(200))
                Try
                    Await http.GetAsync("slow", cts.Token)
                Catch ex As OperationCanceledException
                    Console.WriteLine("cancelled after 200 ms: " & ex.GetType().Name)
                End Try
            End Using
        End Using

        Using shortLived As New HttpClient With {.BaseAddress = New Uri(Hosting.ServerAddress(app) & "/"), .Timeout = TimeSpan.FromMilliseconds(200)}
            Try
                Await shortLived.GetAsync("slow")
            Catch ex As TaskCanceledException
                Console.WriteLine("HttpClient.Timeout: " & ex.GetType().Name & " / inner " & ex.InnerException.GetType().Name)
            End Try
        End Using
        Await app.StopAsync()
    End Function

End Module
known ISBN:   Shelf Basics (2020)
unknown ISBN: Nothing
added:        /isbn/9780131103627
EnsureSuccessStatusCode: ServiceUnavailable / Response status code does not indicate success: 503 (Service Unavailable).
cancelled after 200 ms: TaskCanceledException
HttpClient.Timeout: TaskCanceledException / inner TimeoutException

Rules shown or implied by the listing:

  • One HttpClient for the life of the program (or one per named/typed client from the factory), not one per request: each instance holds a connection pool, and creating many exhausts sockets. Dispose it with Using only when you really own the lifetime.

  • BaseAddress needs a trailing slash when the relative paths have no leading slash ("isbn/..."): http://host/api plus "isbn/1" resolves to http://host/isbn/1, dropping api. The listing appends "/".

  • GetFromJsonAsync(Of T), PostAsJsonAsync and ReadFromJsonAsync(Of T) (namespace System.Net.Http.Json) replace the manual ReadAsStringAsync plus JsonSerializer.Deserialize pair; they use the web defaults (camelCase, case-insensitive on read). They are reflection-based, so a trimmed or AOT application needs a source-generated context (a C# helper project; see the Serialization, JSON and XML page).

  • EnsureSuccessStatusCode() throws HttpRequestException whose StatusCode property carries the code (ServiceUnavailable above). Decide per call whether a 404 is an error (here it is a normal "not found" result).

  • Cancellation and timeouts both surface as TaskCanceledException (a subclass of OperationCanceledException). The HttpClient.Timeout expiry carries a TimeoutException as its inner exception, as the last line of the output shows; that is how to tell it from a cancellation by your own token. Timeout is set once, before the first request: changing it afterwards throws InvalidOperationException.

  • Async is the rule: see Async and Await; do not call .Result or .GetAwaiter().GetResult() on a UI thread.

The obsolete classes still compile on .NET 10, but every use is reported as warning SYSLIB0014, which becomes an error under TreatWarningsAsErrors. Creating a WebClient with New, or calling WebRequest.Create or HttpWebRequest.Create, each produces a diagnostic of this form:

Program.vb(7,23): warning SYSLIB0014: 'Public Overloads Sub New()' is obsolete: 'WebRequest, HttpWebRequest, ServicePoint, and WebClient are obsolete. Use HttpClient instead.'. (https://aka.ms/dotnet-warnings/SYSLIB0014)

This applies to every book example that downloads with WebClient.DownloadString or builds a request with HttpWebRequest/WebRequest.Create: the replacement is HttpClient.GetStringAsync and friends. The My.Computer.Network members that download and upload files are listed in the table in The My Namespace.

IHttpClientFactory, typed clients and resilience

In a Generic Host or ASP.NET Core application, IHttpClientFactory manages the handler lifetimes and offers typed clients: a class that receives an HttpClient in its constructor, registered with AddHttpClient(Of TClient). Message handlers (the HttpClient pipeline’s middleware) are added in registration order. Microsoft.Extensions.Http.Resilience adds AddStandardResilienceHandler (retry, circuit breaker, timeouts) in one call. The program below plays a service that fails twice with 503 and then answers; the client retries.

Built and run: a typed client, a counting handler and the standard resilience handler against a flaky service
Option Strict On

Imports System.Net
Imports System.Net.Http
Imports System.Net.Sockets
Imports System.Threading
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports Microsoft.AspNetCore.Http
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

' A DelegatingHandler is the HttpClient pipeline's middleware.
Public NotInheritable Class CountingHandler
    Inherits DelegatingHandler

    Public Shared Attempts As Integer

    Protected Overrides Async Function SendAsync(request As HttpRequestMessage,
                                                 cancellationToken As CancellationToken) As Task(Of HttpResponseMessage)
        Dim n As Integer = Interlocked.Increment(Attempts)
        Dim response As HttpResponseMessage = Await MyBase.SendAsync(request, cancellationToken)
        Console.WriteLine("  attempt " & n & " -> " & CInt(response.StatusCode))
        Return response
    End Function
End Class

Public NotInheritable Class ShelfApiClient
    Private ReadOnly _http As HttpClient

    Public Sub New(http As HttpClient)
        _http = http
    End Sub

    Public Function GetTitleAsync() As Task(Of String)
        Return _http.GetStringAsync("title")
    End Function
End Class

Module FactoryDemo

    Private Function FreePort() As Integer
        Dim listener As New TcpListener(IPAddress.Loopback, 0)
        listener.Start()
        Dim port As Integer = CType(listener.LocalEndpoint, IPEndPoint).Port
        listener.Stop()
        Return port
    End Function

    Sub Main(args As String())
        RunAsync(args).GetAwaiter().GetResult()
    End Sub

    Async Function RunAsync(args As String()) As Task
        Dim port As Integer = FreePort()

        ' The flaky service: the first two calls answer 503, the third succeeds.
        Dim serverBuilder = WebApplication.CreateBuilder(args)
        serverBuilder.WebHost.UseUrls("http://127.0.0.1:" & port)
        serverBuilder.Configuration("Logging:LogLevel:Default") = "None"
        Dim server = serverBuilder.Build()
        Dim calls As Integer = 0
        server.MapGet("/title",
                      Function() As IResult
                          calls += 1
                          Return If(calls < 3, Results.StatusCode(503), Results.Text("Shelf Basics"))
                      End Function)
        Await server.StartAsync()

        ' The client side: Generic Host, typed client, resilience handler.
        Dim builder = Host.CreateApplicationBuilder(args)
        builder.Configuration("Logging:LogLevel:Default") = "None"
        builder.Services.AddTransient(Of CountingHandler)()
        Dim clientBuilder = builder.Services.AddHttpClient(Of ShelfApiClient)(
            Sub(client) client.BaseAddress = New Uri("http://127.0.0.1:" & port & "/"))
        ' Handlers run in registration order: the resilience handler first, so the counting handler sees every attempt.
        clientBuilder.AddStandardResilienceHandler(
            Sub(options)
                options.Retry.MaxRetryAttempts = 3
                options.Retry.Delay = TimeSpan.FromMilliseconds(20)
                options.Retry.UseJitter = False
            End Sub)
        clientBuilder.AddHttpMessageHandler(Of CountingHandler)()
        Using host As IHost = builder.Build()
            Dim api = host.Services.GetRequiredService(Of ShelfApiClient)()
            Console.WriteLine("title: " & Await api.GetTitleAsync())
        End Using
        Await server.StopAsync()
    End Function

End Module
  attempt 1 -> 503
  attempt 2 -> 503
  attempt 3 -> 200
title: Shelf Basics

Visual Basic points. A handler subclass overrides SendAsync with an Async Function declared Protected Overrides and returning Task(Of HttpResponseMessage); the base call is MyBase.SendAsync. The builder calls are made as separate statements on the IHttpClientBuilder because the order of registration is the order of execution, and the resilience handler is registered before the counting handler so that the counter sits inside the retry loop and sees all three attempts (registered the other way round it would count once). AddHttpClient(Of ShelfApiClient)(Sub(client) ...) takes a Sub lambda; options.Retry.Delay = ... inside a Sub lambda is an assignment statement and needs no Return. The defaults of the standard handler are tuned for production; the listing shortens the retry delay and turns jitter off so that the run is quick and repeatable. Host.CreateApplicationBuilder(args) is the non-web host builder (no Kestrel); it needs no Imports beyond Microsoft.Extensions.Hosting.

SignalR and WebSocket clients

The SignalR .NET client (Microsoft.AspNetCore.SignalR.Client) is a library. The pieces to get right in Visual Basic: handlers are registered with connection.On(Of T1, T2)("Method", Sub(a, b) ...) before StartAsync, a Sub lambda is the Action<T1, T2> overload (an Async Function lambda would bind the Func<T1, T2, Task> overload), InvokeAsync calls a hub method by name, and a connection is released with Await connection.DisposeAsync() because Visual Basic has no Await Using. The program hosts the hub and a raw WebSocket endpoint and calls both from clients.

Built and run: a SignalR hub with a .NET client, and a WebSocket echo with ClientWebSocket
Option Strict On

Imports System.Net
Imports System.Net.Sockets
Imports System.Net.WebSockets
Imports System.Text
Imports System.Threading
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports Microsoft.AspNetCore.Http
Imports Microsoft.AspNetCore.SignalR
Imports Microsoft.AspNetCore.SignalR.Client
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

' Server side: a hub is an ordinary class; clients call its public methods by name.
Public Class LoanHub
    Inherits Hub

    Public Async Function ReturnBook(isbn As String, member As String) As Task
        Await Clients.All.SendAsync("BookReturned", isbn, member)
    End Function
End Class

Module RealtimeDemo

    Sub Main(args As String())
        RunAsync(args).GetAwaiter().GetResult()
    End Sub

    Async Function RunAsync(args As String()) As Task
        Dim builder = WebApplication.CreateBuilder(args)
        builder.WebHost.UseUrls("http://127.0.0.1:0")
        builder.Configuration("Logging:LogLevel:Default") = "None"
        builder.Services.AddSignalR()
        Dim app = builder.Build()
        app.UseWebSockets()
        app.MapHub(Of LoanHub)("/hubs/loans")
        app.Map("/ws/echo",
                New Func(Of HttpContext, Task)(
                    Async Function(context As HttpContext) As Task
                        If Not context.WebSockets.IsWebSocketRequest Then
                            context.Response.StatusCode = 400
                            Return
                        End If
                        Using socket As WebSocket = Await context.WebSockets.AcceptWebSocketAsync()
                            Dim buffer(1023) As Byte
                            Dim received = Await socket.ReceiveAsync(New ArraySegment(Of Byte)(buffer), CancellationToken.None)
                            Await socket.SendAsync(New ArraySegment(Of Byte)(buffer, 0, received.Count),
                                                   WebSocketMessageType.Text, True, CancellationToken.None)
                            Await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None)
                        End Using
                    End Function))
        Await app.StartAsync()
        Dim baseAddress As String = Hosting.ServerAddress(app)

        ' SignalR .NET client: build, register handlers with On, start, invoke.
        Dim connection As HubConnection = New HubConnectionBuilder().
            WithUrl(baseAddress & "/hubs/loans").
            Build()
        Dim gotMessage As New TaskCompletionSource(Of String)(TaskCreationOptions.RunContinuationsAsynchronously)
        connection.On(Of String, String)("BookReturned",
                                         Sub(isbn, member) gotMessage.TrySetResult(member & " returned " & isbn))
        Await connection.StartAsync()
        Console.WriteLine("connection state: " & connection.State.ToString())
        Await connection.InvokeAsync("ReturnBook", "9780306406157", "Ada")
        Console.WriteLine("hub pushed: " & Await gotMessage.Task)
        Await connection.StopAsync()
        Await connection.DisposeAsync()

        ' Plain WebSocket client.
        Using client As New ClientWebSocket()
            Dim wsUri As New Uri(baseAddress.Replace("http://", "ws://") & "/ws/echo")
            Await client.ConnectAsync(wsUri, CancellationToken.None)
            Await client.SendAsync(New ArraySegment(Of Byte)(Encoding.UTF8.GetBytes("ping from VB")),
                                   WebSocketMessageType.Text, True, CancellationToken.None)
            Dim buffer(1023) As Byte
            Dim result = Await client.ReceiveAsync(New ArraySegment(Of Byte)(buffer), CancellationToken.None)
            Console.WriteLine("websocket echo: " & Encoding.UTF8.GetString(buffer, 0, result.Count))
        End Using
        Await app.StopAsync()
    End Function

End Module
connection state: Connected
hub pushed: Ada returned 9780306406157
websocket echo: ping from VB

Notes. TaskCompletionSource(Of String) with RunContinuationsAsynchronously turns the push into something the program can Await. Dim buffer(1023) As Byte declares 1,024 bytes (Visual Basic array bounds are upper bounds). The WebSocket endpoint is mapped with an explicit Func(Of HttpContext, Task) for the same reason as in the minimal-API section: a delegate type the framework recognises. SignalR clients for Web Forms and MVC 5 are the older ASP.NET SignalR 2 libraries, documented in SignalR 2 in MVC 5; the ASP.NET Core client above does not talk to a SignalR 2 server.

SOAP and WCF clients

The books show "Add Service Reference" wizards and ASMX proxies. On .NET 10 the equivalents are the WCF client libraries (System.ServiceModel.Http and System.ServiceModel.Primitives) plus dotnet-svcutil, a command-line tool that reads a WSDL and writes the client code. The tool runs for Visual Basic projects but wrote C# in the test: it produced ServiceReference1/Reference.cs next to a .vbproj and did not touch the project, because a Visual Basic project cannot compile a .cs file. The practical pattern is to compile the generated file in a small C# library and reference it from Visual Basic.

To test this without an external service the example hosts a SOAP service with CoreWCF (an open-source server-side port of WCF) in a Visual Basic ASP.NET Core application, then generates a proxy from its WSDL and calls it. The server, built and run on port 5080:

Built and run: a CoreWCF service in Visual Basic (the test server)
Option Strict On

Imports CoreWCF
Imports CoreWCF.Configuration
Imports CoreWCF.Description
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports Microsoft.Extensions.Configuration
Imports Microsoft.Extensions.DependencyInjection

<ServiceContract(Namespace:="urn:shelf")>
Public Interface IIsbnService
    <OperationContract>
    Function TitleOf(isbn As String) As String
End Interface

Public Class IsbnService
    Implements IIsbnService

    Public Function TitleOf(isbn As String) As String Implements IIsbnService.TitleOf
        Return If(isbn = "9780306406157", "Shelf Basics", "(unknown)")
    End Function
End Class

Module Program
    Sub Main(args As String())
        Dim builder = WebApplication.CreateBuilder(args)
        builder.WebHost.UseUrls("http://127.0.0.1:5080")
        builder.Configuration("Logging:LogLevel:Default") = "None"
        builder.Services.AddServiceModelServices().AddServiceModelMetadata()
        builder.Services.AddSingleton(Of IServiceBehavior, UseRequestHeadersForMetadataAddressBehavior)()
        Dim app = builder.Build()
        ' WebApplication is both an IApplicationBuilder and an IHost, and UseServiceModel exists for both: pick one.
        Dim pipeline As IApplicationBuilder = app
        pipeline.UseServiceModel(Sub(serviceBuilder As IServiceBuilder)
                                     serviceBuilder.AddService(Of IsbnService)()
                                     serviceBuilder.AddServiceEndpoint(Of IsbnService, IIsbnService)(New BasicHttpBinding(), "/IsbnService.svc")
                                 End Sub)
        app.Services.GetRequiredService(Of ServiceMetadataBehavior)().HttpGetEnabled = True
        app.Run()
    End Sub
End Module

UseServiceModel exists as an extension of IApplicationBuilder and of IHost, and WebApplication is both, so a direct app.UseServiceModel(...) fails with BC30521 (ambiguous overload); assign app to an IApplicationBuilder variable first. The lambda needs Sub(serviceBuilder As IServiceBuilder) for the same overload reasons.

With the server running, the generator is run against its WSDL (the installed tool, version 8.0.0, targets an older runtime than the container’s .NET 10 and ran with the environment variable DOTNET_ROLL_FORWARD=Major):

dotnet tool install --global dotnet-svcutil
dotnet-svcutil http://127.0.0.1:5080/IsbnService.svc?wsdl -n "*,IsbnClient.Proxy" --noLogo
Attempting to download metadata from 'http://127.0.0.1:5080/IsbnService.svc?wsdl' using WS-Metadata Exchange and HttpGet.
Generating files...
ServiceReference1/Reference.cs

The generated Reference.cs declares IIsbnService, IsbnServiceClient (derived from ClientBase), and a task-based TitleOfAsync. It was compiled in this ProxyLib.csproj and referenced from the Visual Basic client:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.ServiceModel.Http" Version="8.*" />
    <PackageReference Include="System.ServiceModel.Primitives" Version="8.*" />
  </ItemGroup>
</Project>
Built and run: the Visual Basic client of the generated proxy
Option Strict On

Imports System.ServiceModel
Imports IsbnClient.Proxy

Module Program

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub

    Async Function RunAsync() As Task
        Dim client As New IsbnServiceClient(New BasicHttpBinding(), New EndpointAddress("http://127.0.0.1:5080/IsbnService.svc"))
        Try
            Console.WriteLine("SOAP call: " & Await client.TitleOfAsync("9780306406157"))
            Console.WriteLine("SOAP call: " & Await client.TitleOfAsync("0000000000000"))
            Await client.CloseAsync()
        Catch ex As Exception
            client.Abort()
            Throw
        End Try
    End Function

End Module
SOAP call: Shelf Basics
SOAP call: (unknown)

Try ... Catch around the client follows the WCF rule that a faulted channel must be Abort`ed, not `Close`d; the `Await client.CloseAsync() on success is the polite shutdown. If the project that consumes the service is a library whose only job is the proxy, keep it C#, and keep the Visual Basic logic on the other side of an interface. Server-side WCF (ServiceHost, .svc files) and ASMX stay on .NET Framework; if a service must be kept alive on .NET 10 the open-source CoreWCF project hosts WCF contracts on ASP.NET Core, as in the test server above; for new work, prefer REST with the minimal API shown earlier.

gRPC and other protocols

gRPC clients and servers are generated from .proto files by the Grpc.Tools MSBuild package, which emits C# only. A Visual Basic program can use a gRPC client by compiling the generated code in a C# project and referencing it, the same pattern as the WCF proxy. .NET also has an IoT stack (System.Device.Gpio and the Iot.Device.Bindings package) usable from Visual Basic like any .NET library; it is outside this page.

Where the books are outdated

The two Visual Basic books this reference was checked against (see the section bibliography on the index page) present several web topics that need updating for .NET 10:

What the books show What is current

ASP.NET Core, SignalR with the Startup class and Blazor written "in VB.NET".

There is no Visual Basic template and no Razor syntax. The Startup class pattern is replaced by minimal hosting (WebApplication.CreateBuilder). Web Forms and MVC 5 remain the supported VB web stacks, on .NET Framework and IIS only.

Visual Studio 2019 is needed for Visual Basic Web Forms because newer versions lack VB ASP.NET.

Visual Studio 2026 still has "ASP.NET Web Application (.NET Framework)" for Visual Basic. Only ASP.NET Core lacks VB support.

ASP.NET hosted on Apache or nginx.

Web Forms and MVC 5 need IIS and .NET Framework. ASP.NET Core can sit behind nginx or Apache as a reverse proxy, but that does not make Web Forms portable.

WebClient.DownloadString, HttpWebRequest, WebRequest.Create.

Obsolete since .NET 6 (SYSLIB0014). Use HttpClient, IHttpClientFactory, System.Net.Http.Json.

"Add Service Reference" and ASMX proxies.

The WCF client libraries plus dotnet-svcutil (C# output). Servers: CoreWCF or REST.

Session and Application objects as the way to keep state (Classic ASP and Web Forms).

ASP.NET Core session middleware with typed accessors, a distributed cache for real deployments, and singletons for application-wide state.

Classic ASP pages (<% ... %>, Request and Response, global.asa).

Not a .NET technology and not covered here; see The VB Family: VB6, VBA and VBScript for recognition and migration notes only.

Differences from C#

Topic C# Visual Basic

Web templates

webapi, web, blazor, grpc, worker and more for -lang C#.

None for ASP.NET Core; Visual Studio keeps Web Forms and MVC 5 templates for .NET Framework.

Implicit usings

ImplicitUsings adds ASP.NET Core namespaces.

Not applied; import every namespace (extension methods fail with BC30456).

Method group as a handler

app.MapGet("/x", Handlers.Get)

New Func(Of ..., ...)(AddressOf Handlers.Get); a bare AddressOf fails with BC30518.

Attributes on handler parameters

([FromQuery] string q) => ...

Not on lambdas (BC36634); use a named method or <AsParameters>.

Default values in lambdas

(int page = 1) => ...

Optional is not allowed on lambda parameters (BC33010).

Async handlers

async () => "x" works as a delegate.

Type the lambda as Func(Of Task(Of String)) first; an untyped one returned an empty body in the test.

Async filters

async ValueTask<object?> methods.

An Async Function cannot return ValueTask (BC36945); wrap a Task in New ValueTask(Of Object)(...).

Entry point

Top-level statements, await app.RunAsync().

Sub Main that waits on an Async Function; no Async Main (BC30737).

Models

record, required, primary constructors.

Classes with auto-properties and a parameterless constructor.

??

session.GetInt32("k") ?? 0

If(session.GetInt32("k"), 0).

Views

Razor Pages, MVC views, Blazor.

Not available; .cshtml and .razor are ignored. .vbhtml exists only in MVC 5 and Web Pages.

Source generators (request delegates, JSON, OpenAPI comments)

Run and are what enable Native AOT for ASP.NET Core.

The System.Text.Json generator needs a C# helper project (see the serialization page); the others were not tested; the examples use reflection.

Pitfalls

  • An Async Main does not exist. Async Function Main(args As String()) As Task fails with BC30737: No accessible 'Main' method with an appropriate signature was found in 'App'. Use Sub Main and wait: RunAsync(args).GetAwaiter().GetResult().

  • Changing StartupObject on the command line without --no-incremental re-runs the previous program.

  • A local named after a child namespace (schema with Imports System.Text.Json, text with Imports System) hides or is hidden by the namespace, because Visual Basic names are case-insensitive.

  • Imports Microsoft.Extensions.Logging breaks Console.WriteLine in the same file (BC30456): qualify as System.Console or keep that import in another file.

  • Public is required for controllers, hubs and DTOs; a type declared in a default Friend Module is Friend and invisible to discovery and to the JSON serialisers.

  • A lambda passed where a Delegate is expected becomes a compiler-generated VB$AnonymousDelegate type: give the handler a real delegate type when its return type matters (async handlers).

  • Do not trust a build for a view layer: .cshtml/.razor files in a VB project compile to nothing.

  • Security: the shelf examples are demonstrations. Real services need authentication, authorization and hardened headers; see Authentication and Identity and Security Hardening. SQL access from a handler is in Data Access: ADO.NET and EF Core; secrets and cryptography are in Security and Cryptography.

Compiler messages seen on this page

Code Message (first line, as printed by the build) Cause

BC30456

'UseUrls' is not a member of 'ConfigureWebHostBuilder'.

Extension method namespace not imported (no implicit usings in VB).

BC30518

Overload resolution failed because no accessible 'MapGet' can be called with these arguments:

AddressOf passed where a Delegate is required.

BC36634

Attributes cannot be applied to parameters of lambda expressions.

<FromQuery> on a lambda parameter.

BC33010

'Lambda' parameters cannot be declared 'Optional'.

Default value on a lambda parameter.

BC30737

No accessible 'Main' method with an appropriate signature was found in 'App'.

Async Function Main.

BC36945

The 'Async' modifier can only be used on Subs, or on Functions that return Task or Task(Of T).

Async Function ... As ValueTask(Of Object) (endpoint filters).

BC30521

Overload resolution failed because no accessible 'UseServiceModel' is most specific for these arguments:

WebApplication is both IApplicationBuilder and IHost.

BC30002

Type 'Global.System.Web.HttpRequest' is not defined.

MyType=Web on .NET 10.

SYSLIB0014

WebRequest, HttpWebRequest, ServicePoint, and WebClient are obsolete. Use HttpClient instead.

Obsolete networking classes (a warning).

BC codes on this page have no per-code pages in this site, so they are plain text; the Microsoft documentation lists each Visual Basic error under its code in the Visual Basic compiler messages reference.

Version notes

  • Visual Basic 11 (Visual Studio 2012) introduced Async/Await; everything on the client side of this page needs that or later.

  • The If(a, b) null-coalescing operator arrived in Visual Basic 9 (Visual Studio 2008).

  • ASP.NET Core minimal APIs (WebApplication.CreateBuilder) arrived with .NET 6. AddOpenApi() (the in-box OpenAPI document generator) arrived with .NET 9; the examples here target .NET 10.

  • SYSLIB0014 was added in .NET 6.