Skip to content

Commit

Permalink
XML docs, more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
martinothamar committed Apr 1, 2024
1 parent ae39563 commit 1faae55
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 6 deletions.
41 changes: 41 additions & 0 deletions src/Mediator/INotificationPublisher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

namespace Mediator;

/// <summary>
/// Implements a notification publisher that publishes notifications to multiple handlers using a foreach loop with async/await.
/// Tries to be efficient by avoiding unnecessary allocations and async state machines,
/// the optimal case being a single handler which completes synchronously.
/// </summary>
public sealed class ForeachAwaitPublisher : INotificationPublisher
{
public ValueTask Publish<TNotification>(
Expand Down Expand Up @@ -43,6 +48,11 @@ CancellationToken cancellationToken
}
}

/// <summary>
/// Implements a notification publisher that uses the Task.WhenAll pattern to handle multiple notification handlers.
/// Tries to be efficient by avoiding unnecessary allocations and async state machines,
/// the optimal case being a single handler or a collection of handlers that all complete synchronously.
/// </summary>
public sealed class TaskWhenAllPublisher : INotificationPublisher
{
public ValueTask Publish<TNotification>(
Expand Down Expand Up @@ -136,12 +146,22 @@ static async ValueTask AwaitTaskList(List<ValueTask> tasks)
}
}

/// <summary>
/// Represents a collection of notification handlers for a specific notification type.
/// Contains convenience methods for implementing the <see cref="INotificationPublisher"/> in an efficient way.
/// </summary>
/// <typeparam name="TNotification">The type of notification.</typeparam>
public readonly struct NotificationHandlers<TNotification>
where TNotification : INotification
{
private readonly IEnumerable<INotificationHandler<TNotification>> _handlers;
private readonly bool _isArray;

/// <summary>
/// Checks if the handlers are stored as an array and retrieves them if so.
/// </summary>
/// <param name="handlers">The array of notification handlers, if stored as an array.</param>
/// <returns><c>true</c> if the handlers are stored as an array; otherwise, <c>false</c>.</returns>
internal readonly bool IsArray([MaybeNullWhen(false)] out INotificationHandler<TNotification>[] handlers)
{
if (_isArray)
Expand All @@ -167,6 +187,12 @@ public NotificationHandlers(IEnumerable<INotificationHandler<TNotification>> han
_isArray = isArray;
}

/// <summary>
/// Checks wether there is exactly 1 single handler in the collection.
/// NOTE: if the underlying collection is not an array, this will return false.
/// </summary>
/// <param name="handler">The single handler, if there's a exactly 1 handler present</param>
/// <returns><c>true</c> if there is a single handler; otherwise, <c>false</c>.</returns>
public readonly bool IsSingleHandler([MaybeNullWhen(false)] out INotificationHandler<TNotification> handler)
{
if (IsArray(out var handlers) && handlers.Length == 1)
Expand Down Expand Up @@ -234,8 +260,23 @@ public bool MoveNext()
}
}

/// <summary>
/// Represents a notification publisher that is responsible for invoking handlers for a given notification.
/// This is called by the source generated Mediator implementation when <see cref="IPublisher.Publish{TNotification}(TNotification, CancellationToken)"/> is called.
/// Built in implementations are <see cref="ForeachAwaitPublisher"/> and <see cref="TaskWhenAllPublisher"/>.
/// Configure the desired implementation in the generator options.
/// </summary>
public interface INotificationPublisher
{
/// <summary>
/// Receives a notification and a collection of handlers for that notification.
/// The implementor is responsible for invoking each handler in the collection (and how to do it).
/// </summary>
/// <typeparam name="TNotification">The type of the notification.</typeparam>
/// <param name="handlers">The collection of handlers for the notification.</param>
/// <param name="notification">The notification to be published.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
ValueTask Publish<TNotification>(
NotificationHandlers<TNotification> handlers,
TNotification notification,
Expand Down
8 changes: 2 additions & 6 deletions test/Mediator.Tests/BasicHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,13 @@ public async Task Test_Notification_Handler()
}

[Fact]
public unsafe void Test_Struct_Notification_Handler()
public async Task Test_Struct_Notification_Handler()
{
var (sp, mediator) = Fixture.GetMediator();
var concrete = (Mediator)mediator;

var id = Guid.NewGuid();
var notification = new SomeStructNotification(id);
var addr = *(long*)&notification;

var handlers = sp.GetServices<INotificationHandler<SomeStructNotification>>();
Assert.True(handlers.Count() == 2);
Expand All @@ -320,13 +319,10 @@ public unsafe void Test_Struct_Notification_Handler()
var notificationHandler = sp.GetRequiredService<SomeStructNotificationHandler>();
Assert.NotNull(notificationHandler);

#pragma warning disable xUnit1031
concrete.Publish(notification).GetAwaiter().GetResult();
#pragma warning restore xUnit1031
await concrete.Publish(notification);
Assert.Contains(id, SomeStructNotificationHandler.Ids);
if (Mediator.ServiceLifetime != ServiceLifetime.Transient)
Assert.Equal(1, notificationHandler.InstanceIds.GetValueOrDefault(id, 0));
//Assert.Contains(addr, SomeStructNotificationHandler.Addresses);
}

[Fact]
Expand Down
69 changes: 69 additions & 0 deletions test/Mediator.Tests/NonSyncNotificationHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Mediator.Tests;

public class NonSyncNotificationHandlerTests
{
public sealed record SomeNonSyncNotification(Guid Id) : INotification;

public sealed class SomeNonSyncNotificationHandler0 : INotificationHandler<SomeNonSyncNotification>
{
internal readonly ConcurrentDictionary<Guid, (int Count, long Timestamp)> InstanceIds = new();

public async ValueTask Handle(SomeNonSyncNotification notification, CancellationToken cancellationToken)
{
await Task.Yield();
InstanceIds.AddOrUpdate(
notification.Id,
(1, Stopwatch.GetTimestamp()),
(_, data) => (data.Count + 1, Stopwatch.GetTimestamp())
);
}
}

public sealed class SomeNonSyncNotificationHandler1 : INotificationHandler<SomeNonSyncNotification>
{
internal readonly ConcurrentDictionary<Guid, (int Count, long Timestamp)> InstanceIds = new();

public async ValueTask Handle(SomeNonSyncNotification notification, CancellationToken cancellationToken)
{
await Task.Yield();
InstanceIds.AddOrUpdate(
notification.Id,
(1, Stopwatch.GetTimestamp()),
(_, data) => (data.Count + 1, Stopwatch.GetTimestamp())
);
}
}

[Fact]
public async Task Test_NonSync_Notification_Handlers()
{
var (sp, mediator) = Fixture.GetMediator();

var id = Guid.NewGuid();

var handler1 = sp.GetRequiredService<SomeNonSyncNotificationHandler0>();
var handler2 = sp.GetRequiredService<SomeNonSyncNotificationHandler1>();
Assert.NotNull(handler1);
Assert.NotNull(handler2);
var timestampBefore = Stopwatch.GetTimestamp();
await mediator.Publish(new SomeNonSyncNotification(id));
var timestampAfter = Stopwatch.GetTimestamp();
if (Mediator.ServiceLifetime != ServiceLifetime.Transient)
{
var handler1Data = handler1.InstanceIds.GetValueOrDefault(id, default);
var handler2Data = handler2.InstanceIds.GetValueOrDefault(id, default);
Assert.Equal(1, handler1Data.Count);
Assert.Equal(1, handler2Data.Count);
Assert.True(handler1Data.Timestamp > timestampBefore && handler1Data.Timestamp < timestampAfter);
Assert.True(handler2Data.Timestamp > timestampBefore && handler2Data.Timestamp < timestampAfter);
}
}
}

0 comments on commit 1faae55

Please sign in to comment.