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
