Caravelle 0.0.1-alfa.1

This is a prerelease version of Caravelle.
dotnet add package Caravelle --version 0.0.1-alfa.1
                    
NuGet\Install-Package Caravelle -Version 0.0.1-alfa.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Caravelle" Version="0.0.1-alfa.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Caravelle" Version="0.0.1-alfa.1" />
                    
Directory.Packages.props
<PackageReference Include="Caravelle" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Caravelle --version 0.0.1-alfa.1
                    
#r "nuget: Caravelle, 0.0.1-alfa.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Caravelle@0.0.1-alfa.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Caravelle&version=0.0.1-alfa.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Caravelle&version=0.0.1-alfa.1&prerelease
                    
Install as a Cake Tool

Caravelle

Caravelle

Caravelle is a source-generator-driven in-memory request-response bus heavily inspired by Wolverine's "Compound Handler" pattern.

Why

Because I wanted to learn a few things about source generators and I wanted minimal-friction in-memory request-response bus.
I also needed a project to experiment with AI-assisted coding, so this project was built using pair programming with Copilot.

Setup

  1. Register Caravelle core services.
  2. Register generated dispatchers.
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddCaravelleCore();
services.AddGeneratedHandlers(); // generated by Caravelle.Generator
var provider = services.BuildServiceProvider();

Define a handler

Mark a class with [Handler]. That class must contain one handle entry method alias:

  • Handle, HandleAsync, Execute, or ExecuteAsync
  • This method can be sync or async
  • Extra method parameters will be resolved from the registered services
using YourNamespace;

[Handler]
public class GetItemHandler
{
    public record Request(int Id);
    public record Response(string Name);

    public Task<Response> Handle(Request request)
        => Task.FromResult(new Response($"My name is: {request.Id.ToString()}"));
}

Then put the message on the bus to execute the handler.

var bus = provider.GetRequiredService<Caravelle.Bus>();
var result = await bus.Handle(new GetItemHandler.Request(42));

The result is a generated dispatcher-specific Result type with a single Value property. On success, Value contains your response. On short-circuit paths, Value contains either an IValidationResult implementation (for invalid) or a NotFoundResult.

Behind the scenes, the Caravelle source generator creates:

  • an IDispatcher<TRequest, TResult> implementation that orchestrates the handler pipeline
  • an AddGeneratedHandlers() extension method that registers handlers and dispatchers
  • a typed bus.Handle(request) extension method to call the handler without extra generic arguments

Pipeline behavior

Caravelle enables pipeline behavior by looking for pre-handle and post-handle method aliases and naming conventions:

  • Exact aliases: Load, LoadAsync, Validate, ValidateAsync
  • Name patterns: any method name that starts with Before, ends with Before, or ends with BeforeAsync
  • Post-handle name patterns: any method name that starts with After or Post

Generated dispatchers execute pipeline methods in the order required by their dependencies:

  • The return values of one method in the chain can be used as parameters for the next method
  • If a return value is a tuple, the deconstructed items are available to the dependent methods
  • The first unmatched parameter of the handle entry method is considered the request type
  • All other unmatched parameters will be resolved from the registered services
  • Pre-handle methods run before Handle, post-handle methods run after Handle
  • Post-handle methods use the same dependency-based ordering mechanism as pre-handle methods
  • The dispatcher returns success only after all post-handle methods complete

Each pre-handle and post-handle method is optional and may be sync or async.

Special cases:

  • If the return value is nullable and the dependent method defines that type as non-nullable, dispatch short-circuits with a NotFoundResult payload.
  • Any pipeline method may return an IValidationResult implementation, either as the full return value or as one tuple element. The built-in ValidationResult<TError> is one implementation, and the non-generic ValidationResult alias maps to ValidationResult<ValidationError>.
  • If any returned IValidationResult reports IsValid() == false, dispatch stops and returns that result payload.
  • If Handle returns a tuple, the first tuple element is used as the response value and the remaining elements can still participate in validation checks.
  • Post-handle methods do not replace the response payload. The successful response value still comes from Handle.

Result model

Generated dispatchers expose a nested Result class that wraps a single Value object. Typical values are:

  • response payload from Handle on success
  • an IValidationResult payload when validation short-circuits
  • a NotFoundResult payload when nullable dependencies resolve to null

Generated result types also provide a Match method for branch-safe handling without manual casting. Example:

var result = await bus.Handle(new GetItemHandler.Request(42));

var message = result.Match(
  onSuccess: response => $"OK: {response.Name}",
  onInvalid: invalid => $"Invalid ({invalid.Count} error(s))",
  onNotFound: notFound => $"Not found: {notFound.Message}");

The Value property remains available for compatibility and future migration.

NotFoundResult also implements IValidationResult and always returns false from IsValid().

Troubleshooting

  • Avoid mixing abstract/base validation response types (for example IValidationResult or broad base classes) with their concrete implementations in the same handler pipeline.
  • Prefer returning concrete validation result types consistently from a given dispatcher pipeline to keep generated Match(...) branches predictable.

Not supported

The following handler patterns are not supported by source generation:

  • Multiple handlers that collide on request signatures. Duplicate request types (MBG001) omit typed bus Handle(request) extension generation, and duplicate request/response pairs (MBG002) omit dispatcher registration for that pair.
  • Generic or nested handler classes.
    Generic handlers report MBG003 and nested handlers report MBG004.
  • Pipelines where request type inference fails.
    Example: all method parameters are already satisfied by earlier pipeline outputs (MBG005).
  • Pipelines that produce duplicate local values of the same type.
    Example: tuple outputs with two elements of the same type (MBG006).
  • Unsupported pre-handle, post-handle, or handle entry method return types. Example: void BeforeLoad(...), void AfterAudit(...), or non-generic Task ExecuteAsync(...) (MBG007).
  • Cyclic dependencies between pipeline methods.
    Example: Load depends on a type from Validate while Validate also depends on a type from Load (MBG008).

Validation phases may return different IValidationResult implementations; each distinct concrete return type becomes an additional union possibility in the generated dispatcher result type.

Additional runtime limitation:

  • Handle returning null is not supported. The generated dispatcher throws ArgumentNullException when the success response is null.

Tracing

Caravelle emits Activity traces via source name "Caravelle":

  • activity name: caravelle.dispatch {HandlerName}
  • tags: request type and result type
  • canceled dispatches set result status to Canceled
  • unhandled exceptions are recorded as an "exception" event and set activity status to error

Resources

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.0.1-alfa.1 93 6/24/2026