NonboxingUnion 0.0.2
dotnet add package NonboxingUnion --version 0.0.2
NuGet\Install-Package NonboxingUnion -Version 0.0.2
<PackageReference Include="NonboxingUnion" Version="0.0.2" />
<PackageVersion Include="NonboxingUnion" Version="0.0.2" />
<PackageReference Include="NonboxingUnion" />
paket add NonboxingUnion --version 0.0.2
#r "nuget: NonboxingUnion, 0.0.2"
#:package NonboxingUnion@0.0.2
#addin nuget:?package=NonboxingUnion&version=0.0.2
#tool nuget:?package=NonboxingUnion&version=0.0.2
Nonboxing Unions
⚠️ This package is currently in preview, and is subject to change.
Background
C# is gaining first-class discriminated unions. A union is a value that can be exactly one of a fixed, known set of case types at any given time, and which can be exhaustively pattern matched.
The natural way to model "one of several types" today is to store the value in an object field. That works, but it forces every value-type case (int, bool, your own structs, …) to be boxed onto the heap every time it is placed into the union, and unboxed every time it is read back out. For hot paths and allocation-sensitive code, that overhead defeats much of the point of using a value type in the first place.
The runtime exposes [System.Runtime.CompilerServices.Union] and System.Runtime.CompilerServices.IUnion so that a type can opt in to the union language feature (implicit conversions from each case type, and exhaustive pattern matching). But it leaves the storage strategy up to you. The boxing implementation the compiler can generate for you stores everything in a single object? field.
That's where Nonboxing Unions comes in. With Nonboxing Unions, you annotate a partial struct with the case types it should hold, and a source generator emits a union whose storage keeps each value-type case unboxed:
- a discriminator that records which case is active, backed by the smallest unsigned integer that can represent the cases;
- one strongly-typed field per case, so value types are stored inline with no heap allocation and no memory overlap (
StructLayout/FieldOffsetare deliberately avoided, as overlapping unrelated fields hurts the JIT and is unsound for fields containing references); - the union access members (
Value,HasValue,TryGetValue), value equality, and a constructor for each case; - the
[Union]attribute andIUnionimplementation, so the type participates in the C# union language feature (implicit conversions and exhaustive pattern matching).
Usage
Mark a partial struct with [NonBoxingUnion(...)], passing the case types as typeof(...) arguments. The struct body can be left empty — the generator fills in everything.
using NonboxingUnion;
[NonBoxingUnion(typeof(int), typeof(bool))]
public partial struct IntOrBool;
Assigning a case uses the implicit conversions provided by the union language feature:
IntOrBool union = 42; // holds an int
union = true; // now holds a bool
Pattern matching is exhaustive over the case types, plus null for the uninitialized (default) state:
string description = union switch
{
int i => $"int {i}",
bool b => $"bool {b}",
null => "no value",
};
Because each case is stored in its own typed field, matching int i reads an int directly with no unboxing.
The generated access members:
IntOrBool union = 42;
union.HasValue; // true (false for default)
union.Value; // object? -> 42 (boxed only when you ask for it)
if (union.TryGetValue(out int value)) // true; value == 42
{
// ...
}
union.TryGetValue(out bool _); // false; the active case is int, not bool
A default(IntOrBool) has no active case: HasValue is false, Value is null, and every TryGetValue returns false.
Supported case types
| Case kind | Example | Notes |
|---|---|---|
| Value types | typeof(int), typeof(MyStruct) |
Stored inline, never boxed. |
| Reference types | typeof(string), typeof(Dog) |
Stored in a nullable field; the case type and constructor parameter stay non-nullable. |
| Nullable value types | typeof(int?) |
Collapses to the underlying type (int) for the case type and TryGetValue out parameter, per the union spec. |
| Nested types | typeof(Outer.Inner.Thing) |
Fully-qualified names are used, so nesting and namespaces are handled correctly. |
| Generic type parameters | (see below) | The struct's own type parameters are automatically included as cases. |
The union struct itself may be nested inside other types — all of its containing types must be partial.
Generic unions
A union struct can declare generic type parameters; those parameters are automatically treated as cases (in declaration order, before any typeof(...) arguments in the attribute). This makes it straightforward to build generic container types such as Result<T, TError>:
[NonBoxingUnion]
public partial struct Result<T, TError>;
Result<int, string> ok = 42;
Result<int, string> err = "something went wrong";
string description = ok switch
{
int value => $"ok: {value}",
string msg => $"err: {msg}",
null => "no value",
};
Constraints on the type parameters drive the storage strategy — the same rules that apply to concrete case types:
| Constraint | Storage | Behaviour |
|---|---|---|
where T : struct |
T |
Stored inline as a value type, identical to a concrete value-type case. |
where T : class |
T? |
Stored in a nullable reference field, identical to a concrete reference-type case. |
| (unconstrained) | T? |
Stored using C# 8's MaybeNull annotation — not Nullable<T>, so value-type call sites incur no boxing. |
Type parameters and concrete typeof(...) cases can be mixed freely. The type parameters always come first:
// Two cases: T (type parameter, first), string (concrete, second).
[NonBoxingUnion(typeof(string))]
public partial struct TOrString<T>;
For more detail on storage layout and the generated members, see docs/generated-code.md.
Requirements
- The annotated type must be a
partial struct. - It must declare at least one case: either via
typeof(...)arguments in the attribute, via generic type parameters on the struct, or both. - Every containing type must also be
partial. - A target framework whose runtime provides
System.Runtime.CompilerServices.UnionAttributeandIUnion, with the C# union language feature enabled.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net11.0 is compatible. |
-
net11.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.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.2 | 131 | 6/7/2026 |
| 0.0.1 | 101 | 5/29/2026 |
| 0.0.0-preview | 99 | 5/29/2026 |