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 explicit static constructor gives a type a recognizable initialization boundary. Counter's static constructor establishes its starting value and writes one initialization line before its first static method invocation proceeds. This is different from guessing when a type marked beforefieldinit happens to initialize under a particular runtime or optimization. The sample intentionally has an explicit constructor and only one bounded state sequence. Its behavior can be checked on the portable runtime and the real desktop project without inferring JIT tier, thread scheduling, or a universal initialization order for unrelated types.
Trace the portable program
The host prints before without accessing Counter. The first call to Next requires Counter's initialization, so init is printed and value becomes two before Next increments it to three. The second call uses the already initialized type and increments the retained value to four. Output is before, init, first:3, and second:4. There is one initialization line for this type in this execution. The sample does not claim that merely inspecting every kind of metadata triggers a constructor, or that changing the declaration to implicit field initialization preserves exactly the same timing guarantees.
Execute the actual desktop lab
The external project repeats the explicit static-constructor sequence on desktop .NET 10. Static initialization is associated with the runtime type, and closed constructions of a generic type can have separate static state. That is different from a single process-wide value shared by every possible generic argument. Type initialization failure also has a specific lifetime consequence: a failed initializer is not an ordinary retryable instance operation. Keep initialization small and avoid blocking or reaching into complicated cyclic dependencies. The sample deliberately initializes one scalar and writes one diagnostic line instead of making startup depend on external resources.
Pitfalls and boundary limits
A type without an explicit static constructor can be marked beforefieldinit, allowing initialization timing choices that differ from this baseline. Do not remove the constructor and then demand identical early-or-late behavior across JIT, interpreter, and AOT environments. Likewise, reading Type metadata and executing a static method are not interchangeable actions. Constants can be embedded without a normal static field read, and code can observe initialization through several different triggers. The lesson intentionally uses a direct static method invocation with an explicit constructor so the expected order follows a meaningful rule rather than one compiler's incidental generated layout.
Practice one: isolate behavior
First call Next a third time and predict five without another init line. Then create a second ordinary instance-bearing type with its own explicit static constructor and observe its independent first-use boundary. Hint: keep the access sequence deliberate so the order comes from actual triggers rather than from assumptions about source-file declaration order. Avoid inserting metadata inspection and claiming it is equivalent to method execution. These experiments distinguish retained per-type state, one-time initialization, and the separate identity of another runtime type.
Practice two: design and verify
For another exercise, create a small generic holder with explicit static initialization and compare two closed type arguments. State which state belongs to each constructed type before running. Hint: a generic definition and its closed runtime types are different metadata identities. In a separate disposable desktop program, inspect how an intentionally failing initializer is reported and why repeatedly calling the type is not a sensible recovery loop. Keep that failure fixture out of shared examples whose later steps require the type to remain usable.
Review the evidence
Review static initialization by identifying the explicit constructor, actual trigger, type identity, mutable state, and failure policy. The baseline proves one clear sequence and does not establish arbitrary timing for beforefieldinit types or unrelated initialization graphs. The desktop run adds a second real runtime environment, but a passing print order still is not a JIT-tier measurement. Use static state when its lifetime and initialization ownership fit the design, and keep complex recovery or external acquisition in explicit operations where failure and disposal can be handled with a contract suited to those responsibilities. Do not block a static constructor on a task whose work may require that same type to initialize; initialization ownership can become part of a deadlock cycle.
Explicit type initialization has a boundary — desktop runtime lab
Set up the SDK project
- Save both files at their exact paths in a fresh owned directory.
- Use a .NET 10 SDK, then run dotnet run --project RuntimeLab.csproj. This project intentionally uses C# 13 syntax.
- 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.cs
Download 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;
Console.WriteLine("before");
Console.WriteLine($"first:{Counter.Next()}");
Console.WriteLine($"second:{Counter.Next()}");
static class Counter
{
private static int value;
static Counter() { Console.WriteLine("init"); value = 2; }
public static int Next() => ++value;
}
RuntimeLab.csproj
Download 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: before init first:3 second:4 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;
Console.WriteLine("before");
Console.WriteLine($"first:{Counter.Next()}");
Console.WriteLine($"second:{Counter.Next()}");
static class Counter
{
private static int value;
static Counter() { Console.WriteLine("init"); value = 2; }
public static int Next() => ++value;
}
C# practice output
before init first:3 second:4
This type has an explicit static constructor. Removing it changes initialization rules and is not a valid way to promise identical timing for every runtime.