Lightweight namespaced identifier + registry library with freezing, reverse lookups, and thread-safe bootstrap.
$ dotnet add package RegistrarA tiny, zero-dependency (other than the BCL) library that provides a simple, namespaced identifier type and in-memory registries for mapping those identifiers to arbitrary reference-type objects. Targets .NET 8.0 and .NET Standard 2.1 for broad compatibility.
Lightweight, strongly-typed key space for plugins, data packs, game content, or any scenario needing stable IDs like
my-mod:items/sword.
Identifier struct with validated namespace:path formatRegistry.Register(registry, id, value)SimpleDefaultedRegistry<T>) with fallback value/id/raw idregistry.Freeze()) to prevent further mutationnamespace:path
[a-z0-9_.-]+[a-z0-9_.\-/]+example_namespace:some/path, vanilla:items/health_potionCreation helpers:
Identifier.FromNamespaceAndPath(ns, path) – validates & throws on failure.Identifier.Parse("ns:path") – throws on invalid format.Identifier.TryParse(string, out Identifier? id) – returns true/false without throwing.Inspired by Minecraft’s model:
InvalidOperationException("Registry is already frozen").Currently all registries can be frozen manually by calling Freeze(). A recommended pattern is to freeze core registries after loading vanilla + mod content.
Attempting to register AFTER freezing:
Registry.Register(Items.Registry, Identifier.FromNamespaceAndPath("vanilla","late"), new Item());
// => InvalidOperationException("Registry is already frozen")Value -> Identifier / RawId lookups are now O(1). A single value may only be registered under one identifier. Attempting to register the SAME value under a different identifier throws. Re-registering an existing identifier returns the already stored value (idempotent for that ID).
var registry = new SimpleRegistry<string>();
var swordId = Identifier.FromNamespaceAndPath("demo", "items/iron_sword");
Registry.Register(registry, swordId, "Sword");
registry.Freeze(); // make immutable
var sword = registry.Get(swordId); // "Sword"var defaultId = Identifier.FromNamespaceAndPath("base", "items/missing");
var defaulted = new SimpleDefaultedRegistry<string>("<missing>", defaultId);
// Not found -> returns default
var missing = defaulted.Get(Identifier.FromNamespaceAndPath("demo","items/not_there")); // "<missing>"Freeze() is idempotent.Freeze() while no registration is in progress (call it after bootstrap phase).ArgumentException / FormatException).GetRandom(Random) throws if registry empty.dotnet testCovers identifier validation, defaulted behavior, reverse lookup, concurrency, and freezing.
Potential roadmap items may introduce breaking changes until 1.0 (e.g., specialized dynamic registries). Uniqueness enforcement & proper TryParse pattern already introduced.
MIT – see LICENSE.md
Small by design; clarity over premature optimization. Contributions welcome.