4

So I have followed a few tutorials and one of the common themes once you set up your hub in C# is you call a service similar to this:

private hubConnection: signalR.HubConnection = new signalR.HubConnectionBuilder()
.withUrl(`${environment.hub}contacts`)
.build();

constructor(private http: HttpClient) { 
}

public startConnection = () => 
  this.hubConnection
    .start()
    .then(() => console.log('Hub Connection started'))
    .catch(err => console.log('Error while starting connection: ' + err))
    

public addTransferContactDataListener = () => 
    this.hubConnection.on('transfercontactdata', (data) => {
        this.contacts = data.result // where the magic happens on the push
        console.log(data, 'Hub data transfer occuring');
    });

My concern is if you try to inject the private hubConnection: signalR.HubConnection in the constructor it blows up. Even if you set up the connection builder. This matters because what if I want four or more pages that all subscribe to this?

What I have been doing is setting the service up in the app.component.ts and then calling the methods for startConnection and then addTransferContactDataListener. But that seems wrong. I tried to make it injectable in a module but it keeps failing saying it has no provider or other thing. While it is injectable to be used you still have to call it and the constructor at times seems ignored in my practice.

Has anyone delved into calling and setting this on injection and reusing it? Like 'A' or 'B' views call the signalR service and either one can automatically have it run the constructor or some argument only once. I probably am doing something wrong. Like I said, it works. But I have to call the methods in the app.component.ts and that just feels wrong doing it that way and brittle.

luiscla27
  • 4,956
  • 37
  • 49
djangojazz
  • 14,131
  • 10
  • 56
  • 94
  • 1
    So you have the service injected into `app.component.ts` and you don't want to inject in more components because all these constructors that have injected this service are going to blow up?. In words, what do you want to achieve? to have a service that, by its own, call `startConnection ` and `addTransferContactDataListener `? . Sorry if I don't understand your question, I am not familiar with `signalR` – Andres2142 Jun 21 '21 at 23:55
  • Check [this tutorial](https://www.c-sharpcorner.com/article/real-time-angular-11-application-with-signalr-and-net-5/), generally we will inject the SignalR hub in the Content component's `component.ts` page, instead of the in the `app.component.ts` page, when, when calling the content component, it will create a signal connection. You could try to use this method. – Zhi Lv Jun 22 '21 at 08:51
  • We use SignalR for some back-end feeds at work. We simply used `@Injectable({providedIn: 'root'})` and made a method on the service which returns an observable of a `BehaviorSubject` called `getDataStream()`. The `getDataStream` method initializes the connection if none exists. The SignalR connectionHub then merely calls `BehaviorSubject.next(data)` whenever it receives a message. – Mikkel Christensen Jun 23 '21 at 18:30
  • @ZhiLv I was more curious if you can do the hub start and connection create in it's own service on a lazy startup. Where it does it once, if not active, then just keeps going. Generally I am more familiar with services using a providers in a module and not declaring their construction of the hub outside in another component. I would assume you subscribe many times in pages but only start the hub once. – djangojazz Jun 24 '21 at 20:15
  • @MikkelChristensen That sounds promising do you have any code for an example of the 'getDataStream' you could post as an answer I could play with? Also you are saying that method has basically a start if exists too? That's pretty much what I want and if you can inject the service only when needed in modules even better. – djangojazz Jun 24 '21 at 20:17

1 Answers1

6

Utilizing SignalR in Angular

Our objective is to create a service which functions as an intermediary between Angular and our signalR connection. This style of approach means there are two general problems we need to solve

  • Connecting with SignalR
  • Designing an approach to interface with the rest of our app

Angular Interfacing

Our goal is to be able to inject our service anywhere within our application, and have our components react appropriately when an event happens in our signalR hub which is of interest to our service

Making the service available

Since our service is going to potentially be used anywhere, and has lifecycle logic that deals with the connectionHub which will fail if trying to open a non-closed connection, the only injectors we can use are the following:

  • app.module.ts
  • @Injectable({providedIn: 'root'})

The simplest solution is utilizing the @Injectable({providedIn: 'root'}) decorator, however for services with nuanced internal lifecycles such as a SignalR service, I prefer to expose a public API so that we only expose methods that are safe for our team to utilize.

The public Interface

First, let's create an interface which we can use to make the SignalR service available in the rest of our application. However, because we cannot provide interfaces in Angular, we utilize an abstract class instead.

public SignalR interface


import {Observable} from 'rxjs';
import {Injectable} from '@angular/core';

@Injectable()
export abstract class SignalRService {
  abstract getDataStream(): Observable<any>
}

The beginning of our SignalR Service


import {Subject, Observable} from 'rxjs';
import {SignalRService} from './signalr.service';
import {Injectable} from '@angular/core';

@Injectable()
export class PrivateSignalRService extends SignalRService {
  private _signalEvent: Subject<any>;

  constructor() {
    super();
    this._signalEvent = new Subject<any>();
  }

  getDataStream(): Observable<any> {
    return this._signalEvent.asObservable();
  }
}

Now we have the basic setup for our injection. We describe the public interface for which services we want to be available in our abstract class, treating it as an interface, and implement our logic in PrivateSignalRService

Now, all we have left to do is tell the Angular Injector to provide a PrivateSignalRService whenever we ask for a SignalRService

app.module.ts


@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [{
    provide: SignalRService,
    useClass: PrivateSignalRService
  }],
  bootstrap: [AppComponent]
})
export class AppModule {
  // inject signal directly in module if you want to start connecting immediately.
  constructor(private signal: SignalRService) {
  }
}

Mapping SignalR events to subject events

We are now able to inject our SignalRService in our application, but our HubConnection may grow over time and it may not be the case that every event is relevant for every component, so we create a filter.

First we create an enum which represents each of the different HubConnection events we may expect to receive.

signal-event-type.ts


export enum SignalEventType {
  EVENT_ONE,
  EVENT_TWO
}

next we need an interface so that we know what to expect when we call getDataStream

signal-event

import {SignalEventType} from './signal-event-type';

export interface SignalEvent<TDataShape> {
  type: SignalEventType,
  data: TDataShape
}

change the signature of our public abstract class interface

SignalRService


@Injectable()
export abstract class SignalRService {
  abstract getDataStream<TDataShape>(...filterValues: SignalEventType[]): Observable<SignalEvent<TDataShape>>
}

PrivateSignalRService


@Injectable()
export class PrivateSignalRService extends SignalRService {
  private _signalEvent: BehaviorSubject<SignalEvent<any>>;

  constructor() {
    super();
    this._signalEvent = new BehaviorSubject<any>(null);
  }

  getDataStream<TDataShape>(...filterValues: SignalEventType[]): Observable<SignalEvent<TDataShape>> {
    return this._signalEvent.asObservable().pipe(filter(event => filterValues.some(f => f === event.type)));
  }

}

Connecting with SignalR

NOTICE: This example uses the @aspnet/signalr package.

Observe the following changes to the PrivateSignalRService

@Injectable()
export class PrivateSignalRService extends SignalRService {
  private _signalEvent: Subject<SignalEvent<any>>;
  private _openConnection: boolean = false;
  private _isInitializing: boolean = false;
  private _hubConnection!: HubConnection;

  constructor() {
    super();
    this._signalEvent = new Subject<any>();
    this._isInitializing = true;
    this._initializeSignalR();
  }

  getDataStream<TDataShape>(...filterValues: SignalEventType[]): Observable<SignalEvent<TDataShape>> {
    this._ensureConnection();
    return this._signalEvent.asObservable().pipe(filter(event => filterValues.some(f => f === event.type)));
  }

  private _ensureConnection() {
    if (this._openConnection || this._isInitializing) return;
    this._initializeSignalR();
  }

  private _initializeSignalR() {
    this._hubConnection = new HubConnectionBuilder()
      .withUrl('https://localhost:5001/signalHub')
      .build();
    this._hubConnection.start()
      .then(_ => {
        this._openConnection = true;
        this._isInitializing = false;
        this._setupSignalREvents()
      })
      .catch(error => {
        console.warn(error);
        this._hubConnection.stop().then(_ => {
          this._openConnection = false;
        })
      });

  }

  private _setupSignalREvents() {
    this._hubConnection.on('MessageHelloWorld', (data) => {
      // map or transform your data as appropriate here:
      this._onMessage({type: SignalEventType.EVENT_ONE, data})
    })
    this._hubConnection.on('MessageNumberArray', (data) => {
      // map or transform your data as appropriate here:
      const {numbers} = data;
      this._onMessage({type: SignalEventType.EVENT_TWO, data: numbers})
    })
    this._hubConnection.onclose((e) => this._openConnection = false);
  }

  private _onMessage<TDataShape>(payload: SignalEvent<TDataShape>) {
    this._signalEvent.next(payload);
  }

}

Now, the first time you request a getDataStream the service will attempt to create a signalR connection if there is no open connection, so you no longer need to inject the service in the AppModule constructor.

Listening for events in components

  • example-one is interested in EVENT_ONE
  • example-two is interested in EVENT_TWO

example-one.component.ts


@Component({
  selector: 'app-example-one',
  template: `<p>Example One Component</p>`
})

export class ExampleOneComponent implements OnInit, OnDestroy {
  subscription!: Subscription;
  constructor(private signal: SignalRService) {
  }

  ngOnInit(): void {
    this.subscription = this.signal.getDataStream<string>(SignalEventType.EVENT_ONE).subscribe(message => {
      console.log(message.data);
    })
  }
  
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

example-two.component.ts


@Component({
  selector: 'app-example-two',
  template: `<p>Example Two Component</p>`
})
export class ExampleTwoComponent implements OnInit, OnDestroy {
  subscription!: Subscription;

  constructor(private signal: SignalRService) {
  }

  ngOnInit(): void {
    this.subscription = this.signal.getDataStream<string[]>(SignalEventType.EVENT_TWO).subscribe(message => {
      message.data.forEach(m => console.log(m));
    })
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

ExampleOneComponent and ExampleTwoComponent now only receive events when the event received in the HubConnection is the appropriate type for each component.

Final Notes

This example code doesn't have robust error handling, and only demonstrates a general approach we've taken to integrating signalR with Angular.

You also need to determine a strategy for managing local persistence, as the Subject will only display any new incoming messages, which will reset your components when you navigate around in your app, as an example.

Mikkel Christensen
  • 2,502
  • 1
  • 13
  • 22
  • Cool, I will take a look at your MVP and let you know. Looks to be the concepts are there. I am also concerned about describing later, but that is another story ;) – djangojazz Jun 25 '21 at 18:41
  • I am cool with most of this except it seems like a lot of stuff to do the SignalRService in two classes is confusing me. Why do you need to have both for injection into the 'providers' in the app.module? You mention it being an interface but I see an abstract where you are using the private. Is this just to obfuscate the real logic in the private-signal-r.service? I was going to implement something similar to make it lazy load, but I wanted something with as little parts as possible. – djangojazz Jun 28 '21 at 17:53
  • Yes, the `{provide: Foo, useClass: FooBar}` is merely done to create a public interface so that when someone else injects the service, they will only see the methods described on the abstract class. The reason we use an abstract class and not an actual interface is because TypeScript interfaces disappear when compiled, and therefore cannot be used. If you do not care about this abstraction, simply add the actual service to the providers array directly: `providers: [SignalRService]`. If you use `@Injectable({providedIn: 'root'})` you won't even have to add the service to any Module. – Mikkel Christensen Jun 28 '21 at 20:39
  • Cool, you get the points! Thanks for the detailed setup, it's just a bummer that SignalR doesn't have a more injectable means in the package itself. – djangojazz Jun 28 '21 at 22:22
  • @MikkelChristensen Could you please take a look at https://stackoverflow.com/questions/72776033/signalr-the-new-message-does-not-shows-in-target-clients-message-box-angular – user3748973 Jul 03 '22 at 17:03