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.
Note: if you load test these using a browser, you may encounter stalls due to queuing. A purpose-built load generator tool is recommended.
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);
}// 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;
});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.