Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/MediatR/Registration/ServiceRegistrar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private static void ConnectImplementationsToTypesClosing(Type openRequestInterfa
.Where(t => !t.ContainsGenericParameters || configuration.RegisterGenericHandlers)
.Where(t => t.IsConcrete() && t.FindInterfacesThatClose(openRequestInterface).Any())
.Where(configuration.TypeEvaluator)
.ToList();
.ToList();

foreach (var type in types)
{
Expand Down
2 changes: 2 additions & 0 deletions src/MediatR/Wrappers/NotificationHandlerWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public override Task Handle(INotification notification, IServiceProvider service
{
var handlers = serviceFactory
.GetServices<INotificationHandler<TNotification>>()
.GroupBy(static x => x.GetType())
.Select(static g => g.First())
.Select(static x => new NotificationHandlerExecutor(x, (theNotification, theToken) => x.Handle((TNotification)theNotification, theToken)));

return publish(handlers, notification, cancellationToken);
Expand Down
103 changes: 103 additions & 0 deletions test/MediatR.Tests/MicrosoftExtensionsDI/Issue1118Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Xunit;

namespace MediatR.Extensions.Microsoft.DependencyInjection.Tests;

/// <summary>
/// Regression tests for https://github.com/LuckyPennySoftware/MediatR/issues/1118
///
/// When E2 derives from E1, a handler for E1 (C1) would be called twice when publishing E2
/// because some DI containers that support variance (e.g. DryIoc) return C1 both from
/// the explicit INotificationHandler&lt;E2&gt; registration AND from their own contravariant
/// resolution of INotificationHandler&lt;E1&gt;.
/// </summary>
public class Issue1118Tests
{
public class CallLog
{
public List<string> Entries { get; } = new();
}

public class BaseEvent : INotification { }

public class DerivedEvent : BaseEvent { }

// IServiceProvider is always injectable, so CallLog becomes an optional soft dependency.
// This prevents PipelineTests (which uses ValidateOnBuild=true on the same assembly) from
// failing due to CallLog not being registered in its container.
public class BaseEventHandler : INotificationHandler<BaseEvent>
{
private readonly IServiceProvider _serviceProvider;

public BaseEventHandler(IServiceProvider serviceProvider)
=> _serviceProvider = serviceProvider;

public Task Handle(BaseEvent notification, CancellationToken cancellationToken)
{
_serviceProvider.GetService<CallLog>()?.Entries.Add($"BaseEventHandler:{notification.GetType().Name}");
return Task.CompletedTask;
}
}

public class DerivedEventHandler : INotificationHandler<DerivedEvent>
{
private readonly IServiceProvider _serviceProvider;

public DerivedEventHandler(IServiceProvider serviceProvider)
=> _serviceProvider = serviceProvider;

public Task Handle(DerivedEvent notification, CancellationToken cancellationToken)
{
_serviceProvider.GetService<CallLog>()?.Entries.Add($"DerivedEventHandler:{notification.GetType().Name}");
return Task.CompletedTask;
}
}

private static IServiceProvider BuildProvider()
{
var services = new ServiceCollection();
services.AddFakeLogging();
services.AddSingleton<CallLog>();
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Issue1118Tests).Assembly));
return services.BuildServiceProvider();
}

[Fact]
public async Task Publishing_BaseEvent_Should_Call_BaseEventHandler_Once()
{
var provider = BuildProvider();
var log = provider.GetRequiredService<CallLog>();

await provider.GetRequiredService<IPublisher>().Publish(new BaseEvent());

log.Entries.Count(e => e.StartsWith("BaseEventHandler")).ShouldBe(1);
}

[Fact]
public async Task Publishing_DerivedEvent_Should_Call_BaseEventHandler_ExactlyOnce()
{
var provider = BuildProvider();
var log = provider.GetRequiredService<CallLog>();

await provider.GetRequiredService<IPublisher>().Publish(new DerivedEvent());

log.Entries.Count(e => e.StartsWith("BaseEventHandler")).ShouldBe(1,
"BaseEventHandler should be called exactly once for DerivedEvent, not duplicated");
}

[Fact]
public async Task Publishing_DerivedEvent_Should_Call_DerivedEventHandler_Once()
{
var provider = BuildProvider();
var log = provider.GetRequiredService<CallLog>();

await provider.GetRequiredService<IPublisher>().Publish(new DerivedEvent());

log.Entries.Count(e => e.StartsWith("DerivedEventHandler")).ShouldBe(1);
}
}