Skip to content

About

Zero-allocation async event primitives for .NET. ValueTask-based AsyncEvent<T> with sequential and parallel invocation modes, source-generated dispatch, no reflection.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

316 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZeroAlloc.AsyncEvents

NuGet Build codecov License: MIT AOT GitHub Sponsors

Async events for .NET that you can actually await: a drop-in replacement for EventHandler<T> and Func<T, Task> + Task.WhenAll, with no allocations per invocation when handlers complete synchronously.

Why ZeroAlloc.AsyncEvents

  • 0 B per invocation. Raising an event whose handlers complete synchronously allocates nothing, in parallel and in sequential mode; tests assert it on net8.0 and net10.0. Naive async allocates 280–288 B per call.
  • More than 10× faster than Task.WhenAll fan-out, and faster than a plain synchronous multicast delegate, measured in the same run.
  • Real async semantics. Handlers return ValueTask, receive a CancellationToken, and run in parallel or in registration order. Exceptions reach the caller instead of vanishing in an async void.
  • Works under NativeAOT and trimming. No reflection; CI publishes and runs a NativeAOT sample on every build.

Install

dotnet add package ZeroAlloc.AsyncEvents

Quick start

using ZeroAlloc.AsyncEvents;

var orders = new OrderService();

orders.OrderPlaced += async (order, ct) =>
{
    await Task.Delay(10, ct);                        // e.g. send a confirmation email
    Console.WriteLine($"Emailed order {order.Id}");
};
orders.OrderPlaced += (order, ct) =>
{
    Console.WriteLine($"Audited order {order.Id}");  // completes synchronously
    return ValueTask.CompletedTask;
};

await orders.PlaceAsync(new OrderPlaced(42));

public sealed record OrderPlaced(int Id);

public partial class OrderService
{
    // The generator writes `public event AsyncEvent<OrderPlaced> OrderPlaced` for this field.
    [AsyncEvent]
    private AsyncEventHandler<OrderPlaced> _orderPlaced = new(InvokeMode.Parallel);

    public ValueTask PlaceAsync(OrderPlaced order, CancellationToken ct = default)
        => _orderPlaced.InvokeAsync(order, ct);
}

PlaceAsync returns once both handlers have finished. Subscribers use the familiar += and -=.

What you get

  • Parallel or sequential dispatch, chosen per field and overridable per call. Invoke modes
  • Cancelable events. A handler sets Cancel and the remaining handlers are skipped, for validation and veto pipelines. Cancelable events
  • Generated event accessors with [AsyncEvent] on a field or on the whole class. Mistakes are reported at compile time. Source generator, diagnostics
  • Lock-free, thread-safe subscription. Registering and unregistering never takes a lock.
  • Broad reach: netstandard2.0, netstandard2.1, net8.0 and net10.0.
  • Async property and collection notifications, the async INotifyPropertyChanged family, come from ZeroAlloc.Notify, which builds on this package.

Performance

Ten handlers, each returning a completed task, invoked once. net10.0, BenchmarkDotNet, same run; the ratio is against a synchronous multicast delegate. Allocations are exact; speed is a same-run ratio.

Method Ratio Allocated
EventHandler<T>, sync multicast 1.00 0 B
Func<T, Task> + Task.WhenAll 11.93 280 B
ZeroAlloc.AsyncEvents, parallel 0.44 0 B
ZeroAlloc.AsyncEvents, sequential 0.61 0 B

Full results for net8.0 and net10.0, methodology and allocation notes: docs/performance.md.

Learn more

About

Zero-allocation async event primitives for .NET. ValueTask-based AsyncEvent<T> with sequential and parallel invocation modes, source-generated dispatch, no reflection.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages