RequestBatcher 0.0.2

dotnet add package RequestBatcher --version 0.0.2
                    
NuGet\Install-Package RequestBatcher -Version 0.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RequestBatcher" Version="0.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RequestBatcher" Version="0.0.2" />
                    
Directory.Packages.props
<PackageReference Include="RequestBatcher" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RequestBatcher --version 0.0.2
                    
#r "nuget: RequestBatcher, 0.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RequestBatcher@0.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RequestBatcher&version=0.0.2
                    
Install as a Cake Addin
#tool nuget:?package=RequestBatcher&version=0.0.2
                    
Install as a Cake Tool

RequestBatcher

RequestBatcher lets application code submit one request and await one Task, while an application handler receives multiple queued requests as an IReadOnlyList<TRequest>.

Batching does not require callers to assemble a collection. Separate callers can each submit one TRequest concurrently. RequestBatcher coalesces requests already queued for the same partition into handler batches of up to BatchSize. ProcessAsync(IEnumerable<TRequest>) is an additional submission option, not a prerequisite for batching.

Full documentation | 简体中文

When to Use It

Use RequestBatcher for independent database, cache, or downstream operations that benefit from a batch API. It also fits short traffic bursts that need bounded internal queue capacity and downstream concurrency, related requests that benefit from partition-local order, and work where caller cancellation should remove only requests that have not yet been dispatched. Dispatched work is allowed to finish independently of that caller.

For database updates, if partial success within one handler invocation would leave inconsistent state, the handler should execute that invocation in one transaction. RequestBatcher propagates the handler outcome but cannot roll back writes that have already been committed.

When Not to Use It

Do not use RequestBatcher when work must survive process failure, remain inside the caller's transaction, return a direct TResult, wait for a minimum batch size, or rely on automatic retries or exactly-once effects. It is also not suitable when an in-flight downstream operation must stop as soon as its individual caller disconnects, times out, or cancels. One handler batch can contain requests from several callers, so caller cancellation tokens are not forwarded to the handler and cannot cancel the shared handler call.

Install

dotnet add package RequestBatcher

Usage

Define a request and a handler for one batch:

public sealed record OrderWriteRequest(long OrderId, decimal Amount);

public interface IOrderStore
{
    Task WriteBatchAsync(
        IReadOnlyList<OrderWriteRequest> requests,
        CancellationToken cancellationToken);
}

public sealed class OrderWriteBatchHandler(IOrderStore store)
    : IRequestBatchHandler<OrderWriteRequest>
{
    public async ValueTask HandleAsync(
        IReadOnlyList<OrderWriteRequest> requests,
        CancellationToken cancellationToken = default)
    {
        await store.WriteBatchAsync(requests, cancellationToken);
    }
}

The handler's ValueTask is an internal completion signal that RequestBatcher awaits once. Application callers always receive Task.

Register the handler and choose its lifetime:

services.AddRequestBatcher<OrderWriteRequest, OrderWriteBatchHandler>(
    ServiceLifetime.Scoped,
    options =>
    {
        options.BatchSize = 256;
    });

Inject IRequestBatcher<TRequest> and submit requests:

public sealed class OrderService(IRequestBatcher<OrderWriteRequest> batcher)
{
    public Task SaveAsync(
        OrderWriteRequest request,
        CancellationToken cancellationToken = default) =>
        batcher.ProcessAsync(request, cancellationToken);
}

The returned Task completes after the handler has processed that request. If the handler fails, the caller receives the original exception.

If several handler calls fail for one explicit group, await follows normal Task semantics and throws one original exception. All distinct exception instances remain available through Task.Exception.InnerExceptions; one handler exception fanned out to several requests is recorded once.

When requests already exist as a group, submit them together:

await batcher.ProcessAsync(orderWriteRequests, cancellationToken);

This overload snapshots the group and submits it as one producer operation, then waits for every request. In Wait mode, an oversized group enters the queue as consecutive capacity-sized slices. In Fail mode, the whole group must fit immediately. The submission does not force one handler call; it may still be split by BatchSize or partition routing.

An explicit group is not a partition boundary. With more than one partition, every item is routed independently; the group is guaranteed to stay in one partition only when MaxConcurrency = 1, or when every item produces the same partition key.

Submission Behavior

Behavior Single request Explicit group
Capacity Reserves one request slot. Wait admits oversized groups in consecutive capacity-sized slices; Fail requires the whole group to fit immediately.
Routing Routes one request. Routes every item independently and can span partitions.
Handler calls May share a handler batch with other queued requests. Can split by partition and BatchSize; it is not a handler-batch boundary.
Completion Represents this request's actual outcome. Waits for every item in the submission.
Failure Fails when its handler batch fails. Can contain both successful work and failed work; the returned Task faults without rolling back successes.
Cancellation Cancels only before dispatch. Applies one token to every item; dispatched items still report their actual outcome.

Batching and Capacity

  • A handler batch contains at most BatchSize requests that are already queued.
  • RequestBatcher does not add a fixed delay or wait for a minimum batch size.
  • MaxPendingRequests is the internal BufferQueue capacity shared by queued and currently handled requests. It does not limit the size of one explicit submission or the number of callers waiting for capacity.
  • Wait admits a group no larger than the capacity atomically and splits a larger group into consecutive capacity-sized slices. Fail rejects the whole submission unless it fits immediately.
  • Caller cancellation removes a request only before handler dispatch. After dispatch, the caller observes the real handler outcome.
  • Shutdown rejects new calls and drains every submission that started before shutdown, including submissions waiting for capacity. Requests still cannot be recovered after a process failure.

Routing Modes

MaxConcurrency controls both handler concurrency and partition count:

Configuration Routing and ordering
MaxConcurrency = 1 All requests use one partition and retain global append order.
MaxConcurrency > 1, no partition key Requests advance round-robin, including every item in an explicit group. Ordering is per partition.
MaxConcurrency > 1, with UsePartitionKey The selector runs for every item. Equal keys use one partition and retain their input order there.

One explicit group can therefore be handled concurrently by several partitions. Each handler invocation reads from one partition. Concurrent callers have no defined order before their requests are appended.

Partition routing is optional:

services.AddRequestBatcher<OrderWriteRequest, OrderWriteBatchHandler>(
    ServiceLifetime.Scoped,
    options =>
    {
        options.BatchSize = 256;
        options.MaxConcurrency = 4;

        // Optional. Without this selector, routing is round-robin.
        options.UsePartitionKey(request => request.OrderId);
    });

Equal finite, integer-valued numeric keys or equal non-null string keys are routed to one partition and processed there in append order. A partition key controls routing only: it does not force related requests into one handler batch or deduplicate them.

Partition-local ordering can support patterns such as merging repeated updates within each batch. Correctness across batches still requires application safeguards such as version checks, idempotency keys, unique constraints, or transactions.

Do not move an operation into RequestBatcher when it must commit or roll back with the caller's transaction.

The runnable PostgreSQL Web API sample shows batched upserts, version-protected updates, and deduplicated reads.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.2 45 8/16/2026
0.0.1 47 8/15/2026

Supports capacity-sliced batch submissions with BufferQueue 2.0.2, preserves cancellation and partition behavior, and safely drains submissions during shutdown.