Skip to content

xavierjohn/FunctionalDDD

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

258 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Functional Domain Driven Design

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

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

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:

  • βœ… 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


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


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))
    .WhenAllAsync()
    .MapAsync((user, orders, prefs) => new UserProfile(user, orders, prefs));

πŸ” Built-in Tracing

OpenTelemetry integration for automatic distributed tracing.

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

πŸ“– View all features


NuGet Packages

Package Version Description Documentation
RailwayOrientedProgramming NuGet Core Result/Maybe types, error handling, async support πŸ“– Docs
Asp NuGet Convert Result β†’ HTTP responses, Maybe support (MVC & Minimal API) πŸ“– Docs
Http NuGet HTTP client extensions for Result/Maybe with status code handling πŸ“– Docs
FluentValidation NuGet Integrate FluentValidation with ROP πŸ“– Docs
PrimitiveValueObjects NuGet Base classes (RequiredString, RequiredGuid, RequiredUlid, RequiredInt, RequiredDecimal) + 11 ready-to-use VOs πŸ“– Docs
PrimitiveValueObjectGenerator NuGet Source generator for value object boilerplate πŸ“– Docs
Analyzers NuGet NEW! Roslyn analyzers for compile-time ROP safety (14 rules) πŸ“– 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))
    .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


What's New

Recent enhancements:

  • 🎯 Maybe Domain Optionality β€” notnull constraint, Map, Match methods, 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


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.


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