Skip to main content

Getting Started

This guide walks you through installing Synapse, wiring it up in a .NET application, and sending your first command end-to-end.

Install

dotnet add package UnambitiousFx.Synapse

For web API projects, also install the ASP.NET Core integration layer:

dotnet add package UnambitiousFx.Synapse.AspNetCore

Register Synapse

Call AddSynapse in your Program.cs or Startup.cs:

builder.Services.AddSynapse(cfg =>
{
cfg.RegisterRequestHandler<CreateTaskHandler, CreateTaskCommand, Guid>();
});

Every handler must be registered explicitly — Synapse does not auto-discover handlers by convention.

What gets registered automatically

AddSynapse registers these services in the DI container (all scoped by default):

ServiceDescription
IInvokerSend commands and queries.
IEmitterPublish in-process events.
IContextPer-scope correlation ID and metadata bag.
IContextAccessorAccess the current IContext from anywhere.
IOutboxCommitFlush deferred (outbox) events.

Define a command

A command is a plain record or class that implements IRequest<TResponse>:

using UnambitiousFx.Synapse.Abstractions;

public record CreateTaskCommand(string Title) : IRequest<Guid>;

If the command produces no result, use IRequest (no type parameter):

public record DeleteTaskCommand(Guid TaskId) : IRequest;

Implement the handler

using UnambitiousFx.Functional;
using UnambitiousFx.Synapse.Abstractions;

public class CreateTaskHandler : IRequestHandler<CreateTaskCommand, Guid>
{
private readonly ITaskRepository _repository;

public CreateTaskHandler(ITaskRepository repository)
{
_repository = repository;
}

public async ValueTask<Result<Guid>> HandleAsync(
CreateTaskCommand command,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(command.Title))
return Result.Failure<Guid>("Title cannot be empty.");

var task = new TaskEntity { Id = Guid.NewGuid(), Title = command.Title };
await _repository.SaveAsync(task, ct);

return Result.Success(task.Id);
}
}

Handlers receive the command and a CancellationToken. They always return Result<TResponse> — never throw for domain errors.

Send the command

Inject IInvoker and call InvokeAsync:

public class TaskService
{
private readonly IInvoker _invoker;

public TaskService(IInvoker invoker)
{
_invoker = invoker;
}

public async Task CreateAsync(string title, CancellationToken ct)
{
var result = await _invoker.InvokeAsync(new CreateTaskCommand(title), ct);

result.Match(
success: id => Console.WriteLine($"Created task {id}"),
failure: error => Console.WriteLine($"Failed: {error}"));
}
}

Request dispatch flow

Read the result

Result<T> supports several consumption patterns:

// Option 1 — TryGet (out variables)
if (result.TryGet(out var id, out var error))
Console.WriteLine($"Created: {id}");
else
Console.WriteLine($"Error: {error}");

// Option 2 — Match (functional)
var message = result.Match(
success: id => $"Created: {id}",
failure: error => $"Error: {error}");

// Option 3 — IsSuccess guard
if (result.IsSuccess)
Process(result.Value);

For more detail on Result, see the UnambitiousFx.Functional documentation.

Complete example

// Program.cs
using UnambitiousFx.Synapse;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<ITaskRepository, InMemoryTaskRepository>();
builder.Services.AddSynapse(cfg =>
{
cfg.RegisterRequestHandler<CreateTaskHandler, CreateTaskCommand, Guid>();
});

var app = builder.Build();

app.MapPost("/tasks", async (
CreateTaskCommand cmd,
IInvoker invoker,
CancellationToken ct) =>
{
var result = await invoker.InvokeAsync(cmd, ct);
return result.Match(
success: id => Results.Created($"/tasks/{id}", id),
failure: error => Results.BadRequest(error.ToString()));
});

app.Run();

Next steps