Client – Configuring Connection Data
Here’s the URL and target id that the client uses for messaging using SignalR:
namespace MessagingClient { public static class Constants { public static string HostName { get; set; } = @"MY_AZURE_FUNCTION_URL"; public static string CourierId { get; set; } = "some_courier_id"; } }
Client – Establishing Connection
Here’s the client’s SignalR service implementation:
using System; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Specification; namespace MessagingClient { public class SignalRService { HttpClient client; public delegate void MessageReceivedHandler(object sender, CourierLocation message); public delegate void ConnectionHandler(object sender, bool successful, string message); public event MessageReceivedHandler MessageReceived; public event ConnectionHandler Connected; public event ConnectionHandler ConnectionFailed; public bool IsConnected { get; private set; } public bool IsBusy { get; private set; } public SignalRService() => client = new HttpClient(); public async Task SendAsync(CourierLocation location) { IsBusy = true; var json = JsonConvert.SerializeObject(location); var content = new StringContent(json, Encoding.UTF8, "application/json"); var result = await client.PostAsync($"{Constants.HostName}/api/locationfn", content); if (!result.IsSuccessStatusCode) { throw new Exception("Sending message failed"); } IsBusy = false; } public async Task ConnectAsync() { try { IsBusy = true; var negotiateJson = await client.GetStringAsync($"{Constants.HostName}/api/negotiate"); var negotiate = JsonConvert.DeserializeObject(negotiateJson); var connection = new HubConnectionBuilder() .AddNewtonsoftJsonProtocol() .WithUrl(negotiate.Url, options => options.AccessTokenProvider = async () => negotiate.AccessToken) .Build(); connection.Closed += Connection_Closed; connection.On(Constants.CourierId, OnIncomingMessage); await connection.StartAsync(); IsConnected = true; IsBusy = false; Connected?.Invoke(this, true, "Connection successful."); } catch (Exception ex) { ConnectionFailed?.Invoke(this, false, ex.Message); IsConnected = false; IsBusy = false; } } Task Connection_Closed(Exception arg) { ConnectionFailed?.Invoke(this, false, arg.Message); IsConnected = false; IsBusy = false; return Task.CompletedTask; } void OnIncomingMessage(JObject message) { var courierId = message.GetValue("CourierId").ToString(); var location = message.SelectToken("Location"); var latitude = double.Parse (location.SelectToken("Latitude").ToString()); var longitude = double.Parse (location.SelectToken("Longitude").ToString()); var courierLocation = new CourierLocation(courierId, new Coordinate(latitude,longitude)); MessageReceived?.Invoke(this, courierLocation); } } }
The ConnectAsync method above registers the identifier that the SignalR Hub (on the server) will route messages to. In this case, the identifier is the courier id.
Here’s the line:
connection.On(Constants.CourierId, OnIncomingMessage);
Azure Function – Connection
Here’s the Azure Function for negotiating a SignalR connection:
using Microsoft.AspNetCore.Http; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Azure.WebJobs.Extensions.SignalRService; namespace MessagingServer { public static class Negotiate { [FunctionName("Negotiate")] public static SignalRConnectionInfo GetSignalRInfo( [HttpTrigger(AuthorizationLevel.Anonymous,"get",Route = "negotiate")] HttpRequest req, [SignalRConnectionInfo(HubName = "LocationHub")] SignalRConnectionInfo connectionInfo) { return connectionInfo; } } }
Azure Function – Location
Here’s the Azure Function that first receives and then forwards a message to a SignalR client:
using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Azure.WebJobs.Extensions.SignalRService; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Specification; namespace MessagingServer { public static class Location { [FunctionName(nameof(Location))] public static async Task Run( [HttpTrigger( AuthorizationLevel.Anonymous, "post", Route = "locationfn")] HttpRequest req, [SignalR(HubName = "LocationHub")] IAsyncCollector questionR, ILogger log) { log.LogInformation($"{nameof(Location)} has been invoked."); try { var json = await new StreamReader(req.Body).ReadToEndAsync(); var courierLocation = JsonConvert.DeserializeObject(json); await questionR.AddAsync( new SignalRMessage { Target = courierLocation.CourierId, Arguments = new[] { courierLocation } }); var location = courierLocation.Location; var latitude = location.Latitude; var longitude = location.Longitude; var message = $"Hello {courierLocation.CourierId}, your location was '({latitude},{longitude})'"; log.LogInformation($"{nameof(Location)} returned: {message}"); return new OkObjectResult(message); } catch (Exception ex) { return new BadRequestObjectResult("There was an error: " + ex.Message); } } } }
The code above uses the target id to route messages. In this case, the target is the courier-id. The courier-id registered on the server will be matched with the courier-id on the client app when establishing the connection.
Here’s the snippet from the Azure Function above:
await questionR.AddAsync( new SignalRMessage { Target = courierLocation.CourierId, Arguments = new[] { courierLocation } });
Automated Test – Sending a Location Message
Here’s the test that establishes a connection and then sends a location for another client to receive:
module _SignalR open Xunit open Specification.Core.DataTransfer open SignalR.Support let onConnectionChanged (_,_) = () let onMessageReceived _ = () let someCoordinate = { Latitude=30.0; Longitude=50.0 } let someCourierLocation = { CourierId= "some_courier_id"; Location= someCoordinate } [Fact] let ``Courier publishes location``() = // Test async { // Setup let signalR = new SignalRService(); signalR.Connected .Add onConnectionChanged signalR.ConnectionFailed .Add onConnectionChanged signalR.MessageReceived .Add onMessageReceived do! signalR.ConnectAsync() |> Async.AwaitTask // Test do! signalR.SendAsync(someCourierLocation) |> Async.AwaitTask } |> Async.RunSynchronously
Appendix
namespace Specification open System.Runtime.Serialization [DataContract] type Coordinate = { [field: DataMember(Name="Latitude")] Latitude : double [field: DataMember(Name="Longitude")] Longitude : double } [DataContract] type CourierLocation = { [field: DataMember(Name="CourierId")] CourierId : string [field: DataMember(Name="Location")] Location : Coordinate }