Attributes, Caller Information and Reflection
|
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-20. The examples are complete console programs that
start with Option Strict On; they were built with warnings treated as errors (except where the page says the opposite) and run in the .NET 10 SDK
container (Linux, arm64). The CLR and compiler messages include the project’s root namespace, which is ShelfApp for every single-file example
(the assembly is named App); the plugin example is a three-project solution. Where an example is meant to fail, the messages shown are the ones the
SDK printed.
Three features share this page because they share one mechanism: metadata. The compiler writes information about your code into the assembly, and a running program can read it
back. An attribute is metadata that you add; caller information is metadata the compiler fills in for you at each call; reflection is the API that reads it all, and that can
also act on what it finds, by creating objects and calling members whose names it learned at run time. Calling members through an Object variable under Option Strict Off is a related but different mechanism, late binding, and has its own page: Early and Late Binding.
The C# counterpart is on Attributes and Reflection (C#).
Attributes
An attribute is a class that derives from System.Attribute, applied to a declaration with angle brackets. It does nothing by itself: the compiler stores it in the assembly,
and some other code (the compiler, a tool, the runtime, your program) reads it and acts on it. Visual Basic puts the attribute list before the declaration, inside < and >:
<Attribute1(positionalArgument, NamedProperty:=value)>
<Attribute2>
Public Class Example
The syntax is regular:
-
The class name may omit the Attribute suffix:
<Obsolete>isObsoleteAttribute. -
Positional arguments come first and select a constructor. Named arguments follow and set a public field or property with
:=(C# uses=), in any order. -
Parentheses are optional when there are no arguments (
<Serializable>). -
Several attributes can share one list, separated by commas (
<Description("a"), Category("b")>), or be written as separate lists. A list may be broken over lines after a comma; a closing>may sit on the next line after a line-continuation underscore. -
The list is part of the declaration, not a separate statement, so there is no attribute syntax for local variables or statements.
The program below applies attributes to every kind of target Visual Basic allows and then reads each one back by reflection. It exercises the target list in the next table.
Option Strict On
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Reflection
<Assembly: AssemblyMetadata("Catalogue", "Shelf")>
<Module: Description("The catalogue module")>
<Description("A book in the catalogue"), DebuggerDisplay("{Title,nq} ({Year})")>
Public Class Book
<DefaultValue(1990)>
Public Property Year As Integer
Public Property Title As String = ""
<Obsolete("Use Title instead", False)>
Public Property Name As String = ""
<Description("Copies on the shelf")>
Public CopiesOnShelf As Integer
<Description("Raised when a copy is lent")>
Public Event Borrowed As EventHandler
<Description("Pages of one edition")>
Public Function Pages(<Description("Edition number")> edition As Integer) As <Description("The number of pages")> Integer
Return 100 * edition
End Function
End Class
<Description("One loan")>
Public Structure Loan
Public Days As Integer
End Structure
<Description("A shelf")>
Public Interface IShelf
End Interface
<Description("Called when a book changes")>
Public Delegate Sub BookChanged(title As String)
Public Enum Shelfmark
<Description("Fiction")> Fiction = 1
<Description("Reference")> Reference = 2
End Enum
Module Program
Function Member(found As MemberInfo) As MemberInfo
If found Is Nothing Then Throw New InvalidOperationException("Member not found")
Return found
End Function
Sub Show(label As String, attributes As IEnumerable(Of Attribute))
For Each attribute As Attribute In attributes
Console.WriteLine(label.PadRight(22) & attribute.GetType().Name)
Next
End Sub
Sub Main()
Dim bookType As Type = GetType(Book)
Show("class", bookType.GetCustomAttributes(False).Cast(Of Attribute)())
Show("property Year", Member(bookType.GetProperty("Year")).GetCustomAttributes(False).Cast(Of Attribute)())
Show("property Name", Member(bookType.GetProperty("Name")).GetCustomAttributes(False).Cast(Of Attribute)())
Show("event", Member(bookType.GetEvent("Borrowed")).GetCustomAttributes(False).Cast(Of Attribute)())
Show("structure", GetType(Loan).GetCustomAttributes(False).Cast(Of Attribute)())
Show("interface", GetType(IShelf).GetCustomAttributes(False).Cast(Of Attribute)())
Show("delegate", GetType(BookChanged).GetCustomAttributes(False).Cast(Of Attribute)())
Show("field", Member(bookType.GetField("CopiesOnShelf")).GetCustomAttributes(False).Cast(Of Attribute)())
Dim pages As MethodInfo = CType(Member(bookType.GetMethod("Pages")), MethodInfo)
Show("method", pages.GetCustomAttributes(False).Cast(Of Attribute)())
Show("return value", pages.ReturnTypeCustomAttributes.GetCustomAttributes(False).Cast(Of Attribute)())
Show("parameter", pages.GetParameters()(0).GetCustomAttributes(False).Cast(Of Attribute)())
Show("enum member", Member(GetType(Shelfmark).GetField("Fiction")).GetCustomAttributes(False).Cast(Of Attribute)())
Show("assembly", Assembly.GetExecutingAssembly().GetCustomAttributes(GetType(AssemblyMetadataAttribute)).Cast(Of Attribute)())
Show("module", bookType.Module.GetCustomAttributes(False).Cast(Of Attribute)())
End Sub
End Module
class DescriptionAttribute
class DebuggerDisplayAttribute
property Year DefaultValueAttribute
property Name ObsoleteAttribute
event DescriptionAttribute
structure DescriptionAttribute
interface DescriptionAttribute
delegate DescriptionAttribute
field DescriptionAttribute
method DescriptionAttribute
return value DescriptionAttribute
parameter DescriptionAttribute
enum member DescriptionAttribute
assembly AssemblyMetadataAttribute
module DescriptionAttribute
| Target | How to write it in Visual Basic | Seen in the program |
|---|---|---|
Class, structure, interface, delegate, enum |
the list before the declaration |
|
Method ( |
before the declaration |
|
Parameter |
inside the parameter list, before the parameter name |
|
Return value |
between |
|
Property |
before the property |
|
Field and event |
before the declaration |
|
Enum member |
before the member |
|
Assembly |
|
|
Module |
|
|
The things that Visual Basic does not offer follow from that table. An attribute written before an auto-implemented property always goes on the property; the C# [field: NonSerialized] target that
puts it on the compiler-generated backing field does not exist. The <Return: prefix is rejected as shown below. And attributes on lambdas (C# 10) and on local variables do not exist.
Assembly and module attributes, and the generated file
<Assembly: ...> and <Module: ...> apply to the whole assembly or module and are written outside any type, after the Imports statements and before the first declaration. Placing one after a declaration fails:
Option Strict On
Imports System.ComponentModel
Public Class Early
End Class
<Assembly: System.Reflection.AssemblyMetadata("k", "v")>
Module Program
Sub Main()
End Sub
End Module
Program.vb(8,1): error BC30637: Assembly or Module attribute statements must precede any declarations in a file.
Most assembly attributes need not be written by hand. The SDK generates a file, obj/<configuration>/<framework>/<AssemblyName>.AssemblyInfo.vb, from the project’s properties. I built a project that
sets Version, AssemblyVersion, Authors, Company, Product, Description, Copyright and SourceRevisionId in the project file:
<Version>2.1.0-beta.1</Version>
<AssemblyVersion>2.1.0.0</AssemblyVersion>
<Authors>Shelf team</Authors>
<Company>Shelf</Company>
<Product>Shelf catalogue</Product>
<Description>Library catalogue</Description>
<Copyright>Shelf</Copyright>
<SourceRevisionId>abc1234</SourceRevisionId>
and listed the assembly’s attributes at run time:
Option Strict On
Imports System.Reflection
Imports System.Runtime.CompilerServices
Module Program
Sub Main()
Dim assembly As Assembly = GetType(Program).Assembly
Console.WriteLine("AssemblyName.Version: " & assembly.GetName().Version.ToString())
For Each data As CustomAttributeData In assembly.CustomAttributes.OrderBy(Function(d) d.AttributeType.Name)
Dim arguments As String = String.Join(", ", data.ConstructorArguments.Select(Function(a) a.Value.ToString()))
Console.WriteLine($"{data.AttributeType.Name,-40}{arguments}")
Next
End Sub
End Module
AssemblyName.Version: 2.1.0.0
AssemblyCompanyAttribute Shelf
AssemblyConfigurationAttribute Debug
AssemblyCopyrightAttribute Shelf
AssemblyDescriptionAttribute Library catalogue
AssemblyFileVersionAttribute 2.1.0.0
AssemblyInformationalVersionAttribute 2.1.0-beta.1+abc1234
AssemblyProductAttribute Shelf catalogue
AssemblyTitleAttribute App
CompilationRelaxationsAttribute 8
DebuggableAttribute 263
RuntimeCompatibilityAttribute
TargetFrameworkAttribute .NETCoreApp,Version=v10.0
The mapping to remember: Version becomes the informational version (the semantic version, with the source revision appended after a +) and, unless overridden, the file version (without the pre-release suffix) and the assembly version;
AssemblyVersion sets the assembly’s identity; Company, Product, Description and Copyright become the attributes of those names, AssemblyTitle defaults to the assembly name, and Authors is
a NuGet package property that did not produce an attribute here. TargetFramework, Debuggable and CompilationRelaxations come from the compiler.
Because the SDK already writes AssemblyTitle and AssemblyVersion, writing the same attribute yourself is a duplicate:
Option Strict On
Imports System.Reflection
<Assembly: AssemblyTitle("Shelf catalogue")>
<Assembly: AssemblyVersion("1.2.3.0")>
Module Program
Sub Main()
Console.WriteLine("built")
End Sub
End Module
obj/Debug/net10.0/App.AssemblyInfo.vb(20,2): error BC30663: Attribute 'AssemblyTitleAttribute' cannot be applied multiple times.
obj/Debug/net10.0/App.AssemblyInfo.vb(21,2): error BC30663: Attribute 'AssemblyVersionAttribute' cannot be applied multiple times.
There are two fixes. Set the matching project property (the right one for almost every project), or turn the generator off with <GenerateAssemblyInfo>false</GenerateAssemblyInfo>, which makes your file the only source; the same
program with that property built and printed built. Some assembly attributes have an item form: <InternalsVisibleTo Include="Shelf.Tests" /> in the project file
generates <Assembly: InternalsVisibleTo("Shelf.Tests")> for you.
Custom attributes
A custom attribute is a class that derives from Attribute. Its constructor parameters become the positional arguments and its public read/write fields and properties become the named ones. AttributeUsage,
itself an attribute, controls where it may be applied:
AttributeUsage setting |
Meaning |
|---|---|
|
the |
|
the attribute may appear more than once on the same declaration; the default is |
|
derived classes, and overriding members of derived classes, see the attribute through the inheritance-aware reading APIs; the default is |
The program defines a CatalogRule attribute with a constructor argument, properties of five types (String, an enum, an Integer, a String() array and a Type), and
AllowMultiple and Inherited set. It applies it to CatalogItem and its members, reads the rules back, and shows what inheritance does:
Option Strict On
Imports System.Reflection
Public Enum Audience
Everyone
Staff
Admin
End Enum
<AttributeUsage(AttributeTargets.Class Or AttributeTargets.Method Or AttributeTargets.Property,
AllowMultiple:=True, Inherited:=True)>
Public NotInheritable Class CatalogRuleAttribute
Inherits Attribute
Public ReadOnly Property Name As String
Public Property Audience As Audience = Audience.Everyone
Public Property Priority As Integer
Public Property Tags As String()
Public Property Handler As Type
Public Sub New(name As String)
Me.Name = name
End Sub
End Class
<CatalogRule("NoDuplicates", Priority:=2, Audience:=Audience.Staff)>
<CatalogRule("MaxLoans", Tags:=New String() {"loans", "limits"}, Handler:=GetType(String))>
Public Class CatalogItem
<CatalogRule("TitleRequired")>
Public Overridable Property Title As String = ""
<CatalogRule("OnlyAdmins", Audience:=Audience.Admin)>
Public Overridable Sub Remove()
End Sub
End Class
Public Class Magazine
Inherits CatalogItem
Public Overrides Sub Remove()
End Sub
Public Overrides Property Title As String
End Class
Module Program
Sub Describe(label As String, rule As CatalogRuleAttribute)
Dim tags As String = If(rule.Tags Is Nothing, "-", String.Join("/", rule.Tags))
Dim handler As String = If(rule.Handler Is Nothing, "-", rule.Handler.Name)
Console.WriteLine($"{label,-26}{rule.Name,-14}audience={rule.Audience,-9}priority={rule.Priority} tags={tags} handler={handler}")
End Sub
Sub Main()
For Each rule As CatalogRuleAttribute In GetType(CatalogItem).GetCustomAttributes(GetType(CatalogRuleAttribute), False)
Describe("CatalogItem", rule)
Next
For Each rule As CatalogRuleAttribute In GetType(Magazine).GetCustomAttributes(GetType(CatalogRuleAttribute), True)
Describe("Magazine (inherit)", rule)
Next
Console.WriteLine("Magazine IsDefined (inherit False): " & GetType(Magazine).IsDefined(GetType(CatalogRuleAttribute), False))
Console.WriteLine("Magazine IsDefined (inherit True): " & GetType(Magazine).IsDefined(GetType(CatalogRuleAttribute), True))
Dim removeDerived As MethodInfo = GetType(Magazine).GetMethod("Remove")
Dim titleDerived As PropertyInfo = GetType(Magazine).GetProperty("Title")
Console.WriteLine("Overriding method, MemberInfo: " & removeDerived.GetCustomAttributes(GetType(CatalogRuleAttribute), True).Length)
Console.WriteLine("Overriding method, Attribute class: " & Attribute.GetCustomAttributes(removeDerived, GetType(CatalogRuleAttribute), True).Length)
Console.WriteLine("Overriding property, MemberInfo: " & titleDerived.GetCustomAttributes(GetType(CatalogRuleAttribute), True).Length)
Console.WriteLine("Overriding property, Attribute class: " & Attribute.GetCustomAttributes(titleDerived, GetType(CatalogRuleAttribute), True).Length)
Dim titleRule As CatalogRuleAttribute = GetType(CatalogItem).GetProperty("Title").GetCustomAttribute(Of CatalogRuleAttribute)()
Console.WriteLine("GetCustomAttribute(Of T): " & titleRule.Name)
Try
Dim ambiguous As CatalogRuleAttribute = GetType(CatalogItem).GetCustomAttribute(Of CatalogRuleAttribute)()
Console.WriteLine(ambiguous.Name)
Catch ex As AmbiguousMatchException
Console.WriteLine("Two rules on one class: " & ex.GetType().Name)
End Try
End Sub
End Module
CatalogItem NoDuplicates audience=Staff priority=2 tags=- handler=-
CatalogItem MaxLoans audience=Everyone priority=0 tags=loans/limits handler=String
Magazine (inherit) NoDuplicates audience=Staff priority=2 tags=- handler=-
Magazine (inherit) MaxLoans audience=Everyone priority=0 tags=loans/limits handler=String
Magazine IsDefined (inherit False): False
Magazine IsDefined (inherit True): True
Overriding method, MemberInfo: 1
Overriding method, Attribute class: 1
Overriding property, MemberInfo: 0
Overriding property, Attribute class: 1
GetCustomAttribute(Of T): TitleRequired
Two rules on one class: AmbiguousMatchException
What the output teaches:
-
Argument types are limited to what a constant can express: numbers, strings,
Boolean, enum values, aType(GetType(String)) and one-dimensional arrays of those (New String() {"loans", "limits"}). The program used aString, an enum, anInteger, aString()and aType. -
Reading a missing named argument gives the property’s default:
AudienceisEveryoneandPriorityis 0 for the second rule, which set neither. -
GetCustomAttribute(Of T)is for a single attribute. WithAllowMultiple:=Trueand two attributes present it throwsAmbiguousMatchException; useGetCustomAttributesfor lists. -
The
inheritflag is honoured for classes and methods but not for properties and events when you read throughMemberInfo. The overridingMagazine.Titlereported 0 rules throughMemberInfo.GetCustomAttributes(type, True)and 1 throughAttribute.GetCustomAttributes(member, type, True). If you must support inherited property attributes, use theAttributeclass methods (or theCustomAttributeExtensionsones, which call them). -
IsDefineddoes not create an attribute instance. It is the cheap way to test for presence, because reading an attribute runs its constructor.
What arguments are allowed
An attribute argument must be a constant the compiler can evaluate: a literal, a Const, a concatenation of constants, NameOf(...), an enum value, GetType(...), or an array of those. A value only known at run time, including
a Shared ReadOnly field and a method call, is rejected. The program below uses one example of each:
Option Strict On
Imports System.ComponentModel
Public Class Computed
Private Const Fixed As String = "fixed"
Private Shared ReadOnly Dynamic As String = "dynamic"
<Description(Fixed)>
Public Property A As String
<Description(Dynamic)>
Public Property B As String
<Description(String.Concat("a", "b"))>
Public Property C As String
<Description("a" & "b")>
Public Property D As String
<Description(NameOf(A))>
Public Property E As String
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(12,18): error BC30059: Constant expression is required.
Program.vb(15,18): error BC30059: Constant expression is required.
Fixed (a Const), the concatenation "a" & "b" and NameOf(A) compile; the Shared ReadOnly field and String.Concat do not. The shared helpers for the following failing programs live in one file, Rules.vb. It declares
RuleAttribute (valid on classes only, one required constructor argument, a read-only Name and a writable Priority), AnyRuleAttribute and a plain class Plain that is not an attribute:
Option Strict On
<AttributeUsage(AttributeTargets.Class)>
Public NotInheritable Class RuleAttribute
Inherits Attribute
Public ReadOnly Property Name As String
Public Property Priority As Integer
Public Sub New(name As String)
Me.Name = name
End Sub
End Class
<AttributeUsage(AttributeTargets.Class Or AttributeTargets.Method)>
Public NotInheritable Class AnyRuleAttribute
Inherits Attribute
End Class
Public Class Plain
End Class
Attribute errors
Each failing program uses Rules.vb and was built on its own; the compiler messages are those the SDK printed. Every program also has Imports System.ComponentModel for Description, and Rules.vb is not repeated in the listings.
Applying a single-use attribute twice:
Option Strict On
Imports System.ComponentModel
<Rule("a"), Rule("b")>
Public Class Twice
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(5,13): error BC30663: Attribute 'RuleAttribute' cannot be applied multiple times.
Applying an attribute to a declaration its AttributeUsage does not allow (RuleAttribute is valid on classes only):
Option Strict On
Imports System.ComponentModel
Public Class WrongTarget
<Rule("a")>
Public Sub Run()
End Sub
<Rule("b")>
Public Property Title As String
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(6,6): error BC30662: Attribute 'RuleAttribute' cannot be applied to 'Run' because the attribute is not valid on this declaration type.
Program.vb(10,6): error BC30662: Attribute 'RuleAttribute' cannot be applied to 'Title' because the attribute is not valid on this declaration type.
Wrong constructor arguments, unknown or read-only named arguments, wrong types:
Option Strict On
Imports System.ComponentModel
<Rule>
Public Class NoArgument
End Class
<Rule("a", "b")>
Public Class TooMany
End Class
<Rule("a", Missing:=1)>
Public Class UnknownNamed
End Class
<Rule("a", Name:="again")>
Public Class ReadOnlyNamed
End Class
<Rule(42)>
Public Class WrongType
End Class
<Rule("a", Priority:="high")>
Public Class WrongNamedType
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(5,2): error BC30455: Argument not specified for parameter 'name' of 'Public Sub New(name As String)'.
Program.vb(9,12): error BC30057: Too many arguments to 'Public Sub New(name As String)'.
Program.vb(13,12): error BC30661: Field or property 'Missing' is not found.
Program.vb(17,12): error BC31501: 'ReadOnly' attribute property 'Name' cannot be the target of an assignment.
Program.vb(21,7): error BC30512: Option Strict On disallows implicit conversions from 'Integer' to 'String'.
Program.vb(25,22): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Integer'.
Program.vb(25,22): error BC30934: Conversion from 'String' to 'Integer' cannot occur in a constant expression used as an argument to an attribute.
A class that is not an attribute:
Option Strict On
<Plain>
Public Class AlsoNot
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(3,2): error BC31504: 'Plain' cannot be used as an attribute because it does not inherit from 'System.Attribute'.
A generic attribute:
Option Strict On
Imports System.ComponentModel
Public NotInheritable Class KindAttribute(Of T)
Inherits Attribute
End Class
<Kind(Of Integer)>
Public Class UsesGeneric
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(9,2): error BC32066: Type arguments are not valid because attributes cannot be generic.
An attribute on a local variable:
Option Strict On
Imports System.ComponentModel
Public Class Misplaced
<Description("local")>
Public Sub Run()
<Description("x")> Dim local As Integer = 1
Console.WriteLine(local)
End Sub
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(8,9): error BC30660: Attributes cannot be applied to local variables.
The last two programs show Visual Basic-specific limits: attributes cannot be generic, even though C# 11 allows them, and there are no attributes on locals. The two target forms that the other languages offer also fail, each with a syntax error rather than a helpful message:
Option Strict On
Imports System.ComponentModel
Public Class FieldTarget
<field: NonSerialized>
Public Property A As Integer
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(6,11): error BC30636: '>' expected.
Program.vb(6,13): error BC30188: Declaration expected.
Option Strict On
Imports System.ComponentModel
Public Class ReturnTarget
<Return: Description("r")>
Public Function F() As Integer
Return 1
End Function
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(6,12): error BC30636: '>' expected.
Program.vb(6,14): error BC30188: Declaration expected.
Program.vb(6,6): error BC30183: Keyword is not valid as an identifier.
The messages mislead: they do not say that the field: target does not exist or that Return is a keyword. The correct spelling of the return-value attribute is shown in the first program of this page.
Writing attribute lists
All four of these forms (stacked lists, one list over two lines, a trailing underscore, and the list on the same line as the declaration) compile and produce the same four properties:
Option Strict On
Imports System.ComponentModel
Public Class Two
<Description("a")> <Category("b")>
Public Property A As Integer
<Description("a"),
Category("b")>
Public Property B As Integer
<Description("c"), Category("d")> Public Property C As Integer
<Description("e") _
> Public Property D As Integer
End Class
Module Program
Sub Main()
Console.WriteLine(GetType(Two).GetProperties().Length)
End Sub
End Module
4
The common attributes
| Attribute | Namespace | What it does |
|---|---|---|
|
|
marks a member as deprecated; |
|
|
removes calls to the method unless a constant is defined at the call site; see Conditional Compilation and Directives |
|
|
lets an enumeration be used as a bit set; see Enumerations |
|
|
control how the debugger shows a type and whether it steps into code; |
|
|
JIT hints such as |
|
|
marks an extension method; in Visual Basic it is written on a |
|
|
lets a named assembly (typically the tests) see |
|
|
gives each thread its own copy of a |
|
|
native calls and marshalling; see Native Interop: Declare and P/Invoke |
|
|
design-time and serializer metadata; no run-time effect unless something reads them |
|
|
platform and trimming annotations, below |
|
|
written by the compiler for a |
|
|
you write it yourself on a module whose name should not appear in IntelliSense, as the |
Source: Common attributes (Visual Basic)
Obsolete is the one every project meets. With the message only, use is a warning; with True as the second argument it is an error:
Option Strict On
Imports System.ComponentModel
Module Program
<Obsolete("Use NewName", True)>
Sub OldName()
End Sub
<Obsolete("Use NewName")>
Sub OlderName()
End Sub
Sub Main()
OldName()
OlderName()
End Sub
End Module
Program.vb(15,9): error BC30668: 'Public Sub OldName()' is obsolete: 'Use NewName'.
Program.vb(16,9): warning BC40000: 'Public Sub OlderName()' is obsolete: 'Use NewName'.
Source: ObsoleteAttribute
The program contains the error and the warning, so with TreatWarningsAsErrors on, BC40000 is an error too; the build stopped on BC30668 either way.
Caller information
Caller information lets a method learn where it was called from without the caller passing anything. A method declares optional parameters with default values and decorates them with attributes from
System.Runtime.CompilerServices. When a caller omits the argument, the compiler writes the value into the call at compile time. Visual Basic 11 (Visual Studio 2012) introduced the first three; CallerArgumentExpression arrived with
Visual Basic 17.0 (Visual Studio 2022 17.0, .NET 6).
| Attribute | Parameter type | Value supplied |
|---|---|---|
|
|
the name of the calling member (see the table of special names below) |
|
|
the full path of the caller’s source file at compile time |
|
|
the line number of the call in that file |
|
|
the source text of the argument passed to the other parameter named in the attribute |
The first program defines a logging helper that takes all three of the older attributes and calls it from every kind of member, so you can see exactly what the compiler writes:
Option Strict On
Imports System.ComponentModel
Imports System.IO
Imports System.Runtime.CompilerServices
Module Log
Sub Write(message As String,
<CallerMemberName> Optional member As String = "",
<CallerFilePath> Optional file As String = "",
<CallerLineNumber> Optional line As Integer = 0)
Console.WriteLine($"{message,-22} member={member} file={Path.GetFileName(file)} line={line}")
End Sub
End Module
Public Class Book
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private _title As String = ""
Shared Sub New()
Log.Write("shared constructor")
End Sub
Public Sub New()
Log.Write("constructor")
End Sub
Public Property Title As String
Get
Log.Write("property getter")
Return _title
End Get
Set(value As String)
_title = value
Log.Write("property setter")
OnChanged()
End Set
End Property
Private Sub OnChanged(<CallerMemberName> Optional name As String = "")
Console.WriteLine("PropertyChanged for " & name)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
End Sub
Public Shared Operator +(left As Book, right As Book) As Book
Log.Write("operator")
Return left
End Operator
Public Shared Widening Operator CType(value As Book) As String
Log.Write("conversion operator")
Return value._title
End Operator
Default Public ReadOnly Property Item(index As Integer) As Char
Get
Log.Write("default property")
Return _title(index)
End Get
End Property
End Class
Module Program
Sub Plain()
Log.Write("sub")
End Sub
Async Function LoadAsync() As Task
Log.Write("before the await")
Await Task.Delay(1)
Log.Write("after the await")
End Function
Iterator Function Numbers() As IEnumerable(Of Integer)
Log.Write("inside an iterator")
Yield 1
End Function
Sub Main()
Log.Write("Main")
Plain()
Dim lambda As Action = Sub() Log.Write("lambda")
lambda()
Dim book As New Book()
book.Title = "Dune"
Dim ignored As String = book.Title
Dim sum As Book = book + book
Dim text As String = book
Dim first As Char = book(0)
LoadAsync().GetAwaiter().GetResult()
For Each number As Integer In Numbers()
Next
Log.Write("explicit values", "custom member", "custom.vb", 42)
Log.Write("named argument", line:=7)
End Sub
End Module
Main member=Main file=Program.vb line=84
sub member=Plain file=Program.vb line=69
lambda member=Main file=Program.vb line=86
shared constructor member=.cctor file=Program.vb line=24
constructor member=.ctor file=Program.vb line=28
property setter member=Title file=Program.vb line=38
PropertyChanged for Title
property getter member=Title file=Program.vb line=33
operator member=op_Addition file=Program.vb line=49
conversion operator member=op_Implicit file=Program.vb line=54
default property member=Item file=Program.vb line=60
before the await member=LoadAsync file=Program.vb line=73
after the await member=LoadAsync file=Program.vb line=75
inside an iterator member=Numbers file=Program.vb line=79
explicit values member=custom member file=custom.vb line=42
named argument member=Main file=Program.vb line=7
Read the output for the special cases:
| Caller | CallerMemberName |
|---|---|
A |
the name; a getter and a setter both give the property name ( |
A constructor and a shared constructor |
|
An operator |
the metadata name: |
A default property |
|
A lambda |
the enclosing member ( |
An |
the method’s own name, before and after an |
An explicit value, positional or named |
the value you wrote; |
The file value is the full path on the machine that compiled the code. That is useful in logs on a developer machine and a leak of build-machine layout in a shipped binary. The program in the table printed
only Path.GetFileName(file); here is the raw value, and the same program built with <PathMap>$(MSBuildProjectDirectory)=/src</PathMap> in the project file:
Option Strict On
Imports System.Runtime.CompilerServices
Module Program
Function Locate(<CallerFilePath> Optional file As String = "") As String
Return file
End Function
Sub Main()
Console.WriteLine(Locate())
End Sub
End Module
Default build:
/w/a12/Program.vb
With the PathMap property:
/src/Program.vb
Source: CallerFilePathAttribute
PathMap rewrites the path prefix for every path the compiler embeds, including the CallerFilePath text and the paths in debug symbols; the exact value is whatever you choose, so builds on different machines
can produce the same binary.
The attribute is advisory
Visual Basic does not check that a caller-information attribute is on a suitable parameter, and when it is not, the attribute is silently ignored:
Option Strict On
Imports System.Runtime.CompilerServices
Module Program
Sub WrongType(<CallerLineNumber> Optional line As String = "unset")
Console.WriteLine("String parameter received: " & line)
End Sub
Sub WrongName(<CallerMemberName> Optional name As Integer = -1)
Console.WriteLine("Integer parameter received: " & name)
End Sub
Sub Mandatory(<CallerMemberName> name As String)
Console.WriteLine("Mandatory parameter received: " & name)
End Sub
Sub Main()
WrongType()
WrongName()
Mandatory("typed")
End Sub
End Module
String parameter received: unset
Integer parameter received: -1
Mandatory parameter received: typed
CallerLineNumber on a String parameter and CallerMemberName on an Integer parameter were ignored (the defaults arrived), and Mandatory, whose parameter has no Optional, behaved like an ordinary parameter.
The attribute only works on an optional parameter of the right type. If you leave out the optional argument, a later positional argument cannot be skipped: write Trace(, 7) (an empty position) or use a named argument, as in the next listing.
Trace() ' member filled in, extra = 0
Trace(extra:=5) ' member filled in, extra = 5
Trace("by hand") ' the member name is the text you passed
Trace(, 7) ' empty position: member filled in, extra = 7
These four calls were compiled and run against Sub Trace(<CallerMemberName> Optional member As String = "", Optional extra As Integer = 0):
member=Main extra=0
member=Main extra=5
member=typed by hand extra=0
member=Main extra=7
Source: CallerMemberNameAttribute
CallerArgumentExpression
CallerArgumentExpression("name") gives a helper the source text of the argument that the caller wrote for another parameter. It exists for guard clauses and test assertions that want to say "this condition failed" with the
condition itself. The program defines Require and Describe, and also uses the framework’s own users of the attribute, ArgumentNullException.ThrowIfNull and ArgumentException.ThrowIfNullOrEmpty:
Option Strict On
Imports System.Runtime.CompilerServices
Public Class RuleViolationException
Inherits Exception
Public ReadOnly Property Expression As String
Public Sub New(message As String, expression As String)
MyBase.New(message)
Me.Expression = expression
End Sub
End Class
Module Guard
Sub Require(condition As Boolean,
<CallerArgumentExpression("condition")> Optional expression As String = Nothing)
If Not condition Then
Throw New RuleViolationException("Rule failed: " & expression, expression)
End If
End Sub
Function Describe(value As Object,
<CallerArgumentExpression("value")> Optional expression As String = Nothing) As String
Return $"{expression} = {value}"
End Function
End Module
Module Program
Sub Main()
Dim copies As Integer = 3
Dim title As String = "Dune"
Console.WriteLine(Describe(copies))
Console.WriteLine(Describe(copies * 2 + 1))
Console.WriteLine(Describe(title.ToUpperInvariant()))
Console.WriteLine(Describe( copies + 1 ))
Console.WriteLine(Describe(
copies))
Try
Require(copies > 5)
Catch ex As RuleViolationException
Console.WriteLine(ex.Message)
End Try
Try
Require(title.Length = 0 OrElse copies < 0)
Catch ex As RuleViolationException
Console.WriteLine(ex.Message)
End Try
Try
Dim missing As String = Nothing
ArgumentNullException.ThrowIfNull(missing)
Catch ex As ArgumentNullException
Console.WriteLine(ex.Message.Replace(Environment.NewLine, " "))
End Try
Try
ArgumentNullException.ThrowIfNull(Nothing, "explicit name")
Catch ex As ArgumentNullException
Console.WriteLine(ex.Message.Replace(Environment.NewLine, " "))
End Try
Try
ArgumentException.ThrowIfNullOrEmpty(Nothing)
Catch ex As ArgumentException
Console.WriteLine(ex.GetType().Name & ": " & ex.Message.Replace(Environment.NewLine, " "))
End Try
End Sub
End Module
copies = 3
copies * 2 + 1 = 7
title.ToUpperInvariant() = DUNE
copies + 1 = 4
copies = 3
Rule failed: copies > 5
Rule failed: title.Length = 0 OrElse copies < 0
Value cannot be null. (Parameter 'missing')
Value cannot be null. (Parameter 'explicit name')
ArgumentNullException: Value cannot be null. (Parameter 'Nothing')
Notes on the output:
-
The text is the argument as written, trimmed at both ends but not normalised inside:
copies + 1kept its spaces. An argument spread over two lines gave justcopies, the text between the parentheses with the leading newline removed. -
The BCL guard helpers work from Visual Basic, because the attribute is honoured at the call site:
ThrowIfNull(missing)saidParameter 'missing', and an argument that is the literalNothingproduced the parameter nameNothing. -
A name that is not a parameter is a warning, not an error, and the attribute then has no effect:
Program.vb(12,45): warning BC42505: The CallerArgumentExpressionAttribute applied to parameter 'text' will have no effect. It is applied with an invalid parameter name.
-
The expression is source text copied at compile time; it is not evaluated again. Your identifiers end up in the compiled binary, so do not use it for messages meant for users.
NameOf
NameOf (Visual Basic 14, Visual Studio 2015) turns a name in code into a string at compile time. The string is a constant; it is what you use for parameter names in exceptions, for
PropertyChanged notifications, for log messages, and for attribute arguments. When the member is renamed, the string follows it.
Option Strict On
Imports System.Collections.Generic
Imports System.IO
Namespace Shelf
Public Class Book
Public Property Title As String = ""
Public Shared Property Count As Integer
Public Sub Rename(newTitle As String)
Title = newTitle
End Sub
Public Sub Rename(newTitle As String, force As Boolean)
Title = newTitle
End Sub
End Class
End Namespace
Public Class Holder
Public Property Title As String = ""
Public Property Name As String = ""
Public Function Names() As String()
Return {NameOf(Title), NameOf(Me.Name), NameOf(Holder.Title), NameOf(Holder)}
End Function
End Class
Module Program
Private Const TitleName As String = NameOf(Shelf.Book.Title)
Sub Show(code As String, value As String)
Console.WriteLine(code.PadRight(36) & value)
End Sub
Sub Main()
Dim copies As Integer = 3
Dim parts As New List(Of String)()
Show("NameOf(copies)", NameOf(copies))
Show("NameOf(parts)", NameOf(parts))
Show("NameOf(Shelf.Book)", NameOf(Shelf.Book))
Show("NameOf(Shelf.Book.Title)", NameOf(Shelf.Book.Title))
Show("NameOf(Shelf.Book.Count)", NameOf(Shelf.Book.Count))
Show("NameOf(Shelf.Book.Rename)", NameOf(Shelf.Book.Rename))
Show("NameOf(Shelf)", NameOf(Shelf))
Show("NameOf(System.IO)", NameOf(System.IO))
Show("NameOf(System.Console.WriteLine)", NameOf(System.Console.WriteLine))
Show("NameOf(List(Of String))", NameOf(List(Of String)))
Show("NameOf(List(Of String).Count)", NameOf(List(Of String).Count))
Show("NameOf(Program)", NameOf(Program))
Show("NameOf(Main)", NameOf(Main))
Show("Const TitleName", TitleName)
Show("NameOf(copies) in a Select Case", Describe(NameOf(copies)))
Show("Holder.Names()", String.Join(",", New Holder().Names()))
Show("NameOf(parts).Length", NameOf(parts).Length.ToString())
End Sub
Function Describe(name As String) As String
Select Case name
Case NameOf(Describe)
Return "the function itself"
Case "copies"
Return "copies matched as a constant"
Case Else
Return "other"
End Select
End Function
End Module
NameOf(copies) copies
NameOf(parts) parts
NameOf(Shelf.Book) Book
NameOf(Shelf.Book.Title) Title
NameOf(Shelf.Book.Count) Count
NameOf(Shelf.Book.Rename) Rename
NameOf(Shelf) Shelf
NameOf(System.IO) IO
NameOf(System.Console.WriteLine) WriteLine
NameOf(List(Of String)) List
NameOf(List(Of String).Count) Count
NameOf(Program) Program
NameOf(Main) Main
Const TitleName Title
NameOf(copies) in a Select Case copies matched as a constant
Holder.Names() Title,Name,Title,Holder
NameOf(parts).Length 5
Source: NameOf operator (Visual Basic)
The rule: NameOf returns the last identifier of the expression, with no qualification, no generic arguments and no parentheses. NameOf(Shelf.Book.Title) is Title; NameOf(List(Of String)) is List; NameOf(Shelf) for a
namespace is Shelf. It works on locals, parameters, members (including Me.Name), types, namespaces and methods (all overloads of Rename map to Rename), and the result is a constant: it can initialise a Const
(TitleName above), appear in a Case label or in an attribute argument. The things it rejects:
Option Strict On
Module Program
Sub Main()
Dim open As String = NameOf(Dictionary(Of ,))
Dim keyword As String = NameOf(Integer)
Dim literal As String = NameOf(1 + 1)
Dim missing As String = NameOf(DoesNotExist)
Dim call1 As String = NameOf(Console.WriteLine("x"))
Console.WriteLine(open & keyword & literal & missing & call1)
End Sub
End Module
Program.vb(5,51): error BC30182: Type expected.
Program.vb(5,52): error BC30182: Type expected.
Program.vb(6,40): error BC37244: This expression does not have a name.
Program.vb(7,40): error BC37244: This expression does not have a name.
Program.vb(9,38): error BC37244: This expression does not have a name.
Option Strict On
Module Program
Sub Main()
Dim missing As String = NameOf(DoesNotExist)
Console.WriteLine(missing)
End Sub
End Module
Program.vb(5,40): error BC30451: 'DoesNotExist' is not declared. It may be inaccessible due to its protection level.
Source: NameOf operator (Visual Basic)
An open generic type (Dictionary(Of ,)), a keyword type (Integer), a general expression (1 + 1, a call) are all errors, and so is a name that does not exist: NameOf is checked by the compiler, which is the reason to prefer it
to a string literal. The ArgumentException in the reflection program below uses it for the parameter name in exactly this way.
Reflection
Reflection is the API of System.Reflection and System.Type that describes types, members and assemblies and lets you act on that description. The entry point is the Type object.
Getting a Type
The program below gets a Type three ways (the GetType operator, the GetType() method, and TypeOf ... Is as the type test) and lets the Type answer questions about itself. In Visual Basic GetType(X) is the operator (C# typeof) and obj.GetType() is the method
(the same name, different things; the operator needs a type name, the method needs an object):
Option Strict On
Imports System.Reflection
Public Enum LoanState
Active = 1
Returned = 2
End Enum
Public Interface IRenewable
Sub Renew()
End Interface
Public MustInherit Class CatalogItem
Public MustOverride Function Describe() As String
End Class
Public NotInheritable Class Magazine
Inherits CatalogItem
Implements IRenewable
Public Overrides Function Describe() As String
Return "magazine"
End Function
Public Sub Renew() Implements IRenewable.Renew
End Sub
End Class
Public Structure Isbn
Public Value As String
End Structure
Module Program
Sub Row(label As String, type As Type)
Console.WriteLine($"{label,-26}{type.Name,-14}class={type.IsClass,-6} value={type.IsValueType,-6} iface={type.IsInterface,-6} enum={type.IsEnum,-6} abstract={type.IsAbstract,-6} sealed={type.IsSealed,-6} generic={type.IsGenericType}")
End Sub
Sub Main()
Dim item As CatalogItem = New Magazine()
Dim ids As Object = 42
Console.WriteLine("-- three ways to get a Type")
Console.WriteLine("GetType(Magazine): " & GetType(Magazine).Name)
Console.WriteLine("item.GetType(): " & item.GetType().Name & " (the run-time type)")
Console.WriteLine("GetType(CatalogItem): " & GetType(CatalogItem).Name & " (the declared type)")
Console.WriteLine("ids.GetType(): " & ids.GetType().FullName)
Console.WriteLine("TypeOf item Is Magazine: " & (TypeOf item Is Magazine))
Console.WriteLine("TypeOf item Is IRenewable: " & (TypeOf item Is IRenewable))
Console.WriteLine("GetType(List(Of String)): " & GetType(List(Of String)).ToString())
Console.WriteLine("GetType(List(Of )): " & GetType(List(Of )).ToString())
Console.WriteLine("GetType(Dictionary(Of ,)): " & GetType(Dictionary(Of ,)).ToString())
Console.WriteLine("GetType(Integer()): " & GetType(Integer()).ToString())
Console.WriteLine("GetType(Integer): " & GetType(Integer).FullName)
Console.WriteLine("-- what a Type can tell you")
Row("Magazine", GetType(Magazine))
Row("CatalogItem", GetType(CatalogItem))
Row("IRenewable", GetType(IRenewable))
Row("LoanState", GetType(LoanState))
Row("Isbn", GetType(Isbn))
Row("Integer", GetType(Integer))
Row("List(Of String)", GetType(List(Of String)))
Row("String", GetType(String))
Console.WriteLine("-- relations")
Console.WriteLine("Magazine.BaseType: " & GetType(Magazine).BaseType.Name)
Console.WriteLine("Isbn.BaseType: " & GetType(Isbn).BaseType.Name)
Console.WriteLine("LoanState.BaseType: " & GetType(LoanState).BaseType.Name)
Console.WriteLine("Magazine interfaces: " & String.Join(", ", GetType(Magazine).GetInterfaces().Select(Function(i) i.Name)))
Console.WriteLine("IRenewable.IsAssignableFrom(Magazine): " & GetType(IRenewable).IsAssignableFrom(GetType(Magazine)))
Console.WriteLine("CatalogItem.IsAssignableFrom(Magazine): " & GetType(CatalogItem).IsAssignableFrom(GetType(Magazine)))
Console.WriteLine("Magazine.IsSubclassOf(CatalogItem): " & GetType(Magazine).IsSubclassOf(GetType(CatalogItem)))
Console.WriteLine("LoanState names: " & String.Join(", ", [Enum].GetNames(GetType(LoanState))))
Console.WriteLine("List(Of String) type arguments: " & GetType(List(Of String)).GetGenericArguments()(0).Name)
Console.WriteLine("-- the assembly")
Dim assembly As Assembly = GetType(Program).Assembly
Console.WriteLine("Name: " & assembly.GetName().Name)
Console.WriteLine("Entry point: " & assembly.EntryPoint.DeclaringType.Name & "." & assembly.EntryPoint.Name)
Console.WriteLine("Same as GetExecutingAssembly: " & (assembly Is Assembly.GetExecutingAssembly()))
Console.WriteLine("Same as GetEntryAssembly: " & (assembly Is Assembly.GetEntryAssembly()))
Console.WriteLine("Exported types: " & String.Join(", ", assembly.GetExportedTypes().Select(Function(t) t.Name).OrderBy(Function(n) n)))
Console.WriteLine("All types: " & String.Join(", ", assembly.GetTypes().Select(Function(t) t.Name).OrderBy(Function(n) n)))
Console.WriteLine("References: " & String.Join(", ", assembly.GetReferencedAssemblies().Select(Function(r) r.Name).OrderBy(Function(n) n)))
End Sub
End Module
-- three ways to get a Type
GetType(Magazine): Magazine
item.GetType(): Magazine (the run-time type)
GetType(CatalogItem): CatalogItem (the declared type)
ids.GetType(): System.Int32
TypeOf item Is Magazine: True
TypeOf item Is IRenewable: True
GetType(List(Of String)): System.Collections.Generic.List`1[System.String]
GetType(List(Of )): System.Collections.Generic.List`1[T]
GetType(Dictionary(Of ,)): System.Collections.Generic.Dictionary`2[TKey,TValue]
GetType(Integer()): System.Int32[]
GetType(Integer): System.Int32
-- what a Type can tell you
Magazine Magazine class=True value=False iface=False enum=False abstract=False sealed=True generic=False
CatalogItem CatalogItem class=True value=False iface=False enum=False abstract=True sealed=False generic=False
IRenewable IRenewable class=False value=False iface=True enum=False abstract=True sealed=False generic=False
LoanState LoanState class=False value=True iface=False enum=True abstract=False sealed=True generic=False
Isbn Isbn class=False value=True iface=False enum=False abstract=False sealed=True generic=False
Integer Int32 class=False value=True iface=False enum=False abstract=False sealed=True generic=False
List(Of String) List`1 class=True value=False iface=False enum=False abstract=False sealed=False generic=True
String String class=True value=False iface=False enum=False abstract=False sealed=True generic=False
-- relations
Magazine.BaseType: CatalogItem
Isbn.BaseType: ValueType
LoanState.BaseType: Enum
Magazine interfaces: IRenewable
IRenewable.IsAssignableFrom(Magazine): True
CatalogItem.IsAssignableFrom(Magazine): True
Magazine.IsSubclassOf(CatalogItem): True
LoanState names: Active, Returned
List(Of String) type arguments: String
-- the assembly
Name: App
Entry point: Program.Main
Same as GetExecutingAssembly: True
Same as GetEntryAssembly: True
Exported types: CatalogItem, IRenewable, Isbn, LoanState, Magazine
All types: _Closure$__, CatalogItem, IRenewable, Isbn, LoanState, Magazine, Program
References: Microsoft.VisualBasic.Core, System.Collections, System.Console, System.Linq, System.Runtime
Source: Type class
Points to take from it: GetType(Magazine) and GetType(CatalogItem) are declared types that cost nothing at run time, while item.GetType() is the run-time type and depends on the object. A Module is compiled to a sealed class (see
the next listing). An interface is abstract and not a class. An open generic type is written with empty type-argument positions, GetType(Dictionary(Of ,)). And Program (a Friend module by default) is not in GetExportedTypes, but
the compiler’s hidden closure class _Closure$__ is in GetTypes — a reminder that compiler-generated types are real types.
What the compiler generates
Visual Basic syntax produces more members than you wrote. This is the full member list of a class with an auto-implemented property, a WithEvents field, two kinds of event, a default property, a static local, a ParamArray and a lambda that
captures a local. The first column is the kind of member:
Option Strict On
Imports System.Reflection
Public Class Cover
Public Event Opened As EventHandler
End Class
Public Class Book
Private WithEvents Front As New Cover()
Public Property Title As String = ""
Public ReadOnly Property Pages As Integer
Public Event Borrowed As EventHandler
Public Event Returned(sender As Object, e As EventArgs)
Default Public ReadOnly Property Item(index As Integer) As Char
Get
Return Title(index)
End Get
End Property
Public Function Count(Optional step1 As Integer = 1) As Integer
Static calls As Integer
calls += step1
Return calls
End Function
Public Function Match(ParamArray tags As String()) As Integer
Dim wanted As Integer = tags.Length
Dim filter As Func(Of String, Boolean) = Function(tag) tag.Length = wanted
Return tags.Count(filter)
End Function
Private Sub Front_Opened(sender As Object, e As EventArgs) Handles Front.Opened
End Sub
End Class
Module Helpers
Public Sub Hello()
End Sub
End Module
Module Program
Sub List(type As Type)
Console.WriteLine("== " & type.Name)
Dim flags As BindingFlags = BindingFlags.Instance Or BindingFlags.Static Or BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.DeclaredOnly
For Each member As MemberInfo In type.GetMembers(flags).OrderBy(Function(m) m.MemberType.ToString()).ThenBy(Function(m) m.Name)
Console.WriteLine($" {member.MemberType,-12}{member.Name}")
Next
End Sub
Sub Main()
List(GetType(Book))
Console.WriteLine("== nested types of Book")
For Each nested As Type In GetType(Book).GetNestedTypes(BindingFlags.NonPublic Or BindingFlags.Public)
Console.WriteLine(" " & nested.Name)
Next
Console.WriteLine("== Helpers (a Module)")
Dim helpers As Type = GetType(Helpers)
Console.WriteLine($" IsAbstract={helpers.IsAbstract} IsSealed={helpers.IsSealed}")
For Each data As CustomAttributeData In helpers.CustomAttributes
Console.WriteLine(" attribute: " & data.AttributeType.Name)
Next
Console.WriteLine("== attributes behind VB syntax")
For Each data As CustomAttributeData In GetType(Book).GetMethod("Match").GetParameters()(0).CustomAttributes
Console.WriteLine(" ParamArray parameter: " & data.AttributeType.Name)
Next
For Each data As CustomAttributeData In GetType(Book).CustomAttributes
Console.WriteLine(" Book: " & data.AttributeType.Name & " " & String.Join(",", data.ConstructorArguments.Select(Function(a) a.Value)))
Next
End Sub
End Module
== Book
Constructor .ctor
Event Borrowed
Event Returned
Field _Front
Field _Pages
Field _Title
Field $STATIC$Count$20188$calls
Field BorrowedEvent
Field ReturnedEvent
Method add_Borrowed
Method add_Returned
Method Count
Method Front_Opened
Method get_Front
Method get_Item
Method get_Pages
Method get_Title
Method Match
Method remove_Borrowed
Method remove_Returned
Method set_Front
Method set_Title
NestedType _Closure$__24-0
NestedType ReturnedEventHandler
Property Front
Property Item
Property Pages
Property Title
== nested types of Book
ReturnedEventHandler
_Closure$__24-0
== Helpers (a Module)
IsAbstract=False IsSealed=True
attribute: StandardModuleAttribute
== attributes behind VB syntax
ParamArray parameter: ParamArrayAttribute
Book: DefaultMemberAttribute Item
Source: Reflection (Visual Basic)
Reading the listing as a table of Visual Basic features:
| You wrote | The compiler generated |
|---|---|
|
a private field |
|
a field |
|
a field |
|
all of the above plus a new nested delegate type |
|
a property |
|
a field named |
a lambda capturing a local |
a nested class |
|
|
|
a class that is |
The practical lessons: a reflection loop over GetFields(BindingFlags.NonPublic Or BindingFlags.Instance) sees _Front, _Pages and _Title, so serializers and mappers that use fields instead of properties see the
Visual Basic names; and GetMembers() includes the special-name accessor methods unless you filter with IsSpecialName.
Creating objects and calling members
Activator.CreateInstance and ConstructorInfo.Invoke create objects; MethodInfo.Invoke, PropertyInfo.GetValue/SetValue and FieldInfo.GetValue/SetValue use their members. The program creates a Book and drives it entirely by reflection,
and triggers each of the failures a reflection caller must expect:
Option Strict On
Imports System.Reflection
Public Class Book
Private _isbn As String = "978-0-00-000000-2"
Public Property Title As String
Public Shared Property Created As Integer
Public Sub New(title As String)
Me.Title = title
Created += 1
End Sub
Public Sub Rename(newTitle As String)
If String.IsNullOrWhiteSpace(newTitle) Then Throw New ArgumentException("Title is empty", NameOf(newTitle))
Title = newTitle
End Sub
Public Sub Rename(newTitle As String, upperCase As Boolean)
Title = If(upperCase, newTitle.ToUpperInvariant(), newTitle)
End Sub
Public Function Wrap(Of T)(value As T) As String
Return "[" & value.ToString() & "]"
End Function
End Class
Module Program
Sub Main()
Dim bookType As Type = GetType(Book)
Console.WriteLine("-- looking types up by name")
Console.WriteLine("Type.GetType(""Book"") = " & If(Type.GetType("Book") Is Nothing, "Nothing", "found"))
Console.WriteLine("Type.GetType(""ShelfApp.Book"") = " & If(Type.GetType("ShelfApp.Book") Is Nothing, "Nothing", "found"))
Console.WriteLine("FullName = " & bookType.FullName)
Console.WriteLine("-- creating objects")
Dim book As Book = CType(Activator.CreateInstance(bookType, "Dune"), Book)
Dim constructor As ConstructorInfo = bookType.GetConstructor(New Type() {GetType(String)})
Dim second As Book = CType(constructor.Invoke(New Object() {"Emma"}), Book)
Console.WriteLine($"{book.Title}, {second.Title}, created={Book.Created}")
Try
Dim noDefault As Object = Activator.CreateInstance(bookType)
Catch ex As MissingMethodException
Console.WriteLine("No parameterless constructor: " & ex.GetType().Name)
End Try
Console.WriteLine("-- invoking members")
Try
bookType.GetMethod("Rename")
Catch ex As AmbiguousMatchException
Console.WriteLine("GetMethod(""Rename"") with two overloads: " & ex.GetType().Name)
End Try
Dim rename1 As MethodInfo = bookType.GetMethod("Rename", New Type() {GetType(String)})
Dim rename2 As MethodInfo = bookType.GetMethod("Rename", New Type() {GetType(String), GetType(Boolean)})
rename1.Invoke(book, New Object() {"Dune Messiah"})
rename2.Invoke(book, New Object() {"children of dune", True})
Console.WriteLine("Title now " & book.Title)
Dim titleProperty As PropertyInfo = bookType.GetProperty("Title")
titleProperty.SetValue(book, "God Emperor")
Console.WriteLine("Property read: " & CStr(titleProperty.GetValue(book)))
Dim isbnField As FieldInfo = bookType.GetField("_isbn", BindingFlags.Instance Or BindingFlags.NonPublic)
Console.WriteLine("Private field: " & CStr(isbnField.GetValue(book)))
Console.WriteLine("Shared property: " & CInt(bookType.GetProperty("Created").GetValue(Nothing)))
Try
rename1.Invoke(book, New Object() {" "})
Catch ex As TargetInvocationException
Console.WriteLine("Wrapped: " & ex.GetType().Name & " -> " & ex.InnerException.GetType().Name)
End Try
Try
rename1.Invoke(book, New Object() {42})
Catch ex As ArgumentException
Console.WriteLine("Wrong argument type: " & ex.GetType().Name)
End Try
Console.WriteLine("-- generics")
Dim listType As Type = GetType(List(Of )).MakeGenericType(GetType(Book))
Console.WriteLine(listType.ToString())
Dim list As Object = Activator.CreateInstance(listType)
listType.GetMethod("Add").Invoke(list, New Object() {book})
Console.WriteLine("Count = " & CInt(listType.GetProperty("Count").GetValue(list)))
Dim wrap As MethodInfo = bookType.GetMethod("Wrap").MakeGenericMethod(GetType(Integer))
Console.WriteLine(CStr(wrap.Invoke(book, New Object() {42})))
Console.WriteLine("-- a delegate removes the per-call cost")
Dim getTitle As Func(Of Book, String) = CType(titleProperty.GetGetMethod().CreateDelegate(GetType(Func(Of Book, String))), Func(Of Book, String))
Console.WriteLine(getTitle(book))
End Sub
End Module
-- looking types up by name
Type.GetType("Book") = Nothing
Type.GetType("ShelfApp.Book") = found
FullName = ShelfApp.Book
-- creating objects
Dune, Emma, created=2
No parameterless constructor: MissingMethodException
-- invoking members
GetMethod("Rename") with two overloads: AmbiguousMatchException
Title now CHILDREN OF DUNE
Property read: God Emperor
Private field: 978-0-00-000000-2
Shared property: 2
Wrapped: TargetInvocationException -> ArgumentException
Wrong argument type: ArgumentException
-- generics
System.Collections.Generic.List`1[ShelfApp.Book]
Count = 1
[42]
-- a delegate removes the per-call cost
God Emperor
Source: Activator class
What the run shows:
-
The type name includes the project’s root namespace.
Type.GetType("Book")isNothing— there is no exception — andType.GetType("ShelfApp.Book")finds the type.Type.GetType(name, throwOnError:=True)is the variant that fails loudly. -
GetMethodwith an overloaded name throwsAmbiguousMatchExceptionunless you give the parameter types; the same happens with overloaded properties such as indexers. -
An exception inside the invoked method arrives wrapped in
TargetInvocationException; readInnerException. A wrong argument type arrives asArgumentExceptionbefore the method runs. -
Private members are reachable with
BindingFlags.NonPublic, which is why reflection over a library you do not own is fragile and why it is a security-sensitive feature in sandboxed code. -
A shared member needs
Nothingas the target (GetValue(Nothing)). -
Generic types and methods are closed with
MakeGenericTypeandMakeGenericMethod; the open type is writtenGetType(List(Of )). -
Reflection is slower than a direct call. When a reflected call sits in a loop, create a delegate once (the last lines of the program:
CreateDelegate(GetType(Func(Of Book, String)))) and call that; measure with BenchmarkDotNet before deciding anything.
Loading a plugin assembly
The reflection feature that matters most in real applications is finding implementations in an assembly that was not known when the host was compiled. The solution below has three projects: Contracts (an interface IExporter and an
attribute ExporterAttribute), Plugin (four classes) and Host, which references Contracts only and loads Plugin.dll from a plugins folder at run time. The host builds the plugin first through a
ProjectReference with ReferenceOutputAssembly="false" and copies the DLL into its output folder.
Option Strict On
Public Interface IExporter
Function Export(titles As IEnumerable(Of String)) As String
End Interface
<AttributeUsage(AttributeTargets.Class, AllowMultiple:=False)>
Public NotInheritable Class ExporterAttribute
Inherits Attribute
Public ReadOnly Property Format As String
Public Sub New(format As String)
Me.Format = format
End Sub
End Class
Option Strict On
Imports Shelf.Contracts
<Exporter("csv")>
Public Class CsvExporter
Implements IExporter
Public Function Export(titles As IEnumerable(Of String)) As String Implements IExporter.Export
Return String.Join(",", titles)
End Function
End Class
<Exporter("lines")>
Public Class LineExporter
Implements IExporter
Public Function Export(titles As IEnumerable(Of String)) As String Implements IExporter.Export
Return String.Join(" | ", titles.Select(Function(t, i) $"{i + 1}. {t}"))
End Function
End Class
Public Class NotAnExporter
End Class
Friend Class HiddenExporter
Implements IExporter
Public Function Export(titles As IEnumerable(Of String)) As String Implements IExporter.Export
Return "hidden"
End Function
End Class
<ItemGroup>
<ProjectReference Include="..\Contracts\Contracts.vbproj" />
<ProjectReference Include="..\Plugin\Plugin.vbproj" ReferenceOutputAssembly="false" />
</ItemGroup>
<Target Name="CopyPlugin" AfterTargets="Build">
<Copy SourceFiles="..\Plugin\bin\$(Configuration)\$(TargetFramework)\Plugin.dll" DestinationFolder="$(OutDir)plugins" />
</Target>
Option Strict On
Imports System.IO
Imports System.Reflection
Imports System.Runtime.CompilerServices
Imports System.Runtime.Loader
Imports Shelf.Contracts
Module Program
<MethodImpl(MethodImplOptions.NoInlining)>
Function RunPlugin(path As String) As WeakReference
Dim context As New AssemblyLoadContext("plugins", isCollectible:=True)
Dim assembly As Assembly = context.LoadFromAssemblyPath(path)
Console.WriteLine("Loaded " & assembly.GetName().Name & " into context " & context.Name)
Dim titles As String() = {"Dune", "Emma", "Ulysses"}
For Each type As Type In assembly.GetTypes().OrderBy(Function(t) t.Name)
Dim implementsExporter As Boolean = GetType(IExporter).IsAssignableFrom(type)
Dim attribute As ExporterAttribute = type.GetCustomAttribute(Of ExporterAttribute)()
Console.WriteLine($"{type.Name,-14} IExporter={implementsExporter,-5} public={type.IsPublic,-5} attribute={If(attribute Is Nothing, "none", attribute.Format)}")
If implementsExporter AndAlso attribute IsNot Nothing Then
Dim exporter As IExporter = CType(Activator.CreateInstance(type), IExporter)
Console.WriteLine(" -> " & exporter.Export(titles))
End If
Next
Dim weak As New WeakReference(context)
context.Unload()
Return weak
End Function
Sub Main()
Dim path As String = System.IO.Path.Combine(AppContext.BaseDirectory, "plugins", "Plugin.dll")
Console.WriteLine("Plugin file exists: " & File.Exists(path))
Dim weak As WeakReference = RunPlugin(path)
For attempt As Integer = 1 To 10
If Not weak.IsAlive Then Exit For
GC.Collect()
GC.WaitForPendingFinalizers()
Next
Console.WriteLine("Context collected after unload: " & Not weak.IsAlive)
End Sub
End Module
Plugin file exists: True
Loaded Plugin into context plugins
_Closure$__ IExporter=False public=False attribute=none
CsvExporter IExporter=True public=True attribute=csv
-> Dune,Emma,Ulysses
HiddenExporter IExporter=True public=False attribute=none
LineExporter IExporter=True public=True attribute=lines
-> 1. Dune | 2. Emma | 3. Ulysses
NotAnExporter IExporter=False public=True attribute=none
Context collected after unload: True
The design decisions in the host:
-
The shared contract lives in a third assembly that both sides reference; the host loads the plugin into a separate
AssemblyLoadContext, whose fallback to the default context resolvesContractsto the host’s copy, soIExporteris the same type on both sides. Without a shared contract the plugin’sIExporterwould be a different type from the host’s. -
The test
GetType(IExporter).IsAssignableFrom(type)finds the implementations;HiddenExporterimplements the interface but isFriend, so a host that wanted only public plugins would also testIsPublic. The compiler’s_Closure$__class turns up inGetTypes()and is filtered out because it does not implement the interface. -
The attribute carries the name the plugin wants to be known by. Reading the attribute does not require creating the plugin.
-
isCollectible:=TrueandUnload()let the runtime free the plugin assembly, but only when nothing references it: the loading code is in aNoInliningmethod so that no local from the plugin survives in the caller, and the test collects withWeakReferenceandGC.Collectin a loop. The last line shows the context was collected. A stray reference (a static event handler, a running thread, a cachedType) can keep it alive, and the line then readsFalse. -
A real host must not trust arbitrary DLLs; a plugin is code that runs with the host’s rights.
Reflection, trimming and Native AOT
Trimming (PublishTrimmed) and Native AOT (PublishAot) remove code the linker thinks is unused. Reflection reaches code by name at run time, so the linker cannot see it: a type that is only created through Type.GetType(name) may be gone from the published application.
The annotations that tell the tools what reflection needs are the attributes DynamicallyAccessedMembers (on a Type parameter, naming the members that will be reflected on), RequiresUnreferencedCode and RequiresDynamicCode (on a method that cannot work when trimmed). They are
ordinary attributes, so they work in Visual Basic. The program below has one method of each kind:
Option Strict On
Imports System.Diagnostics.CodeAnalysis
Public Class CsvExporter
Public Function Export() As String
Return "csv"
End Function
End Class
Module Program
<RequiresUnreferencedCode("Creates a type from its name")>
Function CreateByName(name As String) As Object
Return Activator.CreateInstance(Type.GetType(name))
End Function
Function CreateKnown(<DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)> type As Type) As Object
Return Activator.CreateInstance(type)
End Function
Function CreateUnannotated(type As Type) As Object
Return Activator.CreateInstance(type)
End Function
Sub Main()
Dim known As Object = CreateKnown(GetType(CsvExporter))
Dim byName As Object = CreateByName("ShelfApp.CsvExporter")
Dim unannotated As Object = CreateUnannotated(GetType(CsvExporter))
Console.WriteLine(known.GetType().Name & " " & byName.GetType().Name & " " & unannotated.GetType().Name)
End Sub
End Module
The project properties were PublishTrimmed with SelfContained and a RuntimeIdentifier of linux-arm64:
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishTrimmed>true</PublishTrimmed>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
dotnet build printed no trimming diagnostics: with EnableTrimAnalyzer on, the compiler-side trim analyzer reported nothing for this Visual Basic program. dotnet publish runs the IL linker, which does report them, in IL terms (with the source line from the symbols; TreatWarningsAsErrors was off):
Program.vb(22,9): Trim analysis warning IL2067: ShelfApp.Program.CreateUnannotated(Type): 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor' in call to 'System.Activator.CreateInstance(Type)'. The parameter 'type' of method 'ShelfApp.Program.CreateUnannotated(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
Program.vb(27,13): Trim analysis warning IL2026: ShelfApp.Program.Main(): Using member 'ShelfApp.Program.CreateByName(String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Creates a type from its name.
CsvExporter CsvExporter CsvExporter
Source: Introduction to trim warnings
The published application ran and printed CsvExporter CsvExporter CsvExporter; the CreateKnown method, whose Type parameter carries DynamicallyAccessedMembers, produced no warning because the annotation propagates the requirement to its callers. With
TreatWarningsAsErrors on, the same warnings become errors and the publish fails:
Program.vb(22,9): Trim analysis error IL2067: ShelfApp.Program.CreateUnannotated(Type): 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor' in call to 'System.Activator.CreateInstance(Type)'. The parameter 'type' of method 'ShelfApp.Program.CreateUnannotated(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
Program.vb(27,13): Trim analysis error IL2026: ShelfApp.Program.Main(): Using member 'ShelfApp.Program.CreateByName(String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Creates a type from its name.
/root/.nuget/packages/microsoft.net.illink.tasks/10.0.13/build/Microsoft.NET.ILLink.targets(103,5): error NETSDK1144: Optimizing assemblies for size failed.
The PublishAot property compiles the same program ahead of time. The container needs a native linker (clang) and zlib headers; installed, the program published and ran (the executable was about 2 MB), with the same two warnings:
Program.vb(22): Trim analysis warning IL2067: ShelfApp.Program.CreateUnannotated(Type): 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor' in call to 'System.Activator.CreateInstance(Type)'. The parameter 'type' of method 'ShelfApp.Program.CreateUnannotated(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
Program.vb(27): Trim analysis warning IL2026: ShelfApp.Program.Main(): Using member 'ShelfApp.Program.CreateByName(String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Creates a type from its name.
CsvExporter CsvExporter CsvExporter
Source: Native AOT deployment
What this does and does not tell you: a small Visual Basic program trims and compiles with Native AOT. It does not show that a larger Visual Basic program does, because the Visual Basic runtime library itself uses reflection in some features
(late binding and CallByName), and none of that was exercised here. See Early and Late Binding for the late-binding side, and
Libraries, Packaging and Deployment for publishing options. Further, the System.Text.Json source generator
that removes reflection from JSON serialization cannot be used from Visual Basic projects (it needs a C# helper project, see Serialization: JSON and XML).
Differences from C#
| C# | Visual Basic | Note |
|---|---|---|
|
|
angle brackets; same attribute classes |
|
|
named arguments use |
|
|
at file level before the first declaration |
|
|
the attribute is part of the return type |
|
no explicit target keywords |
an attribute applies to the declaration it precedes; auto-property attributes go on the property |
|
|
the same two forms |
generic attributes |
not allowed |
BC32066 |
attributes on lambdas (C# 10) and local functions |
not allowed |
no attribute syntax inside a procedure body; |
|
|
and |
|
|
and |
|
|
same result: the last identifier, constant; open generic types are not accepted in Visual Basic |
|
|
same attribute; Visual Basic silently ignores one on the wrong parameter type |
|
|
different mechanism, same use |
Error reference
| Code | Message | Cause |
|---|---|---|
|
outside |
|
BC30663 |
|
no |
|
a required constructor argument is missing |
|
|
no constructor takes that many |
|
|
a named argument names nothing |
|
|
a named argument for a read-only property |
|
|
a named argument of the wrong type |
|
|
an attribute argument that is not a constant |
|
|
not an attribute class |
|
|
generic attribute |
|
|
an attribute inside a body |
|
|
|
|
|
a target the language does not have ( |
|
|
use of an |
|
|
warning for use of an |
|
BC42505 |
|
warning; the attribute names no parameter |
|
|
|
BC37244 |
|
|
BC30451 |
|
|
IL2026 |
|
linker warning at publish |
IL2067 |
|
linker warning at publish |
Source: Visual Basic error messages
Pitfalls checklist
-
An
<Assembly:attribute that the SDK also generates is a duplicate (BC30663); set the project property instead or switch offGenerateAssemblyInfo. -
The reflection name of a type includes the root namespace:
Type.GetType("Book")returnsNothing; use theFullName. -
GetCustomAttribute(Of T)throwsAmbiguousMatchExceptionwhen the attribute allows multiples and appears more than once. -
MemberInfo.GetCustomAttributes(inherit:=True)does not look at the base member of a property or event; useAttribute.GetCustomAttributes. -
Reading an attribute runs its constructor; use
IsDefinedorCustomAttributeDatawhen you only need to know it is there. -
Caller-information attributes need an
Optionalparameter of the right type; Visual Basic does not warn when they are wasted. -
CallerFilePathembeds the build machine’s path; usePathMapfor shipped binaries. -
MethodInfo.Invokewraps exceptions inTargetInvocationException; unwrapInnerException. -
Reflection finds the compiler’s own types and members (
_Closure$__,_Title,$STATIC$...); filter byIsSpecialNameandCompilerGeneratedAttribute. -
A name used only through reflection can be trimmed away; annotate it with
DynamicallyAccessedMembersor keep it with a root descriptor. The Visual Basic build printed no trimming diagnostics here, so run the publish with trim warnings as errors in CI. -
A collectible
AssemblyLoadContextis unloaded only when nothing references it; keep the loading code in a separate non-inlined method and test it. -
Never load or run code from a path supplied by users.
References
-
Attributes overview (Visual Basic), Creating custom attributes, Accessing attributes by using reflection and Common attributes
-
Caller information (Visual Basic) and the CallerMemberName, CallerFilePath, CallerLineNumber and CallerArgumentExpression attribute classes
-
Reflection (Visual Basic), Type, Assembly, Activator and BindingFlags
-
Attribute, AttributeUsageAttribute, CustomAttributeExtensions and ObsoleteAttribute
-
AssemblyLoadContext and How to use and debug assembly unloadability
-
Prepare .NET libraries for trimming, Trimming options and Native AOT deployment
-
Conditional Compilation and Directives, Early and Late Binding, Interoperating with C# and Other .NET Languages, Enumerations and Attributes and Reflection (C#)