NonboxingUnion 0.0.2

dotnet add package NonboxingUnion --version 0.0.2
                    
NuGet\Install-Package NonboxingUnion -Version 0.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NonboxingUnion" Version="0.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NonboxingUnion" Version="0.0.2" />
                    
Directory.Packages.props
<PackageReference Include="NonboxingUnion" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NonboxingUnion --version 0.0.2
                    
#r "nuget: NonboxingUnion, 0.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NonboxingUnion@0.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NonboxingUnion&version=0.0.2
                    
Install as a Cake Addin
#tool nuget:?package=NonboxingUnion&version=0.0.2
                    
Install as a Cake Tool

Nonboxing Unions

NuGet

⚠️ 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/FieldOffset are 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 and IUnion implementation, 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.UnionAttribute and IUnion, with the C# union language feature enabled.
Product Compatible and additional computed target framework versions.
.NET net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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