ASP.NET Core Integration
UnambitiousFx.Synapse.AspNetCore provides two invoker wrappers that translate Result<T> to HTTP responses automatically, so endpoint handlers stay free of mapping boilerplate.
Install
- .NET CLI
- PackageReference
dotnet add package UnambitiousFx.Synapse.AspNetCore
<PackageReference Include="UnambitiousFx.Synapse.AspNetCore" Version="*" />
Setup
builder.Services.AddSynapse(cfg => { /* ... */ });
builder.Services.AddSynapseAspNetCore();
var app = builder.Build();
app.UseCorrelationId(); // optional — reads/writes X-Correlation-Id header
HTTP request flow
IHttpInvoker — Minimal API
IHttpInvoker wraps IInvoker and converts Result<T> to IResult using IFailureHttpMapper for failures:
Default success mapping — 200 OK
app.MapGet("/tasks/{id}", async (
Guid id,
[FromServices] IHttpInvoker invoker,
CancellationToken ct) =>
await invoker.InvokeAsync(new GetTaskQuery(id), ct));
// Success → 200 OK with the value serialized as JSON
// Failure → mapped by IFailureHttpMapper (default: ProblemDetails)
Custom success mapping
Pass a delegate to control the success response:
app.MapPost("/tasks", async (
[FromBody] CreateTaskCommand cmd,
[FromServices] IHttpInvoker invoker,
CancellationToken ct) =>
await invoker.InvokeAsync(
cmd,
id => Results.Created($"/tasks/{id}", id), // ← custom success mapper
ct));
Fire-and-forget commands
app.MapDelete("/tasks/{id}", async (
Guid id,
[FromServices] IHttpInvoker invoker,
CancellationToken ct) =>
await invoker.InvokeAsync(new DeleteTaskCommand(id), ct));
// Success → 200 OK (no body)
// Failure → ProblemDetails
Streaming
app.MapGet("/tasks/stream", (
[FromServices] IHttpInvoker invoker,
CancellationToken ct) =>
invoker.InvokeStreamAsync(new StreamTasksQuery(), ct));
// Returns IAsyncEnumerable<TaskDto> — ASP.NET Core streams the JSON array
IHttpInvoker.InvokeStreamAsync unwraps Result<TItem> automatically: successful items are yielded, failures are silently skipped.
IMvcInvoker — Controller-based API
Same contract as IHttpInvoker but returns IActionResult for use in MVC controllers:
[ApiController]
[Route("tasks")]
public class TasksController : ControllerBase
{
private readonly IMvcInvoker _invoker;
public TasksController(IMvcInvoker invoker) => _invoker = invoker;
[HttpGet("{id}")]
public ValueTask<IActionResult> Get(Guid id, CancellationToken ct) =>
_invoker.InvokeAsync(new GetTaskQuery(id), ct);
[HttpPost]
public ValueTask<IActionResult> Create([FromBody] CreateTaskCommand cmd, CancellationToken ct) =>
_invoker.InvokeAsync(cmd, id => Created($"/tasks/{id}", id), ct);
}
Failure mapping — IFailureHttpMapper
By default, failures are mapped to RFC 9457 ProblemDetails responses. Replace the mapper with a custom implementation by registering it as a singleton before AddSynapseAspNetCore:
builder.Services.AddSingleton<IFailureHttpMapper, MyFailureHttpMapper>();
builder.Services.AddSynapseAspNetCore();
IFailureHttpMapper (from UnambitiousFx.Functional.AspNetCore.Mappers) has a single method, FailureHttpResponse? GetFailureResponse(IFailure failure). Return a FailureHttpResponse carrying the status code and body (or null to fall through). The invokers turn it into the appropriate IResult / IActionResult:
using UnambitiousFx.Functional.AspNetCore.Mappers;
public class MyFailureHttpMapper : IFailureHttpMapper
{
public FailureHttpResponse? GetFailureResponse(IFailure failure)
{
return failure switch
{
NotFoundFailure nf => new FailureHttpResponse { StatusCode = 404, Body = new { nf.Message } },
ValidationFailure vf => new FailureHttpResponse { StatusCode = 422, Body = new { vf.Message } },
_ => new FailureHttpResponse { StatusCode = 500, Body = new { failure.Message } },
};
}
}
To override only a few failure types, subclass DefaultFailureHttpMapper (its GetFailureResponse is virtual) and call base.GetFailureResponse(failure) for the cases you don't handle.
Correlation ID middleware
UseCorrelationId() reads the incoming X-Correlation-Id header and sets it on IContext.CorrelationId. It also writes the correlation ID back to the response header so callers can correlate requests across service boundaries:
app.UseCorrelationId();
If no X-Correlation-Id header is present, the context uses the ID already generated by IContextFactory.
:::caution NativeAOT / Trim compatibility
When publishing with PublishAot=true or full trimming, use WebApplication.CreateSlimBuilder and configure a JSON serializer context. Also use the Source Generator — the generated RegisterGroup implements IEventDispatcherRegistration and wires AOT-safe dispatch delegates automatically.
:::
NativeAOT / Trim compatibility
When publishing with PublishSingleFile=true or PublishAot=true, use WebApplication.CreateSlimBuilder and configure the JSON serializer context so all request/response types are included:
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(opts =>
opts.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));
[JsonSerializable(typeof(CreateTaskCommand))]
[JsonSerializable(typeof(Guid))]
[JsonSerializable(typeof(TaskDto))]
internal partial class AppJsonContext : JsonSerializerContext { }
Also use the Source Generator — the generated RegisterGroup implements both IRegisterGroup and IEventDispatcherRegistration, so a single AddRegisterGroup(new RegisterGroup()) call covers NativeAOT-safe polymorphic event dispatch.
See also
- Getting Started — full end-to-end example with Minimal API.
- Streaming —
IHttpInvoker.InvokeStreamAsyncdetails. - Source Generator — NativeAOT-safe handler registration.
- Context —
IContext.CorrelationIdand theUseCorrelationIdheader propagation.