BeeneticToolkit.Collections 0.12.1

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

BeeneticToolkit.Collections

NuGet Downloads License: MIT

Type-safe "smart enums" — a richer alternative to a C# enum where each value is a real object that can carry data, be looked up by key/name, grouped, sorted, and searched. Plus a small thread-safe object pool.

Targets netstandard2.1 (modern .NET and Unity).

Install

dotnet add package BeeneticToolkit.Collections

Smart enums

A plain C# enum is just a named integer — you can't attach data to it, and parsing/looking it up is clumsy. A smart enum makes each value a strongly-typed object with whatever properties you need, while keeping the "fixed set of named values" feel.

There are two ways to declare one:

You want… Use Items are…
The normal case — a fixed set known at compile time AutoEnumItem<TSelf, TKey, TGroup> declared as static readonly fields, auto-registered
A set built from data at runtime (JSON, mods, DB rows) EnumItem + EnumCollection added manually via Add()

Start with AutoEnumItem. Reach for the manual pair only when items aren't known until runtime.


Declare your items as public static readonly fields on the type. That's it — they register themselves, and you get static lookups (FromKey, All, …) for free. No collection class, no Add calls, no hand-written lookup methods.

using BeeneticToolkit.Collections.Enums;

public enum PlanetGroup { None = 0, Rocky, GasGiant }

// The type names itself as TSelf — "public sealed class Planet : AutoEnumItem<Planet, …>".
public sealed class Planet : AutoEnumItem<Planet, string, PlanetGroup> {

    // Items: public static readonly fields of this type. They are discovered automatically.
    public static readonly Planet Earth   = new("earth",   "Earth",   "E", 5.97e24, PlanetGroup.Rocky);
    public static readonly Planet Mars    = new("mars",    "Mars",    "M", 6.42e23, PlanetGroup.Rocky);
    public static readonly Planet Jupiter = new("jupiter", "Jupiter", "J", 1.90e27, PlanetGroup.GasGiant);

    // Attach any data you like — just add properties.
    public double MassKg { get; }

    // A private constructor keeps the value set closed (only the fields above can exist).
    private Planet(string key, string name, string shortName, double massKg, PlanetGroup group)
        : base(key, name, shortName, group: group) {
        MassKg = massKg;
    }
}

Now use it through static members on the type itself:

Planet earth = Planet.FromKey("earth");              // O(1); throws if missing
if (Planet.TryFromKey("mars", out var mars)) { /* … */ }

Planet byName  = Planet.FromName("Jupiter");         // also O(1) (indexed by name)
Planet byShort = Planet.FromShortName("E");           // also O(1) (indexed by short name)

foreach (Planet p in Planet.All)                     // every item, in declaration order
    Console.WriteLine($"{p.Name}: {p.MassKg} kg");

int howMany = Planet.Count;

Key, name, and short-name lookups are all O(1) — keyed by a dictionary, with the name/short-name indexes built on first use and kept fresh as items are added or removed. Lookups are case-sensitive and, when names repeat, return the first item declared.

The constructor base parameters

AutoEnumItem requires key, name, and shortName, and accepts four optional values. Pass the optional ones by name:

private Planet(...) : base(
    key,                      // unique identity, any non-null type (string, an int enum, a Guid…)
    name,                     // display name, e.g. "Jupiter"
    shortName,                // abbreviation, e.g. "J"
    description: "…",         // optional
    displayOrder: 3,          // optional, for ordered display
    isActive: true,           // optional, filterable via isActive: below
    group: PlanetGroup.GasGiant) { }   // optional

Grouping

Supply a real enum as the third type argument to group items, then filter by group. Use the built-in NoGroup when you don't need grouping:

foreach (Planet rocky in Planet.GetByGroup(PlanetGroup.Rocky)) { /* … */ }

// No grouping needed:
public sealed class Currency : AutoEnumItem<Currency, string, NoGroup> { /* … */ }

Filtering, sorting, and searching

using BeeneticToolkit.Collections.Enums.Comparators;

// Active items only (uses the optional isActive flag on each item).
var active = Planet.GetAll(isActive: true);

// Sort with a built-in comparator (ByKey / ByName / ByShortName / ByDisplayOrder / ByActiveState / ByGroup).
var byName = Planet.GetAll(EnumItemComparators.ByName<string, PlanetGroup>());

// Substring search over any string property (case-insensitive by default).
var hits = Planet.Search(p => p.Name, "ar");          // → Mars

Why it's robust: registration is order-independent

Items register the first time you touch any static member of the type — and registration explicitly runs the type's static constructor first, so the set is always fully populated no matter what you touch first (Planet.All, Planet.FromKey(...), or a field like Planet.Earth). Initialization runs once, under the runtime's type-init lock, so it's thread-safe with no work on your part.

Unity / IL2CPP note: auto-registration uses reflection over the type's static fields. This works under IL2CPP, but aggressive managed-code stripping can remove members only ever accessed via reflection. Your item fields are normally also referenced directly in code (Planet.Earth), which keeps them — but verify on an actual IL2CPP build, or add a link.xml preserve rule for your enum types if you rely on stripping. If you need a reflection-free option, use the manual EnumCollection path below.


EnumItem + EnumCollection — dynamic / runtime-built sets

When the items aren't known at compile time — loaded from JSON, defined by mods, or read from a database — derive the item from EnumItem and add instances to an EnumCollection at runtime.

using BeeneticToolkit.Collections.Enums;

public sealed class Card : EnumItem<string, NoGroup> {
    public Card(string key, string name, string shortName, int cost)
        : base(key, name, shortName) => Cost = cost;

    public int Cost { get; }
}

public sealed class CardCatalog : EnumCollection<Card, string, NoGroup> { }

var catalog = new CardCatalog();
foreach (var dto in LoadCardsFromJson())              // built at runtime
    catalog.Add(new Card(dto.Key, dto.Name, dto.Short, dto.Cost));

Card fireball = catalog.FromKey("fireball");
var cheap = catalog.GetAll(EnumItemComparators.ByName<string, NoGroup>())
                   .Where(c => c.Cost <= 2);

EnumCollection supports Add/AddRange/Remove/RemoveRange and the same lookup/query surface (FromKey, FromName, FromShortName, GetAll, GetByGroup, Search) that AutoEnumItem exposes statically. AutoEnumItem is in fact built on these two types — this is the same engine, with manual control over the item set.

Equality and comparison (both paths)

Every item is equal to another when they share the same runtime type and key, and items sort by key by default. So items work correctly as dictionary keys, in sets, and in sorted collections.


Flag sets (EnumSet<T>)

EnumSet<T> is the smart-enum equivalent of a [Flags] enum — hold a combination of items as one value and do set algebra on it — but without the 64-value ceiling, the manual 1 << n bookkeeping, or losing type safety. It's backed by a compact bitmask (one bit per item), so membership tests and combinations are fast and the in-place operations allocate nothing.

For an AutoEnumItem, the factories come for free on the type:

EnumSet<Planet> inner = Planet.SetOf(Planet.Earth, Planet.Mars);
EnumSet<Planet> rocky = Planet.Domain.From(Planet.GetByGroup(PlanetGroup.Rocky));

bool hasEarth = inner.Contains(Planet.Earth);
EnumSet<Planet> everythingElse = ~inner;             // complement, relative to all planets
EnumSet<Planet> both = inner & rocky;                // intersection
bool subset = inner.IsSubsetOf(Planet.FullSet());

foreach (Planet p in inner) { /* yielded in declaration order */ }

Combine with operators (| union, & intersection, ^ symmetric difference, - difference, ~ complement), or mutate in place without allocating (Add, Remove, UnionWith, IntersectWith, ExceptWith, SymmetricExceptWith). Query with Contains, IsSubsetOf, IsSupersetOf, Overlaps, SetEquals, Count, IsEmpty.

The domain is the complete pool of valid items a set draws from — it's what complement is relative to, and only sets built from the same domain can be combined (mixing throws). For a runtime-built EnumCollection, snapshot a domain with collection.ToDomain():

EnumDomain<Card> domain = catalog.ToDomain();         // snapshot of the current items
EnumSet<Card> starter = domain.Of(catalog.FromKey("strike"), catalog.FromKey("defend"));

Secondary indexes

FromKey / FromName / FromShortName cover the built-in lookups. To look items up O(1) by any other property, register a secondary index. You get back a typed, queryable handle — no stringly-typed names:

// Runtime-built collection: the index stays in sync as items are added/removed.
EnumIndex<string, Card> byColor = catalog.AddIndex(c => c.Color);
IReadOnlyList<Card> reds = byColor.Get("Red");
Card theOnly = byColor.GetSingle("Colorless");        // throws if not exactly one

// Fixed smart enum: built once over the item set.
public sealed class Planet : AutoEnumItem<Planet, string, PlanetGroup> {
    public static readonly EnumIndex<PlanetGroup, Planet> ByGroup = IndexBy(p => p.Group ?? PlanetGroup.None);
}
var gasGiants = Planet.ByGroup.Get(PlanetGroup.GasGiant);

Query with Get (all matches), GetSingle / TryGetSingle (exactly one), Contains, Keys, and Count. Items whose indexed value is null are skipped.


Object pooling

A small, thread-safe object pool for reusing instances and avoiding allocations on hot paths.

using BeeneticToolkit.Collections.ObjectPooling;
using BeeneticToolkit.Collections.ObjectPooling.Policies;
using System.Text;

sealed class StringBuilderPolicy : PooledObjectPolicy<StringBuilder> {
    public override StringBuilder Create() => new StringBuilder();
    public override void Reset(StringBuilder sb) => sb.Clear();
    public override bool Validate(StringBuilder sb) => true;
}

var pool = new StackObjectPool<StringBuilder>(new StringBuilderPolicy());

using (pool.Rent(out var sb)) {   // automatically returned to the pool on dispose
    sb.Append("hello");
}

StackObjectPool<T> is thread-safe. Rent(out …) returns a PooledObjectScope<T> (a struct, so no allocation) that returns the object when disposed; Get() / Return(obj) are available for manual control.

Ring buffer

RingBuffer<T> is a fixed-capacity circular FIFO. Once full, adding overwrites and drops the oldest element — perfect for rolling windows like input/command history, recent frame-time or telemetry samples, netcode snapshots, or "last N events" logs. Backed by a single array, so it allocates nothing in use (foreach is allocation-free via a struct enumerator).

using BeeneticToolkit.Collections;

var recent = new RingBuffer<float>(60);   // keep the last 60 frame times
recent.Add(deltaTime);                    // overwrites the oldest once full

float newest = recent.PeekNewest();
float oldest = recent[0];                 // indexer: 0 = oldest … Count-1 = newest
float average = recent.Sum() / recent.Count;

foreach (float dt in recent) { /* oldest → newest */ }

Use Add for overwrite-oldest behavior, or TryAdd to reject when full (a bounded queue). Drain with RemoveOldest / TryRemoveOldest; inspect with PeekOldest / PeekNewest (and Try… variants), Count, Capacity, IsEmpty, IsFull.

Priority queue

PriorityQueue<TElement, TPriority> is a binary min-heap: each element is enqueued with a priority, and the lowest priority is always dequeued first. .NET 6+ ships one in the BCL, but netstandard2.1 / Unity do not — this is a drop-in with the same API, handy for event scheduling, turn/cooldown ordering, and pathfinding frontiers.

using BeeneticToolkit.Collections;

var events = new PriorityQueue<string, float>();   // priority = time
events.Enqueue("spawn wave", 12.5f);
events.Enqueue("tick",        1.0f);
events.Enqueue("boss",       60.0f);

string next = events.Peek();                        // "tick" (lowest time)
while (events.TryDequeue(out string e, out float t))
    Process(e, t);                                  // fired in time order

Pass an IComparer<TPriority> to change the ordering (e.g. a reverse comparer for a max-heap). Also provides Dequeue, EnqueueDequeue (enqueue then pop the min in one step), Clear, and Count.

On .NET 6+ the BCL also defines PriorityQueue. If you import both System.Collections.Generic and BeeneticToolkit.Collections, alias or fully qualify to disambiguate. On Unity/netstandard2.1 there's no conflict.

LRU cache

LruCache<TKey, TValue> is a fixed-capacity cache that evicts the least-recently-used entry when it fills up — so memory stays bounded for asset, result, or path caches. Lookups, inserts, and eviction are all O(1). Reading or writing a key marks it most-recently-used; TryPeek/ContainsKey deliberately don't.

using BeeneticToolkit.Collections;

var thumbnails = new LruCache<string, Texture>(capacity: 100);
thumbnails.Evicted += (key, tex) => tex.Dispose();         // release what falls out

Texture icon = thumbnails.GetOrAdd(path, LoadTexture);     // cached, or loaded + cached
if (thumbnails.TryGet(path, out Texture cached)) { /* hit, now most-recently-used */ }

Set / the indexer add-or-update; TryGet / GetOrAdd read (and bump recency); TryPeek / ContainsKey inspect without bumping; Remove, Clear, Count, Capacity, and a recency-ordered Keys round it out. Pass an IEqualityComparer<TKey> for things like case-insensitive string keys.

License

Licensed under the MIT License.

Product 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 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 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 netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.1

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on BeeneticToolkit.Collections:

Package Downloads
BeeneticToolkit

Meta-package that references the full BeeneticToolkit family: BeeneticToolkit.Random, BeeneticToolkit.Collections, BeeneticToolkit.Numerics, BeeneticToolkit.Logging, BeeneticToolkit.Spatial, and BeeneticToolkit.BigMath. Install this to get everything, or install the individual packages to take only what you need.

BeeneticToolkit.Spatial

Grids, pathfinding, visibility, and spatial queries for 2D games — engine-agnostic and deterministic. Hex and square grids (coordinates, layouts, lines, ranges/rings), A*/Dijkstra/BFS pathfinding and flow fields over an abstract graph, recursive-shadowcasting and hex line-of-sight field of view, and quadtree / spatial-hash partitioning with rectangle, radius, and nearest-neighbor queries. Uses integer coordinate structs and (float X, float Y) tuples instead of any engine vector type, so it stays portable across Unity, MonoGame, and plain .NET.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.12.1 162 7/15/2026
0.12.0 176 7/14/2026
0.11.0 172 7/14/2026
0.10.0 193 6/21/2026