I have an Asp.Net Core Api 3.1 and an Azure WebJob, both running on an Azure App Service. Both of them have a need to send notifications and neither will be receiving messages. To break it down:
I have a single instance of the Azure SignalR Service in the cloud that each app is pointed to.
The Hub class I've created is in a library that is referenced by both the Api and WebJob projects.
Clients will only connect to the Hub via the Api.
Everything is working fine when the Api sends a message to connections, but the WebJob does not.
I really don't want to run the WebJob as a client because then I'd have to deal with auth. I just want it to run as another instance of the same Hub that will send messages to the same groups that the API does. Also, this WebJob is NOT a candidate to run as an Azure Function because it takes too long.
I'm missing something in how I'm configuring the WebJob as it appears it's not connecting to Azure SignalR.
When the WebJob tries to send out a message, I get the following error: (I've not published yet to Azure so this is all happening on my local machine)
Microsoft.Azure.SignalR.ServiceLifetimeManager[100]
Failed to send message (null).
Microsoft.Azure.SignalR.Common.AzureSignalRNotConnectedException: Azure SignalR Service is not connected yet, please try again later.
at Microsoft.Azure.SignalR.ServiceConnectionManager1.WriteAsync(ServiceMessage serviceMessage) at Microsoft.Azure.SignalR.ServiceLifetimeManagerBase
1.<>c__DisplayClass22_01.<WriteAsync>b__0(T m) at Microsoft.Azure.SignalR.ServiceLifetimeManagerBase
1.WriteCoreAsync[T](T message, Func`2 task)
WebJob Main:
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MyProject.Data;
using MyProject.Hub;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.Storage;
using Microsoft.Azure.Storage.Blob;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureKeyVault;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public static IConfigurationRoot Configuration { get; protected set; }
static async Task Main(string[] args)
{
string buildConfig = "Development";
var builder = new HostBuilder()
.ConfigureWebJobs(b =>
{
b.AddAzureStorageCoreServices();
b.AddTimers();
})
.ConfigureAppConfiguration(config =>
{
config.AddJsonFile($"appsettings.{buildConfig}.json", true, true);
Configuration = config.Build();
})
.ConfigureServices((hostContext, services) =>
{
services.AddSignalR().AddAzureSignalR(options =>
{
options.ConnectionString = Configuration["Azure:SignalR:ConnectionString"];
});
services.AddHostedService<ApplicationHostService>();
services.AddDbContext<MyDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("MyDatabase")));
services.AddScoped<MyDbContext>();
**// The NotificationService expects an injected MyDbContext and IHubContext<NotificationHub>**
services.AddScoped<INotificationService, NotificationService>();
})
.ConfigureLogging((context, b) =>
{
b.AddConsole();
});
var host = builder.Build();
using (host)
{
host.Run();
}
}
}
ApplicationHostService:
public class ApplicationHostService : IHostedService
{
readonly ILogger<ApplicationHostService> _logger;
readonly IConfiguration _configuration;
readonly IHostingEnvironment _hostingEnvironment;
public ApplicationHostService(
ILogger<ApplicationHostService> logger,
IConfiguration configuration,
IHostingEnvironment hostingEnvironment
)
{
_logger = logger;
_configuration = configuration;
_hostingEnvironment = hostingEnvironment;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await Task.CompletedTask;
_logger.LogWarning("Application Host Service started.....");
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogWarning("Application Host Service stopped.....");
await Task.CompletedTask;
}
}
WebJob trigger code:
public class MyImportTrigger
{
private INotificationService _notificationService;
public MyImportTrigger(INotificationService notificationService)
{
_notificationService = notificationService;
}
public async Task Run([TimerTrigger("0 */1 * * * *" )] TimerInfo myTimer, ILogger log)
{
....bunch of non-relevant removed code for brevity....
await _notificationService.CreateFundImportNotificationAsync(upload);
....bunch of non-relevant removed code for brevity....
}
}
NotificationService:
using ProjectName.Auth;
using ProjectName.Data;
using ProjectName.Utils;
using Microsoft.AspNetCore.SignalR;
using SignalR.Mvc;
using System;
using System.ComponentModel;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
namespace ProjectName.Hub
{
public interface INotificationService
{
FundPublishNotification CreateFundPublishNotification(short quarter, short year, string userName);
}
public class NotificationService : INotificationService
{
MyDbContext _context;
IHubContext<NotificationHub> _hubContext;
public NotificationService(MyDbContext context, IHubContext<NotificationHub> hubContext)
{
_context = context;
_hubContext = hubContext;
}
public FundPublishNotification CreateFundPublishNotification(short quarter, short year, string userName)
{
*** removed: do some processing and db persistence to create an object called "notif" ***
**** THIS IS WHERE IT BOMBS WHEN CALLED FROM WEBJOB, BUT DOESN'T WHEN CALLED FROM API******
** the roles value is retrieved from the db and isn't dependent on a current user **
_hubContext.Clients.Groups(roles).SendAsync("NewNotification", notif);
return notif;
}
}
}
The Hub class:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using MyProject.Auth;
namespace SignalR.Mvc
{
[Authorize]
public class NotificationHub : Hub
{
public NotificationHub()
{ }
public override async Task OnConnectedAsync()
{
await AddConnectionToGroups();
await base.OnConnectedAsync();
}
public async Task AddConnectionToGroups()
{
var roles = Context.User.Roles();
foreach (RoleValues role in roles)
{
await Groups.AddToGroupAsync(Context.ConnectionId, role.ToString());
}
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await RemoveConnectionToGroups();
await base.OnDisconnectedAsync(exception);
}
public async Task RemoveConnectionToGroups()
{
var roles = Context.User.Roles();
foreach (RoleValues role in roles)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, role.ToString());
}
}
}
}
appsettings.json:
{
"Azure": {
"SignalR": {
"ConnectionString": "Endpoint=https://myproject-signalr-dev.service.signalr.net;AccessKey=removed-value-before-posting;Version=1.0;"
}
}
}
Package Versions:
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="4.1.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.6.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="Microsoft.Azure.SignalR" Version="1.6.0" />
<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.6.0" />
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.23" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="4.0.1" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.2" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.SignalRService" Version="1.2.2" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="3.1.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.8" />
Well, I figured it out so posting the relevant parts of the solution here.
Using the ServiceManagerBuilder available via the following package:
using Microsoft.Azure.SignalR.Management;
Run task:
public class ImportTrigger
{
private INotificationService _notificationService;
private IConfiguration _config;
private ILogger<ImportTrigger> _logger;
public ImportTrigger(INotificationService notificationService, IConfiguration configuration, ILoggerFactory loggerFactory)
{
_notificationService = notificationService;
_config = configuration;
_logger = loggerFactory.CreateLogger<ImportTrigger>();
}
public async Task Run([TimerTrigger("%CRONTIME%" )] TimerInfo myTimer)
{
... bunch of removed code for brevity ...
try
{
var (importNotif, roles) = _notificationService.CreateFundImportNotificationAsync(upload);
using (var hubServiceManager = new ServiceManagerBuilder().WithOptions(option =>
{
option.ConnectionString = _config["Azure:SignalR:ConnectionString"];
option.ServiceTransportType = ServiceTransportType.Persistent;
}).Build())
{
var hubContext = await hubServiceManager.CreateHubContextAsync("NotificationHub");
await hubContext.Clients.Groups(roles.Select(r => r.ToString()).ToImmutableList<string>()).SendAsync("NewNotification", importNotif.ToModel());
}
}
catch { }
... bunch of removed code for brevity ...
}