Properties and Default Properties

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 console program that starts with Option Strict On, was built with warnings treated as errors (except where the text says otherwise) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The project’s root namespace is ShelfDocs, which matters only where a stack trace or a reflection name prints it. Examples that are meant to fail say so and show the compiler messages that the SDK printed.

A property looks like a field to its caller and runs code like a method inside. It is the unit of encapsulation of the .NET type system: data binding, serialization, designers and reflection-based frameworks work with properties, not with public fields, and an interface can declare a property but not a field. In Visual Basic a property is declared with a Property ... End Property block that holds a Get accessor, a Set accessor or both, or, since Visual Basic 10, in a one-line auto-implemented form. The running example is the shelf library catalogue: Book, Loan, Author and LoanState use properties everywhere, a Shelf collection exposes its books through a Default property, and a view-model reports changes to a user interface.

The page covers the full declaration syntax and its exact rules, auto-implemented properties, ReadOnly and WriteOnly, access levels of the accessors, parameterized and Default properties with the bang operator, overriding and implementing properties, a series of pitfalls that the compiler reports (and two that it only warns about), what Visual Basic can and cannot do with init-only and required members declared in C#, INotifyPropertyChanged, a comparison with C#, and the points where older material is outdated. Fields, constructors and access modifiers are introduced on Classes and Objects; the types a property can return are on Data Types and Literals and Nothing and Nullable Value Types.

Property procedures

The general form, from the Language Reference, is:

[<attributes>] [Default] [accessmodifier] [Shared | Overridable | Overrides | MustOverride | NotOverridable | Shadows]
   [ReadOnly | WriteOnly] Property Name[(parameters)] [As Type] [Implements InterfaceName.Member]
    [<attributes>] [accessmodifier] Get
        ' statements
    End Get
    [<attributes>] [accessmodifier] Set[(ByVal value As Type)]
        ' statements
    End Set
End Property

The rules that decide whether a declaration compiles:

  • A property without ReadOnly or WriteOnly must have both a Get and a Set (otherwise BC30124). A ReadOnly property has only Get; a WriteOnly property has only Set (a Set in a ReadOnly property is BC30022).

  • The Set accessor receives the assigned value in a parameter. By convention it is declared Set(value As T); the parameter name is free, but its type must be the property’s type (BC31064). When the parentheses are omitted, the compiler supplies a parameter named Value of the property’s type.

  • Get is a function without parameters of its own: the property’s parameters (if any) are the parameters of the property, and the same values arrive at Get and Set. Set receives them plus the new value.

  • A property is an ordinary member for the purposes of Shared, Overridable, Overrides, MustOverride, NotOverridable, Shadows and Implements. Shared properties belong to the type and are described on Modules and Shared Members.

  • The access modifier of the property is the default for the accessors; one accessor may be narrower (see below).

  • Under Option Strict On the As clause is mandatory (BC30210); Option Strict Off lets a property default to Object.

  • Each accessor is compiled to a method named get_Name and set_Name, and the property itself is metadata that links them. The Get and Set blocks of one property can carry their own attributes.

The first example uses the full form: validation in the setter, a computed read-only property, a write-only property and the older style in which the property name is assigned to produce the result before Exit Property.

Option Strict On

Public Class Loan
    Private _borrower As String = ""
    Private _dueOn As Date

    ' A read/write property with validation in both accessors.
    Public Property Borrower As String
        Get
            Return _borrower
        End Get
        Set(value As String)
            If String.IsNullOrWhiteSpace(value) Then
                Throw New ArgumentException("A borrower name is required.", NameOf(value))
            End If
            _borrower = value.Trim()
        End Set
    End Property

    ' A read-only property computed from other state: no backing field.
    Public ReadOnly Property IsOverdue(today As Date) As Boolean
        Get
            Return today.Date > _dueOn.Date
        End Get
    End Property

    Public WriteOnly Property DueOn As Date
        Set(value As Date)
            _dueOn = value
        End Set
    End Property

    ' Older style: assign to the property name inside Get, then Exit Property.
    Public ReadOnly Property Label As String
        Get
            If _borrower.Length = 0 Then
                Label = "(nobody)"
                Exit Property
            End If
            Label = "Loan to " & _borrower
        End Get
    End Property
End Class

Module Program
    Sub Main()
        Dim loan As New Loan()
        Console.WriteLine(loan.Label)
        loan.Borrower = "  Ada Lovelace "
        loan.DueOn = New Date(2026, 10, 1)
        Console.WriteLine(loan.Label)
        Console.WriteLine(loan.IsOverdue(New Date(2026, 10, 19)))
        Try
            loan.Borrower = " "
        Catch ex As ArgumentException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message.Replace(Environment.NewLine, " | "))
        End Try
        Console.WriteLine("[" & loan.Borrower & "]")
    End Sub
End Module
(nobody)
Loan to Ada Lovelace
True
ArgumentException: A borrower name is required. (Parameter 'value')
[Ada Lovelace]
  • Return value in Get is the normal form. Assigning to the property’s name inside its own Get sets the return value, exactly as assigning to a function’s name does in Function (see Procedures: Sub and Function). Exit Property leaves an accessor early and is the property counterpart of Exit Function.

  • IsOverdue(today As Date) is a read-only property with a parameter. It is legal, rarely the best design (a method named IsOverdueOn is clearer), and is the topic of the section on parameterized properties.

  • The setter rejects an empty borrower by throwing ArgumentException; the message includes (Parameter 'value') because the setter’s parameter is called value. The Framework Design Guidelines (the page notes that it reprints the 2008 edition) ask that getters be simple and not throw, and that a setter which throws leave the previous value in place, as this one does.

Why a property and not a public field

A Public field would compile to the same call syntax, but a field is not a property in .NET: it cannot implement an interface member, data binding and most serializers ignore it by default, it cannot validate or raise a change notification, and replacing it with a property later is a binary-breaking change (callers are compiled against a field access). The next example shows what reflection and System.Text.Json see.

Option Strict On

Imports System.Text.Json

Public Class Probe
    Public Field As Integer = 1
    Public Property Prop As Integer = 2
    Public ReadOnly Property Computed As Integer
        Get
            Return Prop * 10
        End Get
    End Property
End Class

Module Program
    Sub Main()
        Dim p As New Probe()
        Console.WriteLine(JsonSerializer.Serialize(p))

        Dim t = GetType(Probe)
        Console.WriteLine("members: " & String.Join(", ", t.GetMembers().
            Where(Function(m) m.DeclaringType Is t).
            Select(Function(m) m.MemberType.ToString() & " " & m.Name).
            OrderBy(Function(s) s)))
    End Sub
End Module
{"Prop":2,"Computed":20}
members: Constructor .ctor, Field Field, Method get_Computed, Method get_Prop, Method set_Prop, Property Computed, Property Prop

The serializer wrote Prop and the read-only Computed, and skipped Field. The compiler also produced get_Prop, set_Prop and get_Computed methods; there is no set_Computed. The property is metadata, the accessors are the code. Computed has no backing field at all: it is recomputed on every read, which is the right choice for a value derived from other state.

Auto-implemented properties

Writing a field, a Get that returns it and a Set that assigns it for every plain property is the boilerplate that Visual Basic 10 removed. A property declared on one line, without Get/Set blocks, is auto-implemented: the compiler generates a private backing field and the accessors.

  • Syntax: Public Property Title As String. An initializer follows the As clause: Public Property Title As String = "Untitled". The As New form works too: Public ReadOnly Property Authors As New List(Of Author).

  • The backing field is named with an underscore and the property’s name (_Title). The name is visible to reflection and, within the class, to the editor as a hidden member; other code in the same class cannot refer to it by name.

  • ReadOnly auto-implemented properties (Visual Basic 14 and later) can be given an initializer, and can be assigned in the constructor of the declaring class and nowhere else. Assigning one in an ordinary method is BC30526.

  • An auto-implemented property cannot have accessor blocks, so a narrower setter, a validation or a change notification needs the full form. There is no Init accessor and no way to declare required (see the section on C# interop).

  • Visual Basic 9 and earlier reject the syntax (BC36716); see History, Versions and Language Strategy.

  • Under Option Strict On the As clause cannot be omitted (BC30210), even when an initializer would allow the compiler to infer the type; with Option Strict Off the property silently becomes an Object property (a test printed Object for Public Property Year = 2000).

  • An initializer runs as part of every constructor, before the constructor body and, in a derived class, after the base class’s constructor has run (a test printed Base ctor, P initializer, Derived ctor body in this order). C# runs field initializers before the base constructor.

Option Strict On

Imports System.Reflection

Public Class Author
    Public Property Name As String
    Public Property BornOn As Date?
End Class

Public Class Book
    ' Auto-implemented: initializer, As New, ReadOnly with initializer.
    Public Property Title As String = "Untitled"
    Public Property Year As Integer = 2000
    Public ReadOnly Property Authors As New List(Of Author)
    Public ReadOnly Property Created As Date = New Date(2026, 10, 19)
    Public Property Copies As Integer
    Public ReadOnly Property Shelf As String

    Public Sub New(shelf As String)
        Me.Shelf = shelf          ' a ReadOnly auto-property can be set in the constructor
    End Sub
End Class

Module Program
    Sub Main()
        Dim book As New Book("A-12")
        Console.WriteLine(book.Title & " / " & book.Year & " (" & book.Year.GetType().Name & ") / " & book.Shelf)
        Console.WriteLine(book.Authors.Count & " authors, copies " & book.Copies)

        ' The compiler generates a backing field named _ + property name.
        Dim fields = GetType(Book).GetFields(BindingFlags.Instance Or BindingFlags.NonPublic)
        For Each f In fields.OrderBy(Function(x) x.Name)
            Console.WriteLine("field " & f.Name & " : " & f.FieldType.Name & If(f.IsInitOnly, " (initonly)", ""))
        Next
        Console.WriteLine(GetType(Book).GetProperty("Year")?.PropertyType.Name)
    End Sub
End Module
Untitled / 2000 (Int32) / A-12
0 authors, copies 0
field _Authors : List`1
field _Copies : Int32
field _Created : DateTime
field _Shelf : String
field _Title : String
field _Year : Int32
Int32

Two details of this output are worth remembering. First, Authors is read-only but its list is not: the property cannot be re-assigned, while book.Authors.Add(...) is allowed; if the intention is a collection that callers cannot change, expose IReadOnlyList(Of Author) (see Collections). Second, the backing field of a ReadOnly auto-implemented property is not marked initonly in the IL: the compiler enforces the rule, the runtime does not.

When to leave the auto form

Switch to a full property when the setter must validate, when a change must be announced (see INotifyPropertyChanged below), when the getter computes a value, when the two accessors need different access levels, when the value lives somewhere else (a dictionary, a settings store, another object), or when the property implements an interface member under a different name. You can also convert later without breaking callers, because the property is already the public contract.

ReadOnly, WriteOnly and Mixed Access

ReadOnly means that code outside the constructor, and outside the declaring class, cannot assign the property. WriteOnly properties are unusual: the Framework Design Guidelines say not to provide set-only properties and to write a method named Set... instead (for example SetPassword(value)).

A property can give each accessor a different access level, as long as the property is declared with both accessors and only one accessor is narrower than the property itself:

  • Public Property Count As Integer with Private Set makes a property that is publicly readable and privately writable. The accessor modifier may be Protected, Friend, Private, Protected Friend or Private Protected (both compound forms were compiled for this page), and must be strictly more restrictive than the property’s access level (otherwise BC31100).

  • Only one of the two accessors may carry a modifier (otherwise BC31101), and a ReadOnly or WriteOnly property cannot have one at all (BC31105).

Option Strict On

Public Structure Point
    Public X As Integer
    Public Y As Integer
End Structure

Public Class Shelf
    Private _count As Integer

    ' Public Get, Private Set: callers read, only the class writes.
    Public Property Count As Integer
        Get
            Return _count
        End Get
        Private Set(value As Integer)
            _count = value
        End Set
    End Property

    ' An auto-property cannot have accessor blocks, so a narrower setter needs the full form above.
    Public Property Origin As Point
    Public ReadOnly Property Fixed As Point

    Public Sub Add()
        Count += 1
    End Sub

    Public Sub BumpByRef()
        Program.Increment(Count)   ' ByRef to a property: copy-in, call, copy-out through the setter
    End Sub
End Class

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

    Sub Main()
        Dim s As New Shelf()
        s.Add()
        s.Add()
        Console.WriteLine("Count = " & s.Count)

        ' A property of a structure type returns a copy; the compiler rejects
        ' s.Origin.X = 5 (see the error example below). Copy, modify, assign back:
        Dim p = s.Origin
        p.X = 5
        s.Origin = p
        Console.WriteLine("Origin.X = " & s.Origin.X)

        s.BumpByRef()
        Console.WriteLine("after BumpByRef = " & s.Count)

        ' Passing a property ByRef: copy-in/copy-out.
        Dim copy As Integer = s.Count
        Increment(copy)
        Console.WriteLine("local copy " & copy & ", property still " & s.Count)
    End Sub
End Module
Count = 2
Origin.X = 5
after BumpByRef = 3
local copy 4, property still 3

Three behaviours of the example are not obvious.

  1. Count += 1 inside Shelf reads and writes the property: the compound assignment calls Get, adds and calls Set, so a setter with side effects runs on every +=.

  2. Origin is a property of a structure type. Reading s.Origin returns a copy, so s.Origin.X = 5 modifies a temporary copy; the compiler therefore rejects it (BC30068, below) instead of silently doing nothing. Read the value into a local, change it and assign the whole structure back, as the example does. A class-typed property does not have this problem. See Structures and Value Types.

  3. Passing a property ByRef is accepted, but a ByRef argument that is a property is implemented as copy-in, call and copy-out through Set. Passing Count from inside the class updated the property (3), while passing a local copy left the property alone (the local became 4, Count stayed 3). Passing a ReadOnly property ByRef is accepted without a diagnostic too, and the callee’s change is silently lost (a test with Bump(t.R) left t.R at 3), so avoid passing properties ByRef.

Compiler errors for property declarations

The next three programs fail on purpose. They are separate programs because the compiler reports declaration errors first and stops before it checks the code that uses the declarations. The messages are exactly what the SDK printed.

Option Strict On

Public Class Broken
    Private _x As Integer

    Public Property Untyped = 1

    Public Property HalfDone As Integer
        Get
            Return _x
        End Get
    End Property

    Public ReadOnly Property Mixed As Integer
        Get
            Return _x
        End Get
        Set(value As Integer)
            _x = value
        End Set
    End Property

    Default Public Property Plain As Integer
        Get
            Return _x
        End Get
        Set(value As Integer)
            _x = value
        End Set
    End Property

    Default Public Property First(i As Integer) As Integer
        Get
            Return i
        End Get
        Set(value As Integer)
        End Set
    End Property

    Default Public Property Second(i As String) As Integer
        Get
            Return 0
        End Get
        Set(value As Integer)
        End Set
    End Property
End Class

Module Program
    Sub Main()
    End Sub
End Module
Program.vb(6,21): error BC30210: Option Strict On requires all Function, Property, and Operator declarations to have an 'As' clause.
Program.vb(8,21): error BC30124: Property without a 'ReadOnly' or 'WriteOnly' specifier must provide both a 'Get' and a 'Set'.
Program.vb(18,9): error BC30022: Properties declared 'ReadOnly' cannot have a 'Set'.
Program.vb(23,29): error BC31048: Properties with no required parameters cannot be declared 'Default'.
Program.vb(32,29): error BC30359: 'Default' can be applied to only one property name in a class.
Program.vb(40,29): error BC30359: 'Default' can be applied to only one property name in a class.
Option Strict On

Public Class Broken
    Private _x As Integer

    Protected Property Narrow As Integer
        Public Get
            Return _x
        End Get
        Set(value As Integer)
            _x = value
        End Set
    End Property

    Public Property BothNarrow As Integer
        Private Get
            Return _x
        End Get
        Private Set(value As Integer)
            _x = value
        End Set
    End Property

    Public ReadOnly Property Priv As Integer
        Private Get
            Return _x
        End Get
    End Property
End Class

Module Program
    Sub Main()
    End Sub
End Module
Program.vb(7,9): error BC31100: Access modifier 'Public' is not valid. The access modifier of 'Get' and 'Set' should be more restrictive than the property access level.
Program.vb(19,9): error BC31101: Access modifier can only be applied to either 'Get' or 'Set', but not both.
Program.vb(25,9): error BC31105: 'ReadOnly' properties cannot have an access modifier on 'Get'.

The first program also shows the Default rules that the next section explains: BC31048 for a Default property without parameters, and BC30359 for the Default declarations (First and Second) that follow Plain, which has already used the one default name the class is allowed. The second program shows the access-level rules.

The third program fails while using properties. It is built with warnings as errors, so BC42104 (a warning) is an error too.

Option Strict On

Public Structure Point
    Public X As Integer
End Structure

Public Class Holder
    Public Property Origin As Point
    Public ReadOnly Property Fixed As Integer = 1
    Public Property Count As Integer

    Public Property Name As String
        Get
            Dim before = Name
            Return before
        End Get
        Set(value As String)
        End Set
    End Property
End Class

Module Program
    Sub Main()
        Dim h As New Holder()
        h.Origin.X = 3
        h.Fixed = 5
        Dim n = h.Count(1)
    End Sub
End Module
Program.vb(14,26): error BC42104: Variable 'Name' is used before it has been assigned a value. A null reference exception could result at runtime.
Program.vb(25,9): error BC30068: Expression is a value and therefore cannot be the target of an assignment.
Program.vb(26,9): error BC30526: Property 'Fixed' is 'ReadOnly'.
Program.vb(27,17): error BC32016: 'Public Property Count As Integer' has no parameters and its return type cannot be indexed.
  • BC42104 deserves attention. Inside the Get of a property, a bare Name is not a call to the property: it is the implicit return variable, exactly like a function’s name in a Function. Reading it before assigning it reads Nothing (the default). The next section shows the opposite trap in Set.

  • BC30068 is the structure-copy rule from the previous example; BC30526 is the assignment to a ReadOnly property from outside the constructor; BC32016 is a call with arguments on a property that has none (the compiler tries to index the Integer that Count returns).

Recursion inside accessors

A common mistake is to use the property name inside its own accessors when the intent was the backing field. The two accessors behave differently, and the compiler warns in both cases but does not fail unless warnings are errors.

Option Strict On

Public Class Box
    Private _name As String = "stored"
    Public Property Name As String
        Get
            Dim seen = Name
            Return If(seen, "(Nothing)")
        End Get
        Set(value As String)
            Name = value
            _name = value
        End Set
    End Property
End Class

Module Program
    Sub Main()
        Dim b As New Box()
        b.Name = "x"
        Console.WriteLine(b.Name)
    End Sub
End Module

This program was built with TreatWarningsAsErrors set to false, because both lines are warnings. The compiler printed:

Program.vb(7,24): warning BC42104: Variable 'Name' is used before it has been assigned a value. A null reference exception could result at runtime.
Program.vb(11,13): warning BC42026: Expression recursively calls the containing property 'Public Property Name As String'.

and dotnet run ended in a crash, whose first line is:

Stack overflow.

(The runtime then prints the repeated frames, ShelfDocs.Box.set_Name, followed by ShelfDocs.Program.Main(); the number of repetitions varies from run to run and is omitted.)

  • In Get, Name is the implicit return variable (BC42104 says it is read before being assigned). It is not a recursion.

  • In Set, Name = value is a call to the setter itself (BC42026: Expression recursively calls the containing property), so the first assignment recurses until the stack is exhausted. A .NET process cannot recover from a stack overflow.

  • The cure is to use the backing field (_name = value) or an auto-implemented property.

Parameterized properties

A property may have parameters. The parameters are declared after the name, as for a Sub: Public ReadOnly Property Slot(row As Integer, column As Integer) As String. The call syntax is shelf.Slot(2, 3), and an assignment is shelf.Slot(2, 3) = "x". The rules are those of procedure parameters (Procedures: Sub and Function):

  • ByVal is the default and the only form allowed: a ByRef property parameter is BC30651. Parameters can be Optional with a constant default value (Opt(Optional i As Integer = 5) is read as t.Opt when the argument is omitted), and ParamArray is allowed on the last one (t.PA(1, 2, 3)); both were built and run for this page.

  • Parameter names appear in IntelliSense and support named arguments (ByTitle("PER", ignoreCase:=False)).

  • Properties can be overloaded by parameter list, like methods; the return type is not part of the signature.

  • Set receives the parameters plus value; the Get and Set blocks of one property share the parameter list.

C# has no parameterized properties other than this[...] indexers. C# code sees a Visual Basic parameterized property as its accessor methods (get_Slot, set_Slot), and as an indexer only if the property is the Default one.

Default properties

A Default property is the type’s indexer: shelf(0) is shorthand for shelf.Item(0). The compiler records the name of the default property in the DefaultMemberAttribute on the type, and any language that understands the attribute (C# indexers, VB, F# .[ ]) can use it.

  • Rule: a Default property must have at least one parameter (BC31048); there is no parameterless default property in .NET Visual Basic. This is a change from Visual Basic 6, where Text1 = "x" could mean Text1.Text = "x".

  • Only one name may be the default in a class (BC30359), but that name can be overloaded with several parameter lists. Typically the property is called Item, which is what the .NET collections use and what a C# this[] indexer is compiled to, but any name works.

  • The modifier Default may stand before or after the access modifier (Default Public Property Item(...) and Public Default Property Item(...) are both used in the .NET documentation). It cannot be combined with Shared: that is BC30502.

  • A default property is inherited and is also declared in an interface (Default ReadOnly Property Item(name As String) As String). The implementing class names its default property with Implements IIndexed.Item.

  • Chained use is legal: if shelves is an array of Shelf, shelves(0)(0) indexes the array and then the default property of the element.

  • Under Option Strict On the default property of an Object-typed expression cannot be used: indexing an Object is late binding and is rejected with BC30574 (see Early and Late Binding).

Option Strict On

Imports System.Reflection

Public Class Book
    Public ReadOnly Property Title As String
    Public ReadOnly Property Isbn As String

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

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

Public Class Shelf
    Private ReadOnly _books As New List(Of Book)

    Public Sub Add(book As Book)
        _books.Add(book)
    End Sub

    ' The Default property: shelf(0) is shelf.Item(0).
    Default Public Property Item(index As Integer) As Book
        Get
            Return _books(index)
        End Get
        Set(value As Book)
            If value Is Nothing Then Throw New ArgumentNullException(NameOf(value))
            _books(index) = value
        End Set
    End Property

    ' An overload of the default property with a String key: shelf!Dune and shelf("Dune") both work.
    Default Public ReadOnly Property Item(isbn As String) As Book
        Get
            Return _books.FirstOrDefault(Function(b) b.Isbn = isbn)
        End Get
    End Property

    ' A non-default parameterized property: always written with its name.
    Public ReadOnly Property ByTitle(text As String, Optional ignoreCase As Boolean = True) As IEnumerable(Of Book)
        Get
            Dim comparison = If(ignoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal)
            Return _books.Where(Function(b) b.Title.Contains(text, comparison))
        End Get
    End Property

    ' Two-dimensional parameterized property.
    Public ReadOnly Property Slot(row As Integer, column As Integer) As String
        Get
            Return "R" & row & "C" & column
        End Get
    End Property
End Class

Module Program
    Sub Main()
        Dim shelf As New Shelf()
        shelf.Add(New Book("Dune", "9780441013593"))
        shelf.Add(New Book("Emma", "9780141439587"))

        Console.WriteLine(shelf(0))                       ' default property by index
        Console.WriteLine(shelf.Item(1))                  ' the same, spelled out
        Console.WriteLine(shelf("9780141439587"))         ' overload by String
        shelf(1) = New Book("Persuasion", "9780141439686")
        Console.WriteLine(shelf(1).Title)

        For Each b In shelf.ByTitle("per")
            Console.WriteLine("found " & b.Title)
        Next
        Console.WriteLine(shelf.ByTitle("PER", ignoreCase:=False).Count())
        Console.WriteLine(shelf.Slot(2, 3))

        Try
            Console.WriteLine(shelf(5))
        Catch ex As ArgumentOutOfRangeException
            Console.WriteLine(ex.GetType().Name)
        End Try

        Dim member = GetType(Shelf).GetCustomAttribute(Of DefaultMemberAttribute)()
        Console.WriteLine("DefaultMember = " & member?.MemberName)
        Dim shelves As Shelf() = {shelf}
        Console.WriteLine(shelves(0)(0))                  ' array index first, then the default property
    End Sub
End Module
Dune
Emma
Emma
Persuasion
found Persuasion
0
R2C3
ArgumentOutOfRangeException
DefaultMember = Item
Dune

Notes on the output:

  • shelf(1) and shelf.Item(1) are the same call. shelf("9780141439587") picks the overload on String. After shelf(1) = New Book(…​) the setter ran and shelf(1).Title is Persuasion.

  • ByTitle("per") returned the renamed book because the optional argument ignoreCase defaults to True; with ignoreCase:=False the same text "PER" found nothing (0).

  • An out-of-range index raised ArgumentOutOfRangeException from the underlying List(Of T). The default property did not add its own check; as with any indexer, the contract for invalid indices is yours to document.

  • DefaultMember = Item is the attribute that makes C# see an indexer.

The bang operator

The bang operator ! is a shorthand for a default property whose single parameter is a String: tags!genre means tags("genre"). It is a historical spelling used with dictionaries and with ADO recordsets (rs!Title), and it also works with With blocks (!genre). The key is written as an identifier and is converted to a string literal, so a key that is not a valid identifier needs the parenthesized form. The default property must take one String parameter: with an Integer parameter the key is still a string, and Option Strict On reports BC30512 for the implicit conversion.

Option Strict On

Public Interface IIndexed
    Default ReadOnly Property Item(name As String) As String
End Interface

Public Class Tags
    Implements IIndexed

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

    ' Default, single String parameter: this is what the ! operator needs.
    Default Public Property Item(name As String) As String Implements IIndexed.Item
        Get
            Dim value As String = Nothing
            Return If(_values.TryGetValue(name, value), value, "")
        End Get
        Set(value As String)
            _values(name) = value
        End Set
    End Property
End Class

Module Program
    Sub Main()
        Dim t As New Tags()
        t("genre") = "novel"
        t!language = "en"                    ' bang: t("language") = "en"
        Console.WriteLine(t!genre & "/" & t("LANGUAGE"))   ' keys are case-insensitive here
        Dim viaInterface As IIndexed = t
        Console.WriteLine(viaInterface("genre"))
        With t
            !year = "1965"
            Console.WriteLine(!year)
        End With
    End Sub
End Module
novel/en
novel
1965

Some points of style: ! hides a string key in something that looks like a member access, which means typos are not found by the compiler. New code should write tags("genre") or, better, expose strongly typed properties and keep the dictionary private. Notice also that a property can implement an interface property of the same name and signature in one line by naming it in the Implements clause; the interface declares its own Default property.

Overriding and implementing properties

Properties follow the same inheritance rules as methods (Inheritance and Polymorphism and Interfaces).

  • A property is virtual only if it is declared Overridable, Overrides (which is itself overridable unless NotOverridable) or MustOverride. Without a modifier a property is not overridable, and Shadows hides rather than overrides.

  • An override has the same signature as the property it overrides, including ReadOnly/WriteOnly, and may not widen the access level. In the example, the base class exposes a protected setter and the override keeps it protected.

  • An override reads or writes the base implementation with MyBase.Rating.

  • A class that implements an interface property needs at least the accessors the interface declares. It may have more: a ReadOnly interface property may be implemented by a read/write property, and the extra accessor is simply not part of the interface. In the example the interface’s Price is read-only while Book.Price is read/write.

  • Implements ties a property to the interface member by name, so the class property can use a different name (Remark implements IPriced.Note). A caller through the interface sees Note, through the class Remark.

  • MustOverride properties have no body (Public MustOverride ReadOnly Property Kind As String), and the class must be MustInherit.

Option Strict On

Public Interface IPriced
    ReadOnly Property Price As Decimal
    Property Note As String
End Interface

Public MustInherit Class CatalogItem
    Public Property Title As String = ""

    ' MustOverride property: derived classes must supply it.
    Public MustOverride ReadOnly Property Kind As String

    ' Overridable property with a protected setter.
    Public Overridable Property Rating As Integer
        Get
            Return _rating
        End Get
        Protected Set(value As Integer)
            _rating = Math.Clamp(value, 0, 5)
        End Set
    End Property
    Private _rating As Integer

    Public Sub Rate(value As Integer)
        Rating = value
    End Sub
End Class

Public Class Book
    Inherits CatalogItem
    Implements IPriced

    ' ReadOnly in the interface, read/write in the class: allowed (an extra accessor is fine).
    Public Property Price As Decimal Implements IPriced.Price

    ' Different name than the interface member.
    Private _note As String = ""
    Public Property Remark As String Implements IPriced.Note
        Get
            Return _note
        End Get
        Set(value As String)
            _note = value
        End Set
    End Property

    Public Overrides ReadOnly Property Kind As String
        Get
            Return "Book"
        End Get
    End Property

    Public Overrides Property Rating As Integer
        Get
            Return MyBase.Rating
        End Get
        Protected Set(value As Integer)
            MyBase.Rating = value
            Console.WriteLine("rated " & value & " -> " & MyBase.Rating)
        End Set
    End Property
End Class

Module Program
    Sub Main()
        Dim b As New Book() With {.Title = "Dune", .Price = 9.5D, .Remark = "signed"}
        b.Rate(9)
        Console.WriteLine(b.Kind & ": " & b.Title & " " & b.Rating)
        Dim priced As IPriced = b
        Console.WriteLine(priced.Price & " " & priced.Note)
        priced.Note = "reprint"
        Console.WriteLine(b.Remark)
    End Sub
End Module
rated 9 -> 5
Book: Dune 5
9.5 signed
reprint

Rate(9) calls the protected setter of the base through the public method; the override clamps through MyBase.Rating (Math.Clamp limits it to 0..5), so the printed rating is 5 even though 9 was requested. Because the override’s setter contains the Console.WriteLine, the output shows both numbers. Through the interface, priced.Note = "reprint" reached Remark, as b.Remark shows.

A property with the name of its type

Public Property Isbn As Isbn is legal and common: the property is named like its type (the "Color Color" situation). Within the class, Isbn can mean the property or the type, and the compiler picks by context: Isbn.Parse(...) is a call on the type, Isbn.Value is a member of the property’s value. Shared members of the type are therefore reachable even though the instance property has the same name.

Option Strict On

Public Structure Isbn
    Public Shared ReadOnly Empty As New Isbn("")
    Public ReadOnly Value As String
    Public Sub New(value As String)
        Me.Value = value
    End Sub
    Public Shared Function Parse(text As String) As Isbn
        Return New Isbn(text)
    End Function
End Structure

Public Class Book
    ' "Color Color": the property is named like its type.
    Public Property Isbn As Isbn

    Public Function Describe() As String
        Dim parsed = Isbn.Parse("111")          ' the type's Shared member, or the property?
        Return Isbn.Value & "/" & parsed.Value & "/" & Isbn.Empty.Value
    End Function

    Public Shared Property Total As Integer      ' Shared property: one value for the type
End Class

Module Program
    Sub Main()
        Dim b As New Book() With {.Isbn = New Isbn("999")}
        Console.WriteLine(b.Describe())
        Book.Total += 2
        Console.WriteLine(Book.Total)
    End Sub
End Module
999/111/
2

Book.Total is a Shared property, accessed through the type name. Accessing it through an instance (other.Total) compiles with warning BC42025 (Access of shared member, constant member, enum member or nested type through an instance), because the qualifying expression is not evaluated; with warnings as errors, the build fails, which is a good reason to keep them on.

Consuming C# init-only and required properties

C# 9 added init-only properties ({ get; init; }), which can be assigned only in an object initializer or a constructor, and C# 11 added required members, which an object creation must set. Visual Basic cannot declare either, but its compiler understands both when it consumes a library:

  • Visual Basic 16.9 enables consumption of init-only properties (the What’s new page lists this for 16.9). The property appears as an ordinary property with an assignable setter in an object initializer, and as read-only everywhere else. Assigning one in another context is BC37311.

  • required members declared in a C# library are enforced by the Visual Basic compiler of this SDK: New Reader() without setting a required property is BC37321, unless the constructor is marked [SetsRequiredMembers] in C# (the compiler then trusts the constructor). Do not rely on Visual Basic ignoring required: on this compiler it does not.

  • Visual Basic cannot declare them. There is no Init accessor (the compiler reports a syntax error), and applying RequiredMemberAttribute yourself is BC37325, because that attribute is reserved for the compiler.

  • C# indexers appear as the type’s default property (reader(3)), and C# records give Visual Basic ordinary read-only properties for their positional members.

The example is a two-project solution: a C# class library compiled by the same SDK, and the Visual Basic console that references it. The C# source is:

using System.Diagnostics.CodeAnalysis;

namespace CsLib;

public class Reader
{
    public string Name { get; init; } = "";
    public required string CardNumber { get; init; }
    public int Visits { get; set; }

    public Reader() { }

    [SetsRequiredMembers]
    public Reader(string cardNumber) { CardNumber = cardNumber; }

    // C# indexer: compiled as a property named Item with DefaultMember("Item").
    public string this[int index] => $"slot {index}";
}

public record Member(string Name, int Id);

and the Visual Basic program is:

Option Strict On

Imports CsLib

Module Program
    Sub Main()
        ' Init-only and required properties are set in an object initializer.
        Dim reader As New Reader() With {.Name = "Ada", .CardNumber = "C-1"}

        ' This constructor is marked [SetsRequiredMembers] in the C# source.
        Dim viaCtor As New Reader("C-2")
        Console.WriteLine(reader.Name & " " & reader.CardNumber & " " & viaCtor.CardNumber)

        reader.Visits += 1                    ' an ordinary setter is still fine
        Console.WriteLine(reader.Visits)

        ' A C# indexer is seen as the default property.
        Console.WriteLine(reader(3))

        Dim m As New Member("Grace", 7)
        Console.WriteLine(m.Name & " #" & m.Id)
    End Sub
End Module
Ada C-1 C-2
1
slot 3
Grace #7

Assigning an init-only or required property outside an initializer, or creating the type with a parameterless New and no initializer for a required property, produced these errors (separate program, not part of the example above):

BC37311: Init-only property 'Name' can only be assigned by an object member initializer, or on 'Me', 'MyClass` or 'MyBase' in an instance constructor.
BC37321: Required member 'Public Overloads Property CardNumber As String' must be set in the object initializer or attribute arguments.

Practical consequence: when a Visual Basic type must be consumable from C# with the same guarantee, declare the properties as ordinary read/write properties and a constructor that takes the mandatory values, or provide a factory. Init-only semantics are a C# feature that Visual Basic can honour but not produce.

INotifyPropertyChanged and data binding

User interface frameworks (Windows Forms, WPF, MAUI, Avalonia) bind controls to properties and update the screen when the object raises PropertyChanged from System.ComponentModel.INotifyPropertyChanged. In Visual Basic the interface is implemented with Implements and an Event; the <CallerMemberName> attribute (the .NET compiler fills in the name of the calling member for an optional String parameter) removes the magic strings.

Option Strict On

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

Public MustInherit Class ObservableObject
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

    ' Returns True when the value changed; ByRef lets one helper serve every backing field.
    Protected Function SetField(Of T)(ByRef field As T, value As T,
                                      <CallerMemberName> Optional propertyName As String = Nothing) As Boolean
        If EqualityComparer(Of T).Default.Equals(field, value) Then Return False
        field = value
        OnPropertyChanged(propertyName)
        Return True
    End Function
End Class

Public Class LoanViewModel
    Inherits ObservableObject

    Private _borrower As String = ""
    Private _daysLate As Integer

    Public Property Borrower As String
        Get
            Return _borrower
        End Get
        Set(value As String)
            SetField(_borrower, value)
        End Set
    End Property

    Public Property DaysLate As Integer
        Get
            Return _daysLate
        End Get
        Set(value As Integer)
            If SetField(_daysLate, value) Then
                OnPropertyChanged(NameOf(Fine))        ' dependent, computed property
            End If
        End Set
    End Property

    Public ReadOnly Property Fine As Decimal
        Get
            Return _daysLate * 0.25D
        End Get
    End Property
End Class

Module Program
    Sub Main()
        Dim vm As New LoanViewModel()
        AddHandler vm.PropertyChanged,
            Sub(sender, e)
                Console.WriteLine("changed: " & e.PropertyName)
            End Sub

        vm.Borrower = "Ada"
        vm.Borrower = "Ada"          ' same value: no notification
        vm.DaysLate = 4
        Console.WriteLine("Fine = " & vm.Fine.ToString("N2", Globalization.CultureInfo.InvariantCulture))
    End Sub
End Module
changed: Borrower
changed: DaysLate
changed: Fine
Fine = 1.00
  • SetField takes the backing field ByRef, so one generic helper serves every property. It returns True only when the value changed, which is how the second vm.Borrower = "Ada" produced no notification.

  • DaysLate also raised Fine, because Fine is computed from it. A binding on Fine needs that second notification; the compiler cannot know about the dependency.

  • NameOf(Fine) is checked at compile time and survives a rename; the Event is the standard .NET handler type and is raised with RaiseEvent. Events are the subject of Events and Handlers.

  • Raise the event on the thread that owns the control (the UI thread) in Windows Forms and WPF, or marshal to it; see Windows Forms Applications and WPF Applications for binding to these properties.

  • A WithEvents field is a special case that Visual Basic implements as a property with Get and Set accessors, so that Handles clauses are wired and unwired in Set (see the page on events).

Choosing between a property, a field and a method

Situation Use Reason

Plain data with no rules

auto-implemented property

binding, serialization, interfaces and future validation all work

Value derived from other state, cheap

ReadOnly Property with a Get

always up to date, no backing field

Expensive or side-effecting work

Function/Sub

a property read should not surprise the caller

Must be set only by the class

Private Set or ReadOnly set in constructor

the type controls its invariants

Needs an argument, or may be called often with different values

method, or an indexer if it is a lookup

Default property when it is "the element at"

A constant

Const or Shared ReadOnly field

no accessor cost, usable in attributes

Internal mutable state

Private field with _ prefix

not part of the contract

Properties are called through method calls, so each access has a cost the JIT usually removes by inlining. Do not hold a lock, hit the file system or the network, or throw for ordinary conditions inside a getter.

Visual Basic compared with C#

Feature Visual Basic C#

Auto-implemented

Property Name As String = "x"

public string Name { get; set; } = "x";

Read-only auto

ReadOnly Property Id As Integer

public int Id { get; }

Narrow setter

full form with Private Set (no auto form)

{ get; private set; }

Init-only

not declarable; consumed since VB 16.9

{ get; init; }

Required

not declarable; enforced when consuming C#

required modifier

Expression-bodied

not available; write the Get block

public int Total => a + b;

Indexer

Default Property Item(i As Integer) As T

public T this[int i]

Named indexer

any name with Default

[IndexerName("X")]

Parameterized property

Property Slot(r As Integer, c As Integer)

not available; use an indexer or a method

Bang operator

dict!key

not available

Accessor access

Private Set, Protected Get…​

private set, protected get

Property named like its type

allowed ("Color Color")

allowed

Backing field name

_Name

<Name>k__BackingField

Using the name in an accessor

Name in Get is the return variable; in Set it recurses

Name is a recursive call in both

Interface implementation

Implements I.Member, any name

implicit by name or explicit I.Member

Older material you may meet

Tutorials, books and code written for Visual Basic 6 and the first versions of Visual Basic .NET use idioms that no longer match the current language:

  • VB6 declared Property Get, Property Let and Property Set as three separate procedures, with Set for object-valued properties. In Visual Basic .NET there is one Property block with Get and Set, and object assignment is the plain = statement. The keyword Set for assignment (Set x = New Collection) and Let are gone.

  • VB6 allowed a default property without parameters (Text1 = "x"). It is BC31048 in .NET; write the property name.

  • Older .NET code writes every property in the full form with a field prefixed m_ or _. Since Visual Basic 10 the auto form is the idiom for plain data, and the generated backing field is _PropertyName.

  • Examples that return a value by assigning to the property name inside Get still compile but are rarely idiomatic; use Return.

  • Code that indexes an Object (obj(0), obj!Name) relies on late binding of the default property. It compiles only with Option Strict Off (BC30574 otherwise) and throws MissingMemberException at run time when the object has no default property.

  • Visual Basic Express-era tutorials describe the Properties window and designer-generated My.Settings as the way to keep data; settings are covered on The My Namespace.

  • Code that predates Visual Basic 14 has no ReadOnly auto-implemented property; it writes a ReadOnly full property over a Private field instead.