A C# commons library providing utility types and helper functions for .NET applications.
- Buffers: Memory-efficient pooled array types for high-performance scenarios
- Math: Mathematical helper methods and extensions for common operations
- Monads: Functional programming utilities for safe and expressive error handling
- Types: Cached runtime discovery of assignable types across loaded assemblies
dotnet add package HCommonsThe HCommons.Buffers namespace provides memory-efficient array types that use ArrayPool<T> for reduced allocations and improved performance.
A disposable wrapper around a pooled array that must be disposed to return the array to the pool.
- Automatically rents arrays from
ArrayPool<T>.Shared - Must be disposed to avoid memory leaks
- Supports indexing, spans, and memory operations
- Provides slicing and copying capabilities
using HCommons.Buffers;
// Rent a pooled array
using var pooled = PooledArray<int>.Rent(100);
// Access elements
pooled[0] = 42;
Console.WriteLine(pooled[0]); // 42
// Get a span for efficient operations
Span<int> span = pooled.Span;
span.Fill(1);
// Get memory
Memory<int> memory = pooled.Memory;PooledArray<T> is a struct. Copying it transfers ownership of the underlying array:
using var original = PooledArray<int>.Rent(10);
var copy = original; // Transfers ownership
// Only dispose once - either original or copy, not both
// Using statement will dispose 'original', so don't dispose 'copy'Best practice: Pass Span<T> to methods instead of PooledArray<T> to avoid ownership confusion:
void ProcessData(Span<int> data) {
// Work with data
}
using var pooled = PooledArray<int>.Rent(100);
ProcessData(pooled.Span); // Recommended approachUse RentCopy() when you need independent copies:
using var original = PooledArray<int>.Rent(10);
original.Span.Fill(42);
using var copy = original.RentCopy(); // Independent copy
copy[0] = 1; // Doesn't affect originalusing var original = PooledArray<int>.Rent(100);
using var slice = original.RentSlice(10, 20); // 20 elements starting at index 10var pooled = PooledArray<int>.Rent(100);
PooledArray<int>.Return(ref pooled); // Explicitly return and disposeA builder for constructing pooled arrays incrementally, similar to List<T> but using pooled memory.
using HCommons.Buffers;
// Create a builder
using var builder = new PooledArrayBuilder<int>();
// Add elements
builder.Add(1);
builder.Add(2);
builder.Add(3);
// Build and transfer ownership
using var result = builder.BuildAndDispose();
Console.WriteLine(result.Length); // 3
// Or create a copy while keeping builder valid
using var builder2 = new PooledArrayBuilder<int>();
builder2.Add(1);
using var copy = builder2.BuildCopy();
builder2.Add(2); // Builder still validThe HCommons.Math namespace provides mathematical helper methods and extensions for common operations.
Static utility methods for mathematical operations.
Clamps a double value to the float range and converts it to a float.
using HCommons.Math;
double largeValue = 1e100;
float clamped = HMath.ClampToFloat(largeValue); // Returns float.MaxValue
double smallValue = -1e100;
float clampedSmall = HMath.ClampToFloat(smallValue); // Returns float.MinValue
// With custom range
double value = 150.0;
float inRange = HMath.ClampToFloat(value, 0.0f, 100.0f); // Returns 100.0fMaps values from one range to another range. Available for int, float, and double types.
using HCommons.Math;
// Map integer from 0-100 to 0-255
int brightness = 75;
int byteValue = HMath.Map(brightness, 0, 100, 0, 255); // 191 (75% of 255)
// Map float from 0-1 to -1 to 1
float normalized = 0.75f;
float scaled = HMath.Map(normalized, 0f, 1f, -1f, 1f); // 0.5
// Map double for precise calculations (Celsius to Fahrenheit)
double temperature = 20.0;
double fahrenheit = HMath.Map(temperature, 0.0, 100.0, 32.0, 212.0); // 68.0Extension methods that provide the same functionality as HMath but in a fluent style.
using HCommons.Math;
// ClampToFloat as extension
double value = 1e100;
float clamped = value.ClampToFloat(); // Returns float.MaxValue
// Map as extension
int brightness = 75;
int byteValue = brightness.Map(0, 100, 0, 255); // 191 (75% of 255)
float normalized = 0.75f;
float scaled = normalized.Map(0f, 1f, -1f, 1f); // 0.5The HCommons.Monads namespace provides functional programming utilities for expressive and safe error handling.
Represents a value that may or may not be present, eliminating null reference issues.
using HCommons.Monads;
// Some value
Optional<int> some = Optional<int>.Some(42);
Optional<int> someImplicit = 42; // Implicit conversion
// No value
Optional<int> none = Optional<int>.None();Optional<int> opt = 42;
if (opt.HasValue) {
Console.WriteLine(opt.Value); // 42
}
// Try pattern
if (opt.TryGetValue(out var value)) {
Console.WriteLine(value);
}
// Get value or default
int val = opt.GetValueOrDefault(0);var message = opt.Match(
onValue: v => $"Value is {v}",
onNone: () => "No value"
);
// With actions
opt.Switch(
onValue: v => Console.WriteLine($"Got {v}"),
onNone: () => Console.WriteLine("Nothing")
);Optional<int> opt = 42;
// Select (map)
Optional<string> str = opt.Select(x => x.ToString());
// Bind (flatMap)
Optional<int> doubled = opt.Bind(x => Optional<int>.Some(x * 2));
// Where (filter)
Optional<int> filtered = opt.Where(x => x > 10);
// SelectMany (LINQ query syntax)
var result = from x in opt
from y in Optional<int>.Some(10)
select x + y;All transformation methods (Select, Bind, Where, Match, Switch, and SelectMany) have overloads that accept a state parameter, allowing you to pass additional context without closures:
Optional<int> opt = 42;
// Select with state
var multiplier = 2;
Optional<int> doubled = opt.Select(multiplier, (state, x) => x * state);
// Bind with state
var options = new Dictionary<int, string>();
Optional<string> lookup = opt.Bind(options, (dict, key) =>
dict.TryGetValue(key, out var val) ? Optional<string>.Some(val) : Optional<string>.None());
// Where with state
var threshold = 10;
Optional<int> filtered = opt.Where(threshold, (limit, x) => x > limit);
// Match with state
var context = "Result";
string message = opt.Match(context,
(ctx, v) => $"{ctx}: {v}",
ctx => $"{ctx}: None");
// Switch with state
var logger = new Logger();
opt.Switch(logger,
(log, v) => log.Info($"Got {v}"),
log => log.Info("No value"));Represents a value that is one of two possible types, commonly used for success/error scenarios.
using HCommons.Monads;
// Create Either
Either<string, int> left = "Error occurred";
Either<string, int> right = 42;
// Check which side
if (right.IsRight) {
Console.WriteLine(right.Right); // 42
}
// Pattern matching
var message = right.Match(
leftFunc: err => $"Error: {err}",
rightFunc: val => $"Success: {val}"
);
// Switch with actions
right.Switch(
leftAction: err => Console.WriteLine($"Failed: {err}"),
rightAction: val => Console.WriteLine($"Got: {val}")
);The Either type includes powerful extension methods for transformations and conversions:
using HCommons.Monads;
Either<string, int> either = 42;
// Swap left and right
Either<int, string> swapped = either.Swap(); // Right(42) becomes Left(42)
// Convert to Optional
Optional<int> rightOpt = either.AsRightOptional(); // Some(42)
Optional<string> leftOpt = either.AsLeftOptional(); // None
// Map both sides
Either<int, string> mapped = either.Map(
leftMapper: err => err.Length,
rightMapper: val => val.ToString()
);
// Map only one side
Either<string, string> rightMapped = either.MapRight(x => x.ToString());
Either<int, int> leftMapped = either.MapLeft(err => err.Length);Either also supports stateful transformations for Map, MapLeft, MapRight, Match, and Switch:
Either<string, int> either = 42;
// Map with state
var formatter = new NumberFormatter();
Either<string, string> formatted = either.Map(formatter,
(fmt, err) => fmt.FormatError(err),
(fmt, val) => fmt.FormatValue(val));
// Match with state
var logger = new Logger();
var result = either.Match(logger,
(log, err) => { log.Error(err); return -1; },
(log, val) => { log.Info($"Got {val}"); return val; });Represents the outcome of an operation that can succeed or fail with an error.
using HCommons.Monads;
Result DoOperation() {
try {
// Some operation
return Result.Success();
} catch (Exception ex) {
return Result.Failure(Error.FromException(ex));
}
}
var result = DoOperation();
if (result.IsSuccess) {
Console.WriteLine("Operation succeeded");
} else {
Console.WriteLine($"Operation failed: {result.Error}");
}Result<int> Divide(int a, int b) {
if (b == 0)
return Result<int>.Failure(Error.WithMessage("Division by zero"));
return Result<int>.Success(a / b);
}
var result = Divide(10, 2);
// Pattern matching
var message = result.Match(
onSuccess: value => $"Result: {value}",
onFailure: error => $"Error: {error.Message}"
);
// Get value or default
int value = result.GetValueOrDefault(-1);
// Try pattern
if (result.TryGetValue(out var val)) {
Console.WriteLine(val);
}Result and Result<TValue> have extension methods for functional transformations:
using HCommons.Monads;
Result<int> result = Result<int>.Success(42);
// Select (map) to transform success values
Result<string> strResult = result.Select(x => x.ToString());
// Bind (flatMap) to chain operations that return results
Result<int> doubled = result.Bind(x =>
x > 0 ? Result<int>.Success(x * 2) : Result<int>.Failure("Value must be positive"));
// MapError to transform errors
Result<int> remapped = result.MapError(err =>
Error.WithMessage($"Operation failed: {err.Message}"));
// Match for pattern matching
var message = result.Match(
onSuccess: val => $"Got {val}",
onFailure: err => $"Error: {err.Message}"
);All Result transformation methods support stateful overloads:
Result<int> result = Result<int>.Success(42);
// Select with state
var multiplier = 2;
Result<int> doubled = result.Select(multiplier, (m, x) => x * m);
// Bind with state
var validator = new Validator();
Result<int> validated = result.Bind(validator, (v, x) =>
v.IsValid(x) ? Result<int>.Success(x) : Result<int>.Failure("Invalid"));
// MapError with state
var logger = new Logger();
Result<int> logged = result.MapError(logger, (log, err) => {
log.Error(err.Message);
return err;
});
// Match with state
var formatter = new Formatter();
string formatted = result.Match(formatter,
(fmt, val) => fmt.FormatSuccess(val),
(fmt, err) => fmt.FormatError(err));Extends Result with cancellation support for operations that can be cancelled.
using HCommons.Monads;
OperationResult<int> PerformOperation(CancellationToken ct) {
if (ct.IsCancellationRequested)
return OperationResult<int>.Cancelled(Cancelled.Because("User cancelled"));
try {
int result = 42; // Some operation
return OperationResult<int>.Success(result);
} catch (Exception ex) {
return OperationResult<int>.Failure(Error.FromException(ex));
}
}
var result = PerformOperation(CancellationToken.None);
if (result.IsSuccess) {
Console.WriteLine($"Success: {result.Value}");
} else if (result.IsCancelled) {
Console.WriteLine($"Cancelled: {result.Cancellation.Reason}");
} else {
Console.WriteLine($"Failed: {result.Error.Message}");
}OperationResult<TValue> supports the same functional transformations as Result<TValue>, with cancellation awareness:
using HCommons.Monads;
OperationResult<int> opResult = OperationResult<int>.Success(42);
// Select preserves cancellation state
OperationResult<string> strResult = opResult.Select(x => x.ToString());
// Bind chains operations
OperationResult<int> doubled = opResult.Bind(x =>
OperationResult<int>.Success(x * 2));
// All transformation methods have stateful overloads
var multiplier = 3;
OperationResult<int> tripled = opResult.Select(multiplier, (m, x) => x * m);Represents an error with a message and optional exception.
using HCommons.Monads;
// From message
Error error1 = Error.WithMessage("Something went wrong");
Error error2 = "Something went wrong"; // Implicit conversion
// From exception
try {
throw new InvalidOperationException("Bad operation");
} catch (Exception ex) {
Error error3 = Error.FromException(ex);
Error error4 = ex; // Implicit conversion
Console.WriteLine(error3.Message);
Console.WriteLine(error3.HasException); // true
}
// Error with value
Error<int> error5 = Error<int>.WithMessage(42, "Partial result available");Represents a cancellation with a reason and optional associated value.
using HCommons.Monads;
// Simple cancellation
Cancelled cancellation1 = Cancelled.Because("User requested");
Cancelled cancellation2 = "User requested"; // Implicit conversion
// Cancellation with value
Cancelled<int> cancellation3 = Cancelled<int>.Because(42, "Partial result");The HCommons.Reflection namespace provides cached runtime type discovery without taking a dependency on Unity.
using HCommons.Reflection;
IReadOnlyList<Type> handlers = RuntimeTypeCache.TypesDerivedFrom<IHandler>();
RuntimeTypeFilter concreteFilter = RuntimeTypeFilters.Concrete();
IReadOnlyList<Type> concreteHandlers =
RuntimeTypeCache.TypesDerivedFrom<IHandler>(concreteFilter);
using IDisposable binding = RuntimeTypeCache.Bind<IHandler>(updatedHandlers => {
// The first snapshot is delivered immediately. Later snapshots are delivered
// when newly loaded assemblies add matching types.
});Reusable filters support built-in conditions, immutable record rules, Boolean composition, direct
Matches(Type) evaluation, and explicit filtered-snapshot caching through .Cached(). Delegate
predicate overloads remain available for one-off uncached filtering, including a stateful
Where<TState> form that avoids closure allocation when used with a static lambda. All filter forms
work with Bind, runtime-selected Type queries, generated catalogs, and reflection-backed
results.
HCommons.Reflection includes a source generator. Concrete generic calls and direct
typeof(...) calls automatically emit per-assembly type catalogs:
IReadOnlyList<Type> genericHandlers = RuntimeTypeCache.TypesDerivedFrom<IHandler>();
IReadOnlyList<Type> typedHandlers = RuntimeTypeCache.TypesDerivedFrom(typeof(IHandler));Mark shared contracts when implementations can be compiled in other assemblies:
[GenerateRuntimeTypeCache]
public interface IHandler { }Complete generated catalogs avoid Assembly.GetTypes() for their assembly and base type.
Runtime Type variables, open generic queries, private/file-local implementations, assemblies
built without the generator, and dynamically loaded code retain the reflection fallback. The
same generated catalogs are used by Bind, including updates from later loaded assemblies.
The generator is packaged under analyzers/dotnet/cs and targets the Unity 6-compatible
Roslyn 4.3 / .NET Standard 2.0 toolchain. NuGetForUnity recognizes analyzers in this layout,
but Unity must ultimately import the DLL with plugin platforms disabled and the exact
RoslynAnalyzer asset label. Verify those importer settings when using an older
NuGetForUnity release or if the generator does not run on the first package refresh.
Reflection metadata can be removed by .NET trimming and Unity managed code stripping. Applications must preserve types that are discovered only through this API. In Unity, use a link.xml file, [Preserve], or an equivalent linker annotation. UnityEditor.TypeCache is faster for editor-only discovery; RuntimeTypeCache is intended for player/runtime and shared-library code.
Assemblies loaded after the first query are scanned incrementally. Runtime-emitted types added after their assembly has loaded are not detectable automatically; call RuntimeTypeCache.Clear() to force a complete rescan.
The cache holds strong references to discovered Assembly and Type objects. Clear it and release returned snapshots before unloading a collectible AssemblyLoadContext.
- .NET 9.0
- .NET 8.0
- .NET Standard 2.1
- .NET Standard 2.0
This project is licensed under the MIT License - see the LICENSE file for details.