Extension Methods and Partial Methods

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

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

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is a complete program that starts with Option Strict On, was built with warnings treated as errors and was run in the .NET 10 SDK container (Linux); the output shown is real output. The examples that are meant to fail say so, and show the compiler messages that the SDK printed. The one Windows Forms example is built but not run, because it needs a Windows desktop. The scratch projects use a root namespace (ShelfDocs or Shelf), which matters in two places that the text points out. The project-level imports that the SDK adds (System, System.Collections.Generic, System.Linq and others) are why Console, List(Of T) and the LINQ methods need no Imports line.

Two features share this page because both change how a type is assembled from pieces that live somewhere else. An extension method adds a method to a type you do not own, from a different module and often a different assembly. A partial declaration lets one type be written in several source files, and a partial method is a hook that one part declares and another part may or may not implement. Together they are the reason generated code and hand-written code can coexist: the generator owns one file, you own another, and neither edits the other.

Feature What it is Where it is declared

Extension method

a Sub or Function called with instance syntax on a type that does not declare it

a Module, marked <Extension>, first parameter is the receiver

Partial type

one Class, Structure, Interface or Module split across several declarations

each part says Partial; at most one part may omit it

Partial method

a Private Sub declared in one part, optionally implemented in another

declaration without body, implementation with body, same type

Designer file

the generated part of a form, a control or a My class, normally named Name.Designer.vb

a Partial Class that you do not edit by hand

Both features are older than the baseline: partial types arrived with Visual Basic 2005 (VB 8), extension methods and partial methods with Visual Basic 2008 (VB 9) together with LINQ. Nothing was added to them in the releases up to Visual Basic 17.13, because the language is in maintenance mode (see History, Versions and Language Strategy). The C# features that have grown in the meantime (extension members in C# 14, extended partial methods in C# 9) are not available to define from Visual Basic; what can be consumed is described below.

Extension methods

An extension method is declared once, in a module, and then called as if it were a member of the extended type. The compiler rewrites text.Shorten(8) into an ordinary call of the shared function ShelfExtensions.Shorten(text, 8), so nothing about the extended type changes: no inheritance, no wrapper object, no change to the assembly that declares the type. That is why you can extend String, IEnumerable(Of T) or a type from a NuGet package, and also why an extension method can never see private members of the type it extends.

The rules, all enforced by the compiler:

  • The declaring type must be a Module. A Class or Structure cannot hold extension methods (BC36551); this is the main difference from C#, where the container is a static class.

  • The method must carry the <Extension> attribute from System.Runtime.CompilerServices, so that namespace is imported in the declaring file (the attribute can be written in full instead). Marking the module as well is optional. The attribute can be applied only to a Module, Sub or Function (BC36550); Property and Event cannot be extension members (BC30662 for a property).

  • The first parameter is the receiver, the type being extended. A method with no parameters (BC36552), with an Optional first parameter (BC36553) or a ParamArray first parameter (BC36554) is rejected.

  • The remaining parameters follow the usual rules for procedures, including Optional and ParamArray parameters, named arguments, ByRef and generic type parameters (see Procedures: Sub and Function).

  • The extension is a normal Public, Friend or Private method of its module. A Friend extension is visible only inside its assembly, a Private one only inside its module.

  • The call is a static call: Nothing as the receiver does not throw at the call itself, so a null check in the extension works as in the first example.

Option Strict On

Imports System.Runtime.CompilerServices

Public Class Book
    Public Property Title As String
    Public Property Year As Integer

    Public Sub New(title As String, year As Integer)
        Me.Title = title
        Me.Year = year
    End Sub

    Public Overrides Function ToString() As String
        Return Title & " (" & Year & ")"
    End Function
End Class

Public Module ShelfExtensions
    ' The first parameter is the receiver: it is the type that gets the "new" method
    <Extension>
    Public Function Shorten(text As String, maxLength As Integer) As String
        If text Is Nothing Then Return ""
        If text.Length <= maxLength Then Return text
        Return text.Substring(0, maxLength - 1) & "~"
    End Function

    ' An extension on a sequence of books; optional parameters work as in any Function
    <Extension>
    Public Function PublishedSince(books As IEnumerable(Of Book), year As Integer,
                                   Optional inclusive As Boolean = True) As IEnumerable(Of Book)
        Return books.Where(Function(b) If(inclusive, b.Year >= year, b.Year > year))
    End Function

    ' A generic extension: the receiver type is inferred from the call
    <Extension>
    Public Function SecondOrDefault(Of T)(source As IEnumerable(Of T)) As T
        Return source.Skip(1).FirstOrDefault()
    End Function
End Module

Module Program
    Sub Main()
        Dim books As New List(Of Book) From {
            New Book("The Pragmatic Programmer", 1999),
            New Book("Domain-Driven Design", 2003),
            New Book("Refactoring", 2018)
        }

        ' Instance-call syntax
        Console.WriteLine("Dummy title".Shorten(8))

        ' The receiver may be Nothing: no NullReferenceException at the call itself
        Dim missing As String = Nothing
        Console.WriteLine("[" & missing.Shorten(8) & "]")

        ' The same method called the ordinary way: it is only a Function in a Module
        Console.WriteLine(ShelfExtensions.Shorten("Refactoring: Improving", 12))

        For Each b In books.PublishedSince(2003)
            Console.WriteLine(b)
        Next
        Console.WriteLine(books.PublishedSince(2003, inclusive:=False).Count())

        Console.WriteLine(books.SecondOrDefault())
        Console.WriteLine(New List(Of Integer) From {7}.SecondOrDefault())
    End Sub
End Module
Dummy t~
[]
Refactoring~
Domain-Driven Design (2003)
Refactoring (2018)
1
Domain-Driven Design (2003)
0

In that example missing.Shorten(8) works because the call is static. The same expression with an instance method, such as missing.ToUpper(), throws NullReferenceException. That asymmetry is a feature when the extension is designed for it (a null-tolerant Shorten) and a trap when readers assume every dotted call dereferences the receiver. Document a null-tolerant extension as such, and do not rely on it for general code.

Which receivers are possible

The receiver parameter can be any type: a class, an interface, a structure, an enumeration, a delegate type, an array, a constructed generic type (IEnumerable(Of Book)) or an open generic type parameter. The argument is converted to the parameter type by a widening conversion, which is why an extension on Object accepts a boxed Integer and an extension on an interface applies to every implementing type. A ByRef receiver is allowed for value types and reference types alike, and then the call changes the caller’s variable, not a copy.

Option Strict On
Imports System.Runtime.CompilerServices

Public Interface IShelved
    ReadOnly Property Title As String
End Interface

Public Class Mag
    Implements IShelved
    Public ReadOnly Property Title As String Implements IShelved.Title
        Get
            Return "mag"
        End Get
    End Property
End Class

Public Structure Counter
    Public Value As Integer
End Structure

Public Module Ext
    <Extension>
    Public Function Echo(o As Object) As String
        Return "obj:" & o.ToString()
    End Function

    <Extension>
    Public Sub Reset(ByRef x As Integer)
        x = 0
    End Sub

    <Extension>
    Public Sub Bump(ByRef c As Counter)
        c.Value += 1
    End Sub

    <Extension>
    Public Sub BumpByVal(c As Counter)
        c.Value += 1
    End Sub

    <Extension>
    Public Sub Replace(ByRef s As String)
        s = "replaced"
    End Sub

    <Extension>
    Public Function Loud(x As IShelved) As String
        Return x.Title.ToUpperInvariant()
    End Function

    <Extension>
    Public Function Times(n As Integer) As String
        Return "times" & n
    End Function

    <Extension>
    Public Function Biggest(Of T As IComparable(Of T))(items As IEnumerable(Of T)) As T
        Return items.Max()
    End Function

    <Extension>
    Public Function Twice(f As Func(Of Integer, Integer), x As Integer) As Integer
        Return f(f(x))
    End Function
End Module

Module Program
    Sub Main()
        ' A value type is boxed to reach an extension on Object
        Console.WriteLine(5.Echo())

        ' An extension on an interface works for every implementing type
        Console.WriteLine(New Mag().Loud())

        ' ByRef receiver: the caller's variable is changed
        Dim n = 7
        n.Reset()
        Console.WriteLine("after Reset: " & n)

        ' A ByVal receiver of a Structure is a copy; a ByRef one is the variable
        Dim c As New Counter
        c.Bump()
        c.BumpByVal()
        Console.WriteLine("counter: " & c.Value)

        Dim s As String = "x"
        s.Replace()
        Console.WriteLine("after Replace: " & s)

        ' Literals can be receivers
        Console.WriteLine(3.Times())

        ' Generic extension with a constraint
        Console.WriteLine(New List(Of Integer) From {3, 9, 2}.Biggest())

        ' Delegates can be extended too
        Dim f As Func(Of Integer, Integer) = Function(v) v * 3
        Console.WriteLine(f.Twice(2))
    End Sub
End Module
obj:5
MAG
after Reset: 0
counter: 1
after Replace: replaced
times3
9
18

What the example shows, receiver by receiver:

Receiver Behaviour

Object

5.Echo() boxes the literal. An extension on Object shows up on every expression of a statically known type, so it makes IntelliSense lists noisy; use it rarely.

Interface

New Mag().Loud() applies to every implementing class and structure. This is how LINQ works: its operators extend IEnumerable(Of T).

ByRef value type

n.Reset() assigns the caller’s Integer. A ByVal structure receiver works on a copy, so BumpByVal leaves c.Value at the value Bump produced.

ByRef reference type

s.Replace() makes the caller’s variable point at another string, a trick that only a ByRef receiver offers.

Literal

3.Times() is valid because a literal has a type. A Nothing literal has none, so Nothing.Echo() is late bound and is rejected under Option Strict On (BC30574).

Delegate

f.Twice(2) extends Func(Of Integer, Integer) itself, which allows fluent helpers on lambdas (see Delegates and Lambda Expressions).

Generic with constraint

Biggest(Of T As IComparable(Of T)) is inferred from the receiver and constrained by the usual rules (see Generics).

A ByRef receiver is legal but rare. Callers cannot tell from n.Reset() that n changes. When the receiver is a property, as in holder.Count.Reset(), Visual Basic passes a temporary copy and writes it back afterwards, so both the getter and the setter run (checked with a scratch program: the property became 0). Prefer a function that returns the new value.

How the compiler resolves a call

When the compiler meets b.Label(">> ") it first looks for an ordinary member of the receiver’s type with that name. Only if no instance method is applicable to the arguments does it look at extension methods, scope by scope, from the innermost outwards, and the first scope that holds an applicable candidate decides. Within one scope the usual overload resolution picks the best match, and a tie is an error.

flowchart TD call["Call: receiver.Name(arguments)"] --> inst{"Is an instance member
named Name applicable
to the arguments?"} inst -- yes --> useinst["Call the instance member:
an extension is never considered"] inst -- no --> scope["Take the next scope outwards:
the namespace of the call site,
its enclosing namespaces,
then file and project imports"] scope --> cand{"Does the scope contain extension methods
named Name with a receiver
that the argument converts to?"} cand -- no --> more{"Any scope left?"} more -- yes --> scope more -- no --> err["Compile error:
BC30456 not a member,
BC30057 too many arguments,
or late binding BC30574"] cand -- yes --> best{"Is there one best
overload by the usual rules?"} best -- yes --> useext["Call that extension method"] best -- no --> amb["BC30521 ambiguous"]

The first example below puts an instance Describe() and two extension Describe methods side by side. The instance method wins when both apply; the extension with a different parameter list is chosen when the instance method is not applicable.

Option Strict On

Imports System.Runtime.CompilerServices
Imports Shelf.Plain

Public Class Book
    Public Property Title As String

    Public Sub New(title As String)
        Me.Title = title
    End Sub

    ' An instance method that applies to Describe() wins over any extension method
    Public Function Describe() As String
        Return "instance: " & Title
    End Function

    Public Function Label(prefix As String) As String
        Return prefix & Title
    End Function
End Class

Namespace Plain
    Public Module PlainExtensions
        <Extension>
        Public Function Describe(book As Book) As String
            Return "plain extension: " & book.Title
        End Function

        <Extension>
        Public Function Describe(book As Book, width As Integer) As String
            Return "plain extension (" & width & "): " & book.Title
        End Function

        ' Different arity from the instance Label(String): applicable only with two arguments
        <Extension>
        Public Function Label(book As Book, prefix As String, suffix As String) As String
            Return prefix & book.Title & suffix
        End Function

        <Extension>
        Public Function Summary(book As Book) As String
            Return "plain summary of " & book.Title
        End Function
    End Module
End Namespace

Namespace Fancy
    Public Module FancyExtensions
        <Extension>
        Public Function Summary(book As Book) As String
            Return "FANCY summary of " & book.Title
        End Function
    End Module
End Namespace

Namespace App
    Module Program
        Sub Main()
            Dim b As New Book("Refactoring")
            Console.WriteLine(b.Describe())
            Console.WriteLine(b.Describe(40))
            Console.WriteLine(b.Label(">> "))
            Console.WriteLine(b.Label(">> ", " <<"))
            Console.WriteLine(b.Summary())
        End Sub
    End Module
End Namespace
instance: Refactoring
plain extension (40): Refactoring
>> Refactoring
>> Refactoring <<
plain summary of Refactoring

Two consequences follow, and the first one causes real bugs:

  • An extension method can never replace an existing instance method with the same signature. If the extended type gains an instance method that matches (for example in a new .NET release), your extension silently stops being called and the new member runs instead. The code still compiles, so the change shows up as different behaviour. Give extension methods names that say what is specific to your application (ToShelfLabel, not ToString), and cover them with tests.

  • Label(">> ", " <<") has an instance method called Label too, but with one parameter. Because that instance method is not applicable to two arguments, the extension is used. Overloading across an instance and an extension method is allowed, but it makes readers search two places.

Scope: Imports, namespaces and the project

An extension method is visible only where its module is in scope. A Module is in scope when the call site is inside the same namespace or a nested one, when the file has an Imports statement for the namespace that holds the module, or when the project imports the namespace for all files. Importing the module itself also works (Imports Shelf.Plain.PlainExtensions), and this is the way to bring in one module without the rest of its namespace.

<ItemGroup>
  <Import Include="Shelf.Plain" />
</ItemGroup>

The SDK uses exactly this mechanism to add System.Linq to every project, which is how Where and Select appear without an Imports line. Imports statements must be the first declarations of a file, before the first Namespace or type (BC30465 otherwise), so there is no way to import a namespace for only one namespace block of a file.

Scope also decides priority: an extension in the namespace of the call site beats one that arrived through Imports, even when both would be applicable. The example puts Origin in two places.

Option Strict On

Imports System.Runtime.CompilerServices
Imports Shelf.Imported

Public Class Book
    Public Property Title As String = "Refactoring"
End Class

Namespace Imported
    Public Module ImportedExtensions
        <Extension>
        Public Function Where(b As Book) As String
            Return "imported"
        End Function

        <Extension>
        Public Function Origin(b As Book) As String
            Return "imported (via Imports)"
        End Function
    End Module
End Namespace

Namespace App
    Public Module NearExtensions
        <Extension>
        Public Function Origin(b As Book) As String
            Return "near (same namespace as the caller)"
        End Function
    End Module

    Module Program
        Sub Main()
            Dim b As New Book()
            Console.WriteLine(b.Origin())
            Console.WriteLine(b.Where())
        End Sub
    End Module
End Namespace
near (same namespace as the caller)
imported

b.Where() finds the only candidate in the imported module. Note that an Imports for the namespace changes which method a call binds to, without any change to the call itself. A new Imports line, or a new module added to a namespace that was already imported, can therefore change the meaning of existing code or make a call ambiguous. When two imported namespaces both hold an applicable extension, the compiler cannot choose:

Option Strict On

Imports System.Runtime.CompilerServices
Imports Shelf.Plain
Imports Shelf.Fancy

Public Class Book
    Public Property Title As String
End Class

Namespace Plain
    Public Module PlainExtensions
        <Extension>
        Public Function Summary(b As Book) As String
            Return "plain"
        End Function
    End Module
End Namespace

Namespace Fancy
    Public Module FancyExtensions
        <Extension>
        Public Function Summary(b As Book) As String
            Return "fancy"
        End Function
    End Module
End Namespace

Module Program
    Sub Main()
        Dim b As New Book()
        Console.WriteLine(b.Summary())

        Dim o As Object = b
        Console.WriteLine(o.Summary())
    End Sub
End Module
Program.vb(32,29): error BC30521: Overload resolution failed because no accessible 'Summary' is most specific for these arguments:
Program.vb(35,27): error BC30574: Option Strict On disallows late binding.

BC30521 is fixed by removing one Imports, by importing only the module you need, or by calling the method in its ordinary form (PlainExtensions.Summary(b)), which names the module explicitly. The second message shows the late-binding limit: an Object variable is late bound under Option Strict Off, and the compiler does not look for extension methods at run time. Under Option Strict On the call is a compile error (BC30574). With Option Strict Off the same code compiles and fails when it runs; the scratch program printed MissingMemberException: Public member 'Summary' on type 'Book' not found. for the call on the Object variable, while the call through a variable typed Book worked. Extension methods are therefore a compile-time feature of statically typed code; see Early and Late Binding.

Declaration errors

The declaration rules are enforced one by one. This program contains one violation per rule and is compiled with the SDK:

Option Strict On

Imports System.Runtime.CompilerServices

Public Class Book
    Public Property Title As String
End Class

' Extension methods can be defined only in modules
Public Class NotAModule
    <Extension>
    Public Shared Function Bad1(b As Book) As String
        Return b.Title
    End Function
End Class

' The attribute itself is valid only on a Module, Sub or Function
<Extension>
Public Class Tagged
End Class

Public Module Wrong
    ' The extended type is the first parameter, so there must be one
    <Extension>
    Public Function Bad2() As String
        Return ""
    End Function

    ' The first parameter cannot be Optional
    <Extension>
    Public Function Bad3(Optional b As Book = Nothing) As String
        Return ""
    End Function

    ' The first parameter cannot be ParamArray
    <Extension>
    Public Function Bad4(ParamArray b() As Book) As String
        Return ""
    End Function

    ' There are no extension properties
    <Extension>
    Public Property Bad5(b As Book) As String
        Get
            Return ""
        End Get
        Set(value As String)
        End Set
    End Property
End Module

Module Program
    Sub Main()
    End Sub
End Module
Program.vb(11,6): error BC36551: Extension methods can be defined only in modules.
Program.vb(19,14): error BC36550: 'Extension' attribute can be applied only to 'Module', 'Sub', or 'Function' declarations.
Program.vb(25,21): error BC36552: Extension methods must declare at least one parameter. The first parameter specifies which type to extend.
Program.vb(31,35): error BC36553: 'Optional' cannot be applied to the first parameter of an extension method. The first parameter specifies which type to extend.
Program.vb(37,37): error BC36554: 'ParamArray' cannot be applied to the first parameter of an extension method. The first parameter specifies which type to extend.
Program.vb(42,6): error BC30662: Attribute 'ExtensionAttribute' cannot be applied to 'Bad5' because the attribute is not valid on this declaration type.

LINQ operators are extension methods

Where, Select, OrderBy and the other query operators are extension methods on IEnumerable(Of T) declared in the static class System.Linq.Enumerable. This is also what the Aggregate, From, Where and Select clauses of a query expression translate into (see LINQ and Query Expressions). Two practical results: removing the System.Linq import takes the methods away, and Enumerable.Where(books, predicate) is a valid call at any time, because every extension is also an ordinary shared method of its module. The shelf’s own query helpers should follow the same pattern: take IEnumerable(Of T), return IEnumerable(Of T) and keep the deferred execution that the callers expect (see Iterators and Yield).

Using extension methods from other .NET languages

A Visual Basic extension method is compiled to a shared method with ExtensionAttribute on the method and on its assembly, so it is an ordinary extension method for any .NET language. The reverse is also true: extension methods written in C# are extension methods in Visual Basic, and no Imports of the attribute is needed to consume them. The next two projects were built and run: a Visual Basic library used from C#, and a C# library, with one classic extension method and one C# 14 extension block, used from Visual Basic.

Option Strict On

Imports System.Runtime.CompilerServices

Public Module VbExtensions
    <Extension>
    Public Function Shout(text As String) As String
        Return text.ToUpperInvariant() & "!"
    End Function
End Module
using System;
using VbLib;

Console.WriteLine("dune".Shout());
Console.WriteLine(VbExtensions.Shout("direct"));
DUNE!
DIRECT!
namespace CsLib;

public static class CsExtensions
{
    // Classic extension method
    public static string Whisper(this string text) => text.ToLowerInvariant() + "...";

    // C# 14 extension block with an extension property
    extension(string text)
    {
        public bool IsBlank => string.IsNullOrWhiteSpace(text);
        public string Exclaim() => text + "?";
    }
}
Option Strict On

Imports CsLib

Module Program
    Sub Main()
        Console.WriteLine("Dune".Whisper())
        Console.WriteLine("Dune".Exclaim())
    End Sub
End Module
dune...
Dune?

Exclaim(), declared in a C# 14 extension(string text) block, is callable from Visual Basic with instance syntax, because the C# compiler also emits it as a classic extension method. IsBlank, declared in the same block as an extension property, is not: "Dune".IsBlank fails with BC30456: 'IsBlank' is not a member of 'String' (tested on SDK 10.0.402), because Visual Basic has no notion of an extension property. Visual Basic can neither declare extension properties nor extension blocks, and does not see C# 14 extension members other than methods. Treat C# extension properties as a reason to wrap them in a method when a shared library has to serve Visual Basic callers too. The C# side is described on Extension Members and the general interop rules are on Interop with C# and Other .NET Languages.

Differences from C#

Topic Visual Basic C#

Container

a Module

a non-generic, non-nested static class

Marker

<Extension> on every method (the module marker is optional)

this before the first parameter

Receiver

any parameter type; ByRef allowed

any type; ref this and in this for value types

Properties

not supported

extension properties through C# 14 extension blocks

Resolution

instance method first; then scope by scope from the call site outwards; Imports applies to the whole file

instance method first; then using directives; namespace nesting orders the candidates

Nothing or null receiver

call compiles for typed expressions

call compiles for typed expressions

Late binding

extension methods are not found on Object or dynamic-style variables

extension methods are not found on dynamic

Static call syntax

Module.Method(x) or Method(x) inside the module

Class.Method(x)

Design guidance

  • Put extensions in a module that is named for what it extends or the feature it serves (BookExtensions, CatalogQueryExtensions) and in a namespace that the callers import on purpose.

  • Prefer a real method when you own the type, because an extension is invisible to reflection on the type and to interface contracts, and cannot be overridden.

  • Do not extend Object or a very common type such as String with names that are likely to collide with a later instance method of the .NET type or with other libraries.

  • Keep extensions free of hidden state; because they are shared methods, the only state they can use is their arguments and the module’s own shared members.

  • Do not use an extension method to hide a conversion that Option Strict would otherwise report; see Type Conversions.

  • For a collection of related helpers that need no receiver, a plain Module with ordinary functions is clearer (see Modules and Shared Members).

Partial types

Partial splits a declaration across any number of places in the same assembly and the same namespace. The compiler treats the type as the union of the parts: members from every part exist together, Inherits and Implements clauses are merged, attributes are combined, and every modifier that appears on one part applies to the whole type. The keyword exists for tools: the Windows Forms designer, the WPF markup compiler and the My namespace use it to keep generated code out of the file you edit.

The rules, with the compiler messages for the violations:

Rule Violation Error

Partial is allowed on Class, Structure, Interface and Module

-

-

At most one part may omit Partial

two parts without Partial

BC30179

Parts that state an access level agree on it

Public in one part and Friend in another

BC30925

Only one base class is possible; repeating the same Inherits is fine

Inherits Animal in one part and Inherits Plant in another

BC30928

Modifiers such as MustInherit and NotInheritable apply to the whole type, wherever they are written

-

-

Implements clauses of all parts are merged

-

-

All parts are in one assembly and one namespace; a part cannot be added from another project

-

-

Option Strict On

Public Class Animal
End Class

Partial Public MustInherit Class Shape
    Inherits Animal
    Public MustOverride Function Area() As Double
End Class

Partial Public Class Shape
    Inherits Animal
    Implements IComparable(Of Shape)

    Public Function CompareTo(other As Shape) As Integer Implements IComparable(Of Shape).CompareTo
        Return Area().CompareTo(other.Area())
    End Function
End Class

Public Class Square
    Inherits Shape
    Public Side As Double = 3
    Public Overrides Function Area() As Double
        Return Side * Side
    End Function
End Class

Module Program
    Sub Main()
        Dim a As New Square()
        Dim b As New Square() With {.Side = 4}
        Console.WriteLine(a.CompareTo(b))
        Console.WriteLine(GetType(Shape).IsAbstract)
        Console.WriteLine(GetType(Shape).GetInterfaces().Length)
        ' Dim s As New Shape()    ' BC30112 / BC30610: MustInherit
    End Sub
End Module
-1
True
1

Shape is declared MustInherit in the first part only, yet GetType(Shape).IsAbstract is True. Both parts repeat Inherits Animal, which is allowed because they agree, and the Implements clause in the second part makes IComparable(Of Shape) the type’s one interface (GetInterfaces().Length is 1). The usual use is much less clever: a tool owns one file, you own the other.

A structure, an interface and a module can be partial too. This is a complete program that splits all three, plus a class that uses the next section’s partial methods:

Option Strict On

Public Interface IShelved
    Property Title As String
End Interface

Partial Public Interface IShelved
    Function Describe() As String
End Interface

Partial Public Structure Slot
    Implements IShelved
    Public Property Title As String Implements IShelved.Title
End Structure

Partial Public Structure Slot
    Public Function Describe() As String Implements IShelved.Describe
        Return "slot holding " & Title
    End Function
End Structure

Partial Public Module Tools
    Public Function Twice(value As Integer) As Integer
        Return value * 2
    End Function
End Module

Partial Public Module Tools
    Public Function Triple(value As Integer) As Integer
        Return value * 3
    End Function
End Module

Partial Public Class Audit
    Private Partial Sub Record(message As String)
    End Sub

    Private Partial Sub RecordNeverImplemented(message As String)
    End Sub

    Private Shared Function Expensive(label As String) As String
        Console.WriteLine("  evaluating " & label)
        Return label
    End Function

    Public Sub Run()
        Console.WriteLine("calling Record")
        Record(Expensive("first argument"))
        Console.WriteLine("calling RecordNeverImplemented")
        RecordNeverImplemented(Expensive("second argument"))
    End Sub
End Class

Partial Public Class Audit
    Private Sub Record(message As String)
        Console.WriteLine("  recorded: " & message)
    End Sub
End Class

Module Program
    Sub Main()
        Dim s As New Slot With {.Title = "Refactoring"}
        Console.WriteLine(s.Describe())
        Console.WriteLine(Tools.Twice(4) & " " & Tools.Triple(4))
        Dim a As New Audit()
        a.Run()
    End Sub
End Module
slot holding Refactoring
8 12
calling Record
  evaluating first argument
  recorded: first argument
calling RecordNeverImplemented

The partial violations (two parts without Partial, different base classes, different accessibility) are compiler errors, as the partial-method rules below also are; the compiler printed all of them for one deliberately broken file:

Option Strict On

Public Class Animal
End Class

Public Class Plant
End Class

' Both parts omit Partial
Public Class Duplicate
End Class

Public Class Duplicate
End Class

' Two different base classes
Partial Public Class Mixed
    Inherits Animal
End Class

Partial Public Class Mixed
    Inherits Plant
End Class

' Two different access levels
Partial Public Class Visible
End Class

Partial Friend Class Visible
End Class

Partial Public Class Hooks
    ' A partial method must be a Sub
    Private Partial Function Compute() As Integer
    End Function

    ' It must be Private
    Public Partial Sub Open()
    End Sub

    ' The declaring part must have an empty body
    Private Partial Sub Close()
        Console.WriteLine("closing")
    End Sub

    Private Partial Sub Rename(first As Integer)
    End Sub

    Private Partial Sub Reset(Optional count As Integer = 1)
    End Sub

    Private Partial Sub Save()
    End Sub
End Class

Partial Public Class Hooks
    ' The parameter name differs from the declaration
    Private Sub Rename(second As Integer)
    End Sub

    ' The default value differs from the declaration
    Private Sub Reset(Optional count As Integer = 2)
    End Sub

    ' Only one implementation is allowed
    Private Sub Save()
    End Sub

    Private Sub Save()
    End Sub
End Class

Module Program
    Sub Main()
    End Sub
End Module
Program.vb(13,14): error BC30179: class 'Duplicate' and class 'Duplicate' conflict in namespace 'Shelf'.
Program.vb(22,14): error BC30928: Base class 'Plant' specified for class 'Mixed' cannot be different from the base class 'Animal' of one of its other partial types.
Program.vb(29,22): error BC30925: Specified access 'Friend' for 'Visible' does not match the access 'Public' specified on one of its other partial types.
Program.vb(34,30): error BC31437: 'Compute' cannot be declared 'Partial' because partial methods must be Subs.
Program.vb(38,5): error BC31431: Partial methods must be declared 'Private' instead of 'Public'.
Program.vb(42,25): error BC31435: Partial methods must have empty method bodies.
Program.vb(58,24): error BC31442: Parameter name 'second' does not match the name of the corresponding parameter, 'first', defined on the partial method declaration 'Rename'.
Program.vb(62,17): error BC37203: Optional parameter of a method 'Private Sub Reset([count As Integer = 2])' does not have the same default value as the corresponding parameter of the partial method 'Private Sub Reset([count As Integer = 1])'.
Program.vb(69,17): error BC31434: Method 'Save' cannot implement partial method 'Save' because 'Save' already implements it. Only one method can implement a partial method.

Things that are per file, not per type

  • Option Strict, Option Explicit, Option Infer and Option Compare statements apply to one source file, so the parts of a type can disagree. A designer file generated without Option Strict On can sit next to a hand-written part that has it, and the project-level setting decides for a file that has no Option line (see Option Statements). Put the same Option lines in every file you write.

  • Imports statements apply to one file as well; a second part does not see the first part’s imports.

  • Inside a module, a partial type declaration disables type promotion (the rule that makes members of a nested type visible from the enclosing module’s namespace), which can produce surprising name resolution; keep nested partial types out of modules.

Partial modules and the My namespace

My is built with partial types. The My.MyApplication, My.MyComputer and My.MySettings classes are partial, which is why a project can add its own file with Namespace My and Partial Friend Class MyApplication to hook application events such as Startup and UnhandledException. A partial Module is the right way to share one logical module between a generated file and a hand-written one. See The My Namespace.

Partial methods

A partial method is a method whose declaration and implementation live in different parts of a partial type. The declaration (Partial Private Sub OnTitleChanged(), or Private Partial Sub; the two modifiers can come in either order) has no body and is called from the generated code like any Sub. The implementation, in another part, has a body and no Partial. If no part implements it, the compiler removes every call, including the evaluation of the arguments. A partial method is therefore a lightweight notification hook with no cost when it is unused.

The rules:

  • Sub only, never a Function (BC31437). The declaration is the whole of the signature, so there is no return value to lose.

  • Private only (BC31431), so a partial method is an implementation detail of the type; it cannot be called from outside and cannot be Overridable. Shared and generic partial methods are allowed.

  • The declaring part has an empty body (BC31435); the implementing part is the one with statements.

  • At most one implementation (BC31434), and the implementing method must repeat the parameter names of the declaration (BC31442) and the default values of Optional parameters (BC37203).

  • No implementation is required: the call is silently removed when none exists, which is why a misspelled implementation name produces no warning; the hook is simply never run. Check names against the declaration, and consider a unit test that proves the hook runs.

The following project has three files, as a generator would produce. Book.Generated.vb declares the properties and the hooks and calls them from the setters, Book.vb implements two of the three, and Program.vb uses the type.

Generated part:

Option Strict On

' Pretend this file is produced by a tool: it declares the hooks and never changes by hand
Partial Public Class Book
    Private _title As String = ""
    Private _year As Integer

    Public Property Title As String
        Get
            Return _title
        End Get
        Set(value As String)
            OnTitleChanging(value)
            _title = value
            OnTitleChanged()
        End Set
    End Property

    Public Property Year As Integer
        Get
            Return _year
        End Get
        Set(value As Integer)
            OnYearChanged(value)
            _year = value
        End Set
    End Property

    ' Partial method declarations: Sub only, Private, no body, no Handles or Implements
    Private Partial Sub OnTitleChanging(newValue As String)
    End Sub

    Private Partial Sub OnTitleChanged()
    End Sub

    Private Partial Sub OnYearChanged(newValue As Integer)
    End Sub
End Class

Hand-written part:

Option Strict On

' The hand-written half. Partial is optional here because the other file already says it
Public Class Book
    Public ReadOnly Property ChangeLog As New List(Of String)

    ' Implementations of two of the three hooks; OnYearChanged has none, so its call vanishes
    Private Sub OnTitleChanging(newValue As String)
        If String.IsNullOrWhiteSpace(newValue) Then
            Throw New ArgumentException("A title is required.", NameOf(newValue))
        End If
        ChangeLog.Add("changing to " & newValue)
    End Sub

    Private Sub OnTitleChanged()
        ChangeLog.Add("changed")
    End Sub
End Class

Program:

Option Strict On

Module Program
    Sub Main()
        Dim b As New Book()
        b.Title = "Refactoring"
        b.Year = 2018
        b.Title = "Refactoring, 2nd edition"
        Try
            b.Title = " "
        Catch ex As ArgumentException
            Console.WriteLine("rejected: " & ex.ParamName)
        End Try
        For Each line In b.ChangeLog
            Console.WriteLine(line)
        Next
        Console.WriteLine(b.Title & " " & b.Year)
    End Sub
End Module
rejected: newValue
changing to Refactoring
changed
changing to Refactoring, 2nd edition
changed
Refactoring, 2nd edition 2018

OnTitleChanging ran before the assignment and rejected the blank title by throwing, and that stopped the setter, so the ChangeLog has no entry for it and the title keeps its earlier value. OnYearChanged has no implementation, so the call in the Year setter generated no code and nothing was logged. The exception lives in the hook and is the author of the type’s responsibility; a hook that throws is part of the type’s contract, so document it.

The Audit class in the program of the previous section makes the two behaviours of an unimplemented partial method visible. Record has an implementation, RecordNeverImplemented does not, and both calls pass an argument produced by a method with a side effect. evaluating second argument is missing from that output, because the compiler removed the whole call statement. The consequence is that an argument with a side effect must not be placed in a call to a partial method, unless the side effect is allowed to be optional: whether it happens depends on a file in another part. The calls and the output of that program also show the order: the argument is evaluated first, then the body runs.

A smaller program shows the remaining modifiers, a generic and Shared partial method, an optional parameter and the Partial Private order of the keywords:

Option Strict On

Partial Public Class Hooks
    ' The declaration may list Partial before or after Private; Shared and generic are allowed
    Partial Private Shared Sub Tick(Of T)(value As T)
    End Sub

    ' Optional parameters need the same default value in both parts
    Private Partial Sub Opt(Optional amount As Integer = 1)
    End Sub

    ' No implementation anywhere: the call below is removed by the compiler
    Private Partial Sub Missing(message As String)
    End Sub

    Public Sub Go()
        Tick(Of Integer)(5)
        Opt()
        Missing("never evaluated")
        Console.WriteLine("done")
    End Sub
End Class

Partial Public Class Hooks
    Private Shared Sub Tick(Of T)(value As T)
        Console.WriteLine("tick " & value.ToString())
    End Sub

    ' Parameter names and optional defaults must repeat the declaration
    Private Sub Opt(Optional amount As Integer = 1)
        Console.WriteLine("opt " & amount)
    End Sub
End Class

Module Program
    Sub Main()
        Dim h As New Hooks()
        h.Go()
    End Sub
End Module
tick 5
opt 1
done

Source: Sub Statement

Extended partial methods in C#, and generators

C# 9 extended partial methods: a C# partial method may have an access modifier, a return value and out parameters, and then an implementation is required. Source generators use this for APIs such as GeneratedRegex and LibraryImport. None of that exists in Visual Basic: a Visual Basic partial method is still the old kind, Private, Sub and optional. A partial Function is rejected with BC31437 (see the table above), so the C# patterns that rely on a generated partial Function or on Public partial methods cannot be written in Visual Basic. Use the non-generated equivalents (New Regex(...) with RegexOptions.Compiled, Declare or <DllImport>; see Native Interop: Declare and P/Invoke). Visual Basic projects can still run analyzers and source generators written in C#, which add files to the compilation, but the generated code must be usable from Visual Basic.

Designer-generated code

Windows Forms projects use partial classes in every form. For a form called SearchForm there are two files:

  • SearchForm.Designer.vb is written by the visual designer and rewritten whenever you change the form in the designer. It contains Partial Class SearchForm, the Friend WithEvents fields for the controls, InitializeComponent, a Dispose override and the <DesignerGenerated> attribute. You do not edit it by hand: hand edits are lost or confuse the designer.

  • SearchForm.vb is yours: constructor, event handlers and logic.

WithEvents fields are what make Handles SearchButton.Click work in the hand-written part (see Events and Handlers). A WPF window uses the same pattern in another form: MainWindow.xaml is compiled into a generated partial class (the file MainWindow.g.vb under obj), and MainWindow.xaml.vb is the code-behind part, which must call InitializeComponent() as well (see WPF Applications).

This pair of files is the shape that Visual Studio produces and was built, but not run, with the .NET 10 SDK on Linux using EnableWindowsTargeting, because Windows Forms cannot start there. The project file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <RootNamespace>Shelf</RootNamespace>
    <UseWindowsForms>true</UseWindowsForms>
    <MyType>WindowsFormsWithCustomSubMain</MyType>
    <StartupObject>Sub Main</StartupObject>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <EnableWindowsTargeting>true</EnableWindowsTargeting>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
  <ItemGroup>
    <Import Include="System.Windows.Forms" />
  </ItemGroup>
</Project>

The generated half:

<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Class SearchForm
    Inherits System.Windows.Forms.Form

    ' Generated by the Windows Forms designer: do not edit by hand.
    <System.Diagnostics.DebuggerNonUserCode()>
    Protected Overrides Sub Dispose(disposing As Boolean)
        Try
            If disposing AndAlso components IsNot Nothing Then
                components.Dispose()
            End If
        Finally
            MyBase.Dispose(disposing)
        End Try
    End Sub

    Private components As System.ComponentModel.IContainer

    <System.Diagnostics.DebuggerStepThrough()>
    Private Sub InitializeComponent()
        Me.SearchBox = New System.Windows.Forms.TextBox()
        Me.SearchButton = New System.Windows.Forms.Button()
        Me.SuspendLayout()
        '
        'SearchBox
        '
        Me.SearchBox.Location = New System.Drawing.Point(12, 12)
        Me.SearchBox.Name = "SearchBox"
        Me.SearchBox.Size = New System.Drawing.Size(200, 23)
        Me.SearchBox.TabIndex = 0
        '
        'SearchButton
        '
        Me.SearchButton.Location = New System.Drawing.Point(218, 12)
        Me.SearchButton.Name = "SearchButton"
        Me.SearchButton.Size = New System.Drawing.Size(75, 23)
        Me.SearchButton.TabIndex = 1
        Me.SearchButton.Text = "Search"
        '
        'SearchForm
        '
        Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!)
        Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
        Me.ClientSize = New System.Drawing.Size(305, 47)
        Me.Controls.Add(Me.SearchBox)
        Me.Controls.Add(Me.SearchButton)
        Me.Name = "SearchForm"
        Me.Text = "Shelf search"
        Me.ResumeLayout(False)
        Me.PerformLayout()
    End Sub

    Friend WithEvents SearchBox As System.Windows.Forms.TextBox
    Friend WithEvents SearchButton As System.Windows.Forms.Button
End Class

The hand-written half:

Option Strict On

Public Class SearchForm
    Public Sub New()
        ' Without this call the designer-generated controls are never created
        InitializeComponent()
        SearchBox.Text = "Refactoring"
    End Sub

    Private Sub SearchButton_Click(sender As Object, e As EventArgs) Handles SearchButton.Click
        MessageBox.Show("Searching for " & SearchBox.Text)
    End Sub
End Class

Friend Module Startup
    <STAThread>
    Public Sub Main()
        Application.EnableVisualStyles()
        Application.Run(New SearchForm())
    End Sub
End Module
Build succeeded.
    0 Warning(s)
    0 Error(s)

In the hand-written half, InitializeComponent() is called by the explicit constructor. Once you write Sub New yourself, you must call it before touching any control, or the control fields are Nothing and the first use throws NullReferenceException. The project sets MyType to WindowsFormsWithCustomSubMain because it supplies its own Sub Main, which is the setting a Windows Forms project needs when it does not use the application framework.

Practical rules for generated-plus-hand-written pairs:

  • Put every change in your part. If a tool needs a change in the generated part, change the tool’s input (the form in the designer, the XAML file) instead.

  • Use partial methods as the extension points of generated code, so that a missing hook costs nothing.

  • Check generated files in the way the repository treats other source files, but exclude them from style checks and analyzer rules that you apply to hand-written code (see XML Documentation, Analyzers and Code Style), and from code coverage thresholds.

  • Resist splitting a large hand-written class across several partial files to make it look smaller; if a class needs that, it has more than one responsibility (see Classes and Objects).

Older books and articles

Material written for Visual Basic 2008 to Visual Basic 2010 shows <Extension()> with parentheses, ByVal on every parameter and the first partial-method examples with a message box. All of that still compiles. What has changed around it: the code now builds as SDK-style projects on .NET, not .NET Framework; designer files are the same in structure, but the project file no longer lists them; Partial has no new rules. Older text that says extension methods need .NET Framework 3.5 or a reference to System.Core is no longer relevant, because the attribute lives in the base class library; and text that shows a designer pair with the Class header repeated, Public Class Form1 with Partial Public Class Form1, is valid for the same reason: at most one part may omit Partial, but it is better to write it on every part.

Common mistakes

Symptom Cause and fix

BC36551 on an extension method

declared in a Class or Structure; move it to a Module

BC36550 or BC30662

<Extension> on a class or on a property; only a module, Sub or Function can carry it

BC30456 ('X' is not a member of 'Y') for a method that exists in a library

the module’s namespace is not imported; add Imports or a project import

BC30521 for an extension call

two imported namespaces define the same extension; remove one import or call Module.Method(x)

BC30574 on o.Method()

o is an Object; extension methods do not apply to late-bound calls, so declare the variable with its real type

The extension is never called after upgrading a library

a new instance method with the same signature was added to the type; rename the extension

BC31437, BC31431, BC31435

a partial method that is a Function, that is not Private or that has a body in the declaring part

A hook never runs and nothing is reported

no implementation exists or its name or parameter list differs; implementation is optional, so there is no error

NullReferenceException in a form constructor

InitializeComponent() is not called in an explicit Sub New

BC30179 on a class declared in two files

neither declaration says Partial

BC30928 or BC30925

the parts disagree about the base class or the access level