Skip to content

Partie Parties

SteffenBlake edited this page Sep 28, 2026 · 1 revision

Parties

A Partie runs as an ordered step around a matching handler. Use one for logging, validation, access checks, and transaction boundaries. It runs even when no context requests its output.

Wrap downstream work

This Partie logs a course search and its completion:

// LogCourseSearchPartie.cs
public sealed record LogCourseSearchContext(
    [Inject] ILogger<LogCourseSearchPartie> Logger
);

public sealed class LogCourseSearchPartie :
    IQueryPartie<Unit, LogCourseSearchContext, CourseSearchQuery, CourseSearchResult>
{
    public static async ValueTask<Result<CourseSearchResult>> OnQueryAsync(
        LogCourseSearchContext context,
        CourseSearchQuery query,
        Next<Unit, CourseSearchResult> next,
        CancellationToken cancellationToken
    )
    {
        context.Logger.LogInformation("Searching for course {CourseId}", query.CourseId);
        try
        {
            return await next(Unit.Default);
        }
        finally
        {
            context.Logger.LogInformation("Finished course search {CourseId}", query.CourseId);
        }
    }
}
[LogCourseSearchPartie]
...

The four generic arguments are the output value, context, request, and handler result. This step provides Unit because it only adds behavior. A step can provide a useful value instead; later contexts receive it with [Provide].

Registration order

Parties enter in registration order. Code after an awaited next runs as the call returns through those steps. With logging before access checks, the flow is:

Logging before
    Access check before
        Handler
    Access check after
Logging after

Registrations on outer groups precede inner groups, followed by route registrations in source order. Bundles expand their members at the position where the bundle is registered.

An outer Partie can observe a result returned by an inner Partie. Put logging before validation when you want to record validation failures as well as handler results.

Stop a request early

Return a result without calling next to skip all later steps and the handler:

if (!allowed)
{
    return new Forbidden();
}

return await next(Unit.Default);

Here, allowed represents your access check inside an asynchronous hook. The return type remains Result<TResult> for the handler's result. Earlier Parties still resume and can log or map the failure.

Calling next(value) continues the pipeline with the step's output. You can call it repeatedly, but each call can execute downstream work again. For commands, that can repeat writes. Call it once for ordinary wrapping behavior.

Failures, exceptions, and cancellation

A returned failure travels back through earlier Parties as a normal result. An exception or cancellation also unwinds through normal C# try and finally blocks. Partie does not catch it automatically. Use finally for cleanup that must run on either path, and pass the invocation token to asynchronous work.

Command Parties

Implement ICommandPartie<TProvided, TContext, TCommand, TResult> with static OnCommandAsync for command behavior. It takes context, command, Next<TProvided, TResult>, and cancellation token, and returns exactly ValueTask<Result<TResult>>.

A class can implement both query and command contracts when its role, output type, and context agree. Declare both hooks. Use Generic matching to apply such a step to several request types.

See Providers for lazy values and Unit of work for the built-in command Partie.

Clone this wiki locally