RustlikeValues.Extensions 2.0.1

The owner has unlisted this package. This could mean that the package is deprecated, has security vulnerabilities or shouldn't be used anymore.
dotnet add package RustlikeValues.Extensions --version 2.0.1
                    
NuGet\Install-Package RustlikeValues.Extensions -Version 2.0.1
                    
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="RustlikeValues.Extensions" Version="2.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RustlikeValues.Extensions" Version="2.0.1" />
                    
Directory.Packages.props
<PackageReference Include="RustlikeValues.Extensions" />
                    
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 RustlikeValues.Extensions --version 2.0.1
                    
#r "nuget: RustlikeValues.Extensions, 2.0.1"
                    
#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 RustlikeValues.Extensions@2.0.1
                    
#: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=RustlikeValues.Extensions&version=2.0.1
                    
Install as a Cake Addin
#tool nuget:?package=RustlikeValues.Extensions&version=2.0.1
                    
Install as a Cake Tool

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

  1. 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
    
  2. 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;
    
  3. 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 });
    }
    
  4. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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