RustlikeValues.Extensions
2.0.1
dotnet add package RustlikeValues.Extensions --version 2.0.1
NuGet\Install-Package RustlikeValues.Extensions -Version 2.0.1
<PackageReference Include="RustlikeValues.Extensions" Version="2.0.1" />
<PackageVersion Include="RustlikeValues.Extensions" Version="2.0.1" />
<PackageReference Include="RustlikeValues.Extensions" />
paket add RustlikeValues.Extensions --version 2.0.1
#r "nuget: RustlikeValues.Extensions, 2.0.1"
#:package RustlikeValues.Extensions@2.0.1
#addin nuget:?package=RustlikeValues.Extensions&version=2.0.1
#tool nuget:?package=RustlikeValues.Extensions&version=2.0.1
RustLike Extensions & Logger for C#
Comprehensive extension methods and a functional logger that enhance the RustLike Option and Result types with additional functionality inspired by Rust's standard library.
Installation
dotnet add package RustLikeValues.Extensions
Note: This package includes both extension methods and the RLogger functionality.
Overview
This package provides:
- 60+ Extension Methods: Enhanced functionality for Option and Result types
- RLogger: A functional, Result-based logger with multiple providers
- Try Patterns: Functional try-finally patterns without exceptions
- Collection Operations: Safe operations on collections returning Option/Result
- Parsing Helpers: Safe parsing methods that return Option instead of throwing
Extension Methods
Nullable to Option Conversions
// Convert nullable reference types
string? nullable = GetNullableString();
Option<string> option = nullable.ToOption();
// Convert nullable value types
int? nullableInt = GetNullableInt();
Option<int> optionInt = nullableInt.ToOption();
// Convert back to nullable
Option<int> some = 42;
int? nullable = some.ToNullable();
Safe Parsing
All parsing operations return Option<T>
instead of throwing:
Option<int> number = "42".ParseInt();
Option<double> price = "19.99".ParseDouble();
Option<DateTime> date = "2024-01-01".ParseDateTime();
Option<Guid> id = "550e8400-e29b-41d4-a716-446655440000".ParseGuid();
Option<decimal> amount = "100.50".ParseDecimal();
Option<long> bigNumber = "9223372036854775807".ParseLong();
// Enum parsing
Option<DayOfWeek> day = "Monday".ParseEnum<DayOfWeek>();
Option<LogLevel> level = "Debug".ParseEnum<LogLevel>(ignoreCase: true);
Collection Operations
// Find operations returning Option
var users = new List<User> { ... };
Option<User> admin = users.FindFirst(u => u.IsAdmin);
Option<User> lastActive = users.FindLast(u => u.IsActive);
// Dictionary operations
var dict = new Dictionary<string, int> { ["key"] = 42 };
Option<int> value = dict.Get("key"); // Some(42)
Option<int> missing = dict.Get("missing"); // None
// Safe element access
var list = new List<int> { 1, 2, 3 };
Option<int> first = list.TryFirst(); // Some(1)
Option<int> last = list.TryLast(); // Some(3)
Option<int> fifth = list.TryElementAt(4); // None
Sequence Operations
// Convert IEnumerable<Option<T>> to Option<IEnumerable<T>>
var options = new[] { Option.Some(1), Option.Some(2), Option.Some(3) };
Option<IEnumerable<int>> all = options.Sequence(); // Some([1, 2, 3])
var withNone = new[] { Option.Some(1), Option.None, Option.Some(3) };
Option<IEnumerable<int>> none = withNone.Sequence(); // None
// Convert IEnumerable<Result<T,E>> to Result<IEnumerable<T>,E>
var results = new[] { Result.Ok(1), Result.Ok(2), Result.Ok(3) };
Result<IEnumerable<int>, string> allOk = results.Sequence(); // Ok([1, 2, 3])
Filtering and Collecting
// Filter and map Options
var options = new[] { Option.Some(1), Option.None, Option.Some(3) };
var values = options.FilterMap(); // [1, 3]
// Filter and map with transformation
var strings = new[] { "1", "abc", "3" };
var numbers = strings.FilterMap(s => s.ParseInt()); // [1, 3]
// Partition Results
var results = new[] {
Result<int, string>.Ok(1),
Result<int, string>.Err("error"),
Result<int, string>.Ok(3)
};
var (successes, failures) = results.Partition();
// successes: [1, 3]
// failures: ["error"]
Functional Operations
// Tap - execute side effects without changing the value
var result = option
.Tap(value => Console.WriteLine($"Processing: {value}"))
.Map(v => v * 2)
.TapNone(() => Console.WriteLine("No value to process"));
var result2 = result
.Tap(value => logger.LogInfo($"Success: {value}"))
.TapErr(error => logger.LogError($"Failed: {error}"));
// Conditional operations
var processed = option.MapIf(
condition: x => x > 0,
mapper: x => x * 2
); // Only maps if condition is true
// Checking values
bool hasAdmin = option.Exists(user => user.IsAdmin);
bool isSpecific = option.SomeIs("expected");
LINQ Support
Both Option and Result support LINQ query syntax:
// Option LINQ
var query = from user in GetUser(id)
from email in user.Email
where email.EndsWith("@company.com")
select email.ToUpper();
// Result LINQ
var calculation = from a in Calculate1()
from b in Calculate2()
from c in Calculate3()
select a + b + c;
// Where clause
var filtered = option.Where(x => x > 0); // Filter support
Async Extensions
// Async mapping
Task<Option<Data>> asyncOpt = LoadDataAsync()
.MapAsync(async data => await ProcessAsync(data));
// Async chaining
Task<Result<Final, Error>> chain = GetDataAsync()
.AndThenAsync(async data => await ValidateAsync(data))
.MapAsync(async valid => await TransformAsync(valid));
Try Patterns
Functional try-catch-finally without exceptions:
// Basic try-catch as Result
var result = Try.Execute(() => {
return JsonSerializer.Deserialize<Data>(json);
}); // Returns Result<Data, Exception>
// Try with finally
var data = Try.Run(() => OpenFile())
.Finally(() => CloseFile());
// Multiple cleanup actions
var result = Try.Run(() => ProcessData())
.Finally(
() => CloseConnection(),
() => ReleaseResources(),
() => LogCompletion()
);
// Conditional finally
var result = Try.Run(() => PerformOperation())
.Finally(
onSuccess: value => logger.LogInfo($"Success: {value}"),
onFailure: ex => logger.LogError($"Failed: {ex}")
);
// Async try-finally
var result = await Try.RunAsync(async () => await LoadDataAsync())
.Finally(async () => await CloseConnectionAsync());
RLogger - Functional Logger
A Result-based logger that integrates seamlessly with Option and Result types.
Basic Usage
// Create a logger
var logger = new RLogger<MyClass>();
// Log messages - returns Result<Unit, Exception>
var result = logger.LogInfo("Application started");
if (result.IsErr)
HandleLoggingFailure(result.UnwrapErr());
// Different log levels
logger.LogTrace("Detailed trace information");
logger.LogDebug("Debug information");
logger.LogInfo("Information");
logger.LogWarning("Warning message");
logger.LogError("Error occurred");
logger.LogCritical("Critical failure");
// Log with exceptions
logger.LogError("Operation failed", exception);
logger.LogCritical("System failure", exception);
String Interpolation Support
Efficient string interpolation that only builds strings if the log level is enabled:
var userId = 42;
var userName = "Alice";
// String is only built if Info level is enabled
logger.LogInfo($"User {userName} (ID: {userId}) logged in");
// Check if level is enabled
if (logger.IsEnabled(LogLevel.Debug))
{
var debugInfo = GatherExpensiveDebugInfo();
logger.LogDebug($"Debug info: {debugInfo}");
}
Integration with Option/Result
The logger provides special methods for logging Option and Result states:
// Log if Option is None
var user = GetUser(id);
if (logger.IfNone(user, "User not found"))
return; // Returns true if None, logs and allows early exit
// Log if Result is Err
var result = ProcessData();
if (logger.IfErr(result, "Data processing failed"))
return; // Returns true if Err, logs error
// Custom messages
logger.IfNone(option, () => $"Missing value at {DateTime.Now}");
logger.IfErr(result, error => $"Operation failed with code: {error.Code}");
// Log success cases too
logger.IfSome(option, value => $"Processing value: {value}");
logger.IfOk(result, value => $"Success: {value}");
// Log in the middle of chains
var finalResult = GetData()
.Map(data => logger.LogOption(data, "After GetData"))
.AndThen(data => Process(data))
.Map(result => logger.LogResult(result, "After Process"));
Multiple Log Providers
// Default console logger
var logger = new RLogger<MyClass>();
// Add file logging
var fileLogger = new FileLogProvider("app.log", LogLevel.Information);
logger = logger.AddProvider(fileLogger);
// Create with multiple providers
var logger2 = new RLogger<MyService>(
new ConsoleLogProvider(LogLevel.Debug),
new FileLogProvider("service.log", LogLevel.Warning)
);
// Global configuration
RLoggerFactory.AddProvider(new FileLogProvider("global.log"));
var logger3 = RLoggerFactory.CreateLogger<MyClass>();
Custom Log Providers
public class DatabaseLogProvider : ILogProvider
{
private readonly string _connectionString;
public DatabaseLogProvider(string connectionString)
{
_connectionString = connectionString;
}
public bool IsEnabled(LogLevel level) => level >= LogLevel.Warning;
public Result<Unit, Exception> WriteLog(LogEntry entry)
{
try
{
using var connection = new SqlConnection(_connectionString);
connection.Execute(
"INSERT INTO Logs (Level, Category, Message, Timestamp) VALUES (@Level, @Category, @Message, @Timestamp)",
entry
);
return Result.Ok(Unit.Value);
}
catch (Exception ex)
{
return Result.Err(ex);
}
}
}
// Use custom provider
var dbLogger = new DatabaseLogProvider("connection string");
var logger = new RLogger<MyClass>(dbLogger);
Log Entry Structure
public readonly struct LogEntry
{
public LogLevel Level { get; init; }
public string Category { get; init; } // Type name
public string Message { get; init; }
public Option<Exception> Exception { get; init; }
public DateTime Timestamp { get; init; }
public string ThreadId { get; init; }
}
Advanced Patterns
Railway-Oriented Logging
public async Task<Result<Order, OrderError>> ProcessOrderWithLogging(OrderRequest request)
{
var logger = new RLogger<OrderService>();
return await ValidateRequest(request)
.Tap(r => logger.LogInfo($"Request validated: {r.Id}"))
.TapErr(e => logger.LogWarning($"Validation failed: {e}"))
.AndThenAsync(async req => await CreateOrder(req))
.Tap(order => logger.LogInfo($"Order created: {order.Id}"))
.AndThenAsync(async order => await ChargePayment(order))
.Tap(order => logger.LogInfo($"Payment processed for order {order.Id}"))
.TapErr(error => logger.LogError($"Order processing failed: {error}"))
.MapErr(error => new OrderError(error));
}
Conditional Logging
public Result<Data, Error> ProcessWithConditionalLogging(Input input)
{
var logger = new RLogger<Processor>();
return Process(input)
.Map(data => {
// Only log if data meets certain criteria
if (data.IsImportant)
logger.LogInfo($"Important data processed: {data.Id}");
return data;
})
.TapErr(error => {
// Different log levels based on error type
var level = error.IsCritical ? LogLevel.Critical : LogLevel.Warning;
logger.Log(level, $"Processing failed: {error}");
});
}
Flattening Operations
// Flatten nested Options
Option<Option<int>> nested = Option.Some(Option.Some(42));
Option<int> flat = nested.Flatten(); // Some(42)
// Flatten nested Results
Result<Result<int, Error>, Error> nestedResult = GetNestedResult();
Result<int, Error> flatResult = nestedResult.Flatten();
// Transpose Result<Option<T>, E> to Option<Result<T, E>>
Result<Option<User>, Error> result = GetOptionalUser();
Option<Result<User, Error>> transposed = result.Transpose();
Best Practices
Use appropriate log levels
logger.LogTrace("Entering method with parameters..."); // Very detailed logger.LogDebug("Calculated intermediate value"); // Debugging logger.LogInfo("User logged in"); // Normal flow logger.LogWarning("Retry attempt 3 of 5"); // Warnings logger.LogError("Failed to connect to service"); // Errors logger.LogCritical("Database connection lost"); // Critical
Leverage Option/Result integration
// Don't do this var result = GetUser(id); if (result.IsErr) { logger.LogError($"Failed: {result.UnwrapErr()}"); return result; } // Do this var result = GetUser(id); logger.IfErr(result, "Failed to get user"); return result;
Use parsing extensions for input validation
public Result<Config, string> ParseConfig(Dictionary<string, string> input) { return input.Get("port") .AndThen(p => p.ParseInt()) .Filter(port => port > 0 && port < 65536) .ToResult("Invalid or missing port") .Map(port => new Config { Port = port }); }
Chain operations for cleaner code
return userInput .ParseInt() // Option<int> .Filter(x => x > 0) // Option<int> .ToResult("Invalid number") // Result<int, string> .AndThen(id => GetUser(id)) // Result<User, string> .Tap(user => logger.LogInfo($"Found user: {user.Name}")) .TapErr(err => logger.LogError(err));
Performance Considerations
- Extension methods are inlined where beneficial
- String interpolation in logging only executes if level is enabled
- No allocations for Option/Result operations (struct-based)
- Try patterns avoid exception overhead when possible
Complete Example
public class UserService
{
private readonly RLogger<UserService> _logger;
private readonly IUserRepository _repository;
public UserService(IUserRepository repository)
{
_repository = repository;
_logger = RLoggerFactory.CreateLogger<UserService>();
}
public async Task<Result<UserDto, ServiceError>> GetUserByEmailAsync(string email)
{
_logger.LogInfo($"Fetching user by email: {email}");
return await email
.ToOption()
.Filter(e => e.Contains("@"))
.ToResult(new ServiceError("Invalid email format"))
.AndThenAsync(async e => await _repository.FindByEmailAsync(e))
.AndThen(opt => opt.ToResult(new ServiceError("User not found")))
.Tap(user => _logger.LogInfo($"Found user: {user.Id}"))
.TapErr(err => _logger.LogWarning($"User lookup failed: {err}"))
.MapAsync(async user => await EnrichUserDataAsync(user))
.Map(user => new UserDto(user));
}
public Result<List<int>, ServiceError> ParseUserIds(string input)
{
return Try.Execute(() =>
{
return input
.Split(',')
.Select(s => s.Trim())
.FilterMap(s => s.ParseInt())
.Where(id => id > 0)
.ToList();
})
.MapErr(ex => new ServiceError($"Failed to parse IDs: {ex.Message}"))
.Tap(ids => _logger.LogDebug($"Parsed {ids.Count} user IDs"));
}
}
License
MIT License - see LICENSE file for details
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | 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 was computed. 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. |
-
net9.0
- RustlikeValues.Option (>= 2.0.1)
- RustlikeValues.Result (>= 2.0.1)
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 |
---|