question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Notification handlers resolved twice

See original GitHub issue

Reading past issues, this is a topic that showed up a couple of times and most often than not it was about registering types/assemblies twice. Unfortunately this does not seem to be the case in our scenario. What is more troubling is that we do get the correct behavior on a local windows machine, but not on a linux docker container running the very same code.

Trying to simplify things as much as possible it boils down to this

  • we send a RemindCommand that is handled by a RemindHandler
  • when the handler completes it publishes a RemindedEvent to potential listeners
  • there are two notification handlers that listen, namely RemindedAlert and RemindedEmail
  • therefore there is one command, one command handler, one event, two notification handlers
  • unfortunately
    • while the command handler is invoked once
    • and the event is published once
    • each notification handler is invoked twice (!)

image

I suspected that there must have been double entries at IServiceCollection level, but this was not the case. Diagnosing the registered services everything shows up as expected on both windows and linux docker container

{
  "results": [
    {
      "lifetime": "transient",
      "contract": "MediatR.IRequestHandler`2[[Core.Domain.Remind.RemindCommand, Core.Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[Core.Domain.Remind.RemindResult, Core.Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]",
      "implementation": "Core.Domain.Remind.RemindHandler"
    },
    {
      "lifetime": "transient",
      "contract": "MediatR.INotificationHandler`1[[Core.Contracts.RemindedEvent, Core.Contracts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]",
      "implementation": "Notifications.Module.RemindedAlert"
    },
    {
      "lifetime": "transient",
      "contract": "MediatR.INotificationHandler`1[[Core.Contracts.RemindedEvent, Core.Contracts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]",
      "implementation": "Notifications.Module.RemindedEmail"
    }
  ]
}

Due to the size of the system things are split into several assemblies, but I suspect this should not matter much. In the end in order to register the handlers we just rely on one of the AddMediatR extension methods.

            services.AddMediatR(new[]
            {
                typeof(IDomainModule),
                typeof(IQueriesModule),
                typeof(IAuditModule),
                typeof(INotificationsModule),
                ...
            });

While I am running out of ideas, I was wondering if maybe there is something obvious that I might have missed along the way…

Stack: .net: 6.0 mediatr: 10.0.1 os: centos

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:6 (4 by maintainers)

github_iconTop GitHub Comments

3reactions
dandohotarucommented, Feb 4, 2022

It turns out duplicate registrations were not that much of an issue, but it is more related with the way open generics were resolved to the unexpected handlers. Initially I was simply registering various application modules/dlls via the available conventions, and that seemed to work just fine.

            services.AddMediatR(new[]
            {
                typeof(IDomainModule),
                typeof(IQueriesModule),
                typeof(INotificationsModule),
                typeof(IAuditModule),
                ...
            }, options =>

Behind the scenes though there are a mix of open and closed types for INotificationHandler<>

domain module

public class RemindedEvent : INotification, IChanged {...}

queries module

public class ListedEvent : INotification, IViewed {...}

audit module

    // Handle events marked as IChanged
    public class ChangedHandler<TEvent> : INotificationHandler<TEvent> where TEvent : INotification, IChanged {...}
    // Handle events marked as IViewed
    public class ViewedHandler<TEvent> : INotificationHandler<TEvent> where TEvent : INotification, IViewed {...}

notifications module

    // Handle only RemindedEvent
    public class RemindedAlert : INotificationHandler<RemindedEvent> {...}
    // Handle only RemindedEvent
    public class RemindedEmail : INotificationHandler<RemindedAlert> {...}

the behavior I was witnessing was that

  • all closed generics implementations would resolve as expected RemindedEvent -> RemindedAlert handler RemindedEvent -> RemindedEmail handler
  • all open generics were expected to resolve to their corresponding handler RemindedEvent -> ChangedHandler<T> handler instead would somehow resolve to the same once more hence the double invocation RemindedEvent -> RemindedAlert handler RemindedEvent -> RemindedEmail handler

To correct the behavior, for the time being I ensured the open generics are defined explicitly. The only drawback would be that mixing manual registration with convention registration risks having multiple registrations in the container, therefore had to filter out those that were expected to have already been manually registered

            // Manual
            services.AddTransient(typeof(INotificationHandler<>), typeof(ViewedHandler<>));
            services.AddTransient(typeof(INotificationHandler<>), typeof(ChangedHandler<>));

            // Conventions
            services.AddMediatR(new[]
            {
                typeof(IDomainModule),
                typeof(IQueriesModule),
                typeof(INotificationsModule),
                typeof(IAuditModule),
                ...
            }, options =>
            {
                var exclude = new[]
                {
                    typeof(ViewedHandler<>).Namespace,
                    typeof(ChangedHandler<>).Namespace,
                };
                options.WithEvaluator(type => !exclude.Contains(type.Namespace));
            });

This seems to have fixed the issue for now, and I might have missed something along the way, but I am still not sure how is it possible to observe it only when deployed to a docker container and not on a windows machine 😃

2reactions
jbogardcommented, Feb 4, 2022

See the lesson here is to always use Windows everywhere /s

Read more comments on GitHub >

github_iconTop Results From Across the Web

NotificationHandler gets called twice · Issue #718
The type 'IInstaller' is in the corresponding project where my NoticationHandlers and RequestHandlers are. This is how I publish a Notification.
Read more >
MediatR: INotification handler is being called multiple times
AddMediatR(typeof(CommandC));. => the handler would be called 3 time etc. How to fix that to make handler being called just once?
Read more >
Managed account showing up twice, preventing login to ...
I have Intune installed and I am trying to log in to Outlook and Teams, but every time I enter my credentials, I...
Read more >
Error handling and automatic retries in AWS Lambda
Asynchronous invocation – Lambda retries function errors twice. ... block processing of the affected shard until the error is resolved or the items...
Read more >
Task notifications are sent twice to users in Lightning ...
This can occur when the field is updated manually or using Process Builder. Resolution. Open a case with Salesforce Customer Support and mention...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found