CLR & Concurrency · Lesson 4

Attribute data versus attribute execution

Read metadata without invoking the attribute constructor, then instantiate explicitly.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

The runtime contract

An attribute declaration records metadata; it does not automatically execute a validation rule or construct a live attribute object when the annotated type is mentioned. CustomAttributeData lets a reader inspect the recorded constructor arguments without invoking the custom attribute's constructor. Attribute.GetCustomAttribute takes a different path and creates an attribute instance. The program makes that distinction observable through an explicit constructor counter. It uses one simple class-level attribute and no external assembly loading. The external desktop project runs the same real reflection behavior rather than pretending metadata alone supplies an application policy.

Trace the portable program

The annotation records demo as its argument. Reading CustomAttributeData therefore prints data:demo, while the custom constructor count is still zero. Requesting the live MarkerAttribute instance then runs its constructor, stores Name, and increments the count once. The remaining lines are constructors:0, instance:demo, and constructors:1 in that order after the initial data line. The attribute does not inspect or modify Annotated. A consumer would need to interpret its meaning deliberately; decorating a class is not evidence that a runtime automatically enforced whatever a marker's name suggests.

Execute the actual desktop lab

The external project uses the same actual attribute APIs and constructor counter, so its output distinguishes two concrete reflection paths. AttributeUsage controls where the attribute may be applied and whether multiple or inherited uses are meaningful; it does not supply the consumer's business policy. A discovery tool can often read constructor and named arguments from data without executing user attribute constructors. A framework that intentionally creates live instances must account for their possible effects and failures. Decide which path the reader needs rather than assuming every attribute API is an equivalent view of inert text.

Pitfalls and boundary limits

Constructor counters are an explicit teaching effect, not a recommended production annotation design. An attribute constructor that performs I/O or mutates global state can make ordinary discovery unexpectedly expensive or failure-prone. The sample also selects its exact custom attribute type so unrelated compiler-emitted metadata does not affect the result. Inherited annotations and allow-multiple annotations need additional selection rules. Do not infer that class decoration executes validation, grants a permission, or changes method dispatch automatically. The actual consumer and its point of enforcement determine those behaviors, and that consumer requires its own tests.

Practice one: isolate behavior

First request the live attribute a second time and observe the construction path separately from another CustomAttributeData read. Hint: do not cache or reuse the first instance while claiming to test a fresh API request. Predict which operations can run the custom constructor and inspect the actual count rather than assuming all attribute access is passive. Then add a second differently typed annotation and preserve exact filtering for MarkerAttribute. This tests whether your reader is selecting a declared contract or simply accepting whichever metadata record happens to appear first.

Practice two: design and verify

For another exercise, design a method-label consumer that builds a bounded route table without instantiating the custom annotation. Validate required names, supported signatures, and duplicate labels before exposing any handler. Hint: the marker's presence supplies metadata, not authorization to execute every discovered method. Give an unknown route and a duplicate declaration separate outcomes. Compare that design with a framework that deliberately constructs attribute instances, documenting the additional constructor and failure behavior instead of pretending the two discovery approaches have identical costs or effects.

Review the evidence

Review attribute use by separating declaration, metadata inspection, instance construction, policy interpretation, and enforcement. The four output lines prove the local constructor distinction, while the external run confirms it on the stated desktop environment. Neither creates a security policy or proves unrestricted reflection survives trimming. Keep the reader's selection and lifetime rules visible, especially when annotations come from extensible or third-party code. A well-defined metadata contract can simplify tooling; assuming that an attribute name automatically performs its intended action hides the code path where correctness actually has to be established. Named attribute arguments are separate metadata from constructor arguments. If a consumer supports them, inspect and validate that additional set deliberately instead of assuming the first positional argument describes the entire annotation contract.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

Attribute data versus attribute execution — desktop runtime lab

Set up the SDK project

  1. Save both files at their exact paths in a fresh owned directory.
  2. Use a .NET 10 SDK, then run dotnet run --project RuntimeLab.csproj. This project intentionally uses C# 13 syntax.
  3. Workers and waits are bounded; a timeout or a violated invariant fails the process. No benchmark or NativeAOT publication is implied by an ordinary run.

Download complete SDK project ↗

Project files

Program.csDownload file ↗
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

var data = CustomAttributeData.GetCustomAttributes(typeof(Annotated)).Single(item => item.AttributeType == typeof(MarkerAttribute));
Console.WriteLine($"data:{data.ConstructorArguments[0].Value}");
Console.WriteLine($"constructors:{MarkerAttribute.Created}");
var instance = (MarkerAttribute)Attribute.GetCustomAttribute(typeof(Annotated), typeof(MarkerAttribute))!;
Console.WriteLine($"instance:{instance.Name}");
Console.WriteLine($"constructors:{MarkerAttribute.Created}");
[Marker("demo")] sealed class Annotated { }
[AttributeUsage(AttributeTargets.Class)]
sealed class MarkerAttribute : Attribute
{
    public static int Created;
    public string Name { get; }
    public MarkerAttribute(string name) { Name = name; Created++; }
}
RuntimeLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>13.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>false</PublishAot>
  </PropertyGroup>
</Project>

Expected result

Exact behavioral stdout on the stated desktop lab: data:demo constructors:0 instance:demo constructors:1 Variable diagnostics, where emitted, go to stderr and must not be treated as portable timing or allocation guarantees.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

var data = CustomAttributeData.GetCustomAttributes(typeof(Annotated)).Single(item => item.AttributeType == typeof(MarkerAttribute));
Console.WriteLine($"data:{data.ConstructorArguments[0].Value}");
Console.WriteLine($"constructors:{MarkerAttribute.Created}");
var instance = (MarkerAttribute)Attribute.GetCustomAttribute(typeof(Annotated), typeof(MarkerAttribute))!;
Console.WriteLine($"instance:{instance.Name}");
Console.WriteLine($"constructors:{MarkerAttribute.Created}");
[Marker("demo")] sealed class Annotated { }
[AttributeUsage(AttributeTargets.Class)]
sealed class MarkerAttribute : Attribute
{
    public static int Created;
    public string Name { get; }
    public MarkerAttribute(string name) { Name = name; Created++; }
}
Open C# practice

C# practice output

data:demo
constructors:0
instance:demo
constructors:1
Try this
Compare metadata arguments with constructed attribute objects. Attributes do not enforce a policy unless a consumer actually interprets them.

Further reading

Interview questions for this lesson