Skip to content

Repository files navigation

HCommons

Tests

A C# commons library providing utility types and helper functions for .NET applications.

Features

  • 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

Installation

dotnet add package HCommons

Buffers

The HCommons.Buffers namespace provides memory-efficient array types that use ArrayPool<T> for reduced allocations and improved performance.

PooledArray

A disposable wrapper around a pooled array that must be disposed to return the array to the pool.

Key Features

  • 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

Basic Usage

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;

Ownership Semantics

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'

Prefer Passing Spans

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 approach

Creating Copies

Use 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 original

Slicing

using var original = PooledArray<int>.Rent(100);

using var slice = original.RentSlice(10, 20); // 20 elements starting at index 10

Returning to Pool

var pooled = PooledArray<int>.Rent(100);
PooledArray<int>.Return(ref pooled); // Explicitly return and dispose

PooledArrayBuilder

A 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 valid

Math

The HCommons.Math namespace provides mathematical helper methods and extensions for common operations.

HMath

Static utility methods for mathematical operations.

ClampToFloat

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.0f

Map

Maps 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.0

HMathExtensions

Extension 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.5

Monads

The HCommons.Monads namespace provides functional programming utilities for expressive and safe error handling.

Optional

Represents a value that may or may not be present, eliminating null reference issues.

Creating Optionals

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();

Checking and Accessing Values

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);

Pattern Matching

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")
);

Transformations

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;

Stateful Transformations

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"));

Either<TLeft, TRight>

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}")
);

Either Extensions

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);

Stateful Either Transformations

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; });

Result and Result

Represents the outcome of an operation that can succeed or fail with an error.

Result (no value)

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 (with value)

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 Extensions

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}"
);

Stateful Result Transformations

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));

OperationResult and OperationResult

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 Extensions

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);

Error

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");

Cancelled

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");

Types

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.

Target Frameworks

  • .NET 9.0
  • .NET 8.0
  • .NET Standard 2.1
  • .NET Standard 2.0

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

Commonly used types and extensions

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages