Azure SignalR Client – Group connection

SignalR Client

using Microsoft.AspNetCore.SignalR.Client;

using static OrderRequest.Core;

namespace SignalR.Support
{
    public enum Connection
    {
        IsDisconnected,
        IsConnecting,
        IsConnected,
    }

    public class ClientSignalR
    {
        static ClientSignalR _clientSignalR;
        public static ClientSignalR Instance 
        { 
            get
            {
                if (_clientSignalR == null)
                {
                    _clientSignalR = new ClientSignalR();
                }

                return _clientSignalR;
            }
        }

        private ClientSignalR() { }

        public event Action<object> OnMessageReceived;

        HubConnection _hub;

        public HubConnection Hub { get => _hub; }

        string _connectionUrl;

        public Connection Connection { get; private set; } = Connection.IsDisconnected;

        public string ConnectionUrl { get => _connectionUrl; }

        public async Task<(bool,string)> Connect(string connectionUrl, string groupId)
        {
            try
            {
                switch (Connection)
                {
                    case Connection.IsConnected    : return (true, "Already connected"); 
                    case Connection.IsConnecting   : return (true, "Already connecting");
                    case Connection.IsDisconnected :
                        {
                            Connection = Connection.IsConnecting;

                            _connectionUrl = connectionUrl;

                            _hub = new HubConnectionBuilder()
                                .WithUrl(_connectionUrl)
                                .Build();

                            await _hub.StartAsync();
                            await _hub.InvokeAsync("JoinGroup", groupId);

                            Connection = Connection.IsConnected;

                            return (true,"Connection succeeeded");
                        }
                    default: return (false, $"Unknown connection state");
                }
            }

            catch(Exception ex)
            {
                Debug.WriteLine($"Error: {nameof(ClientSignalR)}\n\n{ex.GetBaseException().Message}");
                return (false, $"{ex.GetBaseException().Message}");
            }
        }

        public void SubscribeHubMethod() =>

            _hub.On<object>("newMessage", (locationUpdate) => {
                OnMessageReceived?.Invoke(locationUpdate);
            });

        public async Task SendMessage(string groupId, SubjectLocation msg)
        {
            try
            {
                await _hub.InvokeAsync("SendToGroup", groupId, msg);
            }

            catch(Exception ex)
            {
                Debug.WriteLine($"Error: {nameof(ClientSignalR)}\n\n{ex.GetBaseException().Message}");
            }
        }

        public async Task CloseConnection() 
        {
            try
            {
                await _hub.DisposeAsync();
            }
            
            catch(Exception ex)
            {
                Debug.WriteLine($"Error: {nameof(ClientSignalR)}\n\n{ex.GetBaseException().Message}");
            }

            finally
            {
                Connection = Connection.IsDisconnected;
            }
        }
    }
}

Establishing a SignalR Connection

let hubClient  = ClientSignalR.Instance
...

    member x.TrackCourier() =
        
        async {

            match hubClient.Connection with
            | Connection.IsConnected    -> ()
            | Connection.IsConnecting   -> ()
            | Connection.IsDisconnected ->
        
                match! hubClient.Connect($"{locationTracking()}", requestId) |> Async.AwaitTask with
                | false, msg -> failwith msg
                | true , "Connection succeeeded" -> 

                    do! connected |> Post.trackingEnabled 
                                  |> Async.AwaitTask 
                                  |> Async.Ignore

                    hubClient.SubscribeHubMethod()
                    hubClient.add_OnMessageReceived (fun v -> onCourierLocationUpdate v)

                | true , _ -> ()

            | _ -> failwith "Unrecognized connection state"

        } |> Async.StartAsTask

Azure Function

using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;

using static AzureSignalR.LocationReporting.Language;

namespace AzureSignalR.Functions
{
    public class LocationHub : ServerlessHub
    {
        const string NewMessageTarget    = "newMessage";
        const string NewConnectionTarget = "newConnection";

        [FunctionName("negotiate")]
        public SignalRConnectionInfo Negotiate([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req)
        {
            return Negotiate(null, null, new System.TimeSpan(1,0,0));
        }

        [FunctionName(nameof(OnConnected))]
        public async Task OnConnected([SignalRTrigger] InvocationContext invocationContext, ILogger logger)
        {
            invocationContext.Headers.TryGetValue("Authorization", out var auth);
            await Clients.All.SendAsync(NewConnectionTarget, new NewConnection(invocationContext.ConnectionId, auth));
            logger.LogInformation($"{invocationContext.ConnectionId} has connected");
        }

        [FunctionAuthorize]
        [FunctionName(nameof(LocationUpdate))]
        public async Task LocationUpdate([SignalRTrigger] InvocationContext invocationContext, SubjectLocation update, ILogger logger)
        {
            await Clients.All.SendAsync(NewMessageTarget, new NewMessage(invocationContext, update));
            logger.LogInformation($"{invocationContext.ConnectionId} broadcast {update}");
        }

        [FunctionName(nameof(SendToGroup))]
        public async Task SendToGroup([SignalRTrigger] InvocationContext invocationContext, string groupName, SubjectLocation msg)
        {
            await Clients.Group(groupName).SendAsync(NewMessageTarget, new NewMessage(invocationContext, msg));
        }

        [FunctionName(nameof(SendToUser))]
        public async Task SendToUser([SignalRTrigger] InvocationContext invocationContext, string userName, SubjectLocation update)
        {
            await Clients.User(userName).SendAsync(NewMessageTarget, new NewMessage(invocationContext, update));
        }

        [FunctionName(nameof(SendToConnection))]
        public async Task SendToConnection([SignalRTrigger] InvocationContext invocationContext, string connectionId, SubjectLocation update)
        {
            await Clients.Client(connectionId).SendAsync(NewMessageTarget, new NewMessage(invocationContext, update));
        }

        [FunctionName(nameof(JoinGroup))]
        public async Task JoinGroup([SignalRTrigger] InvocationContext invocationContext, string groupName)
        {
            await Groups.AddToGroupAsync(invocationContext.ConnectionId, groupName);
        }

        [FunctionName(nameof(LeaveGroup))]
        public async Task LeaveGroup([SignalRTrigger] InvocationContext invocationContext, string connectionId, string groupName)
        {
            await Groups.RemoveFromGroupAsync(connectionId, groupName);
        }

        [FunctionName(nameof(JoinUserToGroup))]
        public async Task JoinUserToGroup([SignalRTrigger] InvocationContext invocationContext, string userName, string groupName)
        {
            await UserGroups.AddToGroupAsync(userName, groupName);
        }

        [FunctionName(nameof(LeaveUserFromGroup))]
        public async Task LeaveUserFromGroup([SignalRTrigger] InvocationContext invocationContext, string userName, string groupName)
        {
            await UserGroups.RemoveFromGroupAsync(userName, groupName);
        }

        [FunctionName(nameof(OnDisconnected))]
        public void OnDisconnected([SignalRTrigger] InvocationContext invocationContext)
        {
        }

        class NewConnection
        {
            public string ConnectionId { get; }

            public string Authentication { get; }

            public NewConnection(string connectionId, string authentication)
            {
                ConnectionId   = connectionId;
                Authentication = authentication;
            }
        }

        class NewMessage
        {
            public string ConnectionId    { get; }
            public string Sender          { get; }
            public SubjectLocation Update { get; }

            public NewMessage(InvocationContext invocationContext, SubjectLocation update)
            {
                Sender       = string.IsNullOrEmpty(invocationContext.UserId) ? string.Empty : invocationContext.UserId;
                ConnectionId = invocationContext.ConnectionId;
                Update       = update;
            }
        }
    }
}

Ngrok Tool

Command line

ngrok http PORT_NUMBER_GOES_HERE

Result

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s