CSharpFunctionalExtensions
repository·master·Indexed 25 days ago
https://github.com/vkhorikov/csharpfunctionalextensionsA 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.
What's inside CSharpFunctionalExtensions
- You can install the library using the .NET CLI or the Package Manager Console in Visual Studio. There is also a strong-named assembly version available.
Handle potential nulls with the Maybe type
masterUse the
Maybe<T>type to explicitly represent values that might be absent, replacing null checks with theHasNoValueproperty.Maybe<Customer> customerOrNothing = _customerRepository.GetById(id); if (customerOrNothing.HasNoValue) return Error("Customer with such Id is not found: " + id);Avoid primitive obsession using Result and Value Objects
masterInstead of using primitive types (like
stringorEmail), use specialized Value Objects that return aResult<T>. You can then useResult.Combineto 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);Install CSharpFunctionalExtensions.Analyzers
masterInstall the
CSharpFunctionalExtensions.AnalyzersRoslyn analyzer package to receive warnings and recommendations that prevent the misuse ofResultobjects, ensuring more robust implementation of functional patterns.dotnet add package CSharpFunctionalExtensions.AnalyzersMap Results to HttpResults in Web APIs
masterThe
CSharpFunctionalExtensions.HttpResultslibrary allows you to seamlessly mapResulttypes from the core library to ASP.NETHttpResults. This is useful for Minimal APIs and Controllers to maintain a railway-oriented flow while returning standardized HTTP responses (adhering to RFC 9457ProblemDetails).Key features:
- Zero configuration mapping.
- Supports
TypedResultsfor type-safe responses. - Works with
Ok,Created,NoContent,Accepted,FileStream, etc. - Compatible with OpenAPI generation.
Compose Result<T> using LINQ query syntax
masterYou can use C# LINQ query syntax to chain multiple
Result<T>orMaybe<T>operations. This provides a more readable alternative to nestedBindorMapcalls, 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 );Compose operations using Result chaining
masterChain multiple operations together using methods like
ToResult,Ensure,Tap, andFinallyto 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));Use CSharpFunctionalExtensions.FluentAssertions for testing
masterUse the
CSharpFunctionalExtensions.FluentAssertionslibrary to perform more fluent assertions on functional types in your unit tests. It provides custom assertions forMaybe,Result,Result<T>,Result<T, E>, andUnitResult.var result = Result.Success(420); result.Should().Succeed(); // passes result.Should().SucceedWith(420); // passes result.Should().SucceedWith(69); // throws result.Should().Fail(); // throwsWrap operations in a TransactionScope
masterUse
WithTransactionScopeto 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));Use Maybe<T> with collections and dictionaries
masterThe library provides extensions to handle collections and dictionaries safely without returning
nullor default values:TryFirst/TryLast: ReplacesFirstOrDefault/LastOrDefaultto return aMaybeinstead of a default value.TryFind: Safely retrieves a value from aDictionaryas aMaybe.
Execute operations on Maybe<T>
masterPerform side effects based on the presence or absence of a value:
Execute: Runs anActiononly if theMaybehas a value.ExecuteNoValue: Runs anActiononly if theMaybehas no value.Match: Defines two distinct paths: one for when a value is present and one for when it is not.
Transform values in Maybe<T> using Map, Bind, and Where
masterUse functional operators to transform or filter
Maybevalues without manual null checks:Map(orSelect): Transforms the inner value using a delegate. The delegate only runs if a value exists.Bind(orSelectMany): Transforms theMaybeinto anotherMaybe(useful for chaining operations that also returnMaybe).Where: Converts aMaybewith a value toMaybe.Noneif the provided predicate returnsfalse.