Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can you inject signalR in Angular 10+ in a service without calling the methods in the app.component

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.

like image 380
djangojazz Avatar asked Oct 15 '25 18:10

djangojazz


1 Answers

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.

like image 107
Mikkel Christensen Avatar answered Oct 18 '25 10:10

Mikkel Christensen



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!