Skip to content

v7: Handler-based API and awaitable RequestAsync alongside Rx #87

Closed

Description

@christopherread

Today: the only way to consume messages is IObservable<T> (Subscribe, OfType, ObserveOn), and requests return an observable from GetResponses. That's powerful, but easy to misuse: subscriptions get dropped or never disposed, and replies get sent from the wrong place. Async methods take no CancellationToken.

Proposal:

  • Handlers: bus.Handle<PlaceOrder>(async (cmd, ctx, ct) => ...), and ICommandHandler<T>, IEventHandler<T> and IRequestHandler<TReq, TResp> registered through DI.
  • Requests: await bus.RequestAsync<GetQuote, Quote>(request, timeout, ct).
  • Cancellation: a CancellationToken on every async method.
  • Streams: IAsyncEnumerable<T> views of message streams.
  • Observable-first, not observable-only: IObservable stays in the core as the engine. Handlers, RequestAsync and IAsyncEnumerable are thin adapters over it, and Rx stays first class for composing streams.

Concurrency control (from #37 by @fernando-almeida): each handler has a MaxConcurrency option, defaulting to processing one message at a time per handler, which is what Rx subscriptions do today. This gives the control #37 wanted from passing an IScheduler to IMessageSource, without changing the interface every transport implements. Rx users can still ObserveOn their own scheduler.

Done when: the examples and the new testing kit use handlers and RequestAsync, and all public async methods take a CancellationToken, and a test shows MaxConcurrency limits how many messages a handler processes at once.

Design: https://github.com/christopherread/Obvs/blob/master/docs/v7-design.md#4-handlers-and-awaitable-requests

Activity

  1. added this to the v7 milestone on Oct 25, 2026
  2. christopherread commented on Oct 26, 2026

    @christopherread
    OwnerAuthor

    Status on the v7 branch.

    Merged

    • v7: Handlers, RequestAsync and MaxConcurrency #114:
      • HandleCommand, HandleEvent and HandleRequest;
      • class handlers through DI: ICommandHandler<T>, IEventHandler<T> and IRequestHandler<TReq, TResp>;
      • MessageContext, with PublishAsync, SendAsync and replies, each taking a CancellationToken;
      • HandlerOptions.MaxConcurrency, which defaults to one at a time, in order, and has a test showing it limits concurrency;
      • RequestAsync<TReq, TResp>(request, timeout, ct), using TimeProvider.
    • v7: Settle messages after their handler, starting with Azure Service Bus #126: handlers settle their messages with transports that keep messages until they're settled. A message is completed on success, abandoned with the exception on failure, and released if the bus stops first. Azure Service Bus is the first transport to use this.

    Left

  3. added 2 commits that reference this issue on Oct 27, 2026
  4. christopherread commented on Oct 27, 2026

    @christopherread
    OwnerAuthor

    Done in #114 (handlers, RequestAsync and MaxConcurrency), #126 (settling messages after their handler), #138 and #143 (cancellation tokens through the bus and on to the transports), and #148 (ReadEventsAsync and ReadCommandsAsync for await foreach). Merged into v7.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions