Skip to content

Commit

Permalink
Introduce conditional compilation for dynamic dispatch
Browse files Browse the repository at this point in the history
But keep it enabled for now, since bechmarks show it has a possitive effect in allocations and performance.

Closes #116
  • Loading branch information
kzu committed Jan 29, 2024
1 parent c97947b commit a98c576
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 18 deletions.
6 changes: 3 additions & 3 deletions src/Merq.Benchmarks/MerqVsMediatR.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using BenchmarkDotNet.Attributes;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnostics.Windows.Configs;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics;
using BenchmarkDotNet.Diagnostics.Windows.Configs;

namespace Merq.MerqVsMediatR;

Expand Down
1 change: 1 addition & 0 deletions src/Merq.Core/Merq.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
$(Title)
Only the main application assembly needs to reference this package. Components and extensions can simply reference the interfaces in Merq.
</Description>
<DefineConstants>$(DefineConstants);DYNAMIC_DISPATCH</DefineConstants>
</PropertyGroup>

<ItemGroup>
Expand Down
41 changes: 26 additions & 15 deletions src/Merq.Core/MessageBus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public class MessageBus : IMessageBus
readonly IServiceProvider services;
readonly IServiceCollection? collection;

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

try
{
if (type.IsPublic)
#if DYNAMIC_DISPATCH

if (type.IsPublic || type.IsNestedPublic)
// For public types, we can use the faster dynamic dispatch approach
ExecuteCore((dynamic)command);
else
#endif
voidExecutors.GetOrAdd(type, type
=> (VoidDispatcher)Activator.CreateInstance(
typeof(VoidDispatcher<>).MakeGenericType(type),
this)!)
.Execute(command);
=> (VoidDispatcher)Activator.CreateInstance(
typeof(VoidDispatcher<>).MakeGenericType(type),
this)!)
.Execute(command);
}
catch (Exception e)
{
Expand All @@ -221,9 +224,11 @@ public TResult Execute<TResult>(ICommand<TResult> command, [CallerMemberName] st

try
{
if (type.IsPublic)
#if DYNAMIC_DISPATCH
if (type.IsPublic || type.IsNestedPublic)
// For public types, we can use the faster dynamic dispatch approach
return WithResult<TResult>().Execute((dynamic)command);
#endif

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

try
{
if (type.IsPublic)
#if DYNAMIC_DISPATCH
if (type.IsPublic || type.IsNestedPublic)
// For public types, we can use the faster dynamic dispatch approach
return ExecuteAsyncCore((dynamic)command, cancellation);

#endif
return voidAsyncExecutors.GetOrAdd(type, type
=> (VoidAsyncDispatcher)Activator.CreateInstance(
typeof(VoidAsyncDispatcher<>).MakeGenericType(type),
Expand All @@ -275,10 +281,11 @@ public Task<TResult> ExecuteAsync<TResult>(IAsyncCommand<TResult> command, Cance

try
{
if (type.IsPublic)
#if DYNAMIC_DISPATCH
if (type.IsPublic || type.IsNestedPublic)
// For public types, we can use the faster dynamic dispatch approach
return WithResult<TResult>().ExecuteAsync((dynamic)command, cancellation);

#endif
return (Task<TResult>)resultAsyncExecutors.GetOrAdd(type, type
=> (ResultAsyncDispatcher)Activator.CreateInstance(
typeof(ResultAsyncDispatcher<,>).MakeGenericType(type, typeof(TResult)),
Expand All @@ -303,10 +310,11 @@ public IAsyncEnumerable<TResult> ExecuteStream<TResult>(IStreamCommand<TResult>

try
{
#if DYNAMIC_DISPATCH
if (type.IsPublic || type.IsNestedPublic)
// For public types, we can use the faster dynamic dispatch approach
return WithResult<TResult>().ExecuteStream((dynamic)command, cancellation);

#endif
return (IAsyncEnumerable<TResult>)resultAsyncExecutors.GetOrAdd(type, type
=> (ResultAsyncDispatcher)Activator.CreateInstance(
typeof(ResultStreamDispatcher<,>).MakeGenericType(type, typeof(TResult)),
Expand Down Expand Up @@ -734,7 +742,8 @@ abstract class VoidDispatcher

class VoidDispatcher<TCommand>(MessageBus bus) : VoidDispatcher where TCommand : ICommand
{
public override void Execute(IExecutable command) => bus.ExecuteCore((TCommand)command);
public override void Execute(IExecutable command)
=> bus.ExecuteCore((TCommand)command);
}

abstract class ResultDispatcher
Expand All @@ -744,7 +753,8 @@ abstract class ResultDispatcher

class ResultDispatcher<TCommand, TResult>(MessageBus bus) : ResultDispatcher where TCommand : ICommand<TResult>
{
public override object? Execute(IExecutable command) => bus.ExecuteCore<TCommand, TResult>((TCommand)command);
public override object? Execute(IExecutable command)
=> bus.ExecuteCore<TCommand, TResult>((TCommand)command);
}

abstract class VoidAsyncDispatcher
Expand All @@ -754,7 +764,8 @@ abstract class VoidAsyncDispatcher

class VoidAsyncDispatcher<TCommand>(MessageBus bus) : VoidAsyncDispatcher where TCommand : IAsyncCommand
{
public override Task ExecuteAsync(IExecutable command, CancellationToken cancellation) => bus.ExecuteAsyncCore((TCommand)command, cancellation);
public override Task ExecuteAsync(IExecutable command, CancellationToken cancellation)
=> bus.ExecuteAsyncCore((TCommand)command, cancellation);
}

abstract class ResultAsyncDispatcher
Expand Down
18 changes: 18 additions & 0 deletions src/Merq.Tests/MessageBusSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,22 @@ public void when_executing_non_public_command_then_invokes_handler()
bus.Execute(new NonPublicCommand());
}

[Fact]
public void when_executing_nested_public_command_then_invokes_handler()
{
var handler = new Mock<ICommandHandler<NestedPublicCommand>>();
var bus = new MessageBus(new ServiceCollection()
.AddSingleton(handler.Object)
.AddSingleton<IExecutableCommandHandler<NestedPublicCommand>>(handler.Object)
.BuildServiceProvider());

var command = new NestedPublicCommand();

bus.Execute(command);

handler.Verify(x => x.Execute(command));
}

[Fact]
public void when_executing_non_public_command_result_then_invokes_handler()
{
Expand Down Expand Up @@ -570,6 +586,8 @@ public void when_execute_throws_activity_has_error_status()

class NestedEvent { }

public class NestedPublicCommand : ICommand { }

#if NET6_0_OR_GREATER
public record StreamCommand(int Count) : IStreamCommand<int>;

Expand Down

0 comments on commit a98c576

Please sign in to comment.