Skywithin.VnumEnumeration
10.1.0
dotnet add package Skywithin.VnumEnumeration --version 10.1.0
NuGet\Install-Package Skywithin.VnumEnumeration -Version 10.1.0
<PackageReference Include="Skywithin.VnumEnumeration" Version="10.1.0" />
<PackageVersion Include="Skywithin.VnumEnumeration" Version="10.1.0" />
<PackageReference Include="Skywithin.VnumEnumeration" />
paket add Skywithin.VnumEnumeration --version 10.1.0
#r "nuget: Skywithin.VnumEnumeration, 10.1.0"
#:package Skywithin.VnumEnumeration@10.1.0
#addin nuget:?package=Skywithin.VnumEnumeration&version=10.1.0
#tool nuget:?package=Skywithin.VnumEnumeration&version=10.1.0
Vnum
VnumEnumeration provides a base class (Vnum) for strongly-typed, enumeration-like types in C#. A Vnum behaves like an enum but each instance carries a numeric value and a string code, can hold extra metadata, and supports lookup, parsing and JSON serialization.
Installation
.NET CLI
dotnet add package Skywithin.VnumEnumeration
Package Manager
Install-Package Skywithin.VnumEnumeration
PackageReference
<PackageReference Include="Skywithin.VnumEnumeration" Version="10.*" />
Features
- Value and code: every instance has a
longvalue and a non-empty string code. - Reflection-based discovery: all instances of a type are found automatically and cached (thread-safe) after the first lookup.
- Flexible lookup: find instances by value, code (optionally case-insensitive) or enum, with throwing and
Try*variants. - Enum-backed types:
Vnum<TEnum>ties a Vnum to an enum of any underlying integral type (byte,sbyte,short,ushort,int,uint,long,ulong). - JSON serialization: a
System.Text.Jsonconverter factory that writes codes and reads codes or numeric values.
Usage
Defining a Vnum
public sealed class OrderStatus : Vnum
{
private OrderStatus(long value, string code) : base(value, code) { }
public static readonly OrderStatus Pending = new(1, "PENDING");
public static readonly OrderStatus Processing = new(2, "PROCESSING");
public static readonly OrderStatus Shipped = new(3, "SHIPPED");
public static readonly OrderStatus Delivered = new(4, "DELIVERED");
}
Instances can carry extra data by adding properties and constructor parameters:
public sealed class Currency : Vnum
{
public string Symbol { get; }
private Currency(long value, string code, string symbol) : base(value, code)
{
Symbol = symbol;
}
public static readonly Currency Aud = new(1, "AUD", "$");
public static readonly Currency Eur = new(2, "EUR", "€");
}
Enum-backed Vnum
public enum StatusId
{
Pending = 1,
Processing = 2,
Shipped = 3,
Delivered = 4
}
public sealed class OrderStatus : Vnum<StatusId>
{
private OrderStatus(StatusId value, string code) : base(value, code) { }
public static readonly OrderStatus Pending = new(StatusId.Pending, "PENDING");
public static readonly OrderStatus Processing = new(StatusId.Processing, "PROCESSING");
public static readonly OrderStatus Shipped = new(StatusId.Shipped, "SHIPPED");
public static readonly OrderStatus Delivered = new(StatusId.Delivered, "DELIVERED");
}
StatusId id = OrderStatus.Shipped.Id; // StatusId.Shipped
long value = OrderStatus.Shipped.Value; // 3
Lookup
// All instances, optionally filtered
IEnumerable<OrderStatus> all = Vnum.GetAll<OrderStatus>();
IEnumerable<OrderStatus> open = Vnum.GetAll<OrderStatus>(s => s.Value < 3);
// Throwing lookups (InvalidOperationException if no match)
var byValue = Vnum.FromValue<OrderStatus>(1);
var byCode = Vnum.FromCode<OrderStatus>("PENDING");
var byCodeIgnoreCase = Vnum.FromCode<OrderStatus>("pending", ignoreCase: true);
var byEnum = Vnum.FromEnum<OrderStatus, StatusId>(StatusId.Pending);
// Safe lookups
if (Vnum.TryFromValue<OrderStatus>(1, out var fromValue)) { /* ... */ }
if (Vnum.TryFromCode<OrderStatus>("PENDING", out var fromCode)) { /* ... */ }
if (Vnum.TryFromCode<OrderStatus>("pending", ignoreCase: true, out var fromCodeIgnoreCase)) { /* ... */ }
if (Vnum.TryFromEnum<OrderStatus, StatusId>(StatusId.Pending, out var fromEnum)) { /* ... */ }
// ToString() returns the code
Console.WriteLine(OrderStatus.Pending); // PENDING
JSON serialization
Register VnumJsonConverterFactory once and it handles every Vnum type:
public record Order(int Id, OrderStatus Status);
var options = new JsonSerializerOptions();
options.Converters.Add(new VnumJsonConverterFactory());
// Serializes to the code
string json = JsonSerializer.Serialize(new Order(1, OrderStatus.Pending), options);
// {"Id":1,"Status":"PENDING"}
// Deserializes from the code...
Order? fromCode = JsonSerializer.Deserialize<Order>("""{"Id":1,"Status":"PENDING"}""", options);
// ...or from the numeric value (backward compatibility)
Order? fromValue = JsonSerializer.Deserialize<Order>("""{"Id":1,"Status":1}""", options);
In ASP.NET Core:
// Minimal APIs
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.Converters.Add(new VnumJsonConverterFactory()));
// MVC controllers
builder.Services.AddControllers().AddJsonOptions(o =>
o.JsonSerializerOptions.Converters.Add(new VnumJsonConverterFactory()));
Reading notes:
- Codes are matched case-sensitively.
nulland an empty string both deserialize tonull.- An unknown code or value throws
JsonException; for unknown codes the message lists the valid ones.
Behavior
Instance discovery
Instances are discovered by reflection over public static fields declared directly on the Vnum type (typically public static readonly). The following are ignored:
- non-public static fields
- static properties (
public static OrderStatus X { get; } = ...) - fields declared on a base class
Several fields that reference the same instance (e.g. public static readonly OrderStatus Default = Pending;) count as one instance.
Discovery runs once per type; the results are cached for the lifetime of the process. Lookups by value and code are dictionary-based.
Equality
Two Vnums are equal when they have the same runtime type and the same Value. Code is not part of equality or hashing. == and != follow the same rules.
Errors
| Situation | Exception |
|---|---|
FromValue / FromCode / FromEnum finds no match |
InvalidOperationException |
FromCode called with null |
ArgumentNullException |
| Constructor called with a null, empty or whitespace code | ArgumentException |
Vnum<TEnum> constructed with a long value that doesn't fit the enum's underlying type |
ArgumentOutOfRangeException |
| JSON contains an unknown code or value | JsonException |
The Try* lookup methods return false instead of throwing when no match is found.
Limitations
- No duplicate validation: the library does not check that values or codes are unique. If two instances share a value or code, lookups return whichever is found first.
ulongoverflow:ulongenum values greater thanlong.MaxValuethrowOverflowException.
Supported frameworks
- .NET 10.0+
Building from source
dotnet build VnumEnumeration.slnx
dotnet test tests/VnumEnumeration.Tests/VnumEnumeration.Tests.csproj
Releases are published to NuGet by GitHub Actions when a tag is pushed: v1.2.3 publishes 1.2.3, and rc-v1.2.3 publishes the pre-release 1.2.3-rc.<run number>.
Acknowledgements
Inspired by Jimmy Bogard's Enumeration classes.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.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 |
|---|---|---|
| 10.1.0 | 107 | 10/7/2026 |
| 10.1.0-rc.5 | 57 | 10/7/2026 |
| 10.0.3 | 457 | 11/29/2025 |
| 10.0.2 | 174 | 11/29/2025 |
| 10.0.1 | 166 | 11/29/2025 |
| 10.0.0 | 236 | 11/22/2025 |
| 10.0.0-rc.4 | 190 | 11/22/2025 |
| 1.1.0 | 271 | 10/16/2025 |
| 1.0.1 | 245 | 10/15/2025 |