Skip to content

xavierjohn/FunctionalDDD

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Functional Domain Driven Design

Build codecov NuGet NuGet Downloads License: MIT .NET C# GitHub Stars Documentation

Write 60% 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:

  • πŸ“– 60% 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

Table of Contents

Why Use This?

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:

  • βœ… Teams report 40-60% 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


Quick Start

Installation

Install the core railway-oriented programming package:

dotnet add package FunctionalDDD.RailwayOrientedProgramming

For ASP.NET Core integration:

dotnet add package FunctionalDDD.Asp

Basic Usage

using 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));

πŸ‘‰ Next Steps: Browse the Examples section or explore the complete documentation

πŸ’‘ Need help debugging? Check out the Debugging ROP Chains guide


Key Features

πŸš‚ Railway-Oriented Programming

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));

🎯 Type-Safe Value Objects

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 tell

✨ Discriminated Error Matching

Pattern 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)
);

⚑ Async & Parallel Operations

Full support for async/await and parallel execution.

var result = await GetUserAsync(id)
    .ParallelAsync(GetOrdersAsync(id))
    .ParallelAsync(GetPreferencesAsync(id))
    .AwaitAsync()
    .MapAsync((user, orders, prefs) => new UserProfile(user, orders, prefs));

πŸ” Built-in Tracing

OpenTelemetry integration for automatic distributed tracing.

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddFunctionalDddRopInstrumentation()
        .AddOtlpExporter());

πŸ“– View all features


Overview

Functional programming, railway-oriented programming, and domain-driven design combine to create robust, reliable software.

🎯 Functional Programming

Pure functions take inputs and produce outputs without side effects, making code predictable, testable, and composable.

πŸ“š Applying Functional Principles in C# (Pluralsight)

πŸš‚ Railway-Oriented Programming

Handle errors using a railway track metaphor: operations flow along the success track or automatically switch to the error track. This makes error handling explicit and visual.

Key insight: Write what should happen, not what could go wrong.

πŸ—οΈ Domain-Driven Design

Focus on understanding the problem domain and creating an accurate model. Use Aggregates, Entities, and Value Objects to enforce business rules and maintain valid state.

πŸ“š Domain-Driven Design in Practice (Pluralsight)

Why They Work Together

Pure Functions          Clear business logic
     +                        ↓
Railway-Oriented    β†’   Explicit error handling
     +                        ↓
Type Safety         β†’   Compiler-enforced correctness
     +                        ↓
Domain Model        β†’   Business rule enforcement
     =                        ↓
Robust, Maintainable Software

What's New

Recent enhancements:

  • ✨ Discriminated Error Matching - Pattern match on specific error types (ValidationError, NotFoundError, etc.) using MatchError
  • ✨ Tuple Destructuring - Automatically destructure tuples in Match/Switch for cleaner code
  • πŸ“š Enhanced Documentation - Complete documentation site with tutorials, examples, and API reference
  • ⚑ Performance Optimizations - Reduced allocation and improved throughput
  • πŸ” OpenTelemetry Tracing - Built-in distributed tracing support

πŸ“– View changelog


NuGet Packages

Package Version Description Documentation
RailwayOrientedProgramming NuGet Core Result/Maybe types, error handling, async support πŸ“– Docs
Asp NuGet Convert Result β†’ HTTP responses (MVC & Minimal API) πŸ“– Docs
Http NuGet HTTP client extensions for Result/Maybe with status code handling πŸ“– Docs
FluentValidation NuGet Integrate FluentValidation with ROP πŸ“– Docs
CommonValueObjects NuGet RequiredString, RequiredGuid, EmailAddress πŸ“– Docs
CommonValueObjectGenerator NuGet Source generator for value object boilerplate πŸ“– Docs
DomainDrivenDesign NuGet Aggregate, Entity, ValueObject, Domain Events πŸ“– Docs
Testing NuGet FluentAssertions extensions, test builders, fakes πŸ“– Docs

Performance

⚑ Negligible Overhead, Maximum Clarity

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

Documentation

πŸ“š Complete Documentation Site

Learning Paths

πŸŽ“ Beginner (2-3 hours)

πŸ’Ό Integration (1-2 hours)

πŸš€ Advanced (3-4 hours)

Quick References


Examples

Basic Usage

// 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}"
    );

Real-World Scenarios

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))
    .AwaitAsync()
    .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


Contributing

Contributions are welcome! This project follows standard GitHub workflow:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Guidelines

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.

πŸ“– Contributing Guide


License

This project is licensed under the MIT License - see the LICENSE file for details.


Related Projects

  • 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.

Community & Support

Learning Resources


⬆ Back to Top

Made with ❀️ by the FunctionalDDD community

About

Functional programming with Domain Driven Design.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 6

Languages