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.
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
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
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) {
}
}
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)));
}
}
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.
EVENT_ONE
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With