MediatR icon indicating copy to clipboard operation
MediatR copied to clipboard

NotificationHandler gets called twice

Open mkalinski93 opened this issue 2 years ago • 5 comments

Hello,

unfortunately, I´ve discovered that the NotificationHandler is getting called twice. I´ve read a few articles on the web but the problem is still not fixed.

I am using the following versions:

  1. MediatR 10.0.1
  2. MediatR.Extensions.FluentValidation.AspNetCore 2.0.0
  3. MediatR.Extensions.Microsoft.DependencyInjection 10.0.1

This is how I register MediatR

            services.AddMediatR(typeof(IInstaller).Assembly);
            services.AddValidatorsFromAssembly(typeof(IInstaller).Assembly);
            ValidatorOptions.Global.LanguageManager.Culture = new CultureInfo("de-DE");

The type 'IInstaller' is in the corresponding project where my NoticationHandlers and RequestHandlers are.

This is how I publish a Notification

    public abstract class DomainEvent {
        public Guid EventId { get; set; }
        public bool IsPublished { get; set; }
        public DateTimeOffset DateOccurred { get; set; }
        protected DomainEvent() {
            EventId = Guid.NewGuid();
            DateOccurred = DateTimeOffset.Now;
        }
    }
public class UserStateChangedEvent : DomainEvent {
        public ApplicationUser ApplicationUser { get; }
        public UserStateChangedEvent(ApplicationUser applicationUser) {
            ApplicationUser = applicationUser;
        }
    }
private async Task DispatchDomainEventsAsync(ICollection objectsToSave, ICollection objectsToDelete) {
            IEnumerable<DomainEvent> saveDomainEvents = objectsToSave.OfType<AggregatedRoot>().Where(x=> x.DomainEvents != null).Where(x => x.DomainEvents.Any()).SelectMany(x=>x.DomainEvents);
            IEnumerable<DomainEvent> deleteDomainEvents = objectsToDelete.OfType<AggregatedRoot>().Where(x=> x.DomainEvents != null).Where(x => x.DomainEvents.Any()).SelectMany(x=>x.DomainEvents);
            IEnumerable<DomainEvent> concatDomainEvents = saveDomainEvents.Concat(deleteDomainEvents);
            foreach (DomainEvent domainEvent in concatDomainEvents) {
                domainEvent.IsPublished = true;
                await _domainEventService.Publish(domainEvent);
            }
        }
public class DomainEventService : IDomainEventService {

        private readonly IPublisher _publisher;
        public DomainEventService(IPublisher publisher) {
            _publisher = publisher;
        }

        public async Task Publish(DomainEvent domainEvent) {
            await _publisher.Publish(GetNotificationCorrespondingToDomainEvent(domainEvent));
        }
        
        private INotification GetNotificationCorrespondingToDomainEvent(DomainEvent domainEvent)
        {
            return (INotification)Activator.CreateInstance(
                typeof(DomainEventNotification<>).MakeGenericType(domainEvent.GetType()), domainEvent)!;
        }
    }
    public class UserStateChangedEventHandler : INotificationHandler<DomainEventNotification<UserStateChangedEvent>> {     
        public async Task Handle(DomainEventNotification<UserStateChangedEvent> notification, CancellationToken cancellationToken) {
            //DoStuff
        }
    }

mkalinski93 avatar Mar 04 '22 09:03 mkalinski93

Hello, I experienced the same issue I think this one in the DependencyInjection Extension captures the root cause

https://github.com/jbogard/MediatR.Extensions.Microsoft.DependencyInjection/issues/118

Isitar avatar Apr 14 '22 17:04 Isitar

in the blazor mode have the same issue Does anyone know how to solve the code

neozhu avatar Apr 23 '22 05:04 neozhu

Same problem.

diseks avatar Aug 29 '22 10:08 diseks

I think that it is not a Mediatr problem. I have investigated my project and Mediatr source code and found out that the handler was registered twice. In my case I'm using Autofac and MediatR.Extensions.Autofac.DependencyInjection:

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterMediatR(typeof(Startup).Assembly);
    builder.RegisterAssemblyTypes(Assembly.GetEntryAssembly()).AsImplementedInterfaces();
 }

According to my investigation, both RegisterMediatR and RegisterAssemblyTypes registered my handler. I've checked Autofac registration list and saw that handler was registered twice:

var types = container.ComponentRegistry.Registrations
     .Where(r => typeof(MyNotificationHandler).IsAssignableFrom(r.Activator.LimitType))
     .Select(r => r.Activator.LimitType);

So, I think you need to look towards dependencies registration

diseks avatar Aug 30 '22 07:08 diseks

if you follow my link, you'll end up with this issue, which seems to be the root cause: https://github.com/dotnet/runtime/issues/65145

Isitar avatar Aug 30 '22 08:08 Isitar

Hey , after fighting with this today for quite some time it all boils down to your service container setup , make sure your DI is setup correctly.

kek-Sec avatar Aug 31 '22 06:08 kek-Sec

Just noticed this issue yesterday while doing some tests with MediatR notifications, I investigated around, looked at source code and finally found this issue, I can confirm the related issue on dotnet/runtime seems to be the root cause, I set up a pair of tests using the Startup class of my web service using the following structure:

  // Arrange
  var host = Host.CreateDefaultBuilder()
      .ConfigureWebHostDefaults(builder => {
          builder.UseTestServer()
              .UseStartup<Startup>();
      })
      .UseEnvironment(Environments.Development) // Changing this between Development and Production
      .Build();
  var provider = host. Services;

  var handlers = provider
      .GetServices<INotificationHandler<MyDomainEvent>>()
      .ToArray();

  // Assert
  Assert.That(handlers, Has.Length.EqualTo(1));

Both tests are identical except for the Environment, one using Development other using Production and the one for Development fails. Given the timeline for the proper fix (.NET 8 apparently), has anyone found a workaround?

Edit: For now I'm thinking on instantiating and executing notifications on development through custom code on a dispatcher abstraction that filters duplicate handlers, but if anyone knows of a better workaround, it would be appreciated

Inheritech avatar Sep 02 '22 16:09 Inheritech

@Inheritech https://github.com/jbogard/MediatR.Extensions.Microsoft.DependencyInjection/issues/118#issuecomment-1096954146 This is my workaround

Isitar avatar Sep 02 '22 17:09 Isitar

I had same trouble. Check whether in the same asembly is not generic notificationhandler class. App builder creates two instance per this item. Typicaly for decorator.

internal class DomainEventHandlerDecorator<T> : INotificationHandler<T>
    where T : IDomainEvent

Mediator adds it into service collection and app builder creates two instances for this type. Or more when multiple generic class is present.

  1. move this class outside the mediators scaner assembly
  2. remove it from iservices by descriptor
  3. or is there any better solution ?

source https://github.com/Lux2057/.NET-Core-CRUD/issues/75

LukasKuchta avatar Aug 27 '23 16:08 LukasKuchta

@LukasKuchta you are right and you saved my life :-)

Inspired by your issue i decided to move the generic interface outside the assembly and i created an external common class library leaving the same namespaces to avoid a more complex refactoring.

After moving the classes in the external lib i hadn't anymore the duplicate notification.

Thank you a lot!!

Luigi

I had same trouble. Check whether in the same asembly is not generic notificationhandler class. App builder creates two instance per this item. Typicaly for decorator.

internal class DomainEventHandlerDecorator<T> : INotificationHandler<T>
    where T : IDomainEvent

Mediator adds it into service collection and app builder creates two instances for this type. Or more when multiple generic class is present.

  1. move this class outside the mediators scaner assembly
  2. remove it from iservices by descriptor
  3. or is there any better solution ?

source Lux2057/.NET-Core-CRUD#75

muten84 avatar Feb 21 '24 15:02 muten84

  1. solution based on: https://github.com/Lux2057/.NET-Core-CRUD/issues/75 ` public class ReMediatr : Mediator { public ReMediatr(IServiceProvider serviceProvider) : base(serviceProvider) { }

     protected override async Task PublishCore(IEnumerable<NotificationHandlerExecutor> handlerExecutors, INotification notification, CancellationToken cancellationToken)
     {
         HashSet<Type> processedHandlers = new();
    
         foreach (var handler in handlerExecutors)
         {
             if (handler is null)
             {
                 continue;
             }
    
             Type handlerType = handler.GetType();
    
             if (processedHandlers.Contains(handlerType))
             {
                 continue;
             }
    
             processedHandlers.Add(handlerType);
    
             await handler.HandlerCallback(notification, cancellationToken).ConfigureAwait(false);
         }
     }
    

    } `

Then just register:

services.AddMediatR(options=> { options.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); options.MediatorImplementationType = typeof(ReMediatr); });

  1. Remove remove it from IServiceCollection by service descriptor: var serviceDescriptor = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(INotificationHandler<>); if (serviceDescriptor != null) { services.Remove(serviceDescriptor); }

I'm using Mediatr version: 12.0.1

Rad1c avatar Apr 17 '24 16:04 Rad1c