Wiaoj.Primitives
0.3.0-alpha.1
dotnet add package Wiaoj.Primitives --version 0.3.0-alpha.1
NuGet\Install-Package Wiaoj.Primitives -Version 0.3.0-alpha.1
<PackageReference Include="Wiaoj.Primitives" Version="0.3.0-alpha.1" />
<PackageVersion Include="Wiaoj.Primitives" Version="0.3.0-alpha.1" />
<PackageReference Include="Wiaoj.Primitives" />
paket add Wiaoj.Primitives --version 0.3.0-alpha.1
#r "nuget: Wiaoj.Primitives, 0.3.0-alpha.1"
#:package Wiaoj.Primitives@0.3.0-alpha.1
#addin nuget:?package=Wiaoj.Primitives&version=0.3.0-alpha.1&prerelease
#tool nuget:?package=Wiaoj.Primitives&version=0.3.0-alpha.1&prerelease
Wiaoj.Primitives
Wiaoj.Primitives is a high-performance, security-focused .NET foundation library engineered to eliminate Primitive Obsession in domain-driven applications.
In standard .NET development, developers frequently rely on generic primitives (string, byte[], double, long) to represent complex domain concepts like cryptographic keys, identifiers, semantic versions, and encoded payloads. This leads to defensive coding, memory leaks in the managed heap, and severe performance bottlenecks.
This library replaces these generic types with self-validating, strongly-typed value objects. Designed for modern .NET, it heavily utilizes ref struct, Span<T>, SIMD vectorization (SearchValues<T>), fixed-size buffers, and unmanaged memory manipulation to deliver zero-allocation performance and cryptographic-grade security.
Installation & Requirements
dotnet add package Wiaoj.Primitives
Factory Naming
Every type follows the same verbs, so the name of a factory tells you what it does:
| Verb | Meaning | Examples |
|---|---|---|
Parse / TryParse |
Read the type's canonical text form (ISpanParsable<T>, IUtf8SpanParsable<T>) |
SemVer.Parse("1.2.3"), UnixTimestamp.TryParse(text, out _) |
From / FromX / TryFrom |
Wrap one existing value: bytes, a number, a unit | Sha256Hash.FromBytes(bytes), UnixTimestamp.FromSeconds(exp), Ed25519PublicKey.From(x) |
Create / TryCreate |
Build from several parts | GeoCoordinate.Create(lat, lon), RsaPublicKey.Create(modulus, exponent), Urn.Create(nid, nss) |
Generate |
Produce new random key material | AesGcmKey.Generate256(), RsaKeyPair.Generate() |
New / NewId |
Produce a new identifier | SnowflakeId.NewId(), NanoId.NewId() |
Tryvariants: theTryform of each verb returnsfalseinstead of throwing, for untrusted input such as a JWTexpclaim.- Exception:
GuidV7.Create()keeps the name of the BCL'sGuid.CreateVersion7(), which it wraps.
1. Secure Memory Management (Secret<T>)
Standard .NET types like string and byte[] are managed by the Garbage Collector (GC). They are immutable, meaning any modification creates a copy, and they linger in memory until a GC cycle collects them. This makes them highly vulnerable to memory dumps and timing attacks.
Secret<T> solves this by allocating memory outside the managed heap using NativeMemory.AllocZeroed. The memory is pinned, inaccessible to the GC, and guaranteed to be cryptographically wiped (CryptographicOperations.ZeroMemory) when disposed.
using Wiaoj.Primitives;
// 1. Generate a cryptographically strong 256-bit AES key directly into secure memory
using Secret<byte> masterKey = Secret.Factory.Aes256Key();
// 2. Parse a secret from an external source (intermediate strings are wiped)
using Secret<byte> apiKey = Secret.From("super-secret-key-123");
// 3. Controlled Access (The Expose Pattern)
// Data cannot escape this scope. It prevents accidental logging or leaking.
apiKey.Expose(span =>
{
// 'span' is a ReadOnlySpan<byte> pointing directly to the unmanaged memory.
// It is safe to pass this to cryptographic algorithms.
Console.WriteLine($"Key length: {span.Length}");
});
// 4. Secure Key Derivation (HKDF-SHA256)
using Secret<byte> derivedKey = masterKey.DeriveKey(salt: apiKey, outputByteCount: 64);
2. Distributed Identity
Relying on standard auto-incrementing int or random Guid (v4) for primary keys leads to database B-Tree index fragmentation and leaks business intelligence (e.g., how many orders you process daily).
SnowflakeId & GuidV7 (Time-Ordered IDs)
SnowflakeId is a lock-free, thread-safe implementation of the Twitter Snowflake algorithm. It generates 64-bit, k-sorted identifiers that eliminate database fragmentation without requiring a central coordination server. GuidV7 provides the 128-bit RFC 9562 equivalent.
using Wiaoj.Primitives.Snowflake;
// Configure the node identity at application startup
SnowflakeId.Configure(nodeId: 1);
// Generate a 64-bit ID (Zero allocation)
SnowflakeId internalId = SnowflakeId.NewId();
UnixTimestamp creationTime = internalId.ToUnixTimestamp();
Public identifiers
OpaqueId and the Wiaoj.Primitives.Obfuscation namespace have been removed. Their obfuscators looked like encryption but were not: the keys came straight from the seed's first bytes, the ciphers were custom, and nothing detected a forged value. Strongly typed, prefixed public identifiers, with a plain or AES-encrypted form, are moving to Wiaoj.Identifiers.
NanoId
For completely random, highly collision-resistant public identifiers.
// Generates a 21-character URL-safe string.
// Uses a default profanity-safe alphabet (vowels removed).
NanoId videoId = NanoId.NewId();
3. Allocation-Free Cryptographic Hashing
Traditional hashing APIs return a byte[], forcing a heap allocation on every single hash computation.
Wiaoj.Primitives provides Sha256Hash, Sha512Hash, Md5Hash, and Hmac variants as fixed-size struct wrappers containing inline arrays (e.g., fixed byte[32]). They live entirely on the stack. Furthermore, their Equals operators use CryptographicOperations.FixedTimeEquals to prevent timing attacks.
using Wiaoj.Primitives.Cryptography.Hashing;
// Compute hash on the stack (Zero heap allocations)
Sha256Hash documentHash = Sha256Hash.Compute("document content");
// Constant-time equality comparison
if (documentHash == expectedHash) {
// Valid
}
// Memory-efficient async stream hashing (e.g., large file uploads)
using var stream = File.OpenRead("large_data.bin");
Sha256Hash streamHash = await Sha256HashExtensions.ComputeAsync(stream);
4. Strongly-Typed Encodings
A method requiring a Base64 payload should not accept a standard string. By using specialized types, the format is validated instantly upon instantiation using .NET 8 SearchValues<T> for SIMD-accelerated character scanning.
Available types: Base64String, Base64UrlString, Base32String, Base62String, HexString.
// Validates format instantly. Throws FormatException if invalid.
Base64String payload = Base64String.Parse("SGVsbG8gV29ybGQ=");
HexString signature = HexString.Parse("48656C6C6F");
// Decode directly to stack memory without allocating intermediate byte arrays
Span<byte> buffer = stackalloc byte[64];
if (signature.TryDecode(buffer, out int bytesWritten)) {
// Process raw bytes securely
}
5. Domain Primitives & Value Objects
Range<T>
A structural pattern that guarantees Min <= Max at creation. It provides deep domain-specific extensions based on the generic type.
// Numeric Ranges
Range<int> validAges = new Range<int>(18, 65);
int userAge = validAges.Clamp(70); // Returns 65
int span = validAges.Length(); // Returns 47
// Temporal Ranges
Range<DateTime> promotionPeriod = Range<DateTime>.Between(startDate, endDate);
bool isActive = promotionPeriod.IsNowWithin(); // Automatically checks against UTC Now
// Semantic Version Ranges
Range<SemVer> requiredVersion = new Range<SemVer>(SemVer.Parse("1.0.0"), SemVer.Parse("2.0.0"));
SemVer? latest = requiredVersion.GetLatestCompatible(availableVersions);
UnixTimestamp
Eliminates the ambiguity of passing long variables around by explicitly representing UTC milliseconds since Epoch. Provides low-overhead date manipulation without instantiating DateTime objects.
UnixTimestamp now = UnixTimestamp.Now;
// High-performance truncation using integer math
UnixTimestamp startOfDay = now.TruncateToDay();
// Temporal logic
bool isExpired = cacheExpiry.IsOlderThan(TimeSpan.FromMinutes(10));
TimeSpan remaining = cacheExpiry.TimeUntil();
Values are always within UnixTimestamp.MinValue…MaxValue (the range of DateTimeOffset):
- Creating:
FromSeconds,FromMilliseconds, thelongcast and arithmetic throwArgumentOutOfRangeExceptionoutside it. - Parsing:
TryParsereturnsfalse, and JSON throwsJsonException. - Untrusted numbers: such as a JWT
expclaim; use the non-throwing factories:
if(!UnixTimestamp.TryFromSeconds(expSeconds, out UnixTimestamp expiration)) {
return JwtParseStatus.InvalidPayloadJson;
}
MonotonicTimestamp arithmetic throws OverflowException instead of wrapping around, so now + TimeSpan.MaxValue can't turn into an instant in the past.
SemVer
A strict, allocation-free implementation of Semantic Versioning 2.0.0. It supports pre-release and build metadata parsing without the heavy overhead of System.Version.
SemVer current = SemVer.Parse("1.2.3-beta.1");
SemVer next = current.BumpMinor(); // 1.3.0
if (next.IsBackwardCompatibleWith(current)) {
// Validates Major/Minor precedence rules
}
Percentage
Wraps a double representing a value between 0.0 and 1.0. Prevents logic errors involving out-of-bounds percentages.
Percentage discount = Percentage.FromInt(15); // Represents 0.15
Percentage remaining = discount.Remaining; // Represents 0.85
double finalPrice = remaining.ApplyTo(100.0); // 85.0
OperationTimeout
Unifies API parameters that historically required passing both a TimeSpan (for absolute timeouts) and a CancellationToken (for external cancellation).
// Method signature expects a single unified object
public async ValueTask FetchDataAsync(OperationTimeout timeout)
{
// Throws instantly if already expired or cancelled
timeout.ThrowIfExpired();
// Create a combined CancellationTokenSource automatically
using var cts = timeout.CreateCancellationTokenSource();
await _httpClient.GetAsync("/api/data", cts.Token);
}
// Caller can pass either type, and it implicitly converts:
await FetchDataAsync(TimeSpan.FromSeconds(5));
await FetchDataAsync(HttpContext.RequestAborted);
System.Text.Json Integration
All primitives (SnowflakeId, Base64String, SemVer, UnixTimestamp, etc.) are decorated with high-performance JsonConverter implementations. They serialize directly to primitive JSON formats (strings or numbers) using UTF-8 span writers, bypassing intermediate string allocations entirely.
License
Licensed under the MIT 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
- Wiaoj.Preconditions (>= 0.3.0-alpha.1)
NuGet packages (17)
Showing the top 5 NuGet packages that depend on Wiaoj.Primitives:
| Package | Downloads |
|---|---|
|
Tyto
Package Description |
|
|
Wiaoj.Extensions
High-performance, general-purpose extension methods for the Wiaoj ecosystem. Built on top of Wiaoj.Primitives and Preconditions. |
|
|
Wiaoj.Serialization.Security
Encrypts what a Wiaoj serializer writes with a Wiaoj.Security key ring: authenticated AES-GCM that follows key rotation. |
|
|
Tyto.Caching.Abstractions
Package Description |
|
|
Tyto.Transports.InMemory
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.3.0-alpha.1 | 248 | 10/5/2026 |
| 0.2.0-alpha.3 | 191 | 9/24/2026 |
| 0.2.0-alpha.2 | 180 | 9/24/2026 |
| 0.2.0-alpha.1 | 182 | 9/24/2026 |
| 0.1.0-alpha.9 | 295 | 9/21/2026 |
| 0.1.0-alpha.8 | 184 | 9/21/2026 |
| 0.1.0-alpha.7 | 2,174 | 9/18/2026 |
| 0.1.0-alpha.6 | 179 | 9/16/2026 |
| 0.1.0-alpha.5 | 173 | 9/16/2026 |
| 0.1.0-alpha.4 | 175 | 9/16/2026 |
| 0.1.0-alpha.3 | 233 | 9/15/2026 |
| 0.1.0-alpha.2 | 503 | 9/15/2026 |
| 0.1.0-alpha.1 | 294 | 9/14/2026 |
| 0.0.1-alpha.112-preview | 168 | 9/13/2026 |
| 0.0.1-alpha.111-preview | 160 | 9/13/2026 |
| 0.0.1-alpha.110-preview | 156 | 9/12/2026 |
| 0.0.1-alpha.109-preview | 180 | 9/11/2026 |
| 0.0.1-alpha.108-preview | 181 | 9/8/2026 |
| 0.0.1-alpha.107-preview | 205 | 9/8/2026 |
| 0.0.1-alpha.106-preview | 179 | 9/8/2026 |