Search code examples
asp.netangularsignalrsignalr-hub

SignalR like functionality is not working


I have created a like functionality so that a user can like a post in my app. I've read about SignalR and I tried using it so that the number of likes can be automatically updated in real-time whenever a user likes/unlikes a post. However, it does not work, but I also receive no errors. The only message in my console after pressing the like button is:

Information: WebSocket connected to wss://localhost:44351/hubs/like?access_token=eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxIiwidW5pcXVlX25hbWUiOiJnZW9yZ2lhIiwicm9sZSI6WyJNZW1iZXIiLCJBZG1pbiJdLCJuYmYiOjE2MTk0NjQ3NzAsImV4cCI6MTYyMDA2OTU3MCwiaWF0IjoxNjE5NDY0NzcwfQ.1Bwf_Y2QJP_VjRUXaBeqz5sueV6oTIpVlOLU4kOEmLf2Y_hfxJbc5_f4yksY9R45YGz0qPWw-rc10I7pobFJYQ

This is my .net code:

 public class LikeHub : Hub
  {
        private readonly IPostRepository _postRepository;
        private readonly DataContext _context;
        private readonly IUserRepository _userRepository;

        public LikeHub(IPostRepository postRepository, DataContext context, IUserRepository userRepository)
        {
            _postRepository = postRepository;
            _context = context;
            _userRepository = userRepository;
        }

        public async Task SetLike(int userId, int postId)
        {
            Like l = new Like();

            Like temp = _context.Likes.Where(x => x.PostId == postId && x.UserId == userId).FirstOrDefault();

            if(temp != null)
            {
                _context.Likes.Remove(temp);
            } else
            {
                _context.Likes.Add(l);

                l.UserId = userId;
                l.PostId = postId;
            }

            await _context.SaveChangesAsync();

            int numOfLikes = _context.Likes.Where(x => x.PostId == postId).Count();

            await Clients.All.SendAsync("ReceiveMessage", numOfLikes, postId, userId);

        }
   }

And this is my Angular code in the PostsService:

export class PostsService {

  hubUrl = environment.hubUrl;
  private hubConnection: HubConnection;
  likeMessageReceive: EventEmitter<{ numOfLikes: number, postId: number, userId: number }> = new EventEmitter<{ numOfLikes:number, postId: number, userId: number }>();


  constructor(private http: HttpClient) {}

   connectHubs(user: User) { 
      this.hubConnection = new HubConnectionBuilder()
      .withUrl(this.hubUrl + 'like', { accessTokenFactory: () => user.token, 
      skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets })
      .build();
  
      return  this.hubConnection.start()
                 .then(() => {
                     this.hubConnection.on('ReceiveMessage', (numOfLikes, postId, userId) => {
                       this.likeMessageReceive.emit({ numOfLikes, postId, userId });
                     });
                 })
                 .catch(error => console.log(error)); 
  }
  
  setLike(userId: number, postId: number) {
       this.hubConnection.invoke('SetLike', userId, postId);
  }
  
  closeHubConnections() {
      this.hubConnection.stop();
  }
}

This is the Angular code in my PostCardComponent, where the like button is:

export class PostCardComponent implements OnInit {

 @Input() post: Post;
  likesSubscription: Subscription;

 
  constructor(private postService:PostsService,public accountService:AccountService)
            { this.Login$ = this.accountService.Logged;}
ngOnInit(): void {

    this.likesSubscription = this.postService.likeMessageReceive.subscribe(result =>{
      if (result.postId === this.post.id) {
          this.post.likes.length = result.numOfLikes;
      }
  })
}

liked(post: Post) {
    const user: User = JSON.parse(localStorage.getItem('user'));
    this.postService.setLike(user.id, post.id);
  }
}

This is the PostListComponent, where all the posts are:

export class PostListComponent implements OnInit {

  posts: Post[];
  post: Post;
  likesSubscription: Subscription;
  localUser: User;


  constructor(private postService: PostsService) {}

ngOnInit(): void {
     this.postService.connectHubs(this.localUser);
  }

}

I don't know if the code in this.hubConnection.on() is correct, or if the given parameters are correct. I have also added the LikeHub in my endpoints in the Startup.cs class.


Solution

  • I would highly recommend to start from carefully rewriting this example, this should really help to understand concepts better https://learn.microsoft.com/en-us/aspnet/core/tutorials/signalr?view=aspnetcore-5.0&tabs=visual-studio

    So, there are couple of problems in this code. createLike method of PostsService should be only responsible to post invocation via existing connection. All other code responsible for connection start should be already executed until this moment. https://learn.microsoft.com/en-us/aspnet/core/signalr/javascript-client?view=aspnetcore-5.0#connect-to-a-hub

    So, if you are not familiar with reactive programming and rxjs, i would recommend you to add some method to your PostsService like ConnectHubs(): Promise to prepare your hub connections, before actually invoke some hub methods.

    connectHubs() { 
        this.hubConnection = new HubConnectionBuilder()
        .withUrl(this.hubUrl + 'like', { accessTokenFactory: () => user.token, 
        skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets })
        .build();
    
        return  this.hubConnection.start()
                   .then(() => {
                       this.hubConnection.on('ReceiveMessage', (numOfLikes, postId, userId) => {
                           // some logic to handle invocation
                       });
                   })
                   .catch(error => console.log(error)); 
    }
    
    setLike(userId: number, postId: number) {
         this.hubConnection.invoke('SetLike', userId, postId);
    }
    
    closeHubConnections() {
        this.hubConnection.stop();
    }
    

    And then in component that holds multiple posts, apart of requesting all posts from api, you will need to also invoke this connectHubs method and wait for this promise to show all the posts, to avoid likes being set before it is possible. In this case it will be better to also stop connection in ngOnDestroy to avoid unnecessary multiple active connections to the same hub from one client. Or you can just call this init method in you very base component, like app component, in this case you will not need to stop connection in ngOnDestroy, but you will need to make sure your user is logged in before connection established. Maybe you can find some component where it would be a rare case for it to be destroyed, but it will always be opened after login

    If you know rxjs you can add some BehaviorSubject field like

    private isConnectedSubject = new BehaviorSubject(false);
    isConnected$ = isConnectedSubject.asObservable();
    

    and then instead of returning a promise on connection start, you can just add something like isConnectedSubject.next(true); and in your disconnection method you can add isConnectedSubject.next(false); In your component you can just disable like button while hub isn't connected this way:

    <button [disabled]="!(postService.isConnected$ | async)" ...>
    

    To make your controls aware of changes in this hub, if you know RxJS, you can add some Subject field with its Observable field and post events every time new message is received. Or you can make it easier way with event emitter https://angular.io/api/core/EventEmitter , like below

    service:

    likeMessageReceive = new EventEmitter<{ numOfLikes, postId, userId }>();
    
    connectHubs() {
       ....
       this.hubConnection.on('ReceiveMessage', (numOfLikes, postId, userId) => {
         likeMessageReceive.emit({ numOfLikes, postId, userId })
         console.log(numOfLikes);
       })
       ....
    

    Post component:

    likesSubscription: Subscription;
    
    ngOnInit() {
        this.likesSubscription = this.postsService.likeMessageReceive.subscribe(result =>{
            if (result.postId === this.post.id) {
                this.post.likes.length = numOfLikes;
            }
        })
    }
    
    liked(post: Post) {
        const user: User = JSON.parse(localStorage.getItem('user'));
        this.postService.setLike(user.id, post.id);
    }
    
    ngOnDestroy() {
        if (this.likesSubscription) {
            this.likesSubscription.unsubscribe();
        }
    }
    

    With rxjs it would be pretty same, but you will use Subject instead of emitter, don't forget about unsubscribing to avoid unexpected behavior and leaks.