MassTransit 5.2, SignalR: How can i get the IHubContext inside my Consumer? - asp.net-core

My main problem is to get the right instance of the SignalR hub.
Context: Im building a webapplication which communicates with a couple of external systems. CRUD operations in my application result in updating the databases of the external systems.
In this example i have 3 services running:
ExternalSystem | StateMachine | .NET CORE WebAPI
When i post the 'create employee' form, a RabbitMQ message will be sent from the WebAPI to the statemachine. The statemachine then sends a couple of create messages to my external system service which updates the database. Thereafter, it updates the statemachine to keep track of the createoperation.
Form -> API -> StateMachine -> ExternalSystem -> StateMachine -> API
So far so good. Now i would like to use SignalR to send the status updates to the client. So i've implemented this consumer in the API:
public class UpdatesConsumer :
IConsumer<IExternalSystemUpdateMessage>
{
private readonly IHubContext<UpdatesHub> _updaterHubContext;
public UpdatesConsumer(IHubContext<UpdatesHub> hubContext)
{
_updaterHubContext = hubContext;
}
public Task Consume(ConsumeContext<IExternalSystemUpdateMessage> context)
{
//return _updaterHubContext.Clients.Group(context.Message.CorrelationId.ToString()).SendAsync("SEND_UPDATE", context.Message.Message);// this.SendUpdate(context.Message.CorrelationId, context.Message.Message);
return _updaterHubContext.Clients.All.SendAsync("SEND_UPDATE", context.Message.Message);
}
}
This is my SignalR hub:
public class UpdatesHub :
Hub
{
public Task SendUpdate(Guid correlationId, string message)
{
return Clients.Group(correlationId.ToString()).SendAsync("SEND_UPDATE", message);
}
}
And this is how the Bus and consumer is instantiated:
public void ConfigureServices(IServiceCollection services)
{
_services = services;
services.AddMvc();
services.AddSignalR();
//services.AddSingleton<IHubContext<UpdatesHub>>();
WebAPI.CreateBus();
}
public static IServiceCollection _services;
static IBusControl _busControl;
public static IBusControl Bus
{
get
{
return _busControl;
}
}
public static void CreateBus()
{
IRMQConnection rmqSettings = Config.GetRMQConnectionConfig("rmq-settings.json", "connection");
_busControl = MassTransit.Bus.Factory.CreateUsingRabbitMq(x =>
{
var host = x.Host(BusInitializer.GetUri("", rmqSettings), h =>
{
h.Username(rmqSettings.UserName);
h.Password(rmqSettings.Password);
});
x.ReceiveEndpoint(host, "externalsystems.update",
e => { e.Consumer(() => new UpdatesConsumer((IHubContext<UpdatesHub>)Startup.__serviceProvider.GetService(typeof(IHubContext<UpdatesHub>)))); });
});
TaskUtil.Await(() => _busControl.StartAsync());
}
=========================================================================
So the problem is that _updaterHubContext.Clients in my Consumer class, always turn out to be empty. I've tested accessing the hub in a controller, and the clients do show up:
public class TestController : Controller
{
private readonly IHubContext<UpdatesHub> _hubContext;
public TestController(IHubContext<UpdatesHub> hubContext)
{
_hubContext = hubContext;
}
[HttpGet]
[Route("api/Test/")]
public IActionResult Index()
{
return View();
}
}
How can i get the right instance of the hub in my Consumer class? Or how can i access the IServiceCollection that .net is using?
Thnx in advance!

You can register your consumer so that MassTransit will resolve it from the IServiceProvider using the support provided in the MassTransit.Extensions.DependencyInjection package.
x.ReceiveEndpoint(host, "externalsystems.update", e =>
{
e.Consumer<UpdatesConsumer>(_serviceProvider);
});
Be sure to register your UpdatesConsumer in the container as well. This should resolve a new instance of the consumer for each message received on the endpoint.

Why not register Bus using Microsoft Dependency Injection. It should fix your issue, it will Resolve your consumer using IServiceProvider

Related

ASP.NET Core SignalR Adding service to hub breaks

I'm currently working on an ASP.NET Core Web Application.
I have a MQTT Server, which is connected to a service (IHostedService) and this service references a SignalR Hub.
So if there is a new message comming from the MQTT Server, it is forwarded to the hub and therefore to the client.
This works fine. But now I would like to add a button to send MQTT messages back to the MQTT server.
To do so, I added a function in the hub, which es called by the button via SignalR.
So far so good but when adding the service now to the constructor of the hub it fails, when I open the web app (not during startup), with the following message:
fail: Microsoft.AspNetCore.SignalR.HubConnectionHandler[1]
Error when dispatching 'OnConnectedAsync' on hub.
System.InvalidOperationException: Unable to resolve service for type 'websiteApp.HostedServices.UserPromptService' while attempting to activate 'websiteApp.Hubs.UserPromptHub'.
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
at lambda_method(Closure , IServiceProvider , Object[] )
at Microsoft.AspNetCore.SignalR.Internal.DefaultHubActivator'1.Create()
at Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher'1.OnConnectedAsync(HubConnectionContext connection)
at Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher'1.OnConnectedAsync(HubConnectionContext connection)
at Microsoft.AspNetCore.SignalR.HubConnectionHandler'1.RunHubAsync(HubConnectionContext connection)
The service declaration looks like this:
public class UserPromptService : IHostedService, IDisposable
{
public UserPromptService(ILogger<UserPromptService> logger, IConfiguration config, UserPromptContext userPromptContext, IHubContext<UserPromptHub> userPromptHub)
{
}
}
And my hub looks like this:
public class UserPromptHub : Hub<IUserPromptHub>
{
public UserPromptHub(UserPromptService service) // everything works until I add the service here
{
service.ToString(); // just for testing
}
}
And they are configured in the Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapHub<Hubs.UserPromptHub>("/userPromptHub");
});
}
As well as in the Program.cs:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
// ...
.ConfigureServices(services =>
{
services.AddSingleton<websiteApp.DataContext.UserPromptContext>();
services.AddHostedService<HostedServices.UserPromptService>();
});
Could you please help me to fix the problem?
One option to solve your problem would be to restructure your code a little bit.So instead of your UserPromptService be responsable for the MQTT connection you create a seperate class for that.
The following is only sudo code
You could create a new class
public class MQTTConnection
{
private readonly _yourMQTTServerConnection;
public MQTTConnection()
{
_yourMQTTServerConnection = new ServerConnection(connectionstring etc);
}
public Task<Message> GetMessage()
{
return _yourMQTTServerConnection.GetMessageAsync();
}
public Task SendMessage(Message message)
{
return _yourMQTTServerConnection.SendMessageAsync(message);
}
}
So your Hub look something like this
public class UserPromptHub : Hub<IUserPromptHub>
{
private readonly MQTTConnection _connection;
public UserPromptHub(MQTTConnection connection)
{
_connection = connection
}
public async Task MessageYouReceiveFromTheUser(object object)
{
// your business logic
await _connection.SendMessage(message);
}
public Task MessageYouSendToTheClient(object object)
{
await Clients.All.MessageYouSendToTheClient(message);
}
}
And your UserPromptService looks somehting like that
public class UserPromptService : IHostedService, IDisposable
{
public UserPromptService(ILogger<UserPromptService> logger,
IConfiguration config,
UserPromptContext userPromptContext,
IHubContext<UserPromptHub> userPromptHub,
MQTTConnection connection)
{
// map arguments to private fields
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while(yourAppIsUpAndRunning)
{
var message = await _connection.GetMessage()
// process the message
await _hub.MessageYouSendToTheClient(yourMessage)
}
}
}
I hope my approach is understandable, if not I can add more details.

Error accessing a service that uses DbContext on my quartz job

I am making a web API using ASP.NET Core and now I am having a problem with quartz scheduled jobs. The jobs I have will access my services to update the database. After some researches, I figured how to do the dependency injection so that my jobs can access the services, here is how I overrode the job factory:
public class AspNetCoreJobFactory : SimpleJobFactory
{
IServiceProvider _provider;
public AspNetCoreJobFactory(IServiceProvider provider)
{
_provider = provider;
}
public override IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
try
{
return (IJob)this._provider.GetService(bundle.JobDetail.JobType);
}
catch(Exception e)
{
throw new SchedulerException(string.Format("Problem while instantiating job '{0}' from the AspNet Core IOC.", bundle.JobDetail.Key), e);
}
}
}
and I added this line on my startup configure:
_quartzScheduler.JobFactory = new AspNetCoreJobFactory(app.ApplicationServices);
Lastly I added those two lines on my ConfigureServices method:
services.AddSingleton<IUserService, UserService>();
services.AddTransient<BatchJobCheckContract>();
right now I am getting this exception when trying to execute the job, it seems like it's because my service uses the DbContext, how can I solve this?
Cannot consume scoped service 'RHP.data.RHPDbContext' from singleton
'RHP.data.IServices.Administration.IUserService'.
After playing around with Quartz (version 3.2.3), it looks like you do not have to write your own JobFactory to use Microsoft DI. (See ASP.NET Core Integration and Microsoft DI Integration):
Add the Quartz.AspNetCore nuget package and you can scoped services like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IScopedService, ScopedService>();
// Job has scoped dependencies, so it must be scoped as well
services.AddScoped<Job>();
services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionScopedJobFactory();
var jobKey = new JobKey("job");
q.AddJob<Job>(jobKey);
q.AddTrigger(t => /* ... */));
});
services.AddQuartzServer(opts => opts.WaitForJobsToComplete = true);
}
However, if you cannot use the current version of Quartz.AspNetCore, you could still
use IServiceProvider as dependency in your Job class and resolve services there:
public class Job : IJob
{
private readonly IServiceProvider _serviceProvider;
public Job(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task Execute(IJobExecutionContext context)
{
using var scope = _serviceProvider.CreateScope();
var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
// ...
}
}
Like this the Job class controls the lifetime of scoped services.
I've previously had a similar problem with background tasks, you might need to create a scope.
I've adapted this code and applied it to your use case.
public class AspNetCoreJobFactory : SimpleJobFactory
{
IServiceProvider _provider;
public AspNetCoreJobFactory(IServiceProvider provider)
{
_provider = provider;
}
public override IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
try
{
using(var serviceScope = _provider.CreateScope())
{
var services = serviceScope.ServiceProvider.
return (IJob)services.GetService(bundle.JobDetail.JobType);
}
}
catch(Exception e)
{
throw new SchedulerException(string.Format("Problem while instantiating job '{0}' from the AspNet Core IOC.", bundle.JobDetail.Key), e);
}
}
}

Unable to resolve service for type 'SignalR.XXXX' while attempting to activate 'YYYYAPIController'

I have two services.
services.AddControllers();
and
services.AddSignalR()
.AddHubOptions<OutputMessages>(options =>
{
options.EnableDetailedErrors = true;
});
services.AddScoped<IOutputMessages, OutputMessages>();
second services is also confured:
app.UseEndpoints(endpoints =>
endpoints.MapHub<OutputMessages>("/OutputMessages", options =>
{
options.Transports =
HttpTransportType.WebSockets |
HttpTransportType.LongPolling;
});
I have interface for my SignalR service:
public interface ISignalRHandler
{
public void RestartProcessor(Guid containerId, string userId, string message);
....
}
and I inherits this interface and SignalR interface in my SignalR service realization
public class OutputMessages : Hub, IOutputMessages
{
public OutputMessages(IHubContext<OutputHub> hubContext, ILogger<OutputMessages> logger, ApplicationDbContext dbContext)
{
_hubContext = hubContext;
_db = dbContext;
_logger = logger;
}
public void SendUserMessage(string discordId, Guid containerId, string message)
....
}
I try to inject my SignalR service to controller service:
public class ApplicationAPIController : ControllerBase
{
public ApplicationAPIController(ILogger<ApplicationAPIController> logger, ApplicationDbContext dbContext, IConfiguration Configuration, CoreObjectDumper.CoreObjectDumper dump, OutputMessages _outputMessages)
{
But receive error message
Unable to resolve service for type 'SignalR.OutputMessages' while attempting to activate 'ApplicationAPIController'.
How is possible to solver this problem?
You don't need to add the OutputMessages as transient because you already doing that when you map your hub and add/use signalr.
app.UseSignalR(routes =>
{
routes.MapHub<OutputMessages>("/OutputMessages");
});
Then when you injecting to controller, it is recommended to inject the HubContext, and not the hub itself.
Example:
private IHubContext<NotificationsHub, INotificationsHub> NotificationsHub
{
get
{
return this.serviceProvider.GetRequiredService<IHubContext<NotificationsHub, INotificationsHub>>();
}
}
or in your case:
public ApplicationAPIController(ILogger<ApplicationAPIController> logger, ApplicationDbContext dbContext, IConfiguration Configuration, CoreObjectDumper.CoreObjectDumper dump, IHubContext<OutputMessages, IOutputMessages> _outputMessages)
You generally shouldn't resolve the Hub out of DI. If you need to share code between your Hub and some other component, I'd suggest using either IHubContext or putting the shared code in a separate DI service instead.

How to get the current ClaimsPrincipal in SignalR core outside of a Hub

I have a SignalR Core hub which has a dependency on a service. That service itself has it's own dependencies and one of them requires access to the current ClaimsPrincipal.
I know, that I can access the ClaimsPrincipal inside the hub using the Context.User property and pass it as a parameter to the service, which can also pass it as a parameter and so on. But I really don't like to pollute the service API by passing this kind of ambient info as a parameter.
I've tried to use the IHttpContextAccessor as described in: https://learn.microsoft.com/en-us/aspnet/core/migration/claimsprincipal-current?view=aspnetcore-2.2
This seems to be working with a simple SignalR setup, but it isn't working with the Azure SignalR service, which will be our production setup.
Is there a reliable way how to get the ClaimsPrincipal outside of the hub that will work for both a simple local setup and Azure SignalR service?
In the current SignalR version (1.1.0) there is no support for this. I've created a feature request: https://github.com/dotnet/aspnetcore/issues/18657 but it was rejected. Eventually, I've ended up doing it like this:
services.AddSingleton(typeof(HubDispatcher<>), typeof(HttpContextSettingHubDispatcher<>));
public class HttpContextSettingHubDispatcher<THub> : DefaultHubDispatcher<THub> where THub : Hub
{
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpContextSettingHubDispatcher(IServiceScopeFactory serviceScopeFactory, IHubContext<THub> hubContext,
IOptions<HubOptions<THub>> hubOptions, IOptions<HubOptions> globalHubOptions,
ILogger<DefaultHubDispatcher<THub>> logger, IHttpContextAccessor httpContextAccessor) :
base(serviceScopeFactory, hubContext, hubOptions, globalHubOptions, logger)
{
_httpContextAccessor = httpContextAccessor;
}
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await InvokeWithContext(connection, () => base.OnConnectedAsync(connection));
}
public override async Task OnDisconnectedAsync(HubConnectionContext connection, Exception exception)
{
await InvokeWithContext(connection, () => base.OnDisconnectedAsync(connection, exception));
}
public override async Task DispatchMessageAsync(HubConnectionContext connection, HubMessage hubMessage)
{
switch (hubMessage)
{
case InvocationMessage _:
case StreamInvocationMessage _:
await InvokeWithContext(connection, () => base.DispatchMessageAsync(connection, hubMessage));
break;
default:
await base.DispatchMessageAsync(connection, hubMessage);
break;
}
}
private async Task InvokeWithContext(HubConnectionContext connection, Func<Task> action)
{
var cleanup = false;
if (_httpContextAccessor.HttpContext == null)
{
_httpContextAccessor.HttpContext = connection.GetHttpContext();
cleanup = true;
}
await action();
if (cleanup)
{
_httpContextAccessor.HttpContext = null;
}
}
}

NServiceBus Dependency Injection not working for Saga Timeouts

I am having problems getting NServiceBus 4.6.1 dependency injection working with Saga timeouts. I am using self-hosting in an ASP.NET web application and have property injection setup. It works when messages are sent from web controllers however, when a Timeout message is handled in the saga the same DI property is not being set and is null.
Here are the key bits of the setup:
Global.asax.cs
public class MvcApplication : System.Web.HttpApplication
{
public static IWindsorContainer Container { get; private set; }
protected void Application_Start()
{
ConfigureIoC();
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
DeviceManagerDbInitializer.Instance.InitializeDatabase();
ConfigureNServiceBus();
}
protected void Application_End()
{
if (Container != null)
{
Container.Dispose();
}
}
private static void ConfigureIoC()
{
Container = new WindsorContainer()
.Install(FromAssembly.This());
var controllerFactory = new WindsorControllerFactory(Container.Kernel);
ControllerBuilder.Current.SetControllerFactory(controllerFactory);
GlobalConfiguration.Configuration.DependencyResolver
= new WindsorDependencyResolver(Container);
}
private void ConfigureNServiceBus()
{
Configure.ScaleOut(s => s.UseSingleBrokerQueue());
Configure.Instance.PeekInterval(500);
Configure.Instance.MaximumWaitTimeWhenIdle(2000);
Feature.Enable<TimeoutManager>();
Feature.Enable<Sagas>();
IStartableBus startableBus = Configure.With()
.DefineEndpointName("MyQueue")
.CastleWindsorBuilder(Container) //using NServiceBus CastleWindsor 4.6.1
.UseTransport<AzureStorageQueue>()
.UseAzureTimeoutPersister()
.AzureSagaPersister()
.PurgeOnStartup(false)
.UnicastBus()
.LoadMessageHandlers()
.RunHandlersUnderIncomingPrincipal(false)
.Log4Net(new DebugAppender { Threshold = Level.Warn })
.RijndaelEncryptionService()
.CreateBus();
Configure.Instance.ForInstallationOn<Windows>().Install();
startableBus.Start();
}
}
Saga class
public class MySaga: Saga<MySagaData>,
IAmStartedByMessages<StartMySagaCommand>,
IHandleMessages<SomeMessage>,
IHandleTimeouts<SomeTimeout>
{
public DependentService MyInjectedService {get; set;}
public override void ConfigureHowToFindSaga()
{
ConfigureMapping<StartMySagaCommand>( message => message.MyId).ToSaga( saga => saga.MyId );
ConfigureMapping<SomeMessage>( message => message.MyId).ToSaga( saga => saga.MyId );
ConfigureMapping<SomeTimeout>( message => message.MyId).ToSaga( saga => saga.MyId );
}
public void Handle(SomeMessage message)
{
// Here MyInjectedService is fine
MyInjectedService.DoSomething(message);
}
public void Timeout(SomeTimeout state)
{
// Here MyInjectedService is always null
MyInjectedService.DoSomething(state);
}
}
I have tried solutions found here, here and here but none of them fixed the issue.
I figured out the problem here. Dependency injection was not working in the Saga's timeout handler because the Castle.Windsor lifestyle was set to LifestylePerWebRequest, e.g.:
public class WindsorServicesInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register( Component.For<DependentService>()
.LifestylePerWebRequest() );
}
}
After changing the Lifestyle to LifeStyleTransient it started working. Any of the other 'non web request' lifestyles should work as well here.
In our setup the NServiceBus host is running under the web application and the regular message handlers were fine because they are being called in a controller action, e.g.:
[HttpPost]
public ActionResult DoSomething( int myId)
{
_bus.Send( "MyBus", new SomeMessage { MyId = something.MyId } );
return View();
}
When the saga handles the SomeMessage message for this it must still be part of the web request and Windsor resolves the dependency as normal. However, the timeouts are fired some time later (in this case five minutes) are they are not part of a web request. Windsor is not able to resolve the DependentService object and it stays null.