TypeLevelPresets 0.1.1
dotnet add package TypeLevelPresets --version 0.1.1
NuGet\Install-Package TypeLevelPresets -Version 0.1.1
<PackageReference Include="TypeLevelPresets" Version="0.1.1" />
<PackageVersion Include="TypeLevelPresets" Version="0.1.1" />
<PackageReference Include="TypeLevelPresets" />
paket add TypeLevelPresets --version 0.1.1
#r "nuget: TypeLevelPresets, 0.1.1"
#:package TypeLevelPresets@0.1.1
#addin nuget:?package=TypeLevelPresets&version=0.1.1
#tool nuget:?package=TypeLevelPresets&version=0.1.1
Terms of use<sup>?</sup>
By using this project or its source code, for any purpose and in any shape or form, you grant your implicit agreement to all the following statements:
- You condemn Russia and its military aggression against Ukraine
- You recognize that Russia is an occupant that unlawfully invaded a sovereign state
- You support Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas
- You reject false narratives perpetuated by Russian state propaganda
To learn more about the war and how you can help, click here. Glory to Ukraine!
TypeLevelPresets
Turn a small, fixed set of options into CLR types, so generic static caches can be keyed by both a data type and a preset without a dictionary lookup on the typed hot path.
Why
.NET gives every closed generic type its own static fields. This makes a generic type a convenient per-type cache:
static class MetadataCache<T>
{
public static readonly Metadata Value = Build(typeof(T));
}
MetadataCache<Invoice> and MetadataCache<Customer> each initialize and store a different Value. Microsoft documents this behavior explicitly: each closed generic type has its own copy of static members.
The pattern stops being direct when the cache key also contains values such as a bool, an enum, or BindingFlags: C# cannot use arbitrary values as generic arguments. The usual fallback is a runtime lookup:
Dictionary<(Type, FieldOptions), Metadata> cache;
TypeLevelPresets generates a marker type for each declared option tuple. The cache can then use a distinct closed generic type for every known combination:
MetadataCache<Invoice, FieldOptions.Public>.Value
Quick start
using System.Reflection;
using Raffinert.TypeLevelPresets;
[Preset("Public", BindingFlags.Instance | BindingFlags.Public)]
[Preset("AllInstance",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)]
public readonly partial struct FieldOptions
{
public BindingFlags Flags { get; }
}
public static class Cache<T, TPreset>
where TPreset : struct, IPreset<FieldOptions>
{
public static readonly FieldInfo[] Value = typeof(T).GetFields(
FieldOptions.GetValues<TPreset>().Flags);
}
var fields = Cache<Invoice, FieldOptions.Public>.Value;
The generator emits:
- a zero-field
readonly structmarker for each[Preset]; - an immutable
FieldOptionsValuesvalue type; - a cached
FieldOptions.GetValues<TPreset>()accessor.
The preset values are emitted as code. The typed path uses no attribute reflection and no options dictionary. Each Cache<T, TPreset> combination owns independent static storage and initializes it once.
Good fit
Use this pattern when all of the following are true:
- the presets are finite and known at build time;
- the same type-and-preset combinations are accessed repeatedly;
- a lookup sits on a measured hot path;
- separate generic-static storage is useful for metadata, delegates, serializers, mappers, or similar reusable artifacts.
Keep a normal dictionary for runtime-selected, user-defined, or high-cardinality options. This package is not a general configuration system. Every additional closed generic combination can add initialization work and may add JIT or native-code size, so benchmark the complete workload rather than only the lookup.
Supported values and rules
Schemas are top-level, non-generic, partial classes or structs. Their readable instance properties, in declaration order, define the tuple. Version 1 supports bool, integral primitives, char, string, enums, and System.Type supplied as typeof(...).
Floating point, nullable values, arrays, nested schemas, and schemas split across multiple user declarations are rejected. Invalid names or values, duplicate presets, and generated-name collisions produce TLP001–TLP011 compiler diagnostics.
Benchmark
The benchmark preloads 100 dictionary entries and generates 100 presets. Each invocation reads all 100 values, and BenchmarkDotNet reports the time per lookup. Run it with:
dotnet run -c Release --project benchmarks/TypeLevelPresets.Benchmarks -- --filter '*FieldMetadataCacheBenchmarks*'
Representative short run on Windows 11 with .NET 10.0.11, x64 RyuJIT, and AVX2 (BenchmarkDotNet 0.15.2; three warmups and three measured iterations):
| Access path | Mean per lookup | Allocated |
|---|---|---|
| Composite dictionary with 100 entries | 6.6450 ns | 0 B |
| Direct generic static across 100 presets | 0.0013 ns¹ | 0 B |
¹ BenchmarkDotNet reported the generic-static read as indistinguishable from an empty method. Treat the near-zero value as below measurement resolution, not as a literal access time or an exact speedup. The benchmark shows that this typed path removed measurable dictionary-lookup overhead; it does not promise the same application-level gain.
Background reading
- Static classes and members — documents separate static members for each closed generic type.
- Know Thine Implicit Allocations — demonstrates the
DelegateCache<T>form of the generic-static cache pattern. - Performance improvements in .NET 8 — discusses a real runtime-library optimization involving generic static fields and generic lookup overhead.
- .NET shared-generics design — explains why generic instantiations can involve runtime generic dictionaries and code sharing.
Build and test
dotnet build TypeLevelPresets.slnx
dotnet test TypeLevelPresets.slnx
dotnet pack src/TypeLevelPresets -c Release
The TypeLevelPresets package contains net45 and netstandard2.0 abstraction assemblies under lib/ and the incremental generator under analyzers/dotnet/cs/. Consumers install one package; Roslyn does not become a runtime dependency. See the changelog for release history.
See samples/TypeLevelPresets.Sample.Serialization for the reflection metadata cache example. Interceptors, runtime-generated types, arbitrary runtime values, and call-site rewriting are not part of v1.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net45 is compatible. net451 was computed. net452 was computed. net46 was computed. net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.5
- No dependencies.
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.