I have a client application (Blazor, dual Server/Wasm project, .NET 9.0) that is connected to Azure Signal R and has a hub defined. Within the confines of this app, using a testing page, I can send and receive messages through SignalR.
This application uses Azure Signal R running in Default mode.
What I want to do is have another application send messages to the blazor client using Azure Signal R. In theory, that application should be able to be anything, but in practice it will be from an Azure Durable Function (Functions 4.0, .Net 9, Isolated Mode) that is a long running process which will send a message when completed.
For the purpose of this code/question, I'm using a simple test message just to verify connectivity.
My problem is that when I send messages via the external application, I get no error and yet the client receives no messages.
Client Setup
Program.cs
var signalRConnectionString = configuration.GetValue<string?>(ConfigurationConstants.ConfigurationKeys.SignalR.SignalRConnectionString);
services.AddSignalR(options =>
{
options.MaximumReceiveMessageSize = maximumReceiveMessageSize;
options.EnableDetailedErrors = true;
})
.AddAzureSignalR(options =>
{
options.ConnectionString = signalRConnectionString;
});
...
app.MapHub<MessagesHub>($"/{SignalRConstants.Hubs.Messages.HubName}");
MessagesHub
public interface IMessagesHub
{
Task ConnectedAsync();
Task DisconnectedAsync(Exception? exception = null);
Task ReceiveTestMessage(string name, string message);
}
public class MessagesHub : Hub<IMessagesHub>
{
public override async Task OnConnectedAsync()
{
await Clients.Caller.ConnectedAsync();
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
await Clients.Caller.DisconnectedAsync(exception);
await base.OnDisconnectedAsync(exception);
}
public async Task SendTestMessage(string name, string message)
{
Console.WriteLine($"### - [{Context.ConnectionId}] Send Test Message [{name}]: {message}");
await Clients.All.ReceiveTestMessage(name, message);
}
}
MessagesHubService
public interface IMessagesHubService : IAsyncDisposable
{
bool IsConnected { get; }
string ConnectionId { get; }
Task<bool> BeginMonitoringAsync(CancellationToken cancellationToken = default);
Task StopMonitoringAsync(CancellationToken cancellationToken = default);
Task SendTestMessage(string name, string message);
}
public class MessagesHubService(IConfigurationService configService,
NavigationManager navigationManager,
ISignalRHubConnectionFactory hubConnectionFactory,
IMessagesCallbackService callbackService,
ILoggerFactory loggerFactory)
: IMessagesHubService
{
private static HubConnection? _connection;
private readonly ILogger<MessagesHubService> _logger = loggerFactory.CreateLogger<MessagesHubService>();
public bool IsConnected => (_connection?.State ?? HubConnectionState.Disconnected) == HubConnectionState.Connected;
public string ConnectionId => _connection?.ConnectionId ?? "Not connected";
private async Task<HubConnection?> InitializeSignalRAsync()
{
if (_connection != null)
{
return _connection;
}
var hubRoute = SignalRConstants.Hubs.Messages.HubRoute;
if (await configService.IsFeatureFlagEnabledAsync(ConfigurationConstants.FeatureFlags.UseAzureSignalR))
{
hubRoute = $"/{SignalRConstants.Hubs.Messages.HubName}";
}
var hubConnectionUrl = navigationManager.ToAbsoluteUri(hubRoute);
_connection = await hubConnectionFactory.CreateHubConnectionAsync(hubConnectionUrl);
InitializeEvents(_connection);
return _connection;
}
private void InitializeEvents(HubConnection connection)
{
connection.On(SignalRConstants.Hubs.Messages.Methods.Connected, OnConnectedAsync);
connection.On<Exception?>(SignalRConstants.Hubs.Messages.Methods.Disconnected, OnDisconnectedAsync);
connection.On<string, string>(SignalRConstants.Hubs.Messages.Methods.TestMessage, TestMessageReceivedAsync);
}
private async Task OnConnectedAsync()
{
await callbackService.ConnectedAsync();
}
private async Task OnDisconnectedAsync(Exception? exception = null)
{
await callbackService.DisconnectedAsync(exception);
}
private async Task TestMessageReceivedAsync(string name, string message)
{
await callbackService.ReceiveTestMessageAsync(name, message);
}
public async Task<bool> BeginMonitoringAsync(CancellationToken cancellationToken = default)
{
var connection = await InitializeSignalRAsync();
if (connection == null)
{
return false;
}
if (connection.State == HubConnectionState.Connected)
{
return true;
}
if (connection.State == HubConnectionState.Disconnected)
{
try
{
await connection.StartAsync(cancellationToken);
return true;
}
catch (Exception e)
{
_logger.WriteException(e, "Error while starting Signal R Service");
}
}
return false;
}
public async Task StopMonitoringAsync(CancellationToken cancellationToken = default)
{
var connection = await InitializeSignalRAsync();
if (connection == null)
{
return;
}
if (connection.State != HubConnectionState.Disconnected)
{
await connection.StopAsync(cancellationToken);
}
}
public async Task SendTestMessage(string name, string message)
{
var connection = await InitializeSignalRAsync();
if (connection == null)
{
return;
}
if (connection.State != HubConnectionState.Connected)
{
await BeginMonitoringAsync();
}
await connection.InvokeAsync(SignalRConstants.Hubs.Messages.Methods.Testing.SendTestMessage, name, message);
}
public async ValueTask DisposeAsync()
{
if (_connection != null)
{
await _connection.DisposeAsync();
_connection = null;
}
}
}
SignalRHubConnectionFactory
public class SignalRHubConnectionFactory(IConfigurationService configService,
ILoggerFactory loggerFactory) : ISignalRHubConnectionFactory
{
private readonly ILogger<SignalRHubConnectionFactory> _logger = loggerFactory.CreateLogger<SignalRHubConnectionFactory>();
public async Task<HubConnection> CreateHubConnectionAsync(Uri hubRoute,
CancellationToken cancellationToken = default)
{
try
{
var ignoreCertificate = await configService.IsFeatureFlagEnabledAsync(ConfigurationConstants.FeatureFlags.IgnoreCertificateErrors);
var connection = new HubConnectionBuilder()
.WithUrl(hubRoute, options =>
{
//options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets;
options.HttpMessageHandlerFactory = (innerHandler) =>
{
if (ignoreCertificate && innerHandler is HttpClientHandler clientHandler)
{
// bypass SSL certificate
clientHandler.ServerCertificateCustomValidationCallback +=
(sender, certificate, chain, sslPolicyErrors) => { return true; };
}
return innerHandler;
//return new SignalRHubMessageHandler { InnerHandler = innerHandler };
};
})
.WithAutomaticReconnect()
.ConfigureLogging(config =>
{
config.SetMinimumLevel(LogLevel.Debug);
})
.Build();
return connection;
}
catch (Exception e)
{
_logger.WriteException(e, "Error during connection to SignalR Service");
throw;
}
}
}
MessagesCallbackService
public interface IMessagesCallbackService
{
event Func<Task>? OnConnectedAsync;
event Func<Exception?, Task>? OnDisconnectedAsync;
event Func<string, string, Task>? OnTestMessageReceivedAsync;
Task ReceiveTestMessageAsync(string name, string message);
Task ConnectedAsync();
Task DisconnectedAsync(Exception? exception = null);
}
public class MessagesCallbackService : IMessagesCallbackService
{
public event Func<string, string, Task>? OnTestMessageReceivedAsync;
public event Func<Task>? OnConnectedAsync;
public event Func<Exception?, Task>? OnDisconnectedAsync;
public async Task ConnectedAsync()
{
if (OnConnectedAsync != null)
{
await OnConnectedAsync.Invoke();
}
}
public async Task DisconnectedAsync(Exception? exception = null)
{
if (OnDisconnectedAsync != null)
{
await OnDisconnectedAsync.Invoke(exception);
}
}
public async Task ReceiveTestMessageAsync(string name, string message)
{
if (OnTestMessageReceivedAsync != null)
{
await OnTestMessageReceivedAsync.Invoke(name, message);
}
}
}
The MessagesHubService is injected to start the connection and send messages within the blazor app. The MessagesCallbackService is how I wire up events to receive messages and act on them. These pieces work within the confines of the blazor app where I can use a test page to end a message and have it received in a different component where it is acted on and the UI modified.
External Application (Functions App)
Program.cs
var signalRConnectionString = configuration.GetValue<string?>(ConfigurationConstants.ConfigurationKeys.SignalRConnectionString);
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
}).AddAzureSignalR(signalRConnectionString);
SignalRMessagesService
public class SignalRMessagesService(IConfigurationService configService,
ISignalRConnectionFactory connectionFactory,
ILoggerFactory loggerFactory) : ISignalRMessagesService
{
private readonly ILogger<SignalRMessagesService> _logger = loggerFactory.CreateLogger<SignalRMessagesService>();
private ServiceManager? _serviceManager;
private HubConnection? _connection;
private async Task<ServiceManager?> GetServiceManagerAsync()
{
if (_serviceManager != null)
{
return _serviceManager;
}
try
{
_serviceManager = await connectionFactory.GetAzureServiceManagerAsync();
return _serviceManager;
}
catch (Exception e)
{
_logger.WriteException(e, "Error building Service Manager");
throw;
}
}
public async Task SendTestMessageAsync(string name, string message, CancellationToken cancellationToken = default)
{
var serviceManager = await GetServiceManagerAsync();
if (serviceManager == null)
{
throw new ArgumentNullException(nameof(ServiceManager));
}
try
{
await using var serviceHubContext = await serviceManager.CreateHubContextAsync(SignalRConstants.Hubs.Messages.HubName, cancellationToken);
await serviceHubContext
.Clients
.All
.SendAsync(method:SignalRConstants.Hubs.Messages.Methods.TestMessage, arg1:name, arg2:message, cancellationToken: cancellationToken);
}
catch (Exception e)
{
_logger.WriteException(e, "Error sending Test Message");
throw;
}
}
public void Dispose()
{
_serviceManager?.Dispose();
}
public async ValueTask DisposeAsync()
{
}
}
SignalRConnectionFactory
public class SignalRConnectionFactory(IConfigurationService configService,
ILoggerFactory loggerFactory) : ISignalRConnectionFactory
{
private readonly ILogger<SignalRConnectionFactory> _logger = loggerFactory.CreateLogger<SignalRConnectionFactory>();
public Task<ServiceManager> GetAzureServiceManagerAsync()
{
var serviceManager = new ServiceManagerBuilder()
.WithOptions(option =>
{
option.ConnectionString = configService.GetConfigurationValue(ConfigurationConstants.ConfigurationKeys
.SignalRConnectionString);
option.UseJsonObjectSerializer(new JsonObjectSerializer(new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}));
})
.WithLoggerFactory(loggerFactory)
.BuildServiceManager();
return Task.FromResult(serviceManager);
}
}
Within this external application, I can send the message without error, however, the client application is not receiving the message.
What am I missing or doing wrong that messages aren't passing between applications through the same Azure Signal R connection?