InfiniteEnumFlags 0.6.0

dotnet add package InfiniteEnumFlags --version 0.6.0
                    
NuGet\Install-Package InfiniteEnumFlags -Version 0.6.0
                    
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="InfiniteEnumFlags" Version="0.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="InfiniteEnumFlags" Version="0.6.0" />
                    
Directory.Packages.props
<PackageReference Include="InfiniteEnumFlags" />
                    
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 InfiniteEnumFlags --version 0.6.0
                    
#r "nuget: InfiniteEnumFlags, 0.6.0"
                    
#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 InfiniteEnumFlags@0.6.0
                    
#: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=InfiniteEnumFlags&version=0.6.0
                    
Install as a Cake Addin
#tool nuget:?package=InfiniteEnumFlags&version=0.6.0
                    
Install as a Cake Tool

InfiniteEnumFlags

GitHub Nuget Nuget (with prereleases) NuGet version (InfiniteEnumFlags)

InfiniteEnumFlags gives you [Flags]-style behavior without the 32-bit or 64-bit limit of built-in .NET enums.

Use it when native C# flags are the right model, but int, long, or ulong are too small. This is common for permissions, feature switches, policy rules, and other systems where the list can grow past 64 values.

You still get named values, bitwise operators, and compact storage, but the flags are backed by a growable bit array instead of a fixed-size numeric enum.

Install

dotnet add package InfiniteEnumFlags

Define flags

Create a class that inherits from InfiniteEnum<T> and expose each value as a static Flag<T>.

-1 is reserved for None. Every other number is the zero-based bit index.

using InfiniteEnumFlags;

public sealed class Permissions : InfiniteEnum<Permissions>
{
    public static readonly Flag<Permissions> None = new(-1);      // 0      -> 0
    public static readonly Flag<Permissions> ReadUsers = new(0);  // 1      -> 1
    public static readonly Flag<Permissions> CreateUsers = new(1); // 2    -> 10
    public static readonly Flag<Permissions> DeleteUsers = new(2); // 4    -> 100
    public static readonly Flag<Permissions> ViewReports = new(100);
}

The constructor value is the bit index, not the decimal value. For example, new(2) means "turn on bit 2", which is the same idea as 1 << 2.

Flag Index Decimal value Binary shape
None -1 0 0
ReadUsers 0 1 1
CreateUsers 1 2 10
DeleteUsers 2 4 100
ViewReports 100 very large bit 100 is on

Native [Flags] enums store this shape inside an int, long, or ulong. InfiniteEnumFlags stores the same shape in a growable bit array, so high indexes like 100, 500, or 10_000 are still valid.

Combine and check flags

You can use familiar bitwise operators.

var permissions = Permissions.ReadUsers | Permissions.CreateUsers;
// binary: 1 | 10 = 11

bool canRead = permissions.HasFlag(Permissions.ReadUsers); // true
bool canDelete = permissions.HasFlag(Permissions.DeleteUsers); // false

Supported operators:

Operator Meaning Example
\| add/combine flags ReadUsers \| CreateUsers
& keep only shared flags permissions & required
^ toggle flags permissions ^ DeleteUsers
~ invert known bits in the current value length ~permissions

The helper methods mirror the common operators:

Method Operator Meaning
SetFlag \| add flags
UnsetFlag &~ remove flags
ToggleFlag ^ toggle flags

HasFlag checks for any overlap. This is the default because most permission-style checks ask: "does this value contain at least one of these flags?"

var required = Permissions.ReadUsers | Permissions.CreateUsers;

permissions.HasFlag(required); // true
Permissions.ReadUsers.HasFlag(required); // true

Use HasAllFlags when every requested flag must be present:

permissions.HasAllFlags(required); // true
Permissions.ReadUsers.HasAllFlags(required); // false

Set, unset, and toggle

var permissions = Permissions.None
    .SetFlag(Permissions.ReadUsers, Permissions.ViewReports);

permissions = permissions.UnsetFlag(Permissions.ViewReports);
permissions = permissions.ToggleFlag(Permissions.DeleteUsers);

Work with names

InfiniteEnum<T> can read the static flag fields you define.

var names = Permissions.GetNames().ToList();
// None, ReadUsers, CreateUsers, DeleteUsers, ViewReports

var selectedNames = Permissions.GetNames(
    Permissions.ReadUsers | Permissions.DeleteUsers);
// ReadUsers, DeleteUsers

var read = Permissions.FromName("ReadUsers");

var combined = Permissions.FromNames("ReadUsers", "ViewReports");

if (Permissions.TryFromName("DeleteUsers", out var deleteUsers))
{
    // use deleteUsers
}

All returns all non-empty flags:

var allPermissions = Permissions.All;

Store and restore values

Use ToId when you need a compact string value for storage. IDs are URL-safe, filename-safe, and database-friendly (alphabet A-Z a-z 0-9 - _, plus literal "0" for None).

var permissions = Permissions.ReadUsers | Permissions.ViewReports;

string id = permissions.ToId();
var restored = Permissions.FromId(id);

Console.WriteLine(permissions == restored); // true

Guarantees

  • Canonical. Equal flags always produce the same ID, regardless of the internal bit length they were constructed with.
  • Round-trip safe. Flag.FromId(flag.ToId()).Equals(flag) for every value.
  • Unique. Different flag values always produce different IDs.
  • Compact even at high indices. The encoder picks between a dense byte representation and a sparse delta-varint representation per value, whichever is shorter. High-index flags don't blow up the ID size.
Value ID
None 0
ReadUsers (bit 0) AAE
CreateUsers (bit 1) AAI
ReadUsers \| CreateUsers AAM
DeleteUsers (bit 2) AAQ
ViewReports (bit 100) AWQ

A flag at bit 10000 is roughly 4 characters, not ~1700. This makes InfiniteEnumFlags safe to use as a primary key, query parameter, or document identifier even for very sparse, high-index flag sets.

Wire format: [varint K][body]. K == 0 means dense canonical bytes follow. K > 0 means K delta-encoded bit indices follow. The format is self-describing — the decoder doesn't need a tag byte.

For plain padded base64 storage (e.g. when integrating with systems that already expect base64), use ToBase64String, ToBase64Trimmed, and FromBase64.

Scoped IDs

ToId stores only the flag value. That keeps IDs tiny and close to native enum behavior, but it also means an ID from one enum class can be syntactically valid for another.

If values from different enum classes may share the same database column, queue, or API field — or if you want to detect IDs that were generated for a different context — use scoped IDs:

string id = permissions.ToScopedId();
var restored = Permissions.FromScopedId(id);

The default scope is the enum class name. You can override it when multiple enum classes should intentionally share the same ID space, or when you want to bind IDs to a logical version:

string id = permissions.ToScopedId("permissions-v1");
var restored = Permissions.FromScopedId(id, "permissions-v1");

A scoped ID carries a 2-byte scope fingerprint and a masked payload, so:

  • The raw value ID does not appear verbatim inside a scoped ID.
  • FromScopedId throws InvalidOperationException when the scope doesn't match, giving you a cheap sanity check against ID misuse across contexts.

Scoped IDs are not a security boundary — they are a tamper-evident routing tag. Anything that needs cryptographic integrity should be signed separately.

Performance notes

  • Storage is a packed ulong[], canonicalized so trailing zero words are trimmed. Flag(5, length: 10000) and Flag(5) share the same single-word representation.
  • Bitwise operators (|, &, ^, ~) operate 64 bits at a time and the JIT auto-vectorizes the loops on x86 (AVX2) and ARM64 (NEON).
  • Count uses hardware POPCNT via BitOperations.PopCount.
  • IsEmpty is O(1).
  • Equality is a single SIMD Span<ulong>.SequenceEqual.
  • Set-bit enumeration uses TrailingZeroCount — one CPU instruction per set bit and zero work for empty words. This makes the sparse ID encoder cheap even for very wide flag sets.

Notes

  • Flag<T> values are immutable from public APIs.
  • Equality is canonical: equal logical flags always compare equal and hash equal, regardless of how they were constructed.
  • None is an empty flag set.
  • HasFlag(None) returns false because there are no bits to overlap.
  • HasAllFlags(None) returns true, because an empty requirement is always satisfied.

Supported targets

net6.0, net7.0, net8.0, net10.0.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  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 is compatible.  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 is compatible.  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 is compatible.  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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net6.0

    • No dependencies.
  • net7.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories (1)

Showing the top 1 popular GitHub repositories that depend on InfiniteEnumFlags:

Repository Stars
youssefbennour/AspNetCore.Starter
A modular-monolith ASP.NET Core starter inspired by Evolutionary-architecture