Rustify is a .NET library that brings some of the best features from Rust into the C# world, aiming to provide more robust and expressive ways to handle common programming patterns. This library includes popular Rust constructs like Option<T>, Result<T, E>, Unit, TaggedUnion, Arc<T> (Atomic Reference Counting), and RwLock<T> (Read-Write Lock).
Option<T>: Represents an optional value. It can beSome(value)orNone, helping to avoidnullreference exceptions and making code more explicit about the possibility of missing values.Result<T, E>: Represents a value that can be eitherOk(value)orErr(error). This is useful for error handling without relying on exceptions, making control flow more predictable.Unit: Represents a type with a single value,(). It's often used as a return type for functions that perform an action but don't return a meaningful value, similar tovoidbut can be used as a generic type argument.TaggedUnion<T...>: A type-safe discriminated union that can hold one of several types. Available in 1-ary, 2-ary, and 3-ary variants.
Arc<T>(Atomic Reference Counter): A thread-safe reference-counted pointer.Arc<T>provides shared ownership of a value of typeT, allocated on the heap. It ensures that the value is deallocated only when the lastArcpointer to it is dropped.RwLock<T>(Read-Write Lock): A synchronization primitive that allows multiple readers or a single writer at any point in time.RwLock<T>is useful when you have data that is read frequently but written infrequently.GenericMutex<T>: A generic mutex wrapper that provides safe, exclusive access to a value.
- LINQ Query Syntax: Both
Option<T>andResult<T, E>support LINQ query syntax withSelect,SelectMany, andWheremethods. - Async Extensions:
MapAsync,AndThenAsync, andUnwrapOrAsyncfor seamless async/await integration.
You can install Rustify via NuGet Package Manager:
Install-Package RustifyOr via .NET CLI:
dotnet add package RustifyOption<T> is used to represent a value that might be absent.
usingRustify.Monads;// Creating Optionsvarsome=Option<string>.Some("Hello");varnone=Option<string>.None();// Pattern matchingsome.Match(some: value =>Console.WriteLine($"Got: {value}"),none:()=>Console.WriteLine("Nothing here"));// Chaining operationsvarresult=some.Map(s =>s.Length)// Option<int>.Filter(len =>len>3)// Option<int>.UnwrapOr(0);// int// Contains checkif(some.Contains("Hello")){Console.WriteLine("Found it!");}// Convert nullable framework values at the boundarystring?frameworkValue=GetFrameworkValue();varoptionValue=frameworkValue.ToOption();// Option<string>int?nullableCount=GetNullableCount();varoptionCount=nullableCount.ToOption();// Option<int>usingRustify.Monads;varoptionA=Option<int>.Some(10);varoptionB=Option<int>.Some(20);// Compose Options with LINQvarsum=fromainoptionAfrombinoptionBwherea>0&&b>0selecta+b;Console.WriteLine(sum.UnwrapOr(0));// Output: 30usingRustify.Monads;varoption=Option<int>.Some(42);// Async mappingvarresult=awaitoption.MapAsync(async x =>{awaitTask.Delay(100);returnx*2;});// Async chainingvarchained=awaitoption.AndThenAsync(async x =>{vardata=awaitFetchDataAsync(x);returnOption<string>.Some(data);});// Works with Task<Option<T>> tooTask<Option<int>>optionTask=GetOptionAsync();varmapped=awaitoptionTask.MapAsync(x =>x*2);Result<T, E> is used for functions that can return a value or an error.
usingRustify.Monads;publicenumParseError{InvalidFormat,OutOfRange}publicstaticResult<int,ParseError>ParsePositive(stringinput){if(!int.TryParse(input,outvarvalue))returnResult<int,ParseError>.Err(ParseError.InvalidFormat);if(value<=0)returnResult<int,ParseError>.Err(ParseError.OutOfRange);returnResult<int,ParseError>.Ok(value);}// Usagevarresult=ParsePositive("42");result.Match(ok: value =>Console.WriteLine($"Parsed: {value}"),err: error =>Console.WriteLine($"Error: {error}"));// Chainingvardoubled=result.Map(x =>x*2).MapErr(e =>$"Failed: {e}");// Contains checkif(result.Contains(42)){Console.WriteLine("Got 42!");}if(result.ContainsErr(ParseError.InvalidFormat)){Console.WriteLine("Invalid format error");}usingRustify.Monads;varresultA=Result<int,string>.Ok(10);varresultB=Result<int,string>.Ok(20);// Compose Results with LINQvarsum=fromainresultAfrombinresultBselecta+b;Console.WriteLine(sum.UnwrapOr(0));// Output: 30usingRustify.Monads;varresult=Result<int,string>.Ok(42);// Async mappingvarmapped=awaitresult.MapAsync(async x =>{vardata=awaitProcessAsync(x);returndata;});// Async chaining with error propagationvarchained=awaitresult.AndThenAsync(async x =>{try{vardata=awaitFetchAsync(x);returnResult<string,string>.Ok(data);}catch(Exceptionex){returnResult<string,string>.Err(ex.Message);}});Unit is used when a function doesn't return a meaningful value but needs a return type for generic contexts.
usingRustify.Monads;usingRustify.Utilities;publicstaticResult<Unit,string>SaveData(stringdata){try{File.WriteAllText("data.txt",data);returnResult<Unit,string>.Ok(Unit.New);}catch(Exceptionex){returnResult<Unit,string>.Err(ex.Message);}}// UsageSaveData("Hello").Match(ok: _ =>Console.WriteLine("Saved successfully"),err: error =>Console.WriteLine($"Failed: {error}"));TaggedUnion provides type-safe discriminated unions for scenarios where a value can be one of several types.
usingRustify.Utilities;// Can hold either a string or an intTaggedUnion<string,int>result;// Using implicit conversionresult="success";// Becomes case 0 (string)result=42;// Becomes case 1 (int)// Using static factory methods (for type inference)varstrResult=TaggedUnion._0<string,int>("hello");varintResult=TaggedUnion._1<string,int>(100);// Pattern matching with return valuestringmessage=result.Match(case0: s =>$"Got string: {s}",case1: i =>$"Got int: {i}");// Pattern matching with actionsresult.Match(case0: s =>Console.WriteLine($"String: {s}"),case1: i =>Console.WriteLine($"Int: {i}"));// Type checkingif(result.Is0)Console.WriteLine("It's a string");if(result.Is1)Console.WriteLine("It's an int");usingRustify.Utilities;// Useful for representing states like: Value | Warning | ErrorTaggedUnion<int,string,Exception>parseResult;parseResult=42;// Success valueparseResult="Input was negative";// Warning message parseResult=newFormatException("Bad");// Errorvaroutput=parseResult.Match(case0: value =>$"Success: {value}",case1: warning =>$"Warning: {warning}",case2: error =>$"Error: {error.Message}");When the union contains the same type multiple times, use instance factory methods:
usingRustify.Utilities;// Both cases are int, so implicit conversion won't workvarfirst=TaggedUnion<int,int>.__0(1);// Explicitly case 0varsecond=TaggedUnion<int,int>.__1(2);// Explicitly case 1Console.WriteLine(first.Is0);// TrueConsole.WriteLine(second.Is1);// TrueArc<T> allows safe sharing of data across multiple threads by using atomic operations for reference counting.
usingRustify.Utilities.Sync;publicclassSharedData{publicintValue{get;set;}}// Create shared datavararc=Arc<SharedData>.New(newSharedData{Value=42});// Clone to share ownershipvarclone1=arc.Clone();varclone2=arc.Clone();// Access the datavardata=arc.Lock();Console.WriteLine(data.Value);// Run parallel tasks with shared datavartasks=Enumerable.Range(0,5).Select(_ =>Task.Run(()=>{varlocal=arc.Clone();Console.WriteLine($"Value: {local.Lock().Value}");}));awaitTask.WhenAll(tasks);RwLock<T> provides a mechanism for multiple readers or a single writer.
usingRustify.Utilities.Sync;usingRustify.Interfaces;publicclassConfig:IClone<Config>{publicstringSetting{get;set;}="";publicConfigClone()=>newConfig{Setting=Setting};}varconfigLock=newRwLock<Config>(newConfig{Setting="initial"});// Multiple concurrent readersvarreadResult=configLock.GetValue();if(readResult.IsOk()){varconfig=readResult.Unwrap();Console.WriteLine($"Setting: {config.Setting}");}// Exclusive writervarwriteResult=configLock.UpdateValue(config =>{config.Setting="updated";returnconfig;});// Async variants with cancellation supportusingvarcts=newCancellationTokenSource(TimeSpan.FromSeconds(5));varasyncRead=awaitconfigLock.GetValueAsync(cts.Token);varasyncWrite=awaitconfigLock.UpdateValueAsync(config =>{config.Setting="updated async";returnconfig;},cts.Token);GenericMutex<T> provides exclusive access to a value with timeout and cancellation support.
usingRustify.Utilities.Sync;varmutex=newGenericMutex<List<int>>(newList<int>());// Update under exclusive accessvarupdateResult=mutex.UpdateValue(list =>{list.Add(42);returnlist;});// Non-blocking readvartryResult=mutex.TryGetValue();// Async with cancellationusingvarcts=newCancellationTokenSource();varasyncResult=awaitmutex.GetValueAsync(cts.Token);| Method | Description |
|---|---|
Some(T value) | Creates an Option containing a value |
None() | Creates an empty Option |
IsSome() / IsNone() | Check if Option has a value |
Unwrap() | Get value or throw if None |
UnwrapOr(T default) | Get value or return default |
UnwrapOrElse(Func<T>) | Get value or compute default |
Map(Func<T, U>) | Transform the inner value |
MapOr(U default, Func<T, U>) | Transform or return default |
AndThen(Func<T, Option<U>>) | Chain Option-returning operations |
ToOption() | Convert nullable reference or value types to Option |
Filter(Func<T, bool>) | Keep value only if predicate matches |
Contains(T value) | Check if Option contains a specific value |
Match(some, none) | Pattern match on the Option |
Ok<E>() / Err<T>() | Convert to Result |
| Method | Description |
|---|---|
Ok(T value) | Creates a successful Result |
Err(E error) | Creates an error Result |
IsOk() / IsErr() | Check Result state |
Unwrap() | Get value or throw if Err |
UnwrapOr(T default) | Get value or return default |
UnwrapErr() | Get error or throw if Ok |
Map(Func<T, U>) | Transform the success value |
MapErr(Func<E, F>) | Transform the error value |
AndThen(Func<T, Result<U, E>>) | Chain Result-returning operations |
Contains(T value) | Check if Result contains a specific Ok value |
ContainsErr(E error) | Check if Result contains a specific Err value |
Match(ok, err) | Pattern match on the Result |
Ok() / Err() | Convert to Option |
| Method | Description |
|---|---|
MapAsync(Func<T, Task<U>>) | Async transform of inner value |
AndThenAsync(Func<T, Task<Option<U>>>) | Async chaining for Option |
AndThenAsync(Func<T, Task<Result<U, E>>>) | Async chaining for Result |
UnwrapOrAsync(T default) | Async unwrap with default |
| Member | Description |
|---|---|
Is0, Is1, Is2 | Check which case is active |
Match(...) | Pattern match on all cases |
_0<...>(value), _1<...>(value) | Static factory methods |
__0(value), __1(value) | Instance factory methods (for same-type cases) |
Both Option<T> and Result<T, E> implement IEquatable<T> and IComparable<T>:
vara=Option<int>.Some(5);varb=Option<int>.Some(5);varc=Option<int>.Some(10);Console.WriteLine(a.Equals(b));// TrueConsole.WriteLine(a.CompareTo(c));// -1 (5 < 10)// Works with sortingvaroptions=new[]{c,a,Option<int>.None()};Array.Sort(options);// None, Some(5), Some(10)// Before (v0.3.x)if(result.IsOk){ ...}if(result.IsErr){ ...}// After (v0.4.0)if(result.IsOk()){ ...}if(result.IsErr()){ ...}// Before (v0.3.x) - implicit conversionResult<int,string>result=42;Result<int,string>error="error message";// After (v0.4.0) - explicit factory methods requiredResult<int,string>result=Result<int,string>.Ok(42);Result<int,string>error=Result<int,string>.Err("error message");// Or using the static helper:varresult=Result.Ok<int,string>(42);varerror=Result.Err<int,string>("error message");// Each clone is now a separate handle with its own lifetimeusingvararc1=Arc<Data>.New(data);usingvararc2=arc1.Clone();// arc2 is a NEW Arc instance// Both must be disposed independently// The underlying data is disposed when the last handle is released// Before (v0.3.x)Result<T,ISynchronizerError>result=rwLock.GetValue();if(result.IsErr()&&result.UnwrapErr()==ISynchronizerError.Failed){ ...}// After (v0.4.0)Result<T,SynchronizerError>result=rwLock.GetValue();if(result.IsErr()){varerror= result.UnwrapErr();// SynchronizerError now provides richer error information:// - error.Kind (SynchronizerErrorKind enum)// - error.Message (Option<string>)// - error.InnerException (Option<Exception>)if(error.Kind==SynchronizerErrorKind.Disposed){ ...}if(error.Kind==SynchronizerErrorKind.Cancelled){ ...}if(error.Kind==SynchronizerErrorKind.Failed){ ...}}- TaggedUnion equality: All TaggedUnion variants now implement
IEquatable<T>,Equals,GetHashCode,ToString, and equality operators - Extended TaggedUnion: Now supports 4, 5, and 6 type parameters in addition to the original 1-3
Weak<T>for Arc: Break circular references with weak pointers viaarc.Downgrade()andweak.Upgrade()RwLockRef<T>: Callback-based read-write lock for reference types withoutIClone<T>requirementGenericMutexErrorremoved: Now usesSynchronizerErrorfor consistency with other sync primitives- Richer error information:
SynchronizerErrorstruct providesKind,Message, andInnerExceptionproperties - Improved null safety: Delegate parameters (Func/Action) now throw
ArgumentNullExceptionwhen null - IDisposable handling:
GenericMutexandRwLocknow properly dispose contained values that implementIDisposable
Weak<T> provides weak references to Arc<T> values, allowing circular reference breaking similar to Rust's std::sync::Weak.
usingRustify.Utilities.Sync;vararc=Arc<MyClass>.New(newMyClass());// Create a weak reference (does not increment strong count)varweak=arc.Downgrade();// Check if the referenced value is still aliveif(weak.IsAlive){// Try to upgrade to a strong referencevarupgraded=weak.Upgrade();if(upgraded.IsSome()){usingvarstrongRef=upgraded.Unwrap();Console.WriteLine("Successfully upgraded weak reference");}}// After all Arc instances are disposed, Upgrade returns Nonearc.Dispose();varresult=weak.Upgrade();// Returns NoneRwLockRef<T> provides a read-write lock for reference types that doesn't require IClone<T>. Access is through callbacks only, ensuring locks are always properly released.
usingRustify.Utilities.Sync;varrwLock=newRwLockRef<List<int>>(newList<int>{1,2,3});// Read access (multiple concurrent readers allowed)varcount=rwLock.WithRead(list =>list.Count);Console.WriteLine($"Count: {count.Unwrap()}");// Output: 3// Write access (exclusive, replaces the value)rwLock.WithWrite(list =>newList<int>{4,5,6});// Write access (exclusive, mutates in place)rwLock.WithWriteMutate(list =>list.Add(7));// Async variants with cancellation supportvarresult=awaitrwLock.WithReadAsync(async list =>{awaitTask.Delay(100);returnlist.Count;});Contributions are welcome! Please feel free to submit a pull request or open an issue.
This project is licensed under the MIT License.