Write less code that reads like English using Railway-Oriented Programming and Domain-Driven Design
Transform error-prone imperative code into readable, succinct functional pipelinesβwith zero performance overhead.
// β Before: 20 lines of nested error checking
var firstName = ValidateFirstName(input.FirstName);
if (firstName == null) return BadRequest("Invalid first name");
var lastName = ValidateLastName(input.LastName);
if (lastName == null) return BadRequest("Invalid last name");
// ... 15 more lines of repetitive checks
// β
After: 8 lines that read like a story
return FirstName.TryCreate(input.FirstName)
.Combine(LastName.TryCreate(input.LastName))
.Combine(EmailAddress.TryCreate(input.Email))
.Bind((first, last, email) => User.TryCreate(first, last, email))
.Ensure(user => !_repository.EmailExists(user.Email), Error.Conflict("Email exists"))
.Tap(user => _repository.Save(user))
.Tap(user => _emailService.SendWelcome(user.Email))
.Match(onSuccess: user => Ok(user), onFailure: error => BadRequest(error.Detail));Key Benefits:
- βοΈ Less boilerplate - Write less, understand more
- π― Self-documenting - Code reads like English: "Create β Validate β Save β Notify"
- π Compiler-enforced - Impossible to skip error handling
- β‘ Zero overhead - Only 11-16ns (0.002% of I/O operations)
- β Production-ready - Type-safe, testable, maintainable
- Why Use This?
- Quick Start
- Key Features
- NuGet Packages
- Performance
- Documentation
- Examples
- What's New
- Contributing
- License
The Problem: Traditional error handling in C# creates verbose, error-prone code with nested if-statements that obscure business logic and make errors easy to miss.
The Solution: Railway-Oriented Programming (ROP) treats your code like railway tracksβoperations flow along the success track or automatically switch to the error track. You write what should happen, not what could go wrong.
Real-World Impact:
- β Dramatic reduction in error-handling boilerplate
- β Bugs caught at compile-time instead of runtime
- β New developers understand code faster thanks to readable chains
- β Zero performance penalty β same speed as imperative code
π Read the full introduction
Install the core railway-oriented programming package:
dotnet add package FunctionalDdd.RailwayOrientedProgrammingFor ASP.NET Core integration:
dotnet add package FunctionalDdd.Aspusing FunctionalDdd;
// Create a Result with validation
var emailResult = EmailAddress.TryCreate("user@example.com")
.Ensure(email => email.Domain != "spam.com",
Error.Validation("Email domain not allowed"))
.Tap(email => Console.WriteLine($"Valid email: {email}"));
// Handle success or failure
var message = emailResult.Match(
onSuccess: email => $"Welcome {email}!",
onFailure: error => $"Error: {error.Detail}"
);
// Chain multiple operations
var result = await GetUserAsync(userId)
.ToResultAsync(Error.NotFound("User not found"))
.BindAsync(user => SaveUserAsync(user))
.TapAsync(user => SendEmailAsync(user.Email));π Quick Start Guide
π Next Steps: Browse the Examples section or explore the complete documentation
Chain operations that automatically handle success/failure pathsβno more nested if-statements.
return GetUserAsync(id)
.ToResultAsync(Error.NotFound("User not found"))
.BindAsync(user => UpdateUserAsync(user))
.TapAsync(user => AuditLogAsync(user))
.MatchAsync(user => Ok(user), error => NotFound(error.Detail));Prevent primitive obsession and parameter mix-ups with strongly-typed domain objects.
// β
Compiler catches this mistake
CreateUser(lastName, firstName); // Error: Wrong parameter types!
// β This compiles but has a bug
CreateUser(lastNameString, firstNameString); // Swapped, but compiler can't tellPattern match on specific error types for precise error handling.
return ProcessOrder(order).MatchError(
onValidation: err => BadRequest(err.FieldErrors),
onNotFound: err => NotFound(err.Detail),
onConflict: err => Conflict(err.Detail),
onSuccess: order => Ok(order)
);Full support for async/await and parallel execution.
var result = await GetUserAsync(id)
.ParallelAsync(GetOrdersAsync(id))
.ParallelAsync(GetPreferencesAsync(id))
.WhenAllAsync()
.MapAsync((user, orders, prefs) => new UserProfile(user, orders, prefs));OpenTelemetry integration for automatic distributed tracing.
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddRailwayOrientedProgrammingInstrumentation()
.AddOtlpExporter());π View all features
| Package | Version | Description | Documentation |
|---|---|---|---|
| RailwayOrientedProgramming | Core Result/Maybe types, error handling, async support | π Docs | |
| Asp | Convert Result β HTTP responses, Maybe support (MVC & Minimal API) | π Docs | |
| Http | HTTP client extensions for Result/Maybe with status code handling | π Docs | |
| FluentValidation | Integrate FluentValidation with ROP | π Docs | |
| PrimitiveValueObjects | Base classes (RequiredString, RequiredGuid, RequiredUlid, RequiredInt, RequiredDecimal) + 11 ready-to-use VOs | π Docs | |
| PrimitiveValueObjectGenerator | Source generator for value object boilerplate | π Docs | |
| Analyzers | NEW! Roslyn analyzers for compile-time ROP safety (14 rules) | π Docs | |
| DomainDrivenDesign | Aggregate, Entity, ValueObject, Domain Events | π Docs | |
| Testing | FluentAssertions extensions, test builders, fakes | π Docs |
Comprehensive benchmarks on .NET 10 show ROP adds only 11-16 nanoseconds of overheadβless than 0.002% of typical I/O operations.
| Operation | Time | Overhead | Memory |
|---|---|---|---|
| Happy Path | 147 ns | 16 ns (12%) | 144 B |
| Error Path | 99 ns | 11 ns (13%) | 184 B |
| Combine (5 results) | 58 ns | - | 0 B |
| Bind chain (5) | 63 ns | - | 0 B |
Real-world context:
Database Query: 1,000,000 ns (1 ms)
ROP Overhead: 16 ns
β
0.0016% of DB query time
The overhead is 1/62,500th of a single database query!
β
Same memory usage as imperative code
β‘ Single-digit to low double-digit nanosecond operations
π View detailed benchmarks
Run benchmarks yourself:
dotnet run --project Benchmark/Benchmark.csproj -c Releaseπ Complete Documentation Site
π Beginner (2-3 hours)
- Introduction - Why use ROP?
- Basics Tutorial - Core concepts
- Examples - Real-world patterns
πΌ Integration (1-2 hours)
π Advanced (3-4 hours)
- Clean Architecture - CQRS patterns
- Advanced Features - LINQ, parallelization
- Error Handling - Custom errors, aggregation
// Chain operations with automatic error handling
var result = EmailAddress.TryCreate("user@example.com")
.Ensure(email => email.Domain != "spam.com", Error.Validation("Domain not allowed"))
.Tap(email => _logger.LogInformation("Validated: {Email}", email))
.Match(
onSuccess: email => $"Welcome {email}!",
onFailure: error => $"Error: {error.Detail}"
);User Registration with Validation
[HttpPost]
public ActionResult<User> Register([FromBody] RegisterUserRequest request) =>
FirstName.TryCreate(request.FirstName)
.Combine(LastName.TryCreate(request.LastName))
.Combine(EmailAddress.TryCreate(request.Email))
.Bind((first, last, email) => User.TryCreate(first, last, email, request.Password))
.Ensure(user => !_repository.EmailExists(user.Email), Error.Conflict("Email exists"))
.Tap(user => _repository.Save(user))
.Tap(user => _emailService.SendWelcome(user.Email))
.ToActionResult(this);Async Operations
public async Task<IResult> ProcessOrderAsync(int orderId)
{
return await GetOrderAsync(orderId)
.ToResultAsync(Error.NotFound($"Order {orderId} not found"))
.EnsureAsync(
order => order.CanProcessAsync(),
Error.Validation("Order cannot be processed"))
.TapAsync(order => ValidateInventoryAsync(order))
.BindAsync(order => ChargePaymentAsync(order))
.TapAsync(order => SendConfirmationAsync(order))
.MatchAsync(
order => Results.Ok(order),
error => Results.BadRequest(error.Detail));
}Parallel Operations
// Fetch data from multiple sources in parallel
var result = await GetUserAsync(userId)
.ParallelAsync(GetOrdersAsync(userId))
.ParallelAsync(GetPreferencesAsync(userId))
.WhenAllAsync()
.BindAsync(
(user, orders, preferences) =>
CreateProfileAsync(user, orders, preferences),
ct);Discriminated Error Matching
return ProcessOrder(order).MatchError(
onValidation: err => Results.BadRequest(new { errors = err.FieldErrors }),
onNotFound: err => Results.NotFound(new { message = err.Detail }),
onConflict: err => Results.Conflict(new { message = err.Detail }),
onUnauthorized: _ => Results.Unauthorized(),
onSuccess: order => Results.Ok(order)
);HTTP Integration
// Read HTTP response as Result with status code handling
var result = await _httpClient.GetAsync($"api/users/{userId}", ct)
.HandleNotFoundAsync(Error.NotFound("User not found"))
.HandleUnauthorizedAsync(Error.Unauthorized("Please login"))
.HandleServerErrorAsync(code => Error.ServiceUnavailable($"API error: {code}"))
.ReadResultFromJsonAsync(UserContext.Default.User, ct)
.TapAsync(user => _logger.LogInformation("Retrieved user: {UserId}", user.Id));
// Or use EnsureSuccess for generic error handling
var product = await _httpClient.GetAsync($"api/products/{productId}", ct)
.EnsureSuccessAsync(code => Error.Unexpected($"Failed to get product: {code}"))
.ReadResultFromJsonAsync(ProductContext.Default.Product, ct);FluentValidation Integration
public class User : Aggregate<UserId>
{
public FirstName FirstName { get; }
public LastName LastName { get; }
public EmailAddress Email { get; }
public static Result<User> TryCreate(FirstName firstName, LastName lastName, EmailAddress email)
{
var user = new User(firstName, lastName, email);
return Validator.ValidateToResult(user);
}
private static readonly InlineValidator<User> Validator = new()
{
v => v.RuleFor(x => x.FirstName).NotNull(),
v => v.RuleFor(x => x.LastName).NotNull(),
v => v.RuleFor(x => x.Email).NotNull(),
};
}π Browse all examples | π Complete documentation
Recent enhancements:
- π― Maybe Domain Optionality β
notnullconstraint,Map,Matchmethods, full ASP.NET Core integration (JSON converter, model binder, MVC validation suppression) - π― RequiredEnum β Type-safe enumerations with behavior, state machine support, and JSON serialization
- π Roslyn Analyzers β 14 compile-time diagnostics to enforce ROP best practices
- β¨ ASP.NET Core Auto-Validation β Value objects automatically validate in requests via
AddScalarValueValidation() - π― 11 New Value Objects β
Url,PhoneNumber,Percentage,Currency,IpAddress,Hostname,Slug,CountryCode,LanguageCode,Age,RequiredInt,RequiredDecimal - β¨ Discriminated Error Matching β Pattern match on specific error types using
MatchError - β¨ Tuple Destructuring β Destructure tuples in Match/Switch for cleaner code
- β‘ Performance Optimizations β Reduced allocation and improved throughput
- π OpenTelemetry Tracing β Built-in distributed tracing support
π View changelog
Contributions are welcome! This project follows standard GitHub workflow:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please ensure:
- β
All tests pass (
dotnet test) - β Code follows existing style conventions
- β New features include tests and documentation
- β Commit messages are clear and descriptive
For major changes, please open an issue first to discuss what you would like to change.
This project is licensed under the MIT License - see the LICENSE file for details.
- CSharpFunctionalExtensions - Functional Extensions for C# by Vladimir Khorikov. This library was inspired by Vladimir's excellent training materials and takes a complementary approach with enhanced DDD support and comprehensive documentation.
- π Documentation
- π¬ Discussions - Ask questions, share ideas
- π Issues - Report bugs or request features
- β Star this repo if you find it useful!
- π₯ YouTube: Functional DDD Explanation - Third-party video explaining the library concepts
- π Pluralsight: Applying Functional Principles in C#
- π Pluralsight: Domain-Driven Design in Practice
Made with β€οΈ by the FunctionalDDD community