Skip to content

Repository files navigation

Shared Action

A simple C# class that allows multiple concurrent requests for the same operation to run that operation just once and share the result. It's intended to be used in systems that expect real-time results but want to avoid the waste that comes with concurrent processing of the same input. The more concurrent requests, the greater the benefit: this solution thrives under intense load tests.

This is not a cache: once an action is completed, the outcome is shared with everyone supplying the same input and discarded.

It's recommended to copy the source for this repository into your own and adjust as needed.

How to Use

Note: if you load test these using a browser, you may encounter stalls due to queuing. A purpose-built load generator tool is recommended.

Web API controller

public record Inventory(decimal Price, int InventoryCount); // Example data object.

// The key must include every input variable.
private static readonly SharedAction<(string Sku, int WarehouseId), Inventory?> inventoryActions = new();

[HttpGet]
public async Task<Inventory?> GetAsync(string sku, int warehouseId, CancellationToken cancellationToken)
{
    return await inventoryActions.RunAsync((sku, warehouseId), async (input, cancellationToken) =>
    {
        // The "input" parameter avoids the overhead of capturing variables.
        // "GetInventoryAsync" is a placeholder for the real call to the underlying system.
        return await GetInventoryAsync(input.Sku, input.WarehouseId, cancellationToken);
    }, cancellationToken);
}

ASP.NET Core Minimal API

// The key must include every input variable.
var inventoryActions = new SharedAction<(string Sku, int WarehouseId), Inventory?>();

app.MapGet("/api/realtimeinventory", async (string sku, int warehouseId, CancellationToken cancellationToken) =>
{
    var result = await inventoryActions.RunAsync((sku, warehouseId), async (input, cancellationToken) =>
    {
        // The "input" parameter avoids the overhead of capturing variables.
        // "GetInventoryAsync" is a placeholder for the real call to the underlying system.
        return await GetInventoryAsync(input.Sku, input.WarehouseId, cancellationToken);
    }, cancellationToken);

    return result;
});

Failures and Cancellation

If the action throws, the exception is shared with every concurrent requestor and the action is not retried. Everyone asking for the same thing at the same time gets the same outcome, whether that outcome is a value or a failure.

A caller that cancels abandons only its own wait. The action carries on for everyone still waiting, and is cancelled only once the last of them has given up.

About

A simple way to run a complex operation just once when there are multiple concurrent requests.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages