CSharpFunctionalExtensions

repository·master·Indexed 25 days ago

https://github.com/vkhorikov/csharpfunctionalextensions

A library for C# developers to implement functional programming principles. It provides Result and Maybe types to explicitly handle errors and nulls, reducing primitive obsession and replacing null checks. Features include functional operators like Map, Bind, and Where, LINQ query syntax support for chaining operations, and integration with TransactionScope. The ecosystem includes FluentAssertions for testing, HttpResults for mapping to ASP.NET responses, and Roslyn analyzers to prevent misuse of Result objects.

Tokens
2.2K
Snippets
7
Records
16
Agent score
34%

What's inside CSharpFunctionalExtensions

  1. Handle potential nulls with the Maybe type

    master

    Use the Maybe<T> type to explicitly represent values that might be absent, replacing null checks with the HasNoValue property.

    Maybe<Customer> customerOrNothing = _customerRepository.GetById(id);
    if (customerOrNothing.HasNoValue)
        return Error("Customer with such Id is not found: " + id);
  2. Avoid primitive obsession using Result and Value Objects

    master

    Instead of using primitive types (like string or Email), use specialized Value Objects that return a Result<T>. You can then use Result.Combine to validate multiple objects at once.

    Result<CustomerName> name = CustomerName.Create(model.Name);
    Result<Email> email = Email.Create(model.PrimaryEmail);
    
    Result result = Result.Combine(name, email);
    if (result.IsFailure)
        return Error(result.Error);
    
    var customer = new Customer(name.Value, email.Value);
  3. Install CSharpFunctionalExtensions.Analyzers

    master

    Install the CSharpFunctionalExtensions.Analyzers Roslyn analyzer package to receive warnings and recommendations that prevent the misuse of Result objects, ensuring more robust implementation of functional patterns.

    dotnet add package CSharpFunctionalExtensions.Analyzers
  4. Map Results to HttpResults in Web APIs

    master

    The CSharpFunctionalExtensions.HttpResults library allows you to seamlessly map Result types from the core library to ASP.NET HttpResults. This is useful for Minimal APIs and Controllers to maintain a railway-oriented flow while returning standardized HTTP responses (adhering to RFC 9457 ProblemDetails).

    Key features:

    • Zero configuration mapping.
    • Supports TypedResults for type-safe responses.
    • Works with Ok, Created, NoContent, Accepted, FileStream, etc.
    • Compatible with OpenAPI generation.
  5. Compose Result<T> using LINQ query syntax

    master

    You can use C# LINQ query syntax to chain multiple Result<T> or Maybe<T> operations. This provides a more readable alternative to nested Bind or Map calls, effectively acting like 'do-notation' in functional languages.

    // Instead of:
    // var customer = nameResult.Bind(name => emailResult.Map(email => new Customer(name, email)));
    
    // Use LINQ syntax:
    var customer = 
        from name in CustomerName.Create("jsmith")
        from email in Email.Create("jsmith@example.com")
        select new Customer(name, email);
    
    // Works with async as well:
    var billing = await (
        from customer in _customerRepository.GetByIdAsync(id)
        from billingInfo in _paymentGateway.ChargeCustomerAsync(customer, amount)
        select billingInfo
    );
  6. Compose operations using Result chaining

    master

    Chain multiple operations together using methods like ToResult, Ensure, Tap, and Finally to create a clean, functional pipeline.

    return _customerRepository.GetById(id)
        .ToResult("Customer with such Id is not found: " + id)
        .Ensure(customer => customer.CanBePromoted(), "The customer has the highest status possible")
        .Tap(customer => customer.Promote())
        .Tap(customer => _emailGateway.SendPromotionNotification(customer.PrimaryEmail, customer.Status))
        .Finally(result => result.IsSuccess ? Ok() : Error(result.Error));
  7. Use CSharpFunctionalExtensions.FluentAssertions for testing

    master

    Use the CSharpFunctionalExtensions.FluentAssertions library to perform more fluent assertions on functional types in your unit tests. It provides custom assertions for Maybe, Result, Result<T>, Result<T, E>, and UnitResult.

    var result = Result.Success(420);
    
    result.Should().Succeed(); // passes
    result.Should().SucceedWith(420); // passes
    result.Should().SucceedWith(69); // throws
    result.Should().Fail(); // throws
  8. Wrap operations in a TransactionScope

    master

    Use WithTransactionScope to wrap a sequence of operations that should be treated as a single unit of work, ensuring that if any part of the inner chain fails, the transaction is handled correctly.

    return _customerRepository.GetById(id)
        .ToResult("Customer with such Id is not found: " + id)
        .Ensure(customer => customer.CanBePromoted(), "The customer has the highest status possible")
        .WithTransactionScope(customer => Result.Success(customer)
            .Tap(customer => customer.Promote())
            .Tap(customer => customer.ClearAppointments()))
        .Tap(customer => _emailGateway.SendPromotionNotification(customer.PrimaryEmail, customer.Status))
        .Finally(result => result.IsSuccess ? Ok() : Error(result.Error));
  9. Use Maybe<T> with collections and dictionaries

    master

    The library provides extensions to handle collections and dictionaries safely without returning null or default values:

    • TryFirst / TryLast: Replaces FirstOrDefault/LastOrDefault to return a Maybe instead of a default value.
    • TryFind: Safely retrieves a value from a Dictionary as a Maybe.
  10. Execute operations on Maybe<T>

    master

    Perform side effects based on the presence or absence of a value:

    • Execute: Runs an Action only if the Maybe has a value.
    • ExecuteNoValue: Runs an Action only if the Maybe has no value.
    • Match: Defines two distinct paths: one for when a value is present and one for when it is not.
  11. Transform values in Maybe<T> using Map, Bind, and Where

    master

    Use functional operators to transform or filter Maybe values without manual null checks:

    • Map (or Select): Transforms the inner value using a delegate. The delegate only runs if a value exists.
    • Bind (or SelectMany): Transforms the Maybe into another Maybe (useful for chaining operations that also return Maybe).
    • Where: Converts a Maybe with a value to Maybe.None if the provided predicate returns false.