Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ public record MyEventData(string Message);
## API

### `WeakEvent`
* `SubscriberCount`\
Number of alive subscribers currently registered to the event.
* `Subscribe(Action handler)`\
`Subscribe(Func<Task> handler)`\
`Subscribe(Func<CancellationToken, Task> handler)`\
Expand All @@ -89,6 +91,8 @@ public record MyEventData(string Message);
Raises the event by invoking all live subscribers. Dead subscribers (whose targets have been garbage-collected) are removed.

### `WeakEvent<TEvent>`
* `SubscriberCount`\
Number of alive subscribers currently registered to the event.
* `Subscribe(Action<TEvent> handler)`\
`Subscribe(Func<TEvent, Task> handler)`\
`Subscribe(Func<TEvent, CancellationToken, Task> handler)`\
Expand Down
42 changes: 36 additions & 6 deletions src/WeakEvent.Tests/WeakEventGenericTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,14 @@ public async Task DeadHandler_IsNotInvoked_AfterGarbageCollection()
var weakEvent = new WeakEvent<string>();
var callCount = 0;

await CreateSubscriberAndInvoke(weakEvent, () => callCount++);
static async Task createSubscriberAndInvoke(WeakEvent<string> weakEvent, Action onEvent)
{
var subscriber = new GenericSubscriber(onEvent);
weakEvent.Subscribe(subscriber.Handler);
await weakEvent.PublishAsync("Test");
// The subscriber goes out of scope after this method, allowing it to be GC’d.
}
await createSubscriberAndInvoke(weakEvent, () => callCount++);

// Force garbage collection to reclaim the subscriber instance.
GC.Collect();
Expand All @@ -129,12 +136,35 @@ public async Task DeadHandler_IsNotInvoked_AfterGarbageCollection()
Assert.Equal(1, callCount);
}

private static async Task CreateSubscriberAndInvoke(WeakEvent<string> weakEvent, Action onEvent)
[Fact]
public void MultipleHandlers_CorrectCount()
{
var subscriber = new GenericSubscriber(onEvent);
weakEvent.Subscribe(subscriber.Handler);
await weakEvent.PublishAsync("Test");
// The subscriber goes out of scope after this method, allowing it to be GC’d.
// Stage 1 - No subscribers yet
var weakEvent = new WeakEvent<string>();
Assert.Equal(0, weakEvent.SubscriberCount);

// Stage 2 - Add local handler
void syncHandler(string _)
{ }
weakEvent.Subscribe(syncHandler);

// Stage 3 - Add a garbage-collected handler
void createSubscriberAndAssert()
{
var subscriber = new GenericSubscriber(() => { });
weakEvent.Subscribe(subscriber.Handler);
Assert.Equal(2, weakEvent.SubscriberCount);
}
createSubscriberAndAssert();

// Stage 4 - Force garbage collection to reclaim the garbage-collectable subscriber instance.
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.Equal(1, weakEvent.SubscriberCount);

// Stage 5 - Remove local handler
weakEvent.Unsubscribe(syncHandler);
Assert.Equal(0, weakEvent.SubscriberCount);
}

private class GenericSubscriber(Action onEvent)
Expand Down
43 changes: 37 additions & 6 deletions src/WeakEvent.Tests/WeakEventNonGenericTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,14 @@ public async Task DeadHandler_IsNotInvoked_AfterGarbageCollection()
var weakEvent = new WeakEvent();
var callCount = 0;

await CreateSubscriberAndInvoke(weakEvent, () => callCount++);
static async Task createSubscriberAndInvoke(WeakEvent weakEvent, Action onEvent)
{
var subscriber = new NonGenericSubscriber(onEvent);
weakEvent.Subscribe(subscriber.Handler);
await weakEvent.PublishAsync();
// The subscriber goes out of scope after this method, allowing it to be GC’d.
}
await createSubscriberAndInvoke(weakEvent, () => callCount++);

// Force garbage collection to reclaim the subscriber instance.
GC.Collect();
Expand All @@ -127,12 +134,36 @@ public async Task DeadHandler_IsNotInvoked_AfterGarbageCollection()
Assert.Equal(1, callCount);
}

private static async Task CreateSubscriberAndInvoke(WeakEvent weakEvent, Action onEvent)
[Fact]
public void MultipleHandlers_CorrectCount()
{
var subscriber = new NonGenericSubscriber(onEvent);
weakEvent.Subscribe(subscriber.Handler);
await weakEvent.PublishAsync();
// The subscriber goes out of scope after this method, allowing it to be GC’d.
// Stage 1 - No subscribers yet
var weakEvent = new WeakEvent();
Assert.Equal(0, weakEvent.SubscriberCount);

// Stage 2 - Add local handler
void syncHandler()
{ }
weakEvent.Subscribe(syncHandler);
Assert.Equal(1, weakEvent.SubscriberCount);

// Stage 3 - Add a garbage-collected handler
void createSubscriberAndAssert()
{
var subscriber = new NonGenericSubscriber(() => { });
weakEvent.Subscribe(subscriber.Handler);
Assert.Equal(2, weakEvent.SubscriberCount);
}
createSubscriberAndAssert();

// Stage 4 - Force garbage collection to reclaim the garbage-collectable subscriber instance.
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.Equal(1, weakEvent.SubscriberCount);

// Stage 5 - Remove local handler
weakEvent.Unsubscribe(syncHandler);
Assert.Equal(0, weakEvent.SubscriberCount);
}

private class NonGenericSubscriber(Action onEvent)
Expand Down
4 changes: 4 additions & 0 deletions src/WeakEvent/PACKAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public record MyEventData(string Message);
## API Overview

### `WeakEvent`
* `SubscriberCount`\
Number of alive subscribers currently registered to the event.
* `Subscribe(Action handler)`\
`Subscribe(Func<Task> handler)`\
`Subscribe(Func<CancellationToken, Task> handler)`\
Expand All @@ -80,6 +82,8 @@ public record MyEventData(string Message);
Raises the event by invoking all live subscribers. Dead subscribers (whose targets have been garbage-collected) are removed.

### `WeakEvent<TEvent>`
* `SubscriberCount`\
Number of alive subscribers currently registered to the event.
* `Subscribe(Action<TEvent> handler)`\
`Subscribe(Func<TEvent, Task> handler)`\
`Subscribe(Func<TEvent, CancellationToken, Task> handler)`\
Expand Down
5 changes: 5 additions & 0 deletions src/WeakEvent/WeakEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ public abstract class WeakEventBase

private readonly SemaphoreSlim _lock = new(1, 1);

/// <summary>
/// Number of alive subscribers currently registered to the event.
/// </summary>
public int SubscriberCount => _handlers.Count(x => x.IsAlive);

/// <summary>
/// Subscribes the specified delegate handler to the event.
/// </summary>
Expand Down