Skip to content

Commit a98c576

Browse files
committed
Introduce conditional compilation for dynamic dispatch
But keep it enabled for now, since bechmarks show it has a possitive effect in allocations and performance. Closes #116
1 parent c97947b commit a98c576

File tree

4 files changed

+48
-18
lines changed

4 files changed

+48
-18
lines changed

src/Merq.Benchmarks/MerqVsMediatR.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
using BenchmarkDotNet.Attributes;
1+
using System.Diagnostics;
22
using System.Threading;
33
using System.Threading.Tasks;
4+
using BenchmarkDotNet.Attributes;
5+
using BenchmarkDotNet.Diagnostics.Windows.Configs;
46
using MediatR;
57
using Microsoft.Extensions.DependencyInjection;
6-
using System.Diagnostics;
7-
using BenchmarkDotNet.Diagnostics.Windows.Configs;
88

99
namespace Merq.MerqVsMediatR;
1010

src/Merq.Core/Merq.Core.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
$(Title)
1111
Only the main application assembly needs to reference this package. Components and extensions can simply reference the interfaces in Merq.
1212
</Description>
13+
<DefineConstants>$(DefineConstants);DYNAMIC_DISPATCH</DefineConstants>
1314
</PropertyGroup>
1415

1516
<ItemGroup>

src/Merq.Core/MessageBus.cs

+26-15
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public class MessageBus : IMessageBus
7373
readonly IServiceProvider services;
7474
readonly IServiceCollection? collection;
7575

76-
// These executors are needed when the commadn types involved are not public.
76+
// These executors are needed when the command types involved are not public.
7777
// For the public cases, we just rely on the built-in dynamic dispatching
7878
readonly ConcurrentDictionary<Type, VoidDispatcher> voidExecutors = new();
7979
readonly ConcurrentDictionary<Type, VoidAsyncDispatcher> voidAsyncExecutors = new();
@@ -194,15 +194,18 @@ public void Execute(ICommand command, [CallerMemberName] string? callerName = de
194194

195195
try
196196
{
197-
if (type.IsPublic)
197+
#if DYNAMIC_DISPATCH
198+
199+
if (type.IsPublic || type.IsNestedPublic)
198200
// For public types, we can use the faster dynamic dispatch approach
199201
ExecuteCore((dynamic)command);
200202
else
203+
#endif
201204
voidExecutors.GetOrAdd(type, type
202-
=> (VoidDispatcher)Activator.CreateInstance(
203-
typeof(VoidDispatcher<>).MakeGenericType(type),
204-
this)!)
205-
.Execute(command);
205+
=> (VoidDispatcher)Activator.CreateInstance(
206+
typeof(VoidDispatcher<>).MakeGenericType(type),
207+
this)!)
208+
.Execute(command);
206209
}
207210
catch (Exception e)
208211
{
@@ -221,9 +224,11 @@ public TResult Execute<TResult>(ICommand<TResult> command, [CallerMemberName] st
221224

222225
try
223226
{
224-
if (type.IsPublic)
227+
#if DYNAMIC_DISPATCH
228+
if (type.IsPublic || type.IsNestedPublic)
225229
// For public types, we can use the faster dynamic dispatch approach
226230
return WithResult<TResult>().Execute((dynamic)command);
231+
#endif
227232

228233
return (TResult)resultExecutors.GetOrAdd(type, type
229234
=> (ResultDispatcher)Activator.CreateInstance(
@@ -248,10 +253,11 @@ public Task ExecuteAsync(IAsyncCommand command, CancellationToken cancellation =
248253

249254
try
250255
{
251-
if (type.IsPublic)
256+
#if DYNAMIC_DISPATCH
257+
if (type.IsPublic || type.IsNestedPublic)
252258
// For public types, we can use the faster dynamic dispatch approach
253259
return ExecuteAsyncCore((dynamic)command, cancellation);
254-
260+
#endif
255261
return voidAsyncExecutors.GetOrAdd(type, type
256262
=> (VoidAsyncDispatcher)Activator.CreateInstance(
257263
typeof(VoidAsyncDispatcher<>).MakeGenericType(type),
@@ -275,10 +281,11 @@ public Task<TResult> ExecuteAsync<TResult>(IAsyncCommand<TResult> command, Cance
275281

276282
try
277283
{
278-
if (type.IsPublic)
284+
#if DYNAMIC_DISPATCH
285+
if (type.IsPublic || type.IsNestedPublic)
279286
// For public types, we can use the faster dynamic dispatch approach
280287
return WithResult<TResult>().ExecuteAsync((dynamic)command, cancellation);
281-
288+
#endif
282289
return (Task<TResult>)resultAsyncExecutors.GetOrAdd(type, type
283290
=> (ResultAsyncDispatcher)Activator.CreateInstance(
284291
typeof(ResultAsyncDispatcher<,>).MakeGenericType(type, typeof(TResult)),
@@ -303,10 +310,11 @@ public IAsyncEnumerable<TResult> ExecuteStream<TResult>(IStreamCommand<TResult>
303310

304311
try
305312
{
313+
#if DYNAMIC_DISPATCH
306314
if (type.IsPublic || type.IsNestedPublic)
307315
// For public types, we can use the faster dynamic dispatch approach
308316
return WithResult<TResult>().ExecuteStream((dynamic)command, cancellation);
309-
317+
#endif
310318
return (IAsyncEnumerable<TResult>)resultAsyncExecutors.GetOrAdd(type, type
311319
=> (ResultAsyncDispatcher)Activator.CreateInstance(
312320
typeof(ResultStreamDispatcher<,>).MakeGenericType(type, typeof(TResult)),
@@ -734,7 +742,8 @@ abstract class VoidDispatcher
734742

735743
class VoidDispatcher<TCommand>(MessageBus bus) : VoidDispatcher where TCommand : ICommand
736744
{
737-
public override void Execute(IExecutable command) => bus.ExecuteCore((TCommand)command);
745+
public override void Execute(IExecutable command)
746+
=> bus.ExecuteCore((TCommand)command);
738747
}
739748

740749
abstract class ResultDispatcher
@@ -744,7 +753,8 @@ abstract class ResultDispatcher
744753

745754
class ResultDispatcher<TCommand, TResult>(MessageBus bus) : ResultDispatcher where TCommand : ICommand<TResult>
746755
{
747-
public override object? Execute(IExecutable command) => bus.ExecuteCore<TCommand, TResult>((TCommand)command);
756+
public override object? Execute(IExecutable command)
757+
=> bus.ExecuteCore<TCommand, TResult>((TCommand)command);
748758
}
749759

750760
abstract class VoidAsyncDispatcher
@@ -754,7 +764,8 @@ abstract class VoidAsyncDispatcher
754764

755765
class VoidAsyncDispatcher<TCommand>(MessageBus bus) : VoidAsyncDispatcher where TCommand : IAsyncCommand
756766
{
757-
public override Task ExecuteAsync(IExecutable command, CancellationToken cancellation) => bus.ExecuteAsyncCore((TCommand)command, cancellation);
767+
public override Task ExecuteAsync(IExecutable command, CancellationToken cancellation)
768+
=> bus.ExecuteAsyncCore((TCommand)command, cancellation);
758769
}
759770

760771
abstract class ResultAsyncDispatcher

src/Merq.Tests/MessageBusSpec.cs

+18
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,22 @@ public void when_executing_non_public_command_then_invokes_handler()
467467
bus.Execute(new NonPublicCommand());
468468
}
469469

470+
[Fact]
471+
public void when_executing_nested_public_command_then_invokes_handler()
472+
{
473+
var handler = new Mock<ICommandHandler<NestedPublicCommand>>();
474+
var bus = new MessageBus(new ServiceCollection()
475+
.AddSingleton(handler.Object)
476+
.AddSingleton<IExecutableCommandHandler<NestedPublicCommand>>(handler.Object)
477+
.BuildServiceProvider());
478+
479+
var command = new NestedPublicCommand();
480+
481+
bus.Execute(command);
482+
483+
handler.Verify(x => x.Execute(command));
484+
}
485+
470486
[Fact]
471487
public void when_executing_non_public_command_result_then_invokes_handler()
472488
{
@@ -570,6 +586,8 @@ public void when_execute_throws_activity_has_error_status()
570586

571587
class NestedEvent { }
572588

589+
public class NestedPublicCommand : ICommand { }
590+
573591
#if NET6_0_OR_GREATER
574592
public record StreamCommand(int Count) : IStreamCommand<int>;
575593

0 commit comments

Comments
 (0)