Skip to content

Commit

Permalink
More perf work, more tests, docs and sample
Browse files Browse the repository at this point in the history
  • Loading branch information
martinothamar committed Apr 3, 2024
1 parent 5e9081b commit 1a0be21
Show file tree
Hide file tree
Showing 15 changed files with 630 additions and 96 deletions.
18 changes: 18 additions & 0 deletions Mediator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InternalMessages.Applicatio
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InternalMessages.Domain", "samples\apps\InternalMessages\InternalMessages.Domain\InternalMessages.Domain.csproj", "{1A16060A-3393-4404-A3AC-63E10B3648DE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "basic", "basic", "{686F96A2-0D44-4A5B-9A7C-78608D95E5DB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NotificationPublisher", "samples\basic\NotificationPublisher\NotificationPublisher.csproj", "{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -407,6 +411,18 @@ Global
{1A16060A-3393-4404-A3AC-63E10B3648DE}.Release|x64.Build.0 = Release|Any CPU
{1A16060A-3393-4404-A3AC-63E10B3648DE}.Release|x86.ActiveCfg = Release|Any CPU
{1A16060A-3393-4404-A3AC-63E10B3648DE}.Release|x86.Build.0 = Release|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|x64.ActiveCfg = Debug|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|x64.Build.0 = Debug|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|x86.ActiveCfg = Debug|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|x86.Build.0 = Debug|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|Any CPU.Build.0 = Release|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|x64.ActiveCfg = Release|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|x64.Build.0 = Release|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|x86.ActiveCfg = Release|Any CPU
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -440,6 +456,8 @@ Global
{10E7F076-1B64-4DF6-9FA0-399BBD87F42F} = {D7662382-63B6-4E3D-A6CE-8EC19E473265}
{B4EE2FF6-3D88-45B5-9F43-9BD2474F2181} = {D7662382-63B6-4E3D-A6CE-8EC19E473265}
{1A16060A-3393-4404-A3AC-63E10B3648DE} = {D7662382-63B6-4E3D-A6CE-8EC19E473265}
{686F96A2-0D44-4A5B-9A7C-78608D95E5DB} = {D3569CDD-7E19-429E-B9AD-75CC05F6C4AA}
{FF68E713-7FA9-42F4-8D0F-81A4387BECCD} = {686F96A2-0D44-4A5B-9A7C-78608D95E5DB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D45B5457-4190-49B6-BF89-7FA5F4C8ABE2}
Expand Down
47 changes: 45 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ See this great video by [@Elfocrash / Nick Chapsas](https://github.com/Elfocrash
- [4.6. Use notifications](#46-use-notifications)
- [4.7. Polymorphic dispatch with notification handlers](#47-polymorphic-dispatch-with-notification-handlers)
- [4.8. Notification handlers also support open generics](#48-notification-handlers-also-support-open-generics)
- [4.9. Use streaming messages](#49-use-streaming-messages)
- [4.9. Notification publishers](#49-notification-publishers)
- [4.10. Use streaming messages](#410-use-streaming-messages)
- [5. Diagnostics](#5-diagnostics)
- [6. Differences from MediatR](#6-differences-from-mediatr)
- [7. Versioning](#7-versioning)
Expand Down Expand Up @@ -451,8 +452,50 @@ public sealed class GenericNotificationHandler<TNotification> : INotificationHan
}
```

### 4.9. Notification publishers

### 4.9. Use streaming messages
Notification publishers are responsible for dispatching notifications to a collection of handlers.
There are two built in implementations:

* `ForeachAwaitPublisher` - the default, dispatches the notifications to handlers in order 1-by-1
* `TaskWhenAllPublisher` - dispatches notifications in parallel

Both of these try to be efficient by handling a number of special cases (early exit on sync completion, single-handler, array of handlers).
Below we implement a custom one by simply using `Task.WhenAll`.

```csharp
services.AddMediator(options =>
{
options.NotificationPublisherType = typeof(FireAndForgetNotificationPublisher);
});

public sealed class FireAndForgetNotificationPublisher : INotificationPublisher
{
public async ValueTask Publish<TNotification>(
NotificationHandlers<TNotification> handlers,
TNotification notification,
CancellationToken cancellationToken
)
where TNotification : INotification
{
try
{
await Task.WhenAll(handlers.Select(handler => handler.Handle(notification, cancellationToken).AsTask()));
}
catch (Exception ex)
{
// Notifications should be fire-and-forget, we just need to log it!
// This way we don't have to worry about exceptions bubbling up when publishing notifications
Console.Error.WriteLine(ex);

// NOTE: not necessarily saying this is a good idea!
}
}
}
```


### 4.10. Use streaming messages

Since version 1.* of this library there is support for streaming using `IAsyncEnumerable`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,6 @@ [new MsBuildArgument("/p:ExtraDefineConstants=Mediator_Publisher_TaskWhenAll")]
private MultiHandlerAsync2 _multiHandlerAsync2;
private MultiHandlersAsyncNotification _multiHandlersAsyncNotification;

// [Params(Mediator.ServiceLifetime)]
// public ServiceLifetime ServiceLifetime { get; set; }

// [Params(Mediator.NotificationPublisherName)]
// public string NotificationPublisherType { get; set; }

public enum ScenarioType
{
SingleHandlerSync,
Expand Down Expand Up @@ -233,10 +227,9 @@ public Task Publish_Notification_MediatR()
{
return Scenario switch
{
ScenarioType.SingleHandlerSync => _mediatr.Publish(_singleHandlerNotification, CancellationToken.None),
ScenarioType.MultiHandlersSync => _mediatr.Publish(_multiHandlersNotification, CancellationToken.None),
ScenarioType.MultiHandlersAsync
=> _mediatr.Publish(_multiHandlersAsyncNotification, CancellationToken.None),
ScenarioType.SingleHandlerSync => _mediatr.Publish(_singleHandlerNotification),
ScenarioType.MultiHandlersSync => _mediatr.Publish(_multiHandlersNotification),
ScenarioType.MultiHandlersAsync => _mediatr.Publish(_multiHandlersAsyncNotification),
};
}

Expand All @@ -245,10 +238,9 @@ public ValueTask Publish_Notification_IMediator()
{
return Scenario switch
{
ScenarioType.SingleHandlerSync => _mediator.Publish(_singleHandlerNotification, CancellationToken.None),
ScenarioType.MultiHandlersSync => _mediator.Publish(_multiHandlersNotification, CancellationToken.None),
ScenarioType.MultiHandlersAsync
=> _mediator.Publish(_multiHandlersAsyncNotification, CancellationToken.None),
ScenarioType.SingleHandlerSync => _mediator.Publish(_singleHandlerNotification),
ScenarioType.MultiHandlersSync => _mediator.Publish(_multiHandlersNotification),
ScenarioType.MultiHandlersAsync => _mediator.Publish(_multiHandlersAsyncNotification),
};
}

Expand All @@ -257,12 +249,9 @@ public ValueTask Publish_Notification_Mediator()
{
return Scenario switch
{
ScenarioType.SingleHandlerSync
=> _concreteMediator.Publish(_singleHandlerNotification, CancellationToken.None),
ScenarioType.MultiHandlersSync
=> _concreteMediator.Publish(_multiHandlersNotification, CancellationToken.None),
ScenarioType.MultiHandlersAsync
=> _concreteMediator.Publish(_multiHandlersAsyncNotification, CancellationToken.None),
ScenarioType.SingleHandlerSync => _concreteMediator.Publish(_singleHandlerNotification),
ScenarioType.MultiHandlersSync => _concreteMediator.Publish(_multiHandlersNotification),
ScenarioType.MultiHandlersAsync => _concreteMediator.Publish(_multiHandlersAsyncNotification),
};
}

Expand All @@ -272,11 +261,11 @@ public ValueTask Publish_Notification_Baseline()
switch (Scenario)
{
case ScenarioType.SingleHandlerSync:
return _singleHandler.Handle(_singleHandlerNotification, CancellationToken.None);
return _singleHandler.Handle(_singleHandlerNotification, default);
case ScenarioType.MultiHandlersSync:
_multiHandler0.Handle(_multiHandlersNotification, CancellationToken.None).GetAwaiter().GetResult();
_multiHandler1.Handle(_multiHandlersNotification, CancellationToken.None).GetAwaiter().GetResult();
_multiHandler2.Handle(_multiHandlersNotification, CancellationToken.None).GetAwaiter().GetResult();
_multiHandler0.Handle(_multiHandlersNotification, default).GetAwaiter().GetResult();
_multiHandler1.Handle(_multiHandlersNotification, default).GetAwaiter().GetResult();
_multiHandler2.Handle(_multiHandlersNotification, default).GetAwaiter().GetResult();
return default;
case ScenarioType.MultiHandlersAsync:
return AwaitMultipleHandlersAsync();
Expand All @@ -286,9 +275,9 @@ public ValueTask Publish_Notification_Baseline()

async ValueTask AwaitMultipleHandlersAsync()
{
await _multiHandlerAsync0.Handle(_multiHandlersAsyncNotification, CancellationToken.None);
await _multiHandlerAsync1.Handle(_multiHandlersAsyncNotification, CancellationToken.None);
await _multiHandlerAsync2.Handle(_multiHandlersAsyncNotification, CancellationToken.None);
await _multiHandlerAsync0.Handle(_multiHandlersAsyncNotification, default);
await _multiHandlerAsync1.Handle(_multiHandlersAsyncNotification, default);
await _multiHandlerAsync2.Handle(_multiHandlersAsyncNotification, default);
}
}
}
10 changes: 10 additions & 0 deletions samples/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project>
<Import Project="..\Directory.Build.props" />

<ItemGroup>
<PackageReference Include="CSharpier.MsBuild" Version="0.27.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
28 changes: 27 additions & 1 deletion samples/Showcase/Program.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Mediator;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddMediator();
services.AddMediator(options =>
{
options.NotificationPublisherType = typeof(FireAndForgetNotificationPublisher);
});

// Ordering of pipeline behavior registrations matter!
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(ErrorLoggerHandler<,>));
Expand Down Expand Up @@ -139,3 +143,25 @@ public ValueTask Handle(TNotification notification, CancellationToken cancellati
return default;
}
}

public sealed class FireAndForgetNotificationPublisher : INotificationPublisher
{
public async ValueTask Publish<TNotification>(
NotificationHandlers<TNotification> handlers,
TNotification notification,
CancellationToken cancellationToken
)
where TNotification : INotification
{
try
{
await Task.WhenAll(handlers.Select(handler => handler.Handle(notification, cancellationToken).AsTask()));
}
catch (Exception ex)
{
// Notifications should be fire-and-forget, we just need to log it!
// This way we don't have to worry about exceptions bubbling up when publishing notifications
Console.Error.WriteLine(ex);
}
}
}
38 changes: 38 additions & 0 deletions samples/basic/NotificationPublisher/NotificationPublisher.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<!-- Tells the compiler to emit the code generated by Mediator.SourceGenerator as files in the project -->
<!-- This is useful for debugging purposes -->
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<!-- The path where the generated files will be placed -->
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

<ItemGroup>
<!-- Tells the compiler to ignore the generated files when compiling the project (it will still be part of the compilation) -->
<Compile Remove="$(CompilerGeneratedFilesOutputPath)/**/*.cs" />
<None Include="$(CompilerGeneratedFilesOutputPath)/**/*.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(DotNetVersion)" />
</ItemGroup>

<!-- Use Mediator from local git repo -->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Mediator.SourceGenerator\Mediator.SourceGenerator.csproj" OutputItemType="Analyzer" />
<ProjectReference Include="..\..\..\src\Mediator\Mediator.csproj" />
</ItemGroup>
<!-- Uncomment below to use Mediator from NuGet -->
<!-- <ItemGroup>
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.0-*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Mediator.Abstractions" Version="3.0.0-*" />
</ItemGroup> -->

</Project>
88 changes: 88 additions & 0 deletions samples/basic/NotificationPublisher/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Mediator;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddMediator(options =>
{
options.NotificationPublisherType = typeof(MyNotificationPublisher);
});

var serviceProvider = services.BuildServiceProvider();

var mediator = serviceProvider.GetRequiredService<IMediator>();

var id = Guid.NewGuid();
var notification = new Notification(id);

Console.WriteLine("Publishing!");
Console.WriteLine("-----------------------------------");

await mediator.Publish(notification);

Console.WriteLine("-----------------------------------");
Console.WriteLine("Finished publishing!");

return 0;

//
// Here are the types used
//

public sealed class MyNotificationPublisher : INotificationPublisher
{
public async ValueTask Publish<TNotification>(
NotificationHandlers<TNotification> handlers,
TNotification notification,
CancellationToken cancellationToken
)
where TNotification : INotification
{
try
{
// IsSingleHandler is a convenience method to check if there is only one handler
// so that we can early exist. Used for optimization purposes by the built in implementations.
if (handlers.IsSingleHandler(out var singleHandler))
{
await singleHandler.Handle(notification, cancellationToken);
return;
}
// IsArray is a convenience method to check if the handlers are an array (for the built-in DI container in BCL, this is the case)
// so that we can iterate and/or index directly. Used for optimization purposes by the built in implementations.
else if (handlers.IsArray(out var array))
{
foreach (var handler in array)
{
await handler.Handle(notification, cancellationToken);
}
}
else
{
// Or we can just box the tasks and await them all
await Task.WhenAll(
handlers.Select(handler => handler.Handle(notification, cancellationToken).AsTask())
);
}
}
catch (Exception ex)
{
// Notifications should be fire-and-forget, we just need to log it!
Console.Error.WriteLine(ex);
}
}
}

public sealed record Notification(Guid Id) : INotification;

public sealed class MyNotificationHandler : INotificationHandler<Notification>
{
public ValueTask Handle(Notification notification, CancellationToken cancellationToken)
{
Console.WriteLine($"{GetType().Name} - {notification.Id}");
throw new Exception("Something went wrong!");
}
}
19 changes: 19 additions & 0 deletions samples/basic/NotificationPublisher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## NotificationPublisher

Simple showcase of using a custom notification publisher, by implementing `INotificationPublisher`.
The custom publisher catches all exceptions and logs them, a so called fire-and-forget implementation.

### Build and run

```console
$ dotnet run
Publishing!
-----------------------------------
MyNotificationHandler - 6ae7d56b-8a2f-404c-a24b-c5df1e6691d2
System.Exception: Something went wrong!
at MyNotificationHandler.Handle(Notification notification, CancellationToken cancellationToken) in /home/martin/code/private/Mediator/samples/basic/NotificationPublisher/Program.cs:line 79
at MyNotificationPublisher.Publish[TNotification](NotificationHandlers`1 handlers, TNotification notification, CancellationToken cancellationToken) in /home/martin/code/private/Mediator/samples/basic/NotificationPublisher/Program.cs:line 46
-----------------------------------
Finished publishing!
```

Loading

0 comments on commit 1a0be21

Please sign in to comment.