Open Closed

Simple SignalR implementation #7609


0
bobingham created

Hi Guys, Following the guidelines at SignalR Integration I have created a new SignalR hub based on the ChatHub in the Web.Core project. I want to be able to call a method on the hub from a service in my Application project but the Web.Core project is not available. The Zero chathub is accessed from the ChatController in the Web.Host project but I do not have a controller, simply a service in the Application project. What's the best way to do this?


20 Answer(s)
  • 0
    ismcagdas created

    Hi @bobingham

    This seems like a reverse dependency. You can move the code to *.Core (Domain) project into a service class and use it both from your Hub and AppService at the same time.

  • 0
    bobingham created

    HI @ismcagdas, sorry but I'm not sure exactly what you mean here. The code is a new hub and I want to call a method in the hub from my service (ProjectName.Application) project. What code, exactly, do I move to the ProjectName.Core project? The hub? If so which nuget packages should be added to ProjectName.Core to support abp's implementation of SignalR? Sorry, but it's not exactly clear from the documentation. I tried to follow your code for notifications but this involves abp dll's and, in the end, it was unfathomable for me.

  • 0
    ismcagdas created

    Hi @bobingham

    What I wanted to say is that, you are trying to call Web.Core project's code from *.Application project. But, Web.Core project references *.Application project, not the opposite.

    My suggestion is, move the code inside your Hub, not the Hub itself, to a common class in your Core project and use this code from your Hub and Application service.

    If that is not clear, could you share your Hub class and AppService class ? I can prepare a sample for you.

  • 0
    bobingham created

    Hi @ismcagdas, thanks for giving over your time. I'm still not sure how to create the code in my Core project and call it from my Hub when the Core project is not available from the Web.Core project (and vice-versa). So I will try and explain with some code. The use case is that each tenant has a set of default Word documents held in Azure blob storage. When a new entity (in my case, a resident in a care home) is added to the system the user selects a set of default documents for the resident. The list of documents is passed back to the server layer (NcDocumentAppService):

    [AbpAuthorize]
    public async Task<string> CreateDefaultDocuments(LoadDefaultDocumentsInput input)
    {
        var tenancyName = await GetTenancyNameFromTenantId();
        var targetContainer = await _blobMethods.SetUpContainer(tenancyName);
        _blobMethods.cloudBlobContainer = targetContainer;
    
        var ncEntity = ObjectMapper.Map<NcEntityDto>(await _ncEntityRepository.FirstOrDefaultAsync(input.NcEntityId));
        var sourceFolderName = "documentTemplates/ncEntities";
    
        foreach (var documentTemplateId in input.NcDocumentTemplateIds)
        {
            var documentTemplate = ObjectMapper.Map<NcDocumentTemplateDto>(await _ncDocumentTemplateRepository.FirstOrDefaultAsync(documentTemplateId));
            var SubFolder = documentTemplate.Uri;
            var defaultDocumentTemplate = await DownloadDefaultDocument(sourceFolderName, documentTemplate.DisplayName);
            var mergedDocument = MergeDocument(defaultDocumentTemplate, ncEntity, tenancyName);
            var documentDefault = await _ncDocumentDefaultRepository.FirstOrDefaultAsync(m => m.Id == documentTemplate.NcDocumentDefaultId);
            var targetFolderName = await GetFolderBreadcrumb(documentDefault.NcDocumentFolderId);
            var metaData = LoadMetaData(GetContentType(documentTemplate.DisplayName), documentTemplate, targetFolderName, ncEntity.Id.ToString());
            var blob = await LoadDocument(mergedDocument, metaData, documentTemplate.DisplayName, GetContentType(documentTemplate.DisplayName));
            var newDocument = await NewCreateOrEditNcDocumentInput(documentTemplate.DisplayName, documentTemplate, ncEntity, blob.Uri.ToString());
            await CreateOrEdit(newDocument);
            //  I WANT TO CALL NcDocumentHub.SendDocumentMergedMessage() HERE AND UPDATE THE UI TO SHOW THE DOCUMENT HAS BEEN LOADED
        }
        return "";
    }
    

    The service method gets the default document template from the database, downloads from blob storage, carries out a merge using known entity values and then uploads the merged document to blob storage and then persists a control record in the database. The process can take up to 10 seconds per document and there are 20-30 documents to prepare. Therefore I would like to use SignalR to update the UI with a messagefor each document which has been downloaded, merged, uploaded and persisted. Here is a simple Hub class to go along with it.

    using Abp.Dependency;
    using Abp.Runtime.Session;
    using Castle.Core.Logging;
    using Microsoft.AspNetCore.SignalR;
    using System.Threading.Tasks;
    
    namespace Nuagecare.Web.App.SignalR.NcDocument
    {
        public class NcDocumentHub : Hub, ITransientDependency
        {
            public IAbpSession AbpSession { get; set; }
            public ILogger Logger { get; set; }
    
            public NcDocumentHub()
            {
                AbpSession = NullAbpSession.Instance;
                Logger = NullLogger.Instance;
            }
    
            public async Task SendDocumentMergedMessage(string documentName)
            {
                await Clients.All.SendAsync("getDocumentMergedMessage", string.Format("{0} merged and loaded.", documentName));
            }
        }
    }
    

    How do I hook this together? Sorry to be such a pain but I've been going round in circles for a couple of days.

  • 0
    bobingham created

    Hi @ismcagdas, Supposing I implemented a controller in the Web.Host project, call my app service to process one document at a time and then called the Hub in Web.Core with my message at the end of each iteration? Architecturally does this break any rules or can you think of any reason why this approach would not work?

  • 0
    bobingham created

    Hi @ismcagdas, I know your up to your ears with 7.2 but when you get a moment afterwards, could you take a look at this. I have created my Hub (above) in Web.Core. I can call this from the angular project and trigger a send from the same server, everything is good. Following your chat code I have attemped to send a message to all clients as follows. I create a new communicator class in my Web.Core in the same folder (namespace) as my hub:

    using Abp.Dependency;
    using Abp.Runtime.Session;
    using Castle.Core.Logging;
    using Microsoft.AspNetCore.SignalR;
    using Nuagecare.App.SignalR;
    using System.Threading.Tasks;
    
    namespace Nuagecare.Web.App.SignalR.FormSubmissionHub
    {
        public class SignalRFormSubmissionCommunicator : ISignalRFormSubmissionCommunicator, ITransientDependency
        {
            private readonly IHubContext<FormSubmissionHub> _formSubmissionHub;
            public IAbpSession AbpSession { get; set; }
            public ILogger Logger { get; set; }
    
            public SignalRFormSubmissionCommunicator(IHubContext<FormSubmissionHub> formSubmissionHub)
            {
                _formSubmissionHub = formSubmissionHub;
                AbpSession = NullAbpSession.Instance;
                Logger = NullLogger.Instance;
            }
    
            public async Task SendFormSubmission(int tenantId)
            {
                await _formSubmissionHub.Clients.All.SendAsync("getFormSubmission", tenantId);
            }
        }
    }
    

    I create the interface in my Core project. I inject my communicator into my app service:

    private readonly ISignalRFormSubmissionCommunicator _signalRFormSubmissionCommunicator;
    

    and make a simple call:

    await _signalRFormSubmissionCommunicator.SendFormSubmission((int)AbpSession.TenantId);
    

    The code hits my communicator method and everything looks fine but the call is not recieved in my angular project. Any simple pointers?

  • 0
    ismcagdas created

    Hi @bobingham

    Sorry for the delay :). Server side code seems fine, could you also share Angular part of it ?

    Thanks,

  • 0
    bobingham created

    Hi @ismcagdas, No problem on the delay, I know 7.2 was a big job and took a lot of your time so I didn't interrupt. This is still a work in progress but as soon as I have SignalR working I will be using it more and more in my dashboards (hence the impatience for the configurable dashboards!). Here is my angular service, formSubmission-signalr.service.ts:

    import { Injectable, Injector, NgZone } from '@angular/core';
    import { AppComponentBase } from '@shared/common/app-component-base';
    import { HubConnection } from '@aspnet/signalr';
    
    @Injectable()
    export class FormSubmissionSignalrService extends AppComponentBase {
    
        constructor(
            injector: Injector,
            public _zone: NgZone
        ) {
            super(injector);
        }
    
        formSubmissionHub: HubConnection;
        isFormSubmissionHubConnected = false;
    
        init(): void {
            this._zone.runOutsideAngular(() => {
                abp.signalr.connect();
                abp.signalr.startConnection(abp.appPath + 'signalr-formSubmission', connection => {
                    abp.event.trigger('app.formSubmission.connected');
                    this.isFormSubmissionHubConnected = true;
                    this.configureConnection(connection);
                });
            });
        }
    
        configureConnection(connection): void {
            // Set the common hub
            this.formSubmissionHub = connection;
    
            // Reconnect if hub disconnects
            connection.onclose(e => {
                this.isFormSubmissionHubConnected = false;
                if (e) {
                    abp.log.debug('Form submission hub connection closed with error: ' + e);
                } else {
                    abp.log.debug('Form submission hub disconnected');
                }
    
                if (!abp.signalr.autoConnect) {
                    return;
                }
    
                setTimeout(() => {
                    connection.start().then(result => {
                        this.isFormSubmissionHubConnected = true;
                    });
                }, 5000);
            });
    
            // Register to get notifications
            //this.registerChatEvents(connection);
            this.registerGetFormSubmission(connection);
    
        }
    
        registerGetFormSubmission(connection): void {
            connection.on('getFormSubmission', message => {
                abp.event.trigger('app.formSubmission.newSubmission', message);
            });
        }
    
        sendMessage(messageData, callback): void {
            if (!this.isFormSubmissionHubConnected) {
                if (callback) {
                    callback();
                }
                abp.notify.warn(this.l('ChatIsNotConnectedWarning'));
                return;
            }
    
            this.formSubmissionHub.invoke('sendMessage', messageData).then(result => {
                if (result) {
                    abp.notify.warn(result);
                }
    
                if (callback) {
                    callback();
                }
            }).catch(error => {
                abp.log.error(error);
    
                if (callback) {
                    callback();
                }
            });
        }
    }
    
    

    My understanding that once we hit the line: await _formSubmissionHub.Clients.All.SendAsync("getFormSubmission", tenantId); in my server the message should be sent to all clients and then be seen at this point in angular:

        registerGetFormSubmission(connection): void {
            connection.on('getFormSubmission', message => {
                abp.event.trigger('app.formSubmission.newSubmission', message);
            });
        }
    

    Am I missing something? I must admit that the plumbing for the communicator class in dotnet-core still has me scratching my head as to how the dependencies work.

  • 0
    bobingham created

    Taking this a bit further I noticed the line: IocManager.RegisterIfNot<IChatCommunicator, NullChatCommunicator>(); in [ProjectName]CoreModule.cs in the Core project. I create a new class, NullFormSubmissionCommunicator in the same folder as my interface in the Core project:

    using System.Threading.Tasks;
    
    namespace Nuagecare.App.SignalR
    {
        public class NullFormSubmissionCommunicator : ISignalRFormSubmissionCommunicator
        {
            public async Task SendFormSubmission(int tenantId)
            {
                await Task.CompletedTask;
            }
        }
    }
    

    and add the following to my module: IocManager.RegisterIfNot<ISignalRFormSubmissionCommunicator, NullFormSubmissionCommunicator>(); But the message is still not received in the angular project. I must be close but can't yet light up my cigar. What am I missing?

  • 0
    bobingham created

    @ismcagdas, I finally got this working. Give the weekend and i will write up how I did it then you can use it in your documentation should you so desire. Then I'll close the issue.

  • 0
    bobingham created

    As promised: The use case here is to send data to the host dashboard from a service in the application layer. The following dashboard widget is interactively updated using SignalR. Capture.JPG

    DOTNET CORE

    In Web.Core project create a new hub, e.g

    using Abp.Dependency;
    using Abp.Runtime.Session;
    using Castle.Core.Logging;
    using Microsoft.AspNetCore.SignalR;
    using System;
    using System.Threading.Tasks;
    
    namespace Nuagecare.Web.App.SignalR.FormSubmissionHub
    {
        public class FormSubmissionHub : Hub, ITransientDependency
        {
            public IAbpSession AbpSession { get; set; }
            public ILogger Logger { get; set; }
    
            public FormSubmissionHub()
            {
                AbpSession = NullAbpSession.Instance;
                Logger = NullLogger.Instance;
            }
    
            public async Task SendMessage(string message)
            {
                await Clients.All.SendAsync("getFormSubmission", string.Format("User {0}: {1}", AbpSession.UserId, message));
            }
    
            public override async Task OnConnectedAsync()
            {
                await base.OnConnectedAsync();
                Logger.Debug("A client connected to FormSubmissionHub: " + Context.ConnectionId);
            }
    
            public override async Task OnDisconnectedAsync(Exception exception)
            {
                await base.OnDisconnectedAsync(exception);
                Logger.Debug("A client disconnected from FormSubmissionHub: " + Context.ConnectionId);
            }
    
        }
    }
    

    Create a hub communicator in the same location:

    public class SignalRFormSubmissionCommunicator : ISignalRFormSubmissionCommunicator, ITransientDependency
        {
            private readonly IHubContext<FormSubmissionHub> _formSubmissionHub;
            public IAbpSession AbpSession { get; set; }
            public ILogger Logger { get; set; }
    
            public SignalRFormSubmissionCommunicator(IHubContext<FormSubmissionHub> formSubmissionHub)
            {
                _formSubmissionHub = formSubmissionHub;
                AbpSession = NullAbpSession.Instance;
                Logger = NullLogger.Instance;
            }
    
            public async Task SendFormSubmission(int tenantId)
            {
                await _formSubmissionHub.Clients.All.SendAsync("getFormSubmission", tenantId);
            }
        }
    

    The interface for the communicator goes in the Core project. Now comes the bit of magic from the Zero guys to give you access to the communicator from the Application project. It’s all a bit wierd and took some fiddling with. In the same location as your communicator interface, in the Core project create a null communicator class:

    public class NullFormSubmissionCommunicator : ISignalRFormSubmissionCommunicator
    {
        public async Task SendFormSubmission(int tenantId)
        {
            await Task.CompletedTask;
        }
    }
    

    To complete the plumbing we register our null communicator class and somehow, by magic, your own communicator is used. Go figure. In the PostInitialize() function of your CoreModule add the following line:

    IocManager.RegisterIfNot<ISignalRFormSubmissionCommunicator, NullFormSubmissionCommunicator>();

    All of this work is so we can call the hub from the application project with a line similar to that below.

    await _signalRFormSubmissionCommunicator.SendFormSubmission((int)AbpSession.TenantId);

    This message is sent to all connected clients so the next job is to hook it up in angular.

    ANGULAR

    In the angular project create a new component using angular-cli below the admin dashboard and display this on your dashboard. In the same location create a new service using angular-cli called HostDashboardSignalR, something like below. I have created an ngx-tabsheet on the Zero dashboard where I add my own tabbed content, my new tab is called ApiTab. angular-tree.png Your service file looks like this:

    import { Injectable, Injector, NgZone } from '@angular/core';
    import { AppComponentBase } from '@shared/common/app-component-base';
    import { HubConnection } from '@aspnet/signalr';
    
    @Injectable()
    export class FormSubmissionSignalrService extends AppComponentBase {
    
        constructor(
            injector: Injector,
            public _zone: NgZone
        ) {
            super(injector);
        }
    
        formSubmissionHub: HubConnection;
        isFormSubmissionHubConnected = false;
    
        init(): void {
            this._zone.runOutsideAngular(() => {
                abp.signalr.connect();
                abp.signalr.startConnection(abp.appPath + 'signalr-formSubmission', connection => {
                    abp.event.trigger('app.formSubmission.connected');
                    this.isFormSubmissionHubConnected = true;
                    this.configureConnection(connection);
                });
            });
        }
    
        configureConnection(connection): void {
            // Set the common hub
            this.formSubmissionHub = connection;
    
            // Reconnect if hub disconnects
            connection.onclose(e => {
                this.isFormSubmissionHubConnected = false;
                if (e) {
                    abp.event.trigger('app.formSubmission.disconnected', e);
                } else {
                    abp.event.trigger('app.formSubmission.disconnected');
                }
    
                if (!abp.signalr.autoConnect) {
                    return;
                }
    
                setTimeout(() => {
                    connection.start().then(result => {
                        console.log(result);
                        this.isFormSubmissionHubConnected = true;
                    });
                }, 5000);
            });
    
            // Register to get notifications
            this.registerGetFormSubmission(connection);
    
        }
    
        registerGetFormSubmission(connection): void {
            connection.on('getFormSubmission', message => {
                abp.event.trigger('app.formSubmission.newSubmission', message);
            });
        }
    }
    

    Make sure you add it as a provider to your admin.module.ts file:

        providers: [
            ImpersonationService,
            TreeDragDropService,
            FormSubmissionSignalrService,
    

    Inject this service into your new component and you’re good to go. Here’s my component:

    import { Component, OnInit, Injector, NgZone } from '@angular/core';
    import { FormSubmissionSignalrService } from '../host-dashboard-signalr/formSubmission-signalr.service';
    import { HostDashboardServiceProxy, GetHostDashboardDataOutput, GetTenantSummaryDataForHostDashboard_Output } from '@shared/service-proxies/service-proxies';
    import { AppComponentBase } from '@shared/common/app-component-base';
    import { SignalRHelper } from '@shared/helpers/SignalRHelper';
    
    import * as moment from 'moment';
    import * as _ from 'lodash';
    
    @Component({
      selector: 'app-host-dashboard-apitab',
      templateUrl: './host-dashboard-apitab.component.html',
      styleUrls: ['./host-dashboard-apitab.component.css']
    })
    export class HostDashboardAPITabComponent extends AppComponentBase implements OnInit {
      public connected: string;
      public formCount: number;
      loading = false;
      hostDashboardData = new GetHostDashboardDataOutput();
      tenantSummary = new Array<GetTenantSummaryDataForHostDashboard_Output>();
      public isFormSubmissionHubConnected: boolean;
    
      constructor(
        injector: Injector,
        private _formSubmissionSignalrService: FormSubmissionSignalrService,
        private _hostDashboardService: HostDashboardServiceProxy,
        public _zone: NgZone
      ) {
        super(injector);
      }
    
      init(): void {
        this.getTenantDataForHostDashboard();
        this.registerEvents();
      }
    
      ngOnInit(): void {
        if (this.appSession.application) {
          SignalRHelper.initSignalR(() => { this._formSubmissionSignalrService.init(); });
        }
        this.init();
      }
    
      getTenantDataForHostDashboard() {
        let midnight = moment().startOf('day');
        let now = moment();
    
        this.loading = true;
    
        this._hostDashboardService.getTenantDataForHostDashboard(
          midnight,
          now,
          0
        ).subscribe(result => {
          this.hostDashboardData = result;
          this.tenantSummary = _.orderBy(result.tenantSummary, 'formSubmissionSum', 'desc');
          this.loading = false;
        });
      }
    
      registerEvents(): void {
        abp.event.on('app.formSubmission.connected', (message) => {
          this._zone.run(() => {
            this.isFormSubmissionHubConnected = true;
            this.connected = 'hub connected';
          });
        });
    
        abp.event.on('app.formSubmission.disconnected', (message) => {
          this._zone.run(() => {
            this.isFormSubmissionHubConnected = false;
            this.connected = 'hub disconnected: ' + message;
          });
        });
    
        abp.event.on('app.formSubmission.newSubmission', (message) => {
          this._zone.run(() => {
            this.updateTenant(message);
            this.formCount = this.formCount + 1;
          });
        });
      }
    
      formatDate(tenant: GetTenantSummaryDataForHostDashboard_Output): string {
        if (tenant.formSubmissionSum === 0) {
          return '';
        } else {
          return moment(moment(tenant.lastUpdated).utc().valueOf()).format('DD/MM/YYYY H:mm:ss');
        }
      }
    

    For good measure here is my html;

    <div class="col-lg-4 col-md-6 col-sm-12">
        <div class="card border-primary rounded-0 mb-2">
            <div class="card-header bg-primary text-white">
                <h5><i class="flaticon-profile mr-3"></i>API Stats</h5>
            </div>
            <div class="card-body">
                <div *ngIf="isFormSubmissionHubConnected" class="small text-muted m--font-success"><small>{{connected}}</small></div>
                <div *ngIf="!isFormSubmissionHubConnected" class="small text-muted m--font-danger"><small>{{connected}}</small></div>
                <div class="row">
                    <div class="col-4">
                        <h6>Tenant</h6>
                    </div>
                    <!-- <div class="col-4">
                        <h6>Users</h6>
                    </div> -->
                    <div class="col-4">
                        <h6>Care actions</h6>
                    </div>
                    <div class="col-4">
                        <h6>Last updated</h6>
                    </div>
                </div>
                <div class="row" *ngFor="let tenant of tenantSummary; index as i">
                    <div class="col-4">{{tenant.tenancyName}}</div>
                    <!-- <div class="col-4">{{tenant.signedInUsersSum}}</div> -->
                    <div class="col-4">
                        <strong>{{tenant.formSubmissionSum}}</strong>
                    </div>
                    <div class="col-4">
                        <strong>{{formatDate(tenant)}}</strong>
                    </div>
                </div>
            </div>
        </div>
    </div>
    

    The result is: Capture.JPG The numbers are updated from the hub and the last updated is modified on the client. I have a lot of apps entering data into my Zero implementation so this simple table allows me to see that all of my tenants are working at a glance. I thought Musa could use this for the new dashboard, number of active users by tenant would be great.

  • 0
    ismcagdas created

    Awesome :) Thanks a lot @bobingham

    We will definitely give it a try for customizable dashboard.

  • 0
    bobingham created

    @ismcagdas, I have some errors in my logs with regard to SignalR for the first time and I suspect it is from the above implementation.

    INFO  2019-10-05 19:07:58,348 [74   ] ore.Mvc.Internal.ControllerActionInvoker - Executed action method Nuagecare.Tenants.Dashboard.TenantDashboardAppService.GetFormSubmissionActivityByStaffForChart (Nuagecare.Application), returned result Microsoft.AspNetCore.Mvc.ObjectResult in 1344.7239ms.
    INFO  2019-10-05 19:08:17,600 [80   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 20286.381ms 101 
    DEBUG 2019-10-05 19:08:21,756 [109  ] Abp.AspNetCore.SignalR.Hubs.AbpCommonHub - A client is disconnected: 5o4znj9JHWHyVaLsqW5VpQ
    DEBUG 2019-10-05 19:08:22,771 [39   ] Nuagecare.Web.Chat.SignalR.ChatHub       - A client is disconnected: X6rtAAzycni2NZ6dSH3KSw
    ERROR 2019-10-05 19:08:23,787 [109  ] .AspNetCore.SignalR.HubConnectionContext - Failed writing message. Aborting connection.
    System.InvalidOperationException: Writing is not allowed after writer was completed.
       at System.IO.Pipelines.Pipe.GetMemory(Int32 sizeHint)
       at System.IO.Pipelines.Pipe.DefaultPipeWriter.GetSpan(Int32 sizeHint)
       at System.IO.Pipelines.PipeWriter.WriteAsync(ReadOnlyMemory`1 source, CancellationToken cancellationToken)
       at Microsoft.AspNetCore.SignalR.HubConnectionContext.<TryWritePingSlowAsync>d__49.MoveNext()
    ERROR 2019-10-05 19:08:23,787 [109  ] .AspNetCore.SignalR.HubConnectionContext - Failed writing message. Aborting connection.
    System.InvalidOperationException: Writing is not allowed after writer was completed.
       at System.IO.Pipelines.Pipe.GetMemory(Int32 sizeHint)
       at System.IO.Pipelines.Pipe.DefaultPipeWriter.GetSpan(Int32 sizeHint)
       at System.IO.Pipelines.PipeWriter.WriteAsync(ReadOnlyMemory`1 source, CancellationToken cancellationToken)
       at Microsoft.AspNetCore.SignalR.HubConnectionContext.<TryWritePingSlowAsync>d__49.MoveNext()
    INFO  2019-10-05 19:08:26,756 [51   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 4579899.7844ms 101 
    INFO  2019-10-05 19:08:27,834 [51   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 4580877.9993ms 101 
    DEBUG 2019-10-05 19:08:29,865 [39   ] Abp.AspNetCore.SignalR.Hubs.AbpCommonHub - A client is disconnected: 5w7N5t6kRGAuhLw7we98nA
    DEBUG 2019-10-05 19:08:29,865 [109  ] Nuagecare.Web.Chat.SignalR.ChatHub       - A client is disconnected: Hhx2T-pD_ZqRnbFltQbOuQ
    DEBUG 2019-10-05 19:08:30,881 [39   ] Abp.AspNetCore.SignalR.Hubs.AbpCommonHub - A client is disconnected: wqcCGN_gb_noqrsQQgLMeg
    DEBUG 2019-10-05 19:08:30,881 [37   ] Nuagecare.Web.Chat.SignalR.ChatHub       - A client is disconnected: AYzqNH3HvZbSQKykQv6v9Q
    ERROR 2019-10-05 19:08:31,896 [37   ] .AspNetCore.SignalR.HubConnectionContext - Failed writing message. Aborting connection.
    System.InvalidOperationException: Writing is not allowed after writer was completed.
       at System.IO.Pipelines.Pipe.GetMemory(Int32 sizeHint)
       at System.IO.Pipelines.Pipe.DefaultPipeWriter.GetSpan(Int32 sizeHint)
       at System.IO.Pipelines.PipeWriter.WriteAsync(ReadOnlyMemory`1 source, CancellationToken cancellationToken)
       at Microsoft.AspNetCore.SignalR.HubConnectionContext.<TryWritePingSlowAsync>d__49.MoveNext()
    INFO  2019-10-05 19:08:32,912 [38   ] Microsoft.AspNetCore.Server.Kestrel      - Connection id "0HLQ8ED367GOK", Request id "0HLQ8ED367GOK:00000011": the connection was closed because the response was not read by the client at the specified minimum data rate.
    INFO  2019-10-05 19:08:34,912 [120  ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 3277540.3999ms 101 
    INFO  2019-10-05 19:08:34,943 [32   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 3277687.8381ms 101 
    INFO  2019-10-05 19:08:34,943 [112  ] Microsoft.AspNetCore.Server.Kestrel      - Connection id "0HLQ8ED367GOI", Request id "0HLQ8ED367GOI:0000000F": the connection was closed because the response was not read by the client at the specified minimum data rate.
    INFO  2019-10-05 19:08:35,897 [109  ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 13730904.8477ms 101 
    INFO  2019-10-05 19:08:35,928 [32   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 13730829.5871ms 101 
    DEBUG 2019-10-05 19:08:35,959 [112  ] Nuagecare.Web.Chat.SignalR.ChatHub       - A client is disconnected: Z-hFhgptsMNrdnm73RDuhQ
    DEBUG 2019-10-05 19:08:35,959 [109  ] Abp.AspNetCore.SignalR.Hubs.AbpCommonHub - A client is disconnected: SZETRL1vsi6cqONnHh7yFg
    DEBUG 2019-10-05 19:08:35,959 [37   ] Abp.AspNetCore.SignalR.Hubs.AbpCommonHub - A client is disconnected: WDBrDzCQZ69KwqbjkNKFFg
    DEBUG 2019-10-05 19:08:35,959 [38   ] nalR.FormSubmissionHub.FormSubmissionHub - A client disconnected from FormSubmissionHub: ECHbXWFhvQDoA5n639DXpQ
    ERROR 2019-10-05 19:08:36,975 [37   ] .AspNetCore.SignalR.HubConnectionContext - Failed writing message. Aborting connection.
    System.InvalidOperationException: Writing is not allowed after writer was completed.
       at System.IO.Pipelines.Pipe.GetMemory(Int32 sizeHint)
       at System.IO.Pipelines.Pipe.DefaultPipeWriter.GetSpan(Int32 sizeHint)
       at System.IO.Pipelines.PipeWriter.WriteAsync(ReadOnlyMemory`1 source, CancellationToken cancellationToken)
       at Microsoft.AspNetCore.SignalR.HubConnectionContext.<TryWritePingSlowAsync>d__49.MoveNext()
    ERROR 2019-10-05 19:08:36,975 [37   ] .AspNetCore.SignalR.HubConnectionContext - Failed writing message. Aborting connection.
    System.InvalidOperationException: Writing is not allowed after writer was completed.
       at System.IO.Pipelines.Pipe.GetMemory(Int32 sizeHint)
       at System.IO.Pipelines.Pipe.DefaultPipeWriter.GetSpan(Int32 sizeHint)
       at System.IO.Pipelines.PipeWriter.WriteAsync(ReadOnlyMemory`1 source, CancellationToken cancellationToken)
       at Microsoft.AspNetCore.SignalR.HubConnectionContext.<TryWritePingSlowAsync>d__49.MoveNext()
    DEBUG 2019-10-05 19:08:37,990 [112  ] Abp.AspNetCore.SignalR.Hubs.AbpCommonHub - A client is disconnected: rRDEBOcsg6HaKs54UWmRyg
    DEBUG 2019-10-05 19:08:37,990 [38   ] Nuagecare.Web.Chat.SignalR.ChatHub       - A client is disconnected: 1x-ARxK-4NfhpmaW0wecFw
    ERROR 2019-10-05 19:08:39,006 [109  ] .AspNetCore.SignalR.HubConnectionContext - Failed writing message. Aborting connection.
    System.InvalidOperationException: Writing is not allowed after writer was completed.
       at System.IO.Pipelines.Pipe.GetMemory(Int32 sizeHint)
       at System.IO.Pipelines.Pipe.DefaultPipeWriter.GetSpan(Int32 sizeHint)
       at System.IO.Pipelines.PipeWriter.WriteAsync(ReadOnlyMemory`1 source, CancellationToken cancellationToken)
       at Microsoft.AspNetCore.SignalR.HubConnectionContext.<TryWritePingSlowAsync>d__49.MoveNext()
    ERROR 2019-10-05 19:08:39,006 [109  ] .AspNetCore.SignalR.HubConnectionContext - Failed writing message. Aborting connection.
    System.InvalidOperationException: Writing is not allowed after writer was completed.
       at System.IO.Pipelines.Pipe.GetMemory(Int32 sizeHint)
       at System.IO.Pipelines.Pipe.DefaultPipeWriter.GetSpan(Int32 sizeHint)
       at System.IO.Pipelines.PipeWriter.WriteAsync(ReadOnlyMemory`1 source, CancellationToken cancellationToken)
       at Microsoft.AspNetCore.SignalR.HubConnectionContext.<TryWritePingSlowAsync>d__49.MoveNext()
    INFO  2019-10-05 19:08:40,975 [120  ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 16790141.1665ms 101 
    INFO  2019-10-05 19:08:40,975 [51   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 429979.9867ms 101 
    INFO  2019-10-05 19:08:40,975 [39   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 16790482.9874ms 101 
    INFO  2019-10-05 19:08:40,975 [80   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 430085.5001ms 101 
    INFO  2019-10-05 19:08:41,037 [112  ] Microsoft.AspNetCore.Server.Kestrel      - Connection id "0HLQ8ED367GOV", Request id "0HLQ8ED367GOV:00000014": the connection was closed because the response was not read by the client at the specified minimum data rate.
    INFO  2019-10-05 19:08:41,428 [39   ] soft.AspNetCore.Hosting.Internal.WebHost - Request starting HTTP/1.1 OPTIONS http://nuagecare.io/api/services/app/NcEntity/GetFormsForEntity?input=56907702-ae3e-4cbd-1770-08d5f63eb2e7  
    INFO  2019-10-05 19:08:41,428 [39   ] pNetCore.Cors.Infrastructure.CorsService - CORS policy execution successful.
    INFO  2019-10-05 19:08:41,428 [51   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 17728315.9452ms 101 
    INFO  2019-10-05 19:08:41,443 [51   ] soft.AspNetCore.Hosting.Internal.WebHost - Request starting HTTP/1.1 OPTIONS http://nuagecare.io/api/services/app/NcForm/SubmitForms  
    INFO  2019-10-05 19:08:41,443 [51   ] pNetCore.Cors.Infrastructure.CorsService - CORS policy execution successful.
    INFO  2019-10-05 19:08:41,443 [51   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 5.4954ms 204 
    INFO  2019-10-05 19:08:41,443 [112  ] soft.AspNetCore.Hosting.Internal.WebHost - Request starting HTTP/1.1 GET http://nuagecarewebapi.azurewebsites.net/  
    INFO  2019-10-05 19:08:41,443 [112  ] ft.AspNetCore.Routing.EndpointMiddleware - Executing endpoint 'Nuagecare.Web.Controllers.HomeController.Index (Nuagecare.Web.Host)'
    INFO  2019-10-05 19:08:41,443 [112  ] ore.Mvc.Internal.ControllerActionInvoker - Route matched with {action = "Index", controller = "Home", area = ""}. Executing action Nuagecare.Web.Controllers.HomeController.Index (Nuagecare.Web.Host)
    INFO  2019-10-05 19:08:41,459 [112  ] ore.Mvc.Internal.ControllerActionInvoker - Executing action method Nuagecare.Web.Controllers.HomeController.Index (Nuagecare.Web.Host) - Validation state: Valid
    INFO  2019-10-05 19:08:41,428 [37   ] soft.AspNetCore.Hosting.Internal.WebHost - Request starting HTTP/1.1 OPTIONS http://nuagecare.io/api/services/app/NcEntity/GetFormsForEntity?input=7e334e4c-db3f-4900-2853-08d6a6218745  
    INFO  2019-10-05 19:08:41,428 [120  ] soft.AspNetCore.Hosting.Internal.WebHost - Request starting HTTP/1.1 OPTIONS http://nuagecare.io/api/services/app/NcEntity/GetFormsForEntity?input=66ae3b69-0eaf-489f-1769-08d5f63eb2e7  
    INFO  2019-10-05 19:08:41,428 [32   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 17728297.6264ms 101 
    INFO  2019-10-05 19:08:41,459 [100  ] soft.AspNetCore.Hosting.Internal.WebHost - Request starting HTTP/1.1 OPTIONS http://nuagecare.io/api/services/app/NcEntity/GetFormsForEntity?input=dce6ebdb-fc90-4aa6-1771-08d5f63eb2e7  
    INFO  2019-10-05 19:08:41,459 [112  ] ore.Mvc.Internal.ControllerActionInvoker - Executed action method Nuagecare.Web.Controllers.HomeController.Index (Nuagecare.Web.Host), returned result Microsoft.AspNetCore.Mvc.RedirectToActionResult in 0.0518ms.
    INFO  2019-10-05 19:08:41,459 [37   ] pNetCore.Cors.Infrastructure.CorsService - CORS policy execution successful.
    INFO  2019-10-05 19:08:41,490 [37   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 57.5895ms 204 
    INFO  2019-10-05 19:08:41,475 [33   ] soft.AspNetCore.Hosting.Internal.WebHost - Request starting HTTP/1.1 OPTIONS http://beverley.nuagecare.io/signalr/negotiate?enc_auth_token=wNYmO41%2F48SHNstaLVXxHCCre29BZQl1NhC6NM3R3rzpXtPQxVzH6jEzA%2FQhXFN5tu6Fk7pO53uppm1mVXMZgxbyRVz26dnepi%2FFyB6axBY%2B6gq1GL%2BuRQgoiFUCjRN2p8w6LevViwKlHyWZZJZO1DGVSjAi1m2U%2Bog9pkHw9%2FSx%2Bo570np2k82P6w%2FormNXR9PL41u7oSMKLOXt0GwuLYro99gg8VeT3cl1X4tKr7w%2FEKe17nck8u0uyzYKEm%2Fn%2BFbG0l1Vc%2B5X2BpYXoGVkFiwXXP34yArvijtwXX3VvR7JY7LekO%2B7Bp4qQpkbydhoCGUcIOBe5PPn7%2BR8WNI%2Bt2q24jimq5Bb5K9t3qhip9pjqs14ViIBbvPhxb77pmFk1nvczZXhcBXOP5RhxLxwxiBwOcxHoHkohS4i0skJrr7w9%2BYtEJXEbOc5rxLbt65Na71jBU6ZyXjYGd68S3OniWP4SKVOwkmRDSPeI%2F8Sy6cbohH%2B5Yeveb5to6u7z%2BQl35SLiK%2Frtn5JaJjqV6ioNOmUbemunGuFymOfwo73HJtIB3FEkCkHq%2FMfioPRYaJfsNdGgK3begAGQMf01613lkubrz3X%2FdQHWScIAjb3lG2xVArf4pMwPyFODhqL3JAdvm6hYNS7zbU67LJE3SoE%2FYub8kYn02Gx%2FJcHJ98HoP0lPG3%2BeUBOiZCH7GbPFV5%2Fc5qozkeDooPy21QRDTa7QetvfQrW0ecynjn%2Bu%2BYJfV82VI2ZWCznZPRRx9ddxZIJjtvLY0nKPYHOxghgzv7R05Q8ZpyMuYxHo9ceaNGrh7eBqtUiFMCaselb8Wx1xKmpHU1FuUkg%2BA%2BnKm5gb2KvOORTlzp%2F%2BG6rMjs7q1wqdmJyq%2BtUCoOtTC8Lud%2BrTPVRu7cNXeZHg%2BxfqfeyFHmI9%2B09182uqntVkMLlKfIj88imfskDQ8nTvCrRU6RTC5W1UUcHjocdQ9C7GYyFpzeVnQ8WSuIJwAUhNL2ZZjluEdXJRTMsJbmQis1HQL%2Fh6zrILORO24nkoFnryUTsvnLWTMhPZzDU8mAKR4v1AV94mx5ccEPBO8tqBbKM7pt12nsdYTOZzqEo4WW%2Brc6UMaXEbpmlN7QK%2FSIviQ8PnG%2BozdZ1kEmTOa2arLwGsqSntaG  
    INFO  2019-10-05 19:08:41,475 [120  ] pNetCore.Cors.Infrastructure.CorsService - CORS policy execution successful.
    INFO  2019-10-05 19:08:41,490 [120  ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 62.4998ms 204 
    INFO  2019-10-05 19:08:41,475 [100  ] pNetCore.Cors.Infrastructure.CorsService - CORS policy execution successful.
    INFO  2019-10-05 19:08:41,490 [112  ] ft.AspNetCore.Mvc.RedirectToActionResult - Executing RedirectResult, redirecting to /Ui.
    INFO  2019-10-05 19:08:41,428 [38   ] soft.AspNetCore.Hosting.Internal.WebHost - Request starting HTTP/1.1 OPTIONS http://nuagecare.io/api/services/app/NcForm/SubmitForms  
    INFO  2019-10-05 19:08:41,490 [100  ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 40.2434ms 204 
    INFO  2019-10-05 19:08:41,506 [112  ] ore.Mvc.Internal.ControllerActionInvoker - Executed action Nuagecare.Web.Controllers.HomeController.Index (Nuagecare.Web.Host) in 46.8225ms
    INFO  2019-10-05 19:08:41,428 [39   ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 3.3461ms 204 
    INFO  2019-10-05 19:08:41,475 [43   ] soft.AspNetCore.Hosting.Internal.WebHost - Request starting HTTP/1.1 OPTIONS http://nuagecare.io/api/services/app/NcForm/SubmitForms
    

    How does SignalR work with authorisation? In other words, I have a dashboard open on the host which displays real time data using SignalR. Once the authorisation for the user expires SignalR continues, is this likely to cause the above errors? If so how do I authorise SignalR or get it to log the user out without raising these errors??

  • 0
    ismcagdas created

    Hi,

    Might be related to that. Authorization is done here https://github.com/aspnetzero/aspnet-zero-core/blob/dev/aspnet-core/src/MyCompanyName.AbpZeroTemplate.Web.Host/Startup/AuthConfigurer.cs#L101. So, SignalR requests should contain enc_auth_token query string value.

    I also found a similar issue report on https://github.com/aspnet/AspNetCore/issues/6701

  • 0
    bobingham created

    Hi @ismacagdas, I had already found "Writing is not allowed after writer was completed", with SignalR 1.1" which seems to suggest it's a benign message and possibly updated in a later version of SignalR, I have no idea which version of SignalR I am using because I only have Abp.AspNetCore.SignalR which is on version 4.4.0. If you're saying that SignalR should already be authorised how is it possible for it to continue working after expiration?

  • 0
    ismcagdas created
  • 0
    bobingham created

    Hi @ismcagdas, Interestingly enough the value is set in my appsettings.json but not in my appsettings.production.json. Is your recommendation that I should set the value of AllowAnonymousSignalRConnection in my production file to true?

  • 0
    bobingham created

    bump***

  • 0
    aaron created

    I believe AllowAnonymousSignalRConnection only affects requests from clients.

    By the way, can the issue be reproduced on your local machine?

    Word from SignalR authors

    aspnet/AspNetCore#6701 ("Writing is not allowed after writer was completed", with SignalR 1.1) appears to be fixed and shipped with ASP.NET Core 3.0 and will not be backported as per dotnet/aspnetcore#10043 (comment 491485101):

    Do we need to backport this?

    Worst case it's an erroneous error log. There is no functional issue here. Based on that I don't think we should, but open to other opinions.

    Possible workaround

    Disclaimer: I have not tried this. Of course, the actual fix is to upgrade to ASP.NET Core 3.0 but I suppose that may be impractical for some.

    Since the issue appears to be with SignalR 1.1, you can try downgrading Abp.AspNetCore.SignalR to 4.0.0, which depends on SignalR 1.0.4.

    Since ABP 4.4+ handles multiple notification notifiers rather than injecting a single implementation, you likely need the following in your module's Initialize method:

    Configuration.Notifications.Notifiers.Add<SignalRRealTimeNotifier>();
    
  • 0
    bobingham created

    Hi Aaron, Long time no see!!!! A clear and precise answer as always, thanks a lot. I'll close this issue now I know what to refer to next time I have to look at this (it's a low priority and a lower priority after your post!). Bob