LibSharp 4.0.0

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

LibSharp

Introduction

A library of C# core components that enhance the standard library. Supports .NET 8.0, .NET 9.0, .NET 10.0.

The public API ships nullable reference type annotations. The library is trim- and Native AOT-friendly, with the exception of the XML serialization helpers, which depend on XmlSerializer and are annotated with [RequiresUnreferencedCode] / [RequiresDynamicCode].

Installation

dotnet add package LibSharp

LibSharp consists of the following namespaces:

  • Common - contains extension methods for standard .NET types, as well as commonly used utilities and value types.
  • Collections - contains extension methods for standard .NET library collections, as well as additional collection types.
  • Caching - contains classes that enable in-memory value caching with custom time-to-live. Both synchronous and asynchronous versions are available.
  • Threading - contains an async-compatible lock and utilities for controlling action invocation frequency.

Performance Benchmarks

BenchmarkDotNet setup and benchmark scripts are available in https://github.com/danylofitel/LibSharp/blob/main/benchmarks/README.md.

Components and Usage

Common

Common namespace contains:

  • The static class Argument for convenient validation of public function arguments.
  • Extension methods for built-in types such as string, int, DateTime, Func, and Regex.
  • Optional<T> — a value type that wraps an optional value.
  • Result<T, TError> — a discriminated union value type for success/error outcomes.
    using LibSharp.Common;

    public static async Task CommonExamples(string stringParam, long longParam, object objectParam, CancellationToken cancellationToken)
    {
        // Argument validation — the parameter name is captured automatically (CallerArgumentExpression);
        // pass it explicitly only when you want a different name.
        Argument.EqualTo(stringParam, "Hello world");
        Argument.NotEqualTo(stringParam, "Hello");

        Argument.GreaterThan(longParam, -1L);
        Argument.GreaterThanOrEqualTo(longParam, 0L);
        Argument.LessThan(longParam, 100L);
        Argument.LessThanOrEqualTo(longParam, 99L);

        Argument.NotNull(stringParam);
        Argument.NotNullOrEmpty(stringParam);
        Argument.NotNullOrWhiteSpace(stringParam);

        Argument.OfType(objectParam, typeof(List<string>));

        // Optional<T> — wraps a value that may or may not be present
        Optional<int> empty = default;
        bool hasValue = empty.HasValue;             // false
        int fallback = empty.GetValueOrDefault(-1); // -1

        Optional<int> present = new Optional<int>(42);
        hasValue = present.HasValue;                // true
        int optValue = present.Value;               // 42
        bool got = present.TryGetValue(out int v);  // true, v == 42

        Optional<int> implicitlyWrapped = 7;                        // implicit conversion from T
        string label = present.Match(x => $"has {x}", () => "none");// project both cases -> "has 42"
        Optional<string> mapped = present.Map(x => x.ToString());   // Optional<string> "42"
        Optional<int> bound = present.Bind(                         // chain another Optional
            x => x > 0 ? new Optional<int>(x * 2) : default);       // Optional<int> 84

        // Result<T, TError> — discriminated union for success/error outcomes
        Result<int, string> success = Result<int, string>.Ok(42);
        bool isSuccess = success.IsSuccess;                     // true
        int successValue = success.Value;                       // 42

        Result<int, string> failure = Result<int, string>.Fail("not found");
        bool isError = failure.IsError;                         // true
        string errorMessage = failure.Error;                    // "not found"
        int valueOrDefault = failure.GetValueOrDefault(-1);     // -1

        string outcome = success.Match(x => $"ok: {x}", e => $"error: {e}"); // "ok: 42"
        Result<string, string> okMapped = success.Map(x => x.ToString());    // Ok("42")
        Result<int, int> errMapped = failure.MapError(e => e.Length);        // Fail(9)
        Result<int, string> chained = success.Bind(x => x >= 0               // chain another Result
            ? Result<int, string>.Ok(x + 1)
            : Result<int, string>.Fail("negative"));                         // Ok(43)

        // DateTime extensions
        DateTime fromEpochMilliseconds = longParam.FromEpochMilliseconds();
        DateTime fromEpochSeconds = longParam.FromEpochSeconds();
        long epochMilliseconds = DateTime.UtcNow.ToEpochMilliseconds();
        long epochSeconds = DateTime.UtcNow.ToEpochSeconds();

        // Func extensions — run an async operation with a cooperative timeout
        Func<CancellationToken, Task<int>> task = async ct =>
        {
            // Example operation that observes cancellation
            await Task.Delay(TimeSpan.FromSeconds(10), ct);
            return 99;
        };

        int taskResult = await task.RunWithTimeout(TimeSpan.FromSeconds(1), cancellationToken);

        // Int extensions
        bool convertedFromInt = 200.TryConvertToEnum<HttpStatusCode>(out HttpStatusCode statusCode);

        // String extensions
        bool convertedFromString = "OK".TryConvertToEnum<HttpStatusCode>(out HttpStatusCode statusCode2);

        string base64Encoded = stringParam.Base64Encode();
        string base64Decoded = base64Encoded.Base64Decode();

        string reversed = stringParam.Reverse();
        string truncated = stringParam.Truncate(10);
        string textElementTruncated = stringParam.TruncateTextElements(10);

        // Regex extensions — safe wrappers that catch RegexMatchTimeoutException
        Regex regex = new Regex(pattern: "\\s+brown\\s+", options: RegexOptions.None, matchTimeout: TimeSpan.FromSeconds(1));

        bool isMatch = regex.TryIsMatch("the quick brown fox", out bool isMatchTimedOut);
        Match match = regex.TryMatch("the quick brown fox", out bool matchTimedOut);
        string replaced = regex.TryReplace("the quick brown fox", " red ", out bool replaceTimedOut);

        // Type extensions
        IComparer<int> intComparer = TypeExtensions.GetDefaultComparer<int>();

        // XML serialization extensions
        // Note: these rely on XmlSerializer and are not compatible with trimming or Native AOT.
        string serializedToXml = objectParam.SerializeToXml();
        List<string> deserializedFromXml = serializedToXml.DeserializeFromXml<List<string>>();
    }

Collections

Collections namespace contains extension methods for ICollection, IDictionary, IEnumerable, and IAsyncEnumerable interfaces, plus ConcurrentHashSet<T>, MinPriorityQueue<T>, and MaxPriorityQueue<T> collections.

    using LibSharp.Collections;

    public static async Task CollectionsExamples(CancellationToken cancellationToken)
    {
        // ICollection extensions
        ICollection<int> collection = new List<int>();  // []
        collection.AddRange(Enumerable.Range(0, 10));   // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

        // IDictionary extensions
        IDictionary<string, string> dictionary = new Dictionary<string, string>();

        _ = dictionary.AddOrUpdate(
            "key",
            "addedValue",
            (key, existingValue) => "updatedValue");

        _ = dictionary.AddOrUpdate(
            "key",
            key => "addedValue",
            (key, existingValue) => "updatedValue");

        _ = dictionary.AddOrUpdate(
            "key",
            (key, argument) => "addedValue" + argument,
            (key, existingValue, argument) => "updatedValue" + argument,
            "argument");

        _ = dictionary.GetOrAdd(
            "key",
            "addedValue");

        _ = dictionary.GetOrAdd(
            "key",
            keyValue => "addedValue");

        _ = dictionary.GetOrAdd(
            "key",
            (keyValue, argument) => "addedValue" + argument,
            "argument");

        IDictionary<string, string> newCopy = dictionary.Copy();

        IDictionary<string, string> destination = new Dictionary<string, string>();
        IDictionary<string, string> result = dictionary.CopyTo(destination);

        // IEnumerable extensions
        List<List<int>> chunks = Enumerable.Range(0, 10).Chunk(20, item => item).ToList();
        // Grouped by total weight ≤ 20: [ [0, 1, 2, 3, 4, 5], [6, 7], [8, 9] ]

        IEnumerable<int> enumerable = Enumerable.Range(0, 100).Concat(Enumerable.Range(0, 100)).ToList();
        int firstIndex = enumerable.FirstIndexOf(x => x == 51);  // 51
        int lastIndex = enumerable.LastIndexOf(x => x == 51);    // 151

        int[] shuffled = enumerable.Shuffle();

        // IAsyncEnumerable extensions
        IAsyncEnumerable<int> asyncEnumerable = GetNumbersAsync();
        List<List<int>> asyncChunks = await CollectAsync(asyncEnumerable.Chunk(20, item => item), cancellationToken);
        // Grouped by total weight ≤ 20: [ [0, 1, 2, 3, 4, 5], [6, 7], [8, 9], ... ]

        int asyncFirstIndex = await asyncEnumerable.FirstIndexOfAsync(x => x == 51, cancellationToken);
        int asyncLastIndex = await asyncEnumerable.LastIndexOfAsync(x => x == 51, cancellationToken);

        // ConcurrentHashSet<T> — thread-safe hash set implementing ISet<T> and IReadOnlySet<T>
        ConcurrentHashSet<int> set = new ConcurrentHashSet<int>();
        bool added = set.Add(1);         // true
        added = set.Add(1);              // false — already present
        bool contains = set.Contains(1); // true
        bool removed = set.Remove(1);    // true

        // Set algebra operations (not atomic at the collection level)
        set.UnionWith(new[] { 2, 3 });
        set.IntersectWith(new[] { 2, 4 });
        set.ExceptWith(new[] { 4 });
        bool subset = set.IsSubsetOf(new[] { 1, 2, 3 });
        bool equal = set.SetEquals(new[] { 2 });

        // Min priority queue
        MinPriorityQueue<int> minPq = new MinPriorityQueue<int>();
        minPq.Enqueue(2);
        minPq.Enqueue(1);
        minPq.Enqueue(3);

        _ = minPq.Peek();    // 1 — smallest element, not removed
        _ = minPq.Dequeue(); // 1
        _ = minPq.Dequeue(); // 2
        _ = minPq.Dequeue(); // 3

        bool minHasValue = minPq.TryPeek(out int minPeeked);
        bool minRemoved = minPq.TryDequeue(out int minDequeued);

        // Max priority queue
        MaxPriorityQueue<int> maxPq = new MaxPriorityQueue<int>();
        maxPq.Enqueue(2);
        maxPq.Enqueue(1);
        maxPq.Enqueue(3);

        _ = maxPq.Peek();    // 3 — largest element, not removed
        _ = maxPq.Dequeue(); // 3
        _ = maxPq.Dequeue(); // 2
        _ = maxPq.Dequeue(); // 1

        bool maxHasValue = maxPq.TryPeek(out int maxPeeked);
        bool maxRemoved = maxPq.TryDequeue(out int maxDequeued);
    }

    private static async IAsyncEnumerable<int> GetNumbersAsync()
    {
        for (int i = 0; i < 200; i++)
        {
            await Task.Yield();
            yield return i;
        }
    }

    private static async Task<List<T>> CollectAsync<T>(IAsyncEnumerable<T> source, CancellationToken cancellationToken)
    {
        List<T> results = new List<T>();

        await foreach (T item in source.WithCancellation(cancellationToken))
        {
            results.Add(item);
        }

        return results;
    }

Threading

Threading namespace contains an async-compatible mutual exclusion lock and utilities for controlling how frequently an action can fire. ThrottledAction and DebouncedAction accept an optional TimeProvider (defaulting to TimeProvider.System), so their timing can be driven deterministically with a FakeTimeProvider in tests.

    using LibSharp.Threading;

    public static async Task ThreadingExamples(CancellationToken cancellationToken)
    {
        // AsyncLock — async-compatible mutual exclusion lock (not re-entrant)
        using AsyncLock asyncLock = new AsyncLock();

        using (AsyncLock.Handle handle = await asyncLock.AcquireAsync(cancellationToken))
        {
            // Only one caller can be inside this block at a time
        }

        // DebouncedAction — fires only after a quiet period since the last invocation
        using DebouncedAction debounced = new DebouncedAction(
            () => Console.WriteLine("Fired"),
            delay: TimeSpan.FromMilliseconds(300));

        debounced.Invoke(); // timer starts
        debounced.Invoke(); // timer resets
        debounced.Invoke(); // timer resets again — action fires 300 ms after this last call
        // Important: do not call debounced.Dispose() from inside its callback.
        // Dispose waits for callback completion and can deadlock in that pattern.

        // ThrottledAction — executes at most once per interval
        ThrottledAction throttled = new ThrottledAction(
            () => Console.WriteLine("Fired"),
            interval: TimeSpan.FromSeconds(1));

        throttled.Invoke(); // executes immediately
        throttled.Invoke(); // ignored — within the 1-second window
        await Task.Delay(TimeSpan.FromSeconds(1));
        throttled.Invoke(); // executes again — window has expired
    }

Caching

Caching namespace contains a number of classes for thread-safe lazy initialization and caching of in-memory values.

Notes:

  • All caches accept an optional TimeProvider (defaulting to TimeProvider.System). Pass a FakeTimeProvider in tests to drive expiration and background refresh deterministically, without real delays.
  • Some of the classes implement IDisposable interface and should be correctly disposed.
  • Be cautious when caching types that implement IDisposable interface as the values will not be automatically disposed by the caches.
  • Be cautious when using classes with LazyThreadSafetyMode.PublicationOnly behavior together with IDisposable types as discarded instances will not be disposed.
  • PublicationOnly implementations may run multiple factories concurrently and publish the first successful result.
  • Async lazy and initializer methods throw InvalidOperationException if a factory returns a null Task.

Quick selection guide:

  • Use LazyAsyncExecutionAndPublication<T> when you want to provide the factory in the constructor and allow at most one in-flight async initialization.
  • Use LazyAsyncPublicationOnly<T> when duplicate concurrent factory executions are acceptable and you want the first successful result to win.
  • Use Initializer<T> / InitializerAsync*<T> when the value should still be initialized once, but the factory is only known at call time.
  • Use ValueCache<T> / ValueCacheAsync<T> when you need one cached value that expires and refreshes over time.
  • Use KeyValueCache<TKey, TValue> / KeyValueCacheAsync<TKey, TValue> when you need the same expiration/refresh behavior per key, and the set of keys is limited.
  • Use ProactiveAsyncCache<T> when refresh should happen in the background before expiry instead of on-demand by the next reader.
Lazy

Two different implementations of async lazy values are available — LazyAsyncPublicationOnly and LazyAsyncExecutionAndPublication. Those are async versions of System.Lazy class with LazyThreadSafetyMode.PublicationOnly and LazyThreadSafetyMode.ExecutionAndPublication modes respectively. The reason that async lazy implementations are separate classes is that LazyAsyncExecutionAndPublication implements IDisposable due to its usage of an instance of SemaphoreSlim whereas LazyAsyncPublicationOnly does not need to implement IDisposable.

LazyAsyncExecutionAndPublication runs at most one in-flight factory and retries after failed or canceled attempts. LazyAsyncPublicationOnly may execute multiple concurrent factories, but only the first successfully published value is retained.

    using LibSharp.Caching;

    public static async Task LazyAsyncPublicationOnlyExample(Func<CancellationToken, Task<int>> factory, CancellationToken cancellationToken)
    {
        LazyAsyncPublicationOnly<int> lazy = new LazyAsyncPublicationOnly<int>(factory);

        bool hasValue = lazy.HasValue;                              // false
        int value = await lazy.GetValueAsync(cancellationToken);    // factory invoked
        hasValue = lazy.HasValue;                                   // true
        value = await lazy.GetValueAsync(cancellationToken);        // factory not invoked
        hasValue = lazy.HasValue;                                   // true
    }

    public static async Task LazyAsyncExecutionAndPublicationExample(Func<CancellationToken, Task<int>> factory, CancellationToken cancellationToken)
    {
        using LazyAsyncExecutionAndPublication<int> lazy = new LazyAsyncExecutionAndPublication<int>(factory);

        bool hasValue = lazy.HasValue;                              // false
        int value = await lazy.GetValueAsync(cancellationToken);    // factory invoked
        hasValue = lazy.HasValue;                                   // true
        value = await lazy.GetValueAsync(cancellationToken);        // factory not invoked
        hasValue = lazy.HasValue;                                   // true
    }
Initializers

Initializers in LibSharp are equivalents of lazy types, with the only difference being that the value factory is provided at lazy initialization time instead of creation time. They also enable cases where different factories can be used to initialize the value, where only one will succeed at setting the value.

InitializerAsyncExecutionAndPublication runs at most one in-flight factory and retries after failed or canceled attempts. InitializerAsyncPublicationOnly may execute multiple concurrent factories, but only the first successfully published value is retained.

    using LibSharp.Caching;

    public static void InitializerExample(Func<int> factory)
    {
        Initializer<int> initializer = new Initializer<int>();

        bool hasValue = initializer.HasValue;       // false
        int value = initializer.GetValue(factory);  // factory invoked
        hasValue = initializer.HasValue;            // true
        value = initializer.GetValue(factory);      // factory not invoked
        hasValue = initializer.HasValue;            // true
    }

    public static async Task InitializerAsyncPublicationOnlyExample(Func<CancellationToken, Task<int>> factory, CancellationToken cancellationToken)
    {
        InitializerAsyncPublicationOnly<int> initializer = new InitializerAsyncPublicationOnly<int>();

        bool hasValue = initializer.HasValue;                                       // false
        int value = await initializer.GetValueAsync(factory, cancellationToken);    // factory invoked
        hasValue = initializer.HasValue;                                            // true
        value = await initializer.GetValueAsync(factory, cancellationToken);        // factory not invoked
        hasValue = initializer.HasValue;                                            // true
    }

    public static async Task InitializerAsyncExecutionAndPublicationExample(Func<CancellationToken, Task<int>> factory, CancellationToken cancellationToken)
    {
        using InitializerAsyncExecutionAndPublication<int> initializer = new InitializerAsyncExecutionAndPublication<int>();

        bool hasValue = initializer.HasValue;                                       // false
        int value = await initializer.GetValueAsync(factory, cancellationToken);    // factory invoked
        hasValue = initializer.HasValue;                                            // true
        value = await initializer.GetValueAsync(factory, cancellationToken);        // factory not invoked
        hasValue = initializer.HasValue;                                            // true
    }
Value Caches

Value caches are lazy types that automatically refresh the value when it expires. It is possible to either provide an exact time-to-live value or a custom function to determine expiration of a value (useful, for example, for in-memory caching of tokens with known expiration time). It is also possible to provide either a factory method for creation of a new value or a factory for updating the existing value.

Note that ValueCacheAsync guarantees LazyThreadSafetyMode.ExecutionAndPublication behavior and implements IDisposable.

    using LibSharp.Caching;

    public static void ValueCacheExample(Func<int> factory)
    {
        ValueCache<int> cache = new ValueCache<int>(factory, TimeSpan.FromMilliseconds(1));

        bool hasValue = cache.HasValue; // false
        int value = cache.GetValue();   // factory invoked
        hasValue = cache.HasValue;      // true

        Thread.Sleep(10);
        value = cache.GetValue();       // factory invoked again — TTL expired
        hasValue = cache.HasValue;      // true
    }

    public static async Task ValueCacheAsyncExample(Func<CancellationToken, Task<int>> factory, CancellationToken cancellationToken)
    {
        using ValueCacheAsync<int> cache = new ValueCacheAsync<int>(factory, TimeSpan.FromMilliseconds(1));

        bool hasValue = cache.HasValue;                             // false
        int value = await cache.GetValueAsync(cancellationToken);   // factory invoked
        hasValue = cache.HasValue;                                  // true

        await Task.Delay(10);
        value = await cache.GetValueAsync(cancellationToken);       // factory invoked again — TTL expired
        hasValue = cache.HasValue;                                  // true
    }
Key-Value Caches

Key-value caches allow caching and automatically refreshing multiple values within a single data structure.

    using LibSharp.Caching;

    public static void KeyValueCacheExample(Func<string, int> factory)
    {
        KeyValueCache<string, int> cache = new KeyValueCache<string, int>(factory, TimeSpan.FromMinutes(1));

        int valueA = cache.GetValue("a");   // factory invoked for "a"
        int valueB = cache.GetValue("b");   // factory invoked for "b"

        valueA = cache.GetValue("a");       // factory not invoked
        valueB = cache.GetValue("b");       // factory not invoked
    }

    public static async Task KeyValueCacheAsyncExample(Func<string, CancellationToken, Task<int>> factory, CancellationToken cancellationToken)
    {
        using KeyValueCacheAsync<string, int> cache = new KeyValueCacheAsync<string, int>(factory, TimeSpan.FromMinutes(1));

        int valueA = await cache.GetValueAsync("a", cancellationToken); // factory invoked for "a"
        int valueB = await cache.GetValueAsync("b", cancellationToken); // factory invoked for "b"

        valueA = await cache.GetValueAsync("a", cancellationToken);     // factory not invoked
        valueB = await cache.GetValueAsync("b", cancellationToken);     // factory not invoked
    }
Proactive Async Cache

ProactiveAsyncCache is an async cache that proactively refreshes its value in the background before it expires. It starts a background loop that re-fetches the value at a configurable interval. A pre-fetch offset allows refresh to happen before expiration, reducing the chance that callers need to wait for the factory.

    using LibSharp.Caching;

    public static async Task ProactiveAsyncCacheExample(Func<CancellationToken, Task<int>> factory, CancellationToken cancellationToken)
    {
        // Default options: background loop starts automatically, stale reads disabled
        await using ProactiveAsyncCache<int> cache = new ProactiveAsyncCache<int>(
            factory,
            refreshInterval: TimeSpan.FromMinutes(5),
            preFetchOffset: TimeSpan.FromSeconds(30));

        bool hasValue = cache.HasValue;                             // false — until first background fetch completes
        int value = await cache.GetValueAsync(cancellationToken);   // waits for background fetch if not yet complete
        hasValue = cache.HasValue;                                  // true
        value = await cache.GetValueAsync(cancellationToken);       // returns cached value
    }

    public static async Task ProactiveAsyncCacheWithOptionsExample(Func<CancellationToken, Task<int>> factory, CancellationToken cancellationToken)
    {
        await using ProactiveAsyncCache<int> cache = new ProactiveAsyncCache<int>(
            factory,
            refreshInterval: TimeSpan.FromMinutes(5),
            preFetchOffset: TimeSpan.FromSeconds(30),
            allowStaleReads: true);                                 // return the previous value while a refresh is in progress

        int value = await cache.GetValueAsync(cancellationToken);
    }
Product Compatible and additional computed target framework versions.
.NET 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 is compatible.  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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.0.0 66 7/4/2026
3.0.0 160 4/1/2026
3.0.0-beta.1 74 3/31/2026
2.0.4 87 3/30/2026
2.0.3 89 3/27/2026
2.0.2 86 3/23/2026
2.0.1 83 3/22/2026
2.0.0 124 3/11/2026
1.1.6 293 9/26/2024
1.1.5 186 8/27/2024
1.1.4 216 6/9/2024
1.1.3 214 2/24/2024
1.1.2 185 2/24/2024
1.1.1 212 2/23/2024
1.1.0 178 2/21/2024
1.0.0 199 2/19/2024

# Changelog

- 4.0.0
 - Enabled nullable reference type annotations across the entire public API; `TryGet*` methods and out parameters are now annotated (e.g. `[MaybeNullWhen(false)]`), and nullable inputs such as optional `Encoding`/`XmlReaderSettings` arguments are marked accordingly
 - `Caching`
   - Updated `ProactiveAsyncCache<T>` to never throw exceptions from `DisposeAsync()`
   - `ValueCache<T>`, `ValueCacheAsync<T>`, `KeyValueCache<TKey, TValue>`, `KeyValueCacheAsync<TKey, TValue>`, and `ProactiveAsyncCache<T>` now accept an optional `TimeProvider` (defaulting to `TimeProvider.System`) so expiration and background refresh can be driven deterministically in tests
 - `Collections`
   - Renamed extension classes to drop the `I` prefix: `IEnumerableExtensions` → `EnumerableExtensions`, `ICollectionExtensions` → `CollectionExtensions`, `IDictionaryExtensions` → `DictionaryExtensions`, `IAsyncEnumerableExtensions` → `AsyncEnumerableExtensions` (extension methods called via instance syntax are unaffected; static-style calls must use the new names)
   - `MinPriorityQueue<T>` and `MaxPriorityQueue<T>`: `Contains` and `Remove` now use element equality (`EqualityComparer<T>.Default`) instead of the ordering comparer, so they honour the `ICollection<T>` contract (reverses the 3.0.0 change; ordering still uses the comparer)
   - `ConcurrentHashSet<T>` now constrains `T` to `notnull` (it is backed by `ConcurrentDictionary`, which never permitted null elements); `IDictionaryExtensions.Copy` likewise constrains its key to `notnull`
   - `DictionaryExtensions` and the key-value caches now validate keys without boxing value-type keys
 - `Common`
   - `Optional<T>` no longer implements `IEquatable<T>`; it now implements only `IEquatable<Optional<T>>`, so equality is defined between two optionals. A bare value still compares equal via the new implicit conversion, but a value typed as `object` never does
   - Added an implicit conversion from `T` to `Optional<T>` (always produces a present optional, even for `null`)
   - Added `Match`, `Map`, and `Bind` to `Optional<T>`
   - Added `Match`, `Map`, `MapError`, and `Bind` to `Result<T, TError>`
   - Removed the `Argument.NotNull(object, string)` overload; calling `NotNull` on a non-nullable value type is now a compile error instead of a silent no-op (the reference-type generic overload is retained)
   - `Argument` methods now capture the argument name automatically via `[CallerArgumentExpression]`, so the `name` parameter is optional; existing calls that pass it explicitly still compile
   - `XmlSerializationExtensions.SerializeToXml` / `DeserializeFromXml` are now annotated with `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` to reflect that `XmlSerializer` is incompatible with trimming and Native AOT
 - `Threading`
   - `ThrottledAction` and `DebouncedAction` now accept an optional `TimeProvider` (defaulting to `TimeProvider.System`) so the throttle interval and debounce timer can be driven deterministically in tests
   - `ThrottledAction` now clamps its interval-to-ticks conversion so an extreme interval near `TimeSpan.MaxValue` cannot overflow into a negative value and defeat throttling

- 3.0.0
 - `Caching`
   - Async cache/lazy/initializer factories now reject null task returns with a deliberate exception instead of failing with `NullReferenceException`
   - `ProactiveAsyncCache<T>` no longer implements `IDisposable`; use `await using` / `DisposeAsync()` instead
   - `ProactiveAsyncCache` no longer supports `refreshTimeout` and `onBackgroundRefreshError` parameters, and now always auto-starts in constructor
 - `Collections`
   - Added `ConcurrentHashSet`
   - Added weighted `Chunk` extension method for `IAsyncEnumerable<T>`
   - Added `TryPeek` and `TryDequeue` to `IPriorityQueue<T>`, `MinPriorityQueue<T>`, and `MaxPriorityQueue<T>`
   - `MinPriorityQueue<T>` and `MaxPriorityQueue<T>`: `Contains` and `Remove` now use the queue's comparer instead of `object.Equals`, making them consistent with the ordering relation
 - `Common`
   - Added `Result`
   - Renamed `Box` to `Optional`; null values are now allowed
   - `Optional<T>.GetHashCode` now differentiates between an empty optional and an optional wrapping `null`
   - Added `StringExtensions.TruncateTextElements` for text-element-aware truncation
   - `DateTimeExtensions` epoch conversions now use Unix-time floor semantics instead of rounding fractional units
   - `TypeExtensions.GetDefaultComparer` now supports types implementing non-generic `IComparable`
   - Regex extensions now return a `bool` indicating whether the regex match timed out
   - `FuncExtensions.RunWithTimeout`: timeout must now be strictly greater than zero
   - `XmlSerializationExtensions`: `XmlSerializer` instances are now cached per type to avoid repeated dynamic assembly generation
 - `Threading`
   - Added `AsyncLock`, `DebouncedAction`, and `ThrottledAction`

- 2.0.4
 - Improved disposal of async caches in edge cases

- 2.0.3
 - `ProactiveAsyncCache` now calculates retry delay based on the refresh interval and pre-fetch offset
 - Minor bug fixes and improvements

- 2.0.2
 - `ProactiveAsyncCache` now supports an optional `refreshTimeout` parameter
 - Minor bug fixes and improvements

- 2.0.1
 - `ProactiveAsyncCache` now supports stale reads mode
 - `ProactiveAsyncCache` now accepts an action to handle failed background refreshes

- 2.0.0
 - Dropped support for .NET Standard 2.0, .NET Standard 2.1, .NET 5.0, .NET 6.0, and .NET 7.0
 - Added support for .NET 10.0
 - Removed `DateTimeExtensions.UnixEpoch`
 - Added `ProactiveAsyncCache`
 - Added `TryConvertToEnum` extension method for `string`
 - Bug fixes and thread safety improvements

- 1.1.6
 - Added `TryConvertToEnum` extension method for `int`

- 1.1.5
 - Added Regex extension methods that handle regex timeouts gracefully
 - Added `Func` extension methods that run asynchronous operations with a timeout

- 1.1.4
 - Added support for .NET 9.0

- 1.1.3
 - Updated NuGet package tags and description

- 1.1.2
 - Added constructors to `KeyValueCache` and `KeyValueCacheAsync` that accept separate factories for creates and updates
 - Added the ability to specify a custom expiration function

- 1.1.1
 - Changed the return type of the `Shuffle` extension method from `IEnumerable<T>` to `T[]`
 - Fixed the signature of `SerializeToXml` so it can be invoked as an extension method
 - All `IDisposable` types now throw `ObjectDisposedException` when a member is accessed after disposal

- 1.1.0
 - Added support for .NET Standard 2.0, .NET Standard 2.1, .NET 5.0, .NET 6.0, and .NET 7.0

- 1.0.0
 - Initial release targeting .NET 8.0