CLR & Concurrency · Lesson 2

Open definitions and constructed generic types

Inspect generic identity and distinguish metadata from creating arbitrary runtime code.

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

Generic reflection distinguishes a definition with unbound parameters from a constructed type whose arguments have been selected. Dictionary with string keys and integer values is a closed construction; Dictionary with empty argument positions identifies the reusable generic definition. Both are Type metadata, but only a suitable closed concrete construction can describe an object you could instantiate. The portable program inspects an already known construction and does not call MakeGenericType on arbitrary user input. That narrower choice avoids confusing metadata inspection with the code-generation and reachability requirements that matter in trimmed or NativeAOT deployment.

Trace the portable program

The selected dictionary reports IsConstructedGenericType true. Its generic definition equals the unbound Dictionary definition, so the second observation is also true. GetGenericArguments returns the arguments in their declared positions, producing String followed by Int32. The definition still contains generic parameters, so the final open observation is true. Output is constructed:True, definition:True, arguments:String,Int32, and open:True. Reading the argument positions matters: swapping key and value creates a different closed type even though both constructions refer back to the same generic definition.

Execute the actual desktop lab

The desktop project intentionally inspects the already known Dictionary construction instead of generating arbitrary combinations at runtime. A constructed generic Type can be closed or can still contain parameters in more complex reflected contexts, so read the relevant properties rather than equating every occurrence of IsGenericType with a usable concrete object. GetGenericTypeDefinition identifies the reusable declaration, and the positional arguments identify this construction. If a library needs to match an implemented generic capability, inspect the actual interfaces and their definitions deliberately. Comparing an open definition directly with a closed type's simple name does not express that requirement.

Pitfalls and boundary limits

MakeGenericType is a further operation with validation and deployment implications. The supplied arguments must satisfy arity and constraints, and an open definition alone cannot be instantiated. NativeAOT also needs code and metadata reachability for supported constructions, so successful desktop reflection does not prove every runtime-selected instantiation is available after native publication. The sample avoids that leap. It also avoids asserting a particular assembly-qualified textual encoding as a stable wire format. Type identity, generic declaration identity, and a serialization protocol's type discriminator require different levels of completeness and stability.

Practice one: isolate behavior

First replace Dictionary of string and integer with Dictionary of integer and string. Predict the reversed argument-name list while the constructed, definition, and open booleans retain their original meanings. Hint: compare the two closed Type objects directly as well as their common generic definition. Then inspect a nongeneric type and decide which generic-only operation must be guarded before calling it. Keep the normal and exceptional cases separate so an invalid GetGenericTypeDefinition call cannot stop the observations that demonstrate the valid dictionary construction.

Practice two: design and verify

For another exercise, add a small generic class with a documented constraint and inspect that constraint through its definition. In a separate desktop experiment, attempt a construction that violates it and record the actual diagnostic or exception boundary. Hint: a runtime Type argument is not exempt from the generic contract simply because source syntax was bypassed. If the intended application supports only a fixed set of constructions, compare an explicit registration design with unrestricted MakeGenericType. State what a real NativeAOT publication would need to verify before selecting the dynamic design.

Review the evidence

Review generic metadata by naming the definition, selected arguments, remaining parameters, constraints, and intended construction or invocation step. The four baseline lines establish a precise dictionary example and are not a promise about every possible reflected generic shape. The external run adds actual desktop execution but does not generate a native artifact. Keep known closed types, open templates, and arbitrary runtime synthesis distinct. That separation makes diagnostics and registries easier to reason about and prevents a convenient reflection API from silently becoming a deployment requirement the application has never tested. Constraint metadata can describe reference-type requirements, constructor requirements, and other type relationships independently. Inspect those declared constraints instead of inferring them from a parameter name such as TEntity or from one successful closed construction.

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.

Open definitions and constructed generic types — 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;

Type closed = typeof(Dictionary<string, int>);
Type definition = closed.GetGenericTypeDefinition();
Console.WriteLine($"constructed:{closed.IsConstructedGenericType}");
Console.WriteLine($"definition:{definition == typeof(Dictionary<,>)}");
Console.WriteLine($"arguments:{string.Join(",", closed.GetGenericArguments().Select(type => type.Name))}");
Console.WriteLine($"open:{definition.ContainsGenericParameters}");
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: constructed:True definition:True arguments:String,Int32 open:True 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;

Type closed = typeof(Dictionary<string, int>);
Type definition = closed.GetGenericTypeDefinition();
Console.WriteLine($"constructed:{closed.IsConstructedGenericType}");
Console.WriteLine($"definition:{definition == typeof(Dictionary<,>)}");
Console.WriteLine($"arguments:{string.Join(",", closed.GetGenericArguments().Select(type => type.Name))}");
Console.WriteLine($"open:{definition.ContainsGenericParameters}");
Open C# practice

C# practice output

constructed:True
definition:True
arguments:String,Int32
open:True
Try this
Compare two closed constructions and their shared definition. An open generic Type is metadata, not an instantiable object type.

Further reading

Interview questions for this lesson