Skip to content

Commit 203a6db

Browse files
committed
Change NotifyAsync to Task from ValueTask
ValueTask is more important in high-perf scenarios, which is hardly the case for business logic and app architecture.
1 parent 5ffd08f commit 203a6db

File tree

7 files changed

+75
-43
lines changed

7 files changed

+75
-43
lines changed

src/Merq.Core/MessageBus.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ public System.Collections.Generic.IAsyncEnumerable<TResult> ExecuteStream<TResul
324324
#endif
325325

326326
/// <inheritdoc/>
327-
public ValueTask NotifyAsync<TEvent>(TEvent e, [CallerMemberName] string? callerName = default, [CallerFilePath] string? callerFile = default, [CallerLineNumber] int? callerLine = default)
327+
public Task NotifyAsync<TEvent>(TEvent e, [CallerMemberName] string? callerName = default, [CallerFilePath] string? callerFile = default, [CallerLineNumber] int? callerLine = default)
328328
{
329329
var type = (e ?? throw new ArgumentNullException(nameof(e))).GetType();
330330
using var activity = StartEventActivity(type, e, callerName, callerFile, callerLine);
@@ -372,7 +372,7 @@ public ValueTask NotifyAsync<TEvent>(TEvent e, [CallerMemberName] string? caller
372372
Publishing.Record(watch.ElapsedMilliseconds, new Tag("Event", type.FullName));
373373
}
374374

375-
return new ValueTask();
375+
return Task.CompletedTask;
376376
}
377377

378378
/// <summary>

src/Merq.Tests/DuckTyping.cs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,22 @@ public partial record OtherMessageEvent(string Message)
2121

2222
public class DynamicDuckTyping : DuckTyping
2323
{
24-
protected override IMessageBus CreateMessageBus(IServiceProvider? services = null) =>
25-
new DynamicallyMessageBus(services ?? new MockServiceProvider());
24+
protected override IMessageBus CreateMessageBus(IServiceCollection? services = null)
25+
{
26+
services ??= new ServiceCollection();
27+
services.AddSingleton<IMessageBus>(sp => new DynamicallyMessageBus(sp));
28+
return services.BuildServiceProvider().GetRequiredService<IMessageBus>();
29+
}
2630
}
2731

2832
public class AutoMapperDuckTyping : DuckTyping
2933
{
30-
protected override IMessageBus CreateMessageBus(IServiceProvider? services = null) =>
31-
new AutoMapperMessageBus(services ?? new MockServiceProvider());
34+
protected override IMessageBus CreateMessageBus(IServiceCollection? services = null)
35+
{
36+
services ??= new ServiceCollection();
37+
services.AddSingleton<IMessageBus>(sp => new AutoMapperMessageBus(sp));
38+
return services.BuildServiceProvider().GetRequiredService<IMessageBus>();
39+
}
3240

3341
[Fact]
3442
public void ExecuteWithExtraCtorArg()
@@ -37,7 +45,7 @@ public void ExecuteWithExtraCtorArg()
3745
var services = new ServiceCollection();
3846
services.AddSingleton(handler.Object);
3947
services.AddSingleton(typeof(IServiceCollection), _ => services);
40-
var bus = CreateMessageBus(services.BuildServiceProvider());
48+
var bus = CreateMessageBus(services);
4149

4250
var cmd = new Library2::Library.Echo2("Foo");
4351

@@ -49,7 +57,7 @@ public void ExecuteWithExtraCtorArg()
4957

5058
public abstract class DuckTyping
5159
{
52-
protected abstract IMessageBus CreateMessageBus(IServiceProvider? services = null);
60+
protected abstract IMessageBus CreateMessageBus(IServiceCollection? services = null);
5361

5462
#if NET6_0_OR_GREATER
5563
[Fact]
@@ -110,7 +118,7 @@ public void CanHandleDuck()
110118
var services = new ServiceCollection();
111119
services.AddSingleton<ICommandHandler<Library1::Library.Echo, string>, Library1::Library.EchoHandler>();
112120
services.AddSingleton(typeof(IServiceCollection), _ => services);
113-
var bus = CreateMessageBus(services.BuildServiceProvider());
121+
var bus = CreateMessageBus(services);
114122

115123
Assert.True(bus.CanHandle<Library2::Library.Echo>());
116124
Assert.True(bus.CanHandle(new Library2::Library.Echo("Foo")));
@@ -122,7 +130,7 @@ public void CanExecuteDuck()
122130
var services = new ServiceCollection();
123131
services.AddSingleton<ICommandHandler<Library1::Library.Echo, string>, Library1::Library.EchoHandler>();
124132
services.AddSingleton(typeof(IServiceCollection), _ => services);
125-
var bus = CreateMessageBus(services.BuildServiceProvider());
133+
var bus = CreateMessageBus(services);
126134

127135
var cmd = new Library2::Library.Echo("Foo");
128136

@@ -135,7 +143,7 @@ public void ExecuteCommand()
135143
var services = new ServiceCollection();
136144
services.AddSingleton<ICommandHandler<Library1::Library.Echo, string>, Library1::Library.EchoHandler>();
137145
services.AddSingleton(typeof(IServiceCollection), _ => services);
138-
var bus = CreateMessageBus(services.BuildServiceProvider());
146+
var bus = CreateMessageBus(services);
139147

140148
var cmd = new Library2::Library.Echo("Foo");
141149

@@ -150,7 +158,7 @@ public void ExecuteNoOpCommand()
150158
var services = new ServiceCollection();
151159
services.AddSingleton<ICommandHandler<Library1::Library.NoOp>, Library1::Library.NoOpHandler>();
152160
services.AddSingleton(typeof(IServiceCollection), _ => services);
153-
var bus = CreateMessageBus(services.BuildServiceProvider());
161+
var bus = CreateMessageBus(services);
154162

155163
var cmd = new Library2::Library.NoOp();
156164

@@ -163,7 +171,7 @@ public async Task ExecuteAsyncCommandAsync()
163171
var services = new ServiceCollection();
164172
services.AddSingleton<IAsyncCommandHandler<Library1::Library.EchoAsync, string>, Library1::Library.EchoAsyncHandler>();
165173
services.AddSingleton(typeof(IServiceCollection), _ => services);
166-
var bus = CreateMessageBus(services.BuildServiceProvider());
174+
var bus = CreateMessageBus(services);
167175

168176
var cmd = new Library2::Library.EchoAsync("Foo");
169177

@@ -178,7 +186,7 @@ public async Task ExecuteNoOpAsyncCommandAsync()
178186
var services = new ServiceCollection();
179187
services.AddSingleton<IAsyncCommandHandler<Library1::Library.NoOpAsync>, Library1::Library.NoOpAsyncHandler>();
180188
services.AddSingleton(typeof(IServiceCollection), _ => services);
181-
var bus = CreateMessageBus(services.BuildServiceProvider());
189+
var bus = CreateMessageBus(services);
182190

183191
var cmd = new Library2::Library.NoOpAsync();
184192

src/Merq/IMessageBus.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public interface IMessageBus
103103
/// <param name="callerName">Optional calling member name, provided by default by the compiler.</param>
104104
/// <param name="callerFile">Optional calling file name, provided by default by the compiler.</param>
105105
/// <param name="callerLine">Optional calling line number, provided by default by the compiler.</param>
106-
ValueTask NotifyAsync<TEvent>(TEvent e, [CallerMemberName] string? callerName = default, [CallerFilePath] string? callerFile = default, [CallerLineNumber] int? callerLine = default);
106+
Task NotifyAsync<TEvent>(TEvent e, [CallerMemberName] string? callerName = default, [CallerFilePath] string? callerFile = default, [CallerLineNumber] int? callerLine = default);
107107

108108
/// <summary>
109109
/// Observes the events of a given type <typeparamref name="TEvent"/>.

src/Merq/IMessageBusExtensions.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.ComponentModel;
33
using System.Runtime.CompilerServices;
4-
using System.Threading;
54
using System.Threading.Tasks;
65

76
namespace Merq;
@@ -18,11 +17,7 @@ public static class IMessageBusExtensions
1817
[EditorBrowsable(EditorBrowsableState.Never)]
1918
[Obsolete("Use await NotifyAsync instead.")]
2019
public static void Notify<TEvent>(this IMessageBus bus, TEvent e, [CallerMemberName] string? callerName = default, [CallerFilePath] string? callerFile = default, [CallerLineNumber] int? callerLine = default)
21-
{
22-
var result = bus.NotifyAsync(e, callerName, callerFile, callerLine);
23-
while (!result.IsCompleted)
24-
Thread.SpinWait(1);
25-
}
20+
=> bus.NotifyAsync(e, callerName, callerFile, callerLine).Forget();
2621

2722
/// <summary>
2823
/// Notifies the bus of a new event instance of <typeparamref name="TEvent"/>.
@@ -35,7 +30,7 @@ public static void Notify<TEvent>(this IMessageBus bus, TEvent e, [CallerMemberN
3530
/// <summary>
3631
/// Notifies the bus of a new event instance of <typeparamref name="TEvent"/>.
3732
/// </summary>
38-
public static ValueTask NotifyAsync<TEvent>(this IMessageBus bus, [CallerMemberName] string? callerName = default, [CallerFilePath] string? callerFile = default, [CallerLineNumber] int? callerLine = default) where TEvent : new()
33+
public static Task NotifyAsync<TEvent>(this IMessageBus bus, [CallerMemberName] string? callerName = default, [CallerFilePath] string? callerFile = default, [CallerLineNumber] int? callerLine = default) where TEvent : new()
3934
=> bus.NotifyAsync(new TEvent(), callerName, callerFile, callerLine);
4035

4136
/// <summary>

src/Merq/PublicAPI.Shipped.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ Merq.IMessageBus.Execute(Merq.ICommand! command, string? callerName = null, stri
2828
Merq.IMessageBus.Execute<TResult>(Merq.ICommand<TResult>! command, string? callerName = null, string? callerFile = null, int? callerLine = null) -> TResult
2929
Merq.IMessageBus.ExecuteAsync(Merq.IAsyncCommand! command, System.Threading.CancellationToken cancellation = default(System.Threading.CancellationToken), string? callerName = null, string? callerFile = null, int? callerLine = null) -> System.Threading.Tasks.Task!
3030
Merq.IMessageBus.ExecuteAsync<TResult>(Merq.IAsyncCommand<TResult>! command, System.Threading.CancellationToken cancellation = default(System.Threading.CancellationToken), string? callerName = null, string? callerFile = null, int? callerLine = null) -> System.Threading.Tasks.Task<TResult>!
31-
Merq.IMessageBus.NotifyAsync<TEvent>(TEvent e, string? callerName = null, string? callerFile = null, int? callerLine = null) -> System.Threading.Tasks.ValueTask
31+
Merq.IMessageBus.NotifyAsync<TEvent>(TEvent e, string? callerName = null, string? callerFile = null, int? callerLine = null) -> System.Threading.Tasks.Task!
3232
Merq.IMessageBus.Observe<TEvent>() -> System.IObservable<TEvent>!
3333
Merq.IMessageBusExtensions
3434
static Merq.IMessageBusExtensions.Execute<TCommand>(this Merq.IMessageBus! bus, string? callerName = null, string? callerFile = null, int? callerLine = null) -> void
3535
static Merq.IMessageBusExtensions.Notify<TEvent>(this Merq.IMessageBus! bus, string? callerName = null, string? callerFile = null, int? callerLine = null) -> void
3636
static Merq.IMessageBusExtensions.Notify<TEvent>(this Merq.IMessageBus! bus, TEvent e, string? callerName = null, string? callerFile = null, int? callerLine = null) -> void
37-
static Merq.IMessageBusExtensions.NotifyAsync<TEvent>(this Merq.IMessageBus! bus, string? callerName = null, string? callerFile = null, int? callerLine = null) -> System.Threading.Tasks.ValueTask
37+
static Merq.IMessageBusExtensions.NotifyAsync<TEvent>(this Merq.IMessageBus! bus, string? callerName = null, string? callerFile = null, int? callerLine = null) -> System.Threading.Tasks.Task!
38+
Merq.TaskExtensions
39+
static Merq.TaskExtensions.Forget(this System.Threading.Tasks.Task! task) -> void

src/Merq/TaskExtensions.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Threading.Tasks;
2+
3+
namespace Merq;
4+
5+
/// <summary>
6+
/// Adds <see cref="Forget(Task)"/> to safely ignore the result of a task execution.
7+
/// </summary>
8+
public static class TaskExtensions
9+
{
10+
/// <summary>
11+
/// Observes the task to avoid the UnobservedTaskException event to be raised.
12+
/// </summary>
13+
public static void Forget(this Task task)
14+
{
15+
// note: this code is inspired by a tweet from Ben Adams: https://twitter.com/ben_a_adams/status/1045060828700037125
16+
// Only care about tasks that may fault (not completed) or are faulted,
17+
// so fast-path for SuccessfullyCompleted and Canceled tasks.
18+
if (!task.IsCompleted || task.IsFaulted)
19+
{
20+
// use "_" (Discard operation) to remove the warning IDE0058: Because this call is not awaited, execution of the current method continues before the call is completed
21+
// https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/discards?WT.mc_id=DT-MVP-5003978#a-standalone-discard
22+
_ = ForgetAwaited(task);
23+
}
24+
25+
// Allocate the async/await state machine only when needed for performance reasons.
26+
// More info about the state machine: https://blogs.msdn.microsoft.com/seteplia/2017/11/30/dissecting-the-async-methods-in-c/?WT.mc_id=DT-MVP-5003978
27+
async static Task ForgetAwaited(Task task)
28+
{
29+
try
30+
{
31+
// No need to resume on the original SynchronizationContext, so use ConfigureAwait(false)
32+
await task.ConfigureAwait(false);
33+
}
34+
catch
35+
{
36+
// Nothing to do here
37+
}
38+
}
39+
}
40+
}

src/Samples/Library1/Commands.cs

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,42 +24,29 @@ public void Execute(NoOp command) { }
2424
}
2525

2626
[Service]
27-
public class EchoHandler : ICommandHandler<Echo, string>
27+
public class EchoHandler(IMessageBus bus) : ICommandHandler<Echo, string>
2828
{
29-
readonly IMessageBus? bus;
30-
31-
public EchoHandler() { }
32-
33-
public EchoHandler(IMessageBus bus) => this.bus = bus;
34-
3529
public bool CanExecute(Echo command) => !string.IsNullOrEmpty(command.Message);
3630

3731
public string Execute(Echo command)
3832
{
3933
if (string.IsNullOrEmpty(command.Message))
4034
throw new NotSupportedException("Cannot echo an empty or null message");
4135

42-
bus?.Notify(new OnDidSay(command.Message));
36+
bus.NotifyAsync(new OnDidSay(command.Message)).Forget();
4337
return command.Message;
4438
}
4539
}
4640

4741
[Service]
48-
public class EchoAsyncHandler : IAsyncCommandHandler<EchoAsync, string>
42+
public class EchoAsyncHandler(IMessageBus bus) : IAsyncCommandHandler<EchoAsync, string>
4943
{
50-
readonly IMessageBus? bus;
51-
52-
public EchoAsyncHandler() { }
53-
54-
public EchoAsyncHandler(IMessageBus bus) => this.bus = bus;
55-
56-
5744
public bool CanExecute(EchoAsync command) => true;
5845

59-
public Task<string> ExecuteAsync(EchoAsync command, CancellationToken cancellation = default)
46+
public async Task<string> ExecuteAsync(EchoAsync command, CancellationToken cancellation = default)
6047
{
61-
bus?.Notify(new OnDidSay(command.Message));
62-
return Task.FromResult(command.Message);
48+
await bus.NotifyAsync(new OnDidSay(command.Message));
49+
return command.Message;
6350
}
6451
}
6552

0 commit comments

Comments
 (0)