ASP.NET Core SignalR Adding service to hub breaks - asp.net-core

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.

Related

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.

Set up logging with Blazor WebAssembly

I'm doing some experiments with Blazor and want to set up logging. I see that Blazor logs to Microsoft.Extensions.Logging out of the box and that the log messages go to the developer console inside the browser. That is a nice start.
Now I want to try and log messages to other destinations as well. It could be a cloud-service. I'm wondering where to set that up. In ASP.NET Core, you would set it up using the ConfigureLogging method in Program.cs. But this isn't available with Blazor:
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
BlazorWebAssemblyHost.CreateDefaultBuilder()
.UseBlazorStartup<Startup>()
.ConfigureLogging(...); // <- compile error
As a fallback, I'm trying to set it up through ConfigureServices in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(builder => builder
.AddMyLogger()
.SetMinimumLevel(LogLevel.Information));
}
with AddMyLogger:
public static ILoggingBuilder AddMyLogger(this ILoggingBuilder builder)
{
builder.Services.AddSingleton<ILoggerProvider, MyLoggerProvider>();
return builder;
}
and MyLoggerProvider:
public class MyLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new MyLogger();
}
public void Dispose()
{
}
}
and MyLogger:
public class MyLogger : ILogger
{
public MyLogger()
{
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
}
}
The AddMyLogger-method is called but my logger is never created or receives any Log-calls.
Am I doing something wrong here or is logging with Blazor WebAssembly simply not ready yet?
I was trying something similar. In my case, the Log method in MyLogger gets called; however it fails at following line of code
using (var streamWriter = new StreamWriter(fullFilePath, true)) //Fails here
{
streamWriter.WriteLine(logRecord);
}
When I put it in try catch block, I got the exception "Children could not be evaluated".
While researching I came across following link. Steve Sanderson's response might make sense of the behavior
Reading local files #16156
BTW It's been a long time, please let me know the solution you came up with.

Asp Net Core SQL Custom Configuration Provider with Dapper and Error Handling

I am setting up my MVC web application to pull configuration data from my SQL Azure database on startup. I have used these two articles (Microsoft, Medium) to guide me but neither include error handling and I want to avoid any Entity Framework references as i'm using Dapper. So far I've got it working with below code but I'm not sure how to handle errors in this scenario. For instance if I remove the try/catch from the Load method in SQLConfigurationProvider then the app crashes on startup but if I include the try/catch then the error is handled and the app starts normally but no config data is available so will eventually break when trying to access a config value. What is the best way to handle these errors gracefully (ie app still loads but displays an error page/message instead)? Also is there any benefit to having the SQLConfigurationSource or would it make more sense just to create the new SqlConnection instance inside SQLConfigurationProvider instead?
Program.cs
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
.UseSetting(WebHostDefaults.DetailedErrorsKey, "true")
.UseApplicationInsights()
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddSQLConfiguration(); // Custom configuration here
})
.UseStartup<Startup>();
}
ConfigurationExtensions.cs
public static class ConfigurationExtensions
{
public static IConfigurationBuilder AddSQLConfiguration(this IConfigurationBuilder builder)
{
var connectionString = builder.Build().GetConnectionString("DefaultConnection");
return builder.Add(new SQLConfigurationSource(connectionString));
}
}
SQLConfigurationSource.cs
public class SQLConfigurationSource : IConfigurationSource
{
private readonly SqlConnection _connection;
public SQLConfigurationSource(string connectionString)
{
_connection = new SqlConnection(connectionString);
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new SQLConfigurationProvider(_connection);
}
}
SQLConfigurationProvider.cs
public class SQLConfigurationProvider : ConfigurationProvider
{
private readonly SqlConnection _connection;
public SQLConfigurationProvider(SqlConnection connection)
{
_connection = connection;
}
public override void Load()
{
try
{
var model = _connection.Query<SQLConfigurationModel>("sp does not exist for example", commandType: CommandType.StoredProcedure);
Data = model.ToDictionary(x => x.Property, x => x.Value);
}
catch (Exception ex)
{
// WHAT TO DO HERE?
}
}
}
public class SQLConfigurationModel
{
public string Property { get; set; }
public string Value { get; set; }
}
---- UPDATE: CLOSE BUT NOT QUITE THERE ----
I added the exception as a configuration value which I then check for in the Configure method of Startup.cs as per below. This helps ensure the app doesn't crash on startup but when I throw the exception it is not getting routed to the Error view even though the exception handler has already been configured with app.UseExceptionHandler("/Home/Error")
// Inside SQLConfigurationProvider
public override void Load()
{
try
{
var model = _connection.Query<SQLConfigurationModel>("sp does not exist for example", commandType: CommandType.StoredProcedure);
Data = model.ToDictionary(x => x.Property, x => x.Value);
}
catch (Exception ex)
{
Data.Add("ConfigurationLoadException", ex.Message);
}
}
// Inside Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandler("/Home/Error");
// Check for custom config exception
string configurationLoadException = Configuration["ConfigurationLoadException"];
if (configurationLoadException.Length > 0)
{
throw new Exception("Configuration Failed: " + configurationLoadException);
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
If your application can't work without the configurations stored in SQL, you should move this code to fetch data to have better error management. That way you will be able to show a proper error message to user and log it better. Other option is use try/catch block in program.cs, and the assumption is that the not having the SQL driven configuration, will not break the startup project but further in the application usage. If that's the case, you will already have error management placed in startup and it can show you a functional error page for this.
This link will give you some views about startup/program.cs error handling
You should configure a custom error handling page Please read following. it's easy to do
Custom Error Page .net Core

MassTransit 5.2, SignalR: How can i get the IHubContext inside my Consumer?

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

Set dummy IP address in integration test with Asp.Net Core TestServer

I have a C# Asp.Net Core (1.x) project, implementing a web REST API, and its related integration test project, where before any test there's a setup similar to:
// ...
IWebHostBuilder webHostBuilder = GetWebHostBuilderSimilarToRealOne()
.UseStartup<MyTestStartup>();
TestServer server = new TestServer(webHostBuilder);
server.BaseAddress = new Uri("http://localhost:5000");
HttpClient client = server.CreateClient();
// ...
During tests, the client is used to send HTTP requests to web API (the system under test) and retrieve responses.
Within actual system under test there's some component extracting sender IP address from each request, as in:
HttpContext httpContext = ReceiveHttpContextDuringAuthentication();
// edge cases omitted for brevity
string remoteIpAddress = httpContext?.Connection?.RemoteIpAddress?.ToString()
Now during integration tests this bit of code fails to find an IP address, as RemoteIpAddress is always null.
Is there a way to set that to some known value from within test code? I searched here on SO but could not find anything similar. TA
You can write middleware to set custom IP Address since this property is writable:
public class FakeRemoteIpAddressMiddleware
{
private readonly RequestDelegate next;
private readonly IPAddress fakeIpAddress = IPAddress.Parse("127.168.1.32");
public FakeRemoteIpAddressMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext httpContext)
{
httpContext.Connection.RemoteIpAddress = fakeIpAddress;
await this.next(httpContext);
}
}
Then you can create StartupStub class like this:
public class StartupStub : Startup
{
public StartupStub(IConfiguration configuration) : base(configuration)
{
}
public override void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware<FakeRemoteIpAddressMiddleware>();
base.Configure(app, env);
}
}
And use it to create a TestServer:
new TestServer(new WebHostBuilder().UseStartup<StartupStub>());
As per this answer in ASP.NET Core, is there any way to set up middleware from Program.cs?
It's also possible to configure the middleware from ConfigureServices, which allows you to create a custom WebApplicationFactory without the need for a StartupStub class:
public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override IWebHostBuilder CreateWebHostBuilder()
{
return WebHost
.CreateDefaultBuilder<Startup>(new string[0])
.ConfigureServices(services =>
{
services.AddSingleton<IStartupFilter, CustomStartupFilter>();
});
}
}
public class CustomStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.UseMiddleware<FakeRemoteIpAddressMiddleware>();
next(app);
};
}
}
Using WebHost.CreateDefaultBuilder can mess up with your app configuration.
And there's no need to change Product code just to accommodate for testing, unless absolutely necessary.
The simplest way to add your own middleware, without overriding Startup class methods, is to add the middleware through a IStartupFilterā€ as suggested by Elliott's answer.
But instead of using WebHost.CreateDefaultBuilder, just use
base.CreateWebHostBuilder().ConfigureServices...
public class CustomWAF : WebApplicationFactory<Startup>
{
protected override IWebHostBuilder CreateWebHostBuilder()
{
return base.CreateWebHostBuilder().ConfigureServices(services =>
{
services.AddSingleton<IStartupFilter, CustomStartupFilter>();
});
}
}
I used Elliott's answer within an ASP.NET Core 2.2 project. However, updating to ASP.NET 5.0, I had to replace the override of CreateWebHostBuilder with the below override of CreateHostBuilder:
protected override IHostBuilder CreateHostBuilder()
{
return Host
.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
})
.ConfigureServices(services =>
{
services.AddSingleton<IStartupFilter, CustomStartupFilter>();
});
}