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].
- Source code: https://github.com/danylofitel/LibSharp.
- NuGet package: https://www.nuget.org/packages/LibSharp.
dotnet add package LibSharpLibSharp 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.
BenchmarkDotNet setup and benchmark scripts are available in https://github.com/danylofitel/LibSharp/blob/main/benchmarks/README.md.
Common namespace contains:
- The static class
Argumentfor convenient validation of public function arguments. - Extension methods for built-in types such as
string,int,DateTime,Func, andRegex. Optional<T>— a value type that wraps an optional value.Result<T, TError>— a discriminated union value type for success/error outcomes.
usingLibSharp.Common;publicstaticasyncTaskCommonExamples(stringstringParam,longlongParam,objectobjectParam,CancellationTokencancellationToken){// 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 presentOptional<int>empty=default;boolhasValue=empty.HasValue;// falseintfallback=empty.GetValueOrDefault(-1);// -1Optional<int>present=newOptional<int>(42);hasValue=present.HasValue;// trueintoptValue=present.Value;// 42boolgot=present.TryGetValue(outintv);// true, v == 42Optional<int>implicitlyWrapped=7;// implicit conversion from Tstringlabel=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?newOptional<int>(x*2):default);// Optional<int> 84// Result<T, TError> — discriminated union for success/error outcomesResult<int,string>success=Result<int,string>.Ok(42);boolisSuccess=success.IsSuccess;// trueintsuccessValue=success.Value;// 42Result<int,string>failure=Result<int,string>.Fail("not found");boolisError=failure.IsError;// truestringerrorMessage=failure.Error;// "not found"intvalueOrDefault=failure.GetValueOrDefault(-1);// -1stringoutcome=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 extensionsDateTimefromEpochMilliseconds=longParam.FromEpochMilliseconds();DateTimefromEpochSeconds=longParam.FromEpochSeconds();longepochMilliseconds=DateTime.UtcNow.ToEpochMilliseconds();longepochSeconds=DateTime.UtcNow.ToEpochSeconds();// Func extensions — run an async operation with a cooperative timeoutFunc<CancellationToken,Task<int>>task=async ct =>{// Example operation that observes cancellationawaitTask.Delay(TimeSpan.FromSeconds(10),ct);return99;};inttaskResult=awaittask.RunWithTimeout(TimeSpan.FromSeconds(1),cancellationToken);// Int extensionsboolconvertedFromInt=200.TryConvertToEnum<HttpStatusCode>(outHttpStatusCodestatusCode);// String extensionsboolconvertedFromString="OK".TryConvertToEnum<HttpStatusCode>(outHttpStatusCodestatusCode2);stringbase64Encoded=stringParam.Base64Encode();stringbase64Decoded=base64Encoded.Base64Decode();stringreversed=stringParam.Reverse();stringtruncated=stringParam.Truncate(10);stringtextElementTruncated=stringParam.TruncateTextElements(10);// Regex extensions — safe wrappers that catch RegexMatchTimeoutExceptionRegexregex=newRegex(pattern:"\\s+brown\\s+",options:RegexOptions.None,matchTimeout:TimeSpan.FromSeconds(1));boolisMatch=regex.TryIsMatch("the quick brown fox",outboolisMatchTimedOut);Matchmatch=regex.TryMatch("the quick brown fox",outboolmatchTimedOut);stringreplaced=regex.TryReplace("the quick brown fox"," red ",outboolreplaceTimedOut);// Type extensionsIComparer<int>intComparer=TypeExtensions.GetDefaultComparer<int>();// XML serialization extensions// Note: these rely on XmlSerializer and are not compatible with trimming or Native AOT.stringserializedToXml=objectParam.SerializeToXml();List<string>deserializedFromXml=serializedToXml.DeserializeFromXml<List<string>>();}Collections namespace contains extension methods for ICollection, IDictionary, IEnumerable, and IAsyncEnumerable interfaces, plus ConcurrentHashSet<T>, MinPriorityQueue<T>, and MaxPriorityQueue<T> collections.
usingLibSharp.Collections;publicstaticasyncTaskCollectionsExamples(CancellationTokencancellationToken){// ICollection extensionsICollection<int>collection=newList<int>();// []collection.AddRange(Enumerable.Range(0,10));// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]// IDictionary extensionsIDictionary<string,string>dictionary=newDictionary<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=newDictionary<string,string>();IDictionary<string,string>result=dictionary.CopyTo(destination);// IEnumerable extensionsList<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();intfirstIndex=enumerable.FirstIndexOf(x =>x==51);// 51intlastIndex=enumerable.LastIndexOf(x =>x==51);// 151int[]shuffled=enumerable.Shuffle();// IAsyncEnumerable extensionsIAsyncEnumerable<int>asyncEnumerable=GetNumbersAsync();List<List<int>>asyncChunks=awaitCollectAsync(asyncEnumerable.Chunk(20, item =>item),cancellationToken);// Grouped by total weight ≤ 20: [ [0, 1, 2, 3, 4, 5], [6, 7], [8, 9], ... ]intasyncFirstIndex=awaitasyncEnumerable.FirstIndexOfAsync(x =>x==51,cancellationToken);intasyncLastIndex=awaitasyncEnumerable.LastIndexOfAsync(x =>x==51,cancellationToken);// ConcurrentHashSet<T> — thread-safe hash set implementing ISet<T> and IReadOnlySet<T>ConcurrentHashSet<int>set=newConcurrentHashSet<int>();booladded=set.Add(1);// trueadded=set.Add(1);// false — already presentboolcontains=set.Contains(1);// trueboolremoved=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});boolsubset=set.IsSubsetOf(new[]{1,2,3});boolequal=set.SetEquals(new[]{2});// Min priority queueMinPriorityQueue<int>minPq=newMinPriorityQueue<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();// 3boolminHasValue=minPq.TryPeek(outintminPeeked);boolminRemoved=minPq.TryDequeue(outintminDequeued);// Max priority queueMaxPriorityQueue<int>maxPq=newMaxPriorityQueue<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();// 1boolmaxHasValue=maxPq.TryPeek(outintmaxPeeked);boolmaxRemoved=maxPq.TryDequeue(outintmaxDequeued);}privatestaticasyncIAsyncEnumerable<int>GetNumbersAsync(){for(inti=0;i<200;i++){awaitTask.Yield();yieldreturni;}}privatestaticasyncTask<List<T>>CollectAsync<T>(IAsyncEnumerable<T>source,CancellationTokencancellationToken){List<T>results=newList<T>();awaitforeach(Titeminsource.WithCancellation(cancellationToken)){results.Add(item);}returnresults;}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.
usingLibSharp.Threading;publicstaticasyncTaskThreadingExamples(CancellationTokencancellationToken){// AsyncLock — async-compatible mutual exclusion lock (not re-entrant)usingAsyncLockasyncLock=newAsyncLock();using(AsyncLock.Handlehandle=awaitasyncLock.AcquireAsync(cancellationToken)){// Only one caller can be inside this block at a time}// DebouncedAction — fires only after a quiet period since the last invocationusingDebouncedActiondebounced=newDebouncedAction(()=>Console.WriteLine("Fired"),delay:TimeSpan.FromMilliseconds(300));debounced.Invoke();// timer startsdebounced.Invoke();// timer resetsdebounced.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 intervalThrottledActionthrottled=newThrottledAction(()=>Console.WriteLine("Fired"),interval:TimeSpan.FromSeconds(1));throttled.Invoke();// executes immediatelythrottled.Invoke();// ignored — within the 1-second windowawaitTask.Delay(TimeSpan.FromSeconds(1));throttled.Invoke();// executes again — window has expired}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 toTimeProvider.System). Pass aFakeTimeProviderin tests to drive expiration and background refresh deterministically, without real delays. - Some of the classes implement
IDisposableinterface and should be correctly disposed. - Be cautious when caching types that implement
IDisposableinterface as the values will not be automatically disposed by the caches. - Be cautious when using classes with
LazyThreadSafetyMode.PublicationOnlybehavior together withIDisposabletypes as discarded instances will not be disposed. PublicationOnlyimplementations may run multiple factories concurrently and publish the first successful result.- Async lazy and initializer methods throw
InvalidOperationExceptionif a factory returns a nullTask.
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.
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.
usingLibSharp.Caching;publicstaticasyncTaskLazyAsyncPublicationOnlyExample(Func<CancellationToken,Task<int>>factory,CancellationTokencancellationToken){LazyAsyncPublicationOnly<int>lazy=newLazyAsyncPublicationOnly<int>(factory);boolhasValue=lazy.HasValue;// falseintvalue=awaitlazy.GetValueAsync(cancellationToken);// factory invokedhasValue=lazy.HasValue;// truevalue=awaitlazy.GetValueAsync(cancellationToken);// factory not invokedhasValue=lazy.HasValue;// true}publicstaticasyncTaskLazyAsyncExecutionAndPublicationExample(Func<CancellationToken,Task<int>>factory,CancellationTokencancellationToken){usingLazyAsyncExecutionAndPublication<int>lazy=newLazyAsyncExecutionAndPublication<int>(factory);boolhasValue=lazy.HasValue;// falseintvalue=awaitlazy.GetValueAsync(cancellationToken);// factory invokedhasValue=lazy.HasValue;// truevalue=awaitlazy.GetValueAsync(cancellationToken);// factory not invokedhasValue=lazy.HasValue;// true}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.
usingLibSharp.Caching;publicstaticvoidInitializerExample(Func<int>factory){Initializer<int>initializer=newInitializer<int>();boolhasValue=initializer.HasValue;// falseintvalue=initializer.GetValue(factory);// factory invokedhasValue=initializer.HasValue;// truevalue=initializer.GetValue(factory);// factory not invokedhasValue=initializer.HasValue;// true}publicstaticasyncTaskInitializerAsyncPublicationOnlyExample(Func<CancellationToken,Task<int>>factory,CancellationTokencancellationToken){InitializerAsyncPublicationOnly<int>initializer=newInitializerAsyncPublicationOnly<int>();boolhasValue=initializer.HasValue;// falseintvalue=awaitinitializer.GetValueAsync(factory,cancellationToken);// factory invokedhasValue=initializer.HasValue;// truevalue=awaitinitializer.GetValueAsync(factory,cancellationToken);// factory not invokedhasValue=initializer.HasValue;// true}publicstaticasyncTaskInitializerAsyncExecutionAndPublicationExample(Func<CancellationToken,Task<int>>factory,CancellationTokencancellationToken){usingInitializerAsyncExecutionAndPublication<int>initializer=newInitializerAsyncExecutionAndPublication<int>();boolhasValue=initializer.HasValue;// falseintvalue=awaitinitializer.GetValueAsync(factory,cancellationToken);// factory invokedhasValue=initializer.HasValue;// truevalue=awaitinitializer.GetValueAsync(factory,cancellationToken);// factory not invokedhasValue=initializer.HasValue;// true}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.
usingLibSharp.Caching;publicstaticvoidValueCacheExample(Func<int>factory){ValueCache<int>cache=newValueCache<int>(factory,TimeSpan.FromMilliseconds(1));boolhasValue=cache.HasValue;// falseintvalue=cache.GetValue();// factory invokedhasValue=cache.HasValue;// trueThread.Sleep(10);value=cache.GetValue();// factory invoked again — TTL expiredhasValue=cache.HasValue;// true}publicstaticasyncTaskValueCacheAsyncExample(Func<CancellationToken,Task<int>>factory,CancellationTokencancellationToken){usingValueCacheAsync<int>cache=newValueCacheAsync<int>(factory,TimeSpan.FromMilliseconds(1));boolhasValue=cache.HasValue;// falseintvalue=awaitcache.GetValueAsync(cancellationToken);// factory invokedhasValue=cache.HasValue;// trueawaitTask.Delay(10);value=awaitcache.GetValueAsync(cancellationToken);// factory invoked again — TTL expiredhasValue=cache.HasValue;// true}Key-value caches allow caching and automatically refreshing multiple values within a single data structure.
usingLibSharp.Caching;publicstaticvoidKeyValueCacheExample(Func<string,int>factory){KeyValueCache<string,int>cache=newKeyValueCache<string,int>(factory,TimeSpan.FromMinutes(1));intvalueA=cache.GetValue("a");// factory invoked for "a"intvalueB=cache.GetValue("b");// factory invoked for "b"valueA=cache.GetValue("a");// factory not invokedvalueB=cache.GetValue("b");// factory not invoked}publicstaticasyncTaskKeyValueCacheAsyncExample(Func<string,CancellationToken,Task<int>>factory,CancellationTokencancellationToken){usingKeyValueCacheAsync<string,int>cache=newKeyValueCacheAsync<string,int>(factory,TimeSpan.FromMinutes(1));intvalueA=awaitcache.GetValueAsync("a",cancellationToken);// factory invoked for "a"intvalueB=awaitcache.GetValueAsync("b",cancellationToken);// factory invoked for "b"valueA=awaitcache.GetValueAsync("a",cancellationToken);// factory not invokedvalueB=awaitcache.GetValueAsync("b",cancellationToken);// factory not invoked}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.
usingLibSharp.Caching;publicstaticasyncTaskProactiveAsyncCacheExample(Func<CancellationToken,Task<int>>factory,CancellationTokencancellationToken){// Default options: background loop starts automatically, stale reads disabledawaitusingProactiveAsyncCache<int>cache=newProactiveAsyncCache<int>(factory,refreshInterval:TimeSpan.FromMinutes(5),preFetchOffset:TimeSpan.FromSeconds(30));boolhasValue=cache.HasValue;// false — until first background fetch completesintvalue=awaitcache.GetValueAsync(cancellationToken);// waits for background fetch if not yet completehasValue=cache.HasValue;// truevalue=awaitcache.GetValueAsync(cancellationToken);// returns cached value}publicstaticasyncTaskProactiveAsyncCacheWithOptionsExample(Func<CancellationToken,Task<int>>factory,CancellationTokencancellationToken){awaitusingProactiveAsyncCache<int>cache=newProactiveAsyncCache<int>(factory,refreshInterval:TimeSpan.FromMinutes(5),preFetchOffset:TimeSpan.FromSeconds(30),allowStaleReads:true);// return the previous value while a refresh is in progressintvalue=awaitcache.GetValueAsync(cancellationToken);}