fullstackhero / dotnet-starter-kit

Production Grade Cloud-Ready .NET 8 Starter Kit (Web API + Blazor Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200+ Development Hours! All Batteries Included.
https://fullstackhero.net/dotnet-webapi-boilerplate/
MIT License
5.07k stars 1.52k forks source link

Sending MediatR INotifications to the client over SignalR #440

Closed fretje closed 2 years ago

fretje commented 2 years ago

For every INotification you want to send to the client over SignalR, you have to create an INotificationHandler for that specific notification, which then calls the hubcontext to send a specific INotificationMessage (another object alltogether) to the connected clients.

In my app I'm building, I have already quite some notifications like that, and was thinking we could make this probably generic in a way you don't have to create separate INotificationHandlers and INotificationMessages for all the INotifications you want to be sent to the client as well.

In my research around this, I found this article which is looking very close to what we need: https://remibou.github.io/Realtime-update-with-Blazor-WASM-SignalR-and-MediatR/ There's an issue there though pointed out in the comments... and another pointer to an interesting library in this regard: https://github.com/KuraiAndras/MediatR.Courier

I think combining those 2 should be able to give a a nice implementation of something like this...

fretje commented 2 years ago

In my app, I have already implemented a "quick and dirty" solution by creating a "catch all" notificationhandler, which checks if the INotification is has a specific interface (IClientNotification<TNotificationMessage>) and then uses Mapster to map the INotification to the INotificationMessage. This way I don't have to implement an INotificationHandler anymore for every INotification... but I do still need to create another INotificationMessage object for every INotifcation with exactly the same properties:

public interface IClientNotification<TNotificationMessage> : INotification
    where TNotificationMessage : INotificationMessage
{
}

public class ClientNotificationHandler<TClientNotification> : INotificationHandler<TClientNotification>
    where TClientNotification : INotification
{
    private readonly INotificationService _notificationService;

    public ClientNotificationHandler(INotificationService notificationService) =>
        _notificationService = notificationService;

    public Task Handle(TClientNotification clientNotification, CancellationToken cancellationToken)
    {
        if (clientNotification.GetType().GetInterfaces().FirstOrDefault(i =>
                i.IsGenericType &&
                i.GetGenericTypeDefinition() == typeof(IClientNotification<>)) is { } clientNotificationInterface &&
            clientNotificationInterface.GetGenericArguments()[0] is { } notificationMessageType &&
            notificationMessageType.IsAssignableTo(typeof(INotificationMessage)) &&
            clientNotification.Adapt(typeof(TClientNotification), notificationMessageType) is INotificationMessage notificationMessage)
        {
            return _notificationService.SendMessageAsync(notificationMessage, cancellationToken);
        }

        return Task.CompletedTask;
    }
}

It works well... but I think a solution like explained here is what we really need.

That way we can also abstract away the signalr connection much more nicely on the client side, so we don't have to deal with the HubConnection anymore in the application code. Only handling of events using mediatr, or probably even better using Courier on top...

fretje commented 2 years ago

Oh, an about the INotificationMessage: I've updated that interface to just be a marker interface. There were 2 properties in there:

One was string MessageType, which in every implementation returned the value nameof(SpecificNotificationMessage). Which was then only used in INotificationService, to us as methodName parameter in the HubContext.SendAsync calls. There, I simply replaced notification.MessageType with notification.GetType().Name in every call to hubContext.SendAsync, which does exactly the same thing (without having to implement a MessageType string on every INotificationMessage...

The other one was just string Message which wasn't actually needed in every INotificationMessage so no need to be on the Interface. One can easily implement it if it's actually needed for a specific INotificationMessage.

Maybe this should be in another issue... but I can create a separate PR for this if you want... just let me know!

CanMehmetK commented 2 years ago

Hi @fretje this is very nice... Just staying away from SignalR becouse of performance issues... In docs using a seperate SignalR server they suggested..

In my senario many tenant and their branches regularly will use app and their screens always will be open.

what you think about network cost?

eddedre commented 2 years ago

I think this would be a nice addition

fretje commented 2 years ago

See https://github.com/fullstackhero/dotnet-webapi-boilerplate/pull/472

dicksonkimeu commented 1 year ago

Hi,

I Am stuck on getting signal r working. Since the documentation is not ready, any basic steps i can follow ?

Is this the url for signalr ? https://localhost:5501/api/v1/notifications

fretje commented 1 year ago

SignalR should be working out of the box... the url is https://localhost:5001/notifications (not under api/v1). This is configured here: https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/main/src/Infrastructure/Notifications/Startup.cs

dicksonkimeu commented 1 year ago

Hi, Yes. That I got that working. The main issue is how to publish messages I.e send to all clients from a controller in the host project. Any sample code ?

I don’t to be able to initialize a notification message .

On Mon, 26 Sept 2022, 20:03 fretje, @.***> wrote:

SignalR should be working out of the box... the url is https://localhost:5001/notifications (not under api/v1). This is configured here: https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/main/src/Infrastructure/Notifications/Startup.cs

— Reply to this email directly, view it on GitHub https://github.com/fullstackhero/dotnet-webapi-boilerplate/issues/440#issuecomment-1258350813, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCE7FQ37J2RJOFGS7QP3E3WAHJPBANCNFSM5NTZH7BQ . You are receiving this because you commented.Message ID: @.***>

dicksonkimeu commented 1 year ago
NotificationSender notificationSender = new NotificationSender(_hubContext, _currentTenant);
_notificationMessage.Equals(salesOrder);
await notificationSender.SendToAllAsync(_notificationMessage, new CancellationToken());

how can i add a INotificationMessage ?

fretje commented 1 year ago

You create a class that implements INotificationMessage, and then you can send a new instance of that class with the INotificationSender. You can find an example here:

https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/8d05f1537aefed2ac5c80d51a10e0c8dfb73a802/src/Infrastructure/Catalog/BrandGeneratorJob.cs#L49-L57

and here:

https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/8d05f1537aefed2ac5c80d51a10e0c8dfb73a802/src/Core/Application/Dashboard/SendStatsChangedNotificationHandler.cs#L41


There's also the option to implement both IEvent (as in a domain event) and INotificationMessage, and when you then publish that domain event using the IEventPublisher, it will automatically get sent to all clients via the SendEventNotificationToClientsHandler (https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/main/src/Infrastructure/Notifications/SendEventNotificationToClientsHandler.cs)

dicksonkimeu commented 1 year ago

Hi @fretje Thank you soo much, it has worked. The only issue i have now is saving changes to local DB. After receiving the requests via a controller and sending notifications, i open a queue to save the data to local DB.

fretje commented 1 year ago

The Mediator.Send method is async, so you have to "await" it...

dicksonkimeu commented 1 year ago

@fretje still same error

fretje commented 1 year ago

You can't do backgroundwork like that in a controller... a controller object is only created for the duration of a request. So by the time your backgroundthread does its work, the request is finished, and so the controller is disposed, hence the object disposed exception.

You have hangfire to do backgroundwork.

dicksonkimeu commented 1 year ago

Any code sample to use hangfire in this sample ?

On Wed, 28 Sept 2022, 22:17 fretje, @.***> wrote:

You can't do backgroundwork like that in a controller... a controller object is only created for the duration of a request. So by the time your backgroundthread does its work, the request is finished, and so the controller is disposed, hence the object disposed exception.

You have hangfire to do backgroundwork.

— Reply to this email directly, view it on GitHub https://github.com/fullstackhero/dotnet-webapi-boilerplate/issues/440#issuecomment-1261360650, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCE7FXGXYKN3FRNVY77ZD3WASKTVANCNFSM5NTZH7BQ . You are receiving this because you commented.Message ID: @.***>

pyt0xic commented 1 year ago

Any code sample to use hangfire in this sample ?

On Wed, 28 Sept 2022, 22:17 fretje, @.***> wrote:

You can't do backgroundwork like that in a controller... a controller object is only created for the duration of a request. So by the time your backgroundthread does its work, the request is finished, and so the controller is disposed, hence the object disposed exception.

You have hangfire to do backgroundwork.

— Reply to this email directly, view it on GitHub https://github.com/fullstackhero/dotnet-webapi-boilerplate/issues/440#issuecomment-1261360650, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCE7FXGXYKN3FRNVY77ZD3WASKTVANCNFSM5NTZH7BQ . You are receiving this because you commented.Message ID: @.***>

Take a look at the generate brand job in the Application project. And look at the Brands controller and the request/handler used to start the job. There's also some Job services/etc. in the Infrastructure project.

One thing to note with Hangfire is that it's not quite asynchronous, if you find that's an issue, maybe look at Coravel, but I wouldn't worry about it until you need to.

dicksonkimeu commented 1 year ago

@pyt0xic I checked. I didn't see a hangfire background queue being initiated from any controller

pyt0xic commented 1 year ago

@pyt0xic I checked. I didn't see a hangfire background queue being initiated from any controller

Alright so starting from here: https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/8d05f1537aefed2ac5c80d51a10e0c8dfb73a802/src/Host/Controllers/Catalog/BrandsController.cs#L54 the GenerateRandomBrandRequestHandler schedules the Job https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/8d05f1537aefed2ac5c80d51a10e0c8dfb73a802/src/Core/Application/Catalog/Brands/GenerateRandomBrandRequest.cs#L16 Which is implemented here https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/8d05f1537aefed2ac5c80d51a10e0c8dfb73a802/src/Infrastructure/Catalog/BrandGeneratorJob.cs, using the Hangfire job service https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/8d05f1537aefed2ac5c80d51a10e0c8dfb73a802/src/Infrastructure/BackgroundJobs/HangfireService.cs#L24

dicksonkimeu commented 1 year ago

@pyt0xic this worked perfectly. thank you. @fretje thank you too for your insights.

jay-cascade commented 1 year ago

You create a class that implements INotificationMessage, and then you can send a new instance of that class with the INotificationSender. You can find an example here:

https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/8d05f1537aefed2ac5c80d51a10e0c8dfb73a802/src/Infrastructure/Catalog/BrandGeneratorJob.cs#L49-L57

and here:

https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/8d05f1537aefed2ac5c80d51a10e0c8dfb73a802/src/Core/Application/Dashboard/SendStatsChangedNotificationHandler.cs#L41

There's also the option to implement both IEvent (as in a domain event) and INotificationMessage, and when you then publish that domain event using the IEventPublisher, it will automatically get sent to all clients via the SendEventNotificationToClientsHandler (https://github.com/fullstackhero/dotnet-webapi-boilerplate/blob/main/src/Infrastructure/Notifications/SendEventNotificationToClientsHandler.cs)

Thanks for the info, but I am really interested in implementing both IEvent and INotificationMessage through SendEventNotificationToClientsHandler, how do I implement both?

fretje commented 1 year ago

Like this:

public class MyEventNotification : IEvent, INotificationMessage
{
}
jay-cascade commented 1 year ago

Like this:

public class MyEventNotification : IEvent, INotificationMessage
{
}

So something like this should work?

public class HomeCreateNotificationEvent : IEvent, INotificationMessage {
  public string Name { get; set; } = default!;
  public string? Description { get; set; }
  public string Phone { get; set; } = default!;
  public string Colour { get; set; } = default!;
  public string Adr1 { get; set; } = default!;
  public string? Adr2 { get; set; }
  public string? Adr3 { get; set; } = default!;
  public string Town { get; set; } = default!;
  public string? County { get; set; }
  public string PostCode { get; set; } = default!;
}

public class CreateHouseRequestHandler : IRequestHandler<CreateHomeRequest, Guid>
{
    // Add Domain Events automatically by using IRepositoryWithEvents
    private readonly IRepositoryWithEvents<Home> _repository;
    private readonly IEventPublisher _publisher;
    public CreateHouseRequestHandler(
      IRepositoryWithEvents<Home> repository,
      IEventPublisher publisher
    ) => (_repository, _publisher) = (repository, publisher);

    public async Task<Guid> Handle(CreateHomeRequest request, CancellationToken cancellationToken)
    {
        var home = new Home(
          request.Name,
          request.Description,
          request.Phone,
          request.Colour,
          request.Adr1,
          request.Adr2,
          request.Adr3,
          request.Town,
          request.County,
          request.PostCode);

        await _repository.AddAsync(home, cancellationToken);

        await _publisher.PublishAsync(
          new HomeCreateNotificationEvent {
          Name = home.Name,
          Description = home.Description,
          Phone = home.Phone,
          Colour = home.Colour,
          Adr1 = home.Adr1,
          Adr2 = home.Adr2,
          Adr3 = home.Adr3,
          Town = home.Town,
          County = home.County,
          PostCode = home.PostCode
        });

        return home.Id;

    }
jay-cascade commented 1 year ago

@fretje Thanks for your help, I have something working... I am so pleased!

tvprasad commented 11 months ago

Hi there, I have successfully implemented Entity Event handling, but I'm encountering difficulties in sending a text message to all clients using Notifications.INotificationMessage. Could you please provide guidance or share a code sample demonstrating how to send a text message from the controller? Thank you

fretje commented 11 months ago

@tvprasad see a previous comment above: https://github.com/fullstackhero/dotnet-webapi-boilerplate/issues/440#issuecomment-1259314218

tvprasad commented 11 months ago

@fretje Your prompt response is much appreciated. I had already encountered the suggested solution and had put it into action even before seeking assistance here. Interestingly, the same code appears to be functioning correctly today. It seems that a simple reboot may have resolved the issue. Again, thanks the quick reply. ---------

On another note, I have also identified an issue with infinite notification execution on the server.

In the context of Web Assembly and the component, when subscribing to the <NotificationWrapper<BasicNotification> on the client side, it triggers an endless notification loop on the Blazor server side. Here is the relevant code snippet:

protected override async Task OnInitializedAsync()
{
     Courier.SubscribeWeak<NotificationWrapper<BasicNotification>>(async _ =>
    { 
      Snackbar.Add("Received BasicNotification");
      StateHasChanged();
    });
}

In the Blazor WebApi section, this code snippet is where the issue occurs:

await _notifications.SendToAllAsync(new BasicNotification() { Message = "Hello World!."}, cancellationToken);

The problem appears to stem from the interaction between these code segments, causing the server to execute notifications indefinitely.