Caravelle 0.0.1-alfa.1
dotnet add package Caravelle --version 0.0.1-alfa.1
NuGet\Install-Package Caravelle -Version 0.0.1-alfa.1
<PackageReference Include="Caravelle" Version="0.0.1-alfa.1" />
<PackageVersion Include="Caravelle" Version="0.0.1-alfa.1" />
<PackageReference Include="Caravelle" />
paket add Caravelle --version 0.0.1-alfa.1
#r "nuget: Caravelle, 0.0.1-alfa.1"
#:package Caravelle@0.0.1-alfa.1
#addin nuget:?package=Caravelle&version=0.0.1-alfa.1&prerelease
#tool nuget:?package=Caravelle&version=0.0.1-alfa.1&prerelease
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
- Register Caravelle core services.
- 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, orExecuteAsync- 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 withBefore, or ends withBeforeAsync - Post-handle name patterns: any method name that starts with
AfterorPost
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 afterHandle - 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
NotFoundResultpayload. - Any pipeline method may return an
IValidationResultimplementation, either as the full return value or as one tuple element. The built-inValidationResult<TError>is one implementation, and the non-genericValidationResultalias maps toValidationResult<ValidationError>. - If any returned
IValidationResultreportsIsValid() == false, dispatch stops and returns that result payload. - If
Handlereturns 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
Handleon success - an
IValidationResultpayload when validation short-circuits - a
NotFoundResultpayload 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
IValidationResultor 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-genericTask ExecuteAsync(...)(MBG007). - Cyclic dependencies between pipeline methods.
Example:Loaddepends on a type fromValidatewhileValidatealso depends on a type fromLoad(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
ArgumentNullExceptionwhen 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 | Versions 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. |
-
net10.0
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 |