I have created a sample solution that will have multiple message broker implementations like Azure Service Bus and Rabbit MQ, for now they just log different messages to distinguish which is used. I would like to configure ("MessageBroker--Name" as below) which message broker client my solution should use from the available implementations. I found that this is a good use case for strategy pattern and implemented the same. The goal is to make the solution have multiple implementations of any service but let configuration decide which one to use.
AppSettings:
"MessageBroker": {
"Name": "RabbitMq",
"ConnectionString": "dummy-connection",
"QueueName": "sample-queue"
}
Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json");
builder.Logging.AddJsonConsole();
builder.Services.AddSingleton<IMessageBrokerContext, MessageBrokerContext>();
builder.Services.AddSingleton<IMessageBrokerClient, NotConfiguredClient>();
var app = builder.Build();
var logger = app.Logger;
try
{
var messageBrokerContextService = app.Services.GetRequiredService<IMessageBrokerContext>();
using var loggerFactory = LoggerFactory.Create(
loggingBuilder => loggingBuilder
.SetMinimumLevel(LogLevel.Information)
.AddJsonConsole());
var messageBrokerConfiguration = builder.Configuration
.GetSection("MessageBroker")
.Get<MessageBrokerConfiguration>();
IMessageBrokerClient messageBrokerClient = messageBrokerConfiguration.Name switch
{
MessageBrokerEnum.NotConfigured => new NotConfiguredClient(
loggerFactory.CreateLogger<NotConfiguredClient>()),
MessageBrokerEnum.AzureServiceBus => new AzureServiceBusClient(
loggerFactory.CreateLogger<AzureServiceBusClient>()),
MessageBrokerEnum.RabbitMq => new RabbitMqClient(
loggerFactory.CreateLogger<RabbitMqClient>()),
_ => new NotConfiguredClient(loggerFactory.CreateLogger<NotConfiguredClient>())
};
await messageBrokerContextService.SetMessageBrokerClientAsync(messageBrokerClient);
await messageBrokerContextService.SendMessageAsync("Hello World!");
await app.RunAsync();
Console.ReadKey();
}
catch (Exception exception)
{
logger.LogError(exception, "Error occurred during startup");
}
My solution is available here - https://github.com/septst/MultiCloudClientSample
Though this works, I am not happy with certain things like explicitly instantiating clients using "new" as the the number of injected dependencies can grow. How can I use dependency injection in this case? Are there any alternatives or any suggestions to improve this solution?
Update: My solution is now updated with the answer from #Nkosi. Thanks.
After reviewing the provided solution, I would suggest the following refactors (see comments in code)
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json");
builder.Logging.AddJsonConsole();
builder.Services.AddSingleton<IMessageBrokerContext, MessageBrokerContext>();
//Add configuration to container so it can be injected/resolved as needed.
builder.Services.AddSingleton<MessageBrokerConfiguration>(_ =>
builder.Configuration.GetSection(MessageBrokerConfiguration.Position).Get<MessageBrokerConfiguration>()
);
//Configure the strategy for resolving the client using factory delegate
builder.Services.AddSingleton<IMessageBrokerClient>(sp => {
//get configuration
MessageBrokerConfiguration config = sp.GetRequiredService<MessageBrokerConfiguration>();
//initialize client based on configuration
IMessageBrokerClient client = config.Name switch {
MessageBrokerEnum.NotConfigured => ActivatorUtilities.CreateInstance<NotConfiguredClient>(sp),
MessageBrokerEnum.AzureServiceBus => ActivatorUtilities.CreateInstance<AzureServiceBusClient>(sp),
MessageBrokerEnum.RabbitMq => ActivatorUtilities.CreateInstance<RabbitMqClient>(sp),
_ => ActivatorUtilities.CreateInstance<NotConfiguredClient>(sp)
};
return client;
});
var app = builder.Build();
var logger = app.Logger;
try {
IMessageBrokerContext messageBrokerContextService =
app.Services.GetRequiredService<IMessageBrokerContext>();
await messageBrokerContextService.SendMessageAsync("Hello World!");
await app.RunAsync();
Console.ReadKey();
} catch (Exception exception) {
logger.LogError(exception, "Error occurred during startup");
}
Note the use of ActivatorUtilities.CreateInstance along with the service provider to create the instances of the clients to avoid any tight coupling to changes to the number of injected dependencies.
There was also no need to manually set the client via SetMessageBrokerClientAsync since the client is being explicitly injected when the context is being resolved.
The client configuration, since registered with the container, can now also be injected into the client implementations so that client specific run time data can be used.
public class RabbitMqClient : IMessageBrokerClient {
private readonly ILogger<RabbitMqClient> logger;
private readonly MessageBrokerConfiguration config;
public RabbitMqClient(MessageBrokerConfiguration config, ILogger<RabbitMqClient> logger) {
this.config = config;
this.logger = logger;
//Now have access to
//config.ConnectionString;
//config.QueueName;
}
public async Task SendMessageAsync(string message) {
logger.LogInformation("RabbitMQ Client sends message {Message}", message);
await Task.CompletedTask;
}
}
Related
The current version of the Microsoft.Azure.Functions.Extensions package exposes an additional property that allows you easy access to the IConfiguration provided to the function. Previously this required manually building a service provider, which was obviously problematic.
Using that package my FunctionsStartup.cs looks like this:
public override void Configure(IFunctionsHostBuilder builder)
{
base.Configure(builder);
var config = builder.GetContext().Configuration; // new in v1.1.0 of Microsoft.Azure.Functions.Extensions
var mySetting = config["MySetting"];
int.Parse(mySetting, out var mySetting);
// ... use mySetting...
}
In order to test my HTTP-triggered functions I've used this article as a base, which details how to manually build and start a host to execute my function as if it was running in Azure, similar to how TestServer works in ASP.NET Core:
var host = new HostBuilder()
.ConfigureWebJobs(new FunctionsStartup().Configure)
.Build();
var functionsInstance = ActivatorUtilities.CreateInstance<MyFunctions>(host.Services);
I can then execute the function methods defined on MyFunctions to test their responses:
var request = new DefaultHttpRequest(new DefaultHttpContext());
var response = (OkObjectResult)functionsInstance.HttpTriggerMethod(request);
... assert that response is valid
The problem is that when I run my tests, builder.GetContext().Configuration is returning null in FunctionsStartup.Configure, which of course causes those tests to fail. How can I work around this?
The article I linked to hasn't been updated to take into account the existence of builder.GetContext().Configuration, but you can make this work for testing purposes with a little tweaking. Instead of using:
var host = new HostBuilder()
.ConfigureWebJobs(new FunctionsStartup().Configure)
.Build();
you need to explicitly copy the host's settings into a new WebJobsBuilderContext that you then pass to your function's startup:
var host = new HostBuilder()
.ConfigureWebJobs((context, builder) => new FunctionsStartup().Configure(new WebJobsBuilderContext
{
ApplicationRootPath = context.HostingEnvironment.ContentRootPath,
Configuration = context.Configuration,
EnvironmentName = context.HostingEnvironment.EnvironmentName,
}, builder))
.Build();
I'm not sure if this is the completely correct way to achieve this, but it has worked well for me.
I have a web server acting as SignalR server today, where the connections from JS are coming in to correct Hub and are handled correctly.
Example of the Register and start JS side
hub = $.connection.webRTCHub;
$.connection.hub.qs = "type=pusher";
$.connection.hub.start().done(function () {
connectionId = $.connection.hub.id;
log("Connected with id ", $.connection.hub.id);
});
When trying to connect to this SignalR server with the C# SignalR Client Nuget-package, I get connected, I get a connection ID, but I do not think I get connected to correct hub because non of the logging is triggered, nor the correct responses are sent to rest of clients.
I am using the trace log for SignalR and it is showing connections, and showing that the ID is connecting. Below is the connection code from the C# client
connection = new HubConnection("http://localhost/signalr/hubs/webRTCHub");
await connection.Start();
MessageBox.Show(connection.ConnectionId);
I have also tried
connection = new HubConnection("http://localhost/signalr/webRTCHub");
and
connection = new HubConnection("http://localhost/");
Can someone point me into the right direction where to start?
I cant see it here, but you need to create a HubProxy for the Hub you want to connect to.
I assume your hub is "webRTCHub".
using(var connection = new HubConnection("http://localhost/"))
{
var hubProxy = _connection.CreateHubProxy("webRTCHub");
hubProxy.On("yourevent", () =>
{
_logger.Debug("Event recieved");
});
await _connection.Start();
}
Make sure you're registering your hub's route in app start, for example in case your using .NET core:
app.UseSignalR(routes =>
{
routes.MapHub<webRTCHubHub>("/signalr/hubs/webRTCHub");
});
While the class webRTCHub should look something like this:
public class webRTCHub : Hub
{
public async Task SendNotification(string userId, string message)
{
await Clients.User(userId).SendAsync("ReceiveNotification", "You have a new message: " + message);
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
}
}
For the js side:
"use strict";
var connection;
connection = new signalR.HubConnectionBuilder()
.withUrl('http://localhost/signalr/hubs/webRTCHub')
.build();
connection.on('ReceiveNotification', (message) => {
// show the message maybe
})
connection.start().catch(function (err) {
return console.error(err.toString())
});
connection.on('finished',(update)=>{
connection.stop();
});
To send back a message from the client to the server you should create a method as well in the class and call that from the script
Update: Packages and Services
for ASP.NET:
NuGet Packages:
Microsoft.AspNet.SignalR
Mapping Route in Application_Start
RouteTable.Routes.MapHubs("/signalr/hubs/webRTCHub", new webRTCHub());
for .NET Core:
Make sure to install the following package and add SignalR in ConfigureServices
Microsoft.AspNetCore.SignalR
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddSignalR();
// ...
}
I guess you have not created any custom routes to handle signalr requests. You should initialize the HubConnection object without any url which will initialize the url of the connection object to "/signalr" as a default value.
connection = new HubConnection("");
or just
connection = new HubConnection();
Since you are using .NET FW and not .NET Core, you should configure the hub on the server like:
On your startup:
public void Configuration(IAppBuilder app)
{
//Branch the pipeline here for requests that start with "/signalr"
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration { };
map.RunSignalR(hubConfiguration);
});
}
The package you use:
Microsoft.AspNet.SignalR;
Microsoft.Owin;
Then on client side is the same for FW and Core, just point to your hub.
I am testing out the code directly out of here for a console app: https://learn.microsoft.com/en-us/azure/azure-monitor/app/ilogger#
I basically copied the code and pointed it to a new azure app insights instance. However, none of the logs are showing up in app insights. Am I missing anything?
static void Main(string[] args)
{
// Create DI container.
IServiceCollection services = new ServiceCollection();
// Add the logging pipelines to use. We are using Application Insights only here.
services.AddLogging(loggingBuilder =>
{
// Optional: Apply filters to configure LogLevel Trace or above is sent to ApplicationInsights for all
// categories.
loggingBuilder.AddFilter<ApplicationInsightsLoggerProvider>("", LogLevel.Trace);
loggingBuilder.AddApplicationInsights(******);
});
// Build ServiceProvider.
IServiceProvider serviceProvider = services.BuildServiceProvider();
ILogger<Program> logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogCritical("critical message working");
// Begin a new scope. This is optional. Epecially in case of AspNetCore request info is already
// present in scope.
using (logger.BeginScope(new Dictionary<string, object> { { "Method", nameof(Main) } }))
{
logger.LogWarning("Logger is working - warning"); // this will be captured by Application Insights.
}
}
The code is correct, but you are hitting a known issue with ApplicationInsights and Console apps - the app is dying before ApplicationInsights can send the data to the backend. (data is not sent immediately, but batched and sent at intervals.)
Adding a sleep of ~30 secs should help your case.
Thread.Sleep(31000);
In regular console apps, docs suggest doing an explicit flush.
https://learn.microsoft.com/en-us/azure/azure-monitor/app/console#full-example
But in the ILogger case, you don't control the TelemetryClient instance. So your best alternative is to control the channel, and call flush on the channel followed by a small sleep. Modified code is given below.
class Program
{
static void Main(string[] args)
{
// Create DI container.
IServiceCollection services = new ServiceCollection();
var channel = new InMemoryChannel();
services.Configure<TelemetryConfiguration>(
(config) =>
{
config.TelemetryChannel = channel;
}
);
// Add the logging pipelines to use. We are using Application Insights only here.
services.AddLogging(loggingBuilder =>
{
// Optional: Apply filters to configure LogLevel Trace or above is sent to ApplicationInsights for all
// categories.
loggingBuilder.AddFilter<ApplicationInsightsLoggerProvider>("", LogLevel.Trace);
loggingBuilder.AddApplicationInsights("***");
});
// Build ServiceProvider.
IServiceProvider serviceProvider = services.BuildServiceProvider();
ILogger<Program> logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogCritical("critical message working");
// Begin a new scope. This is optional. Epecially in case of AspNetCore request info is already
// present in scope.
using (logger.BeginScope(new Dictionary<string, object> { { "Method", nameof(Main) } }))
{
logger.LogWarning("Logger is working - warning"); // this will be captured by Application Insights.
}
channel.Flush();
Thread.Sleep(1000);
}
}
I am using the .NET Core Generic Host (not Web Host) to build a Console app that needs a rather lengthy graceful shutdown. From the source code in
aspnet/Hosting/src/Microsoft.Extensions.Hosting/HostOptions
it seems pretty clear that the ShutdownTimeout option can be used to change the shutdown timeout in the cancellation token that is provided as a parameter to ShutdownAsync. By default it is 5 seconds.
However, I can't figure out where and how to write the code to specify this option in the HostBuilder configuration code that you typically put in the Program.cs file.
Can someone post some code that shows how to do this?
OK, I finally figured it out ... Here's an outline the configuration code in my Program.cs Main function, with most of the items elided, to show where the configuration for HostOptins.ShutdownTimeout goes.
public static async Task Main(string[] args)
{
var host = new HostBuilder()
.ConfigureHostConfiguration(configHost => {...})
.ConfigureAppConfiguration((hostContext, configApp) => {...})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<ApplicationLifetime>();
...
services.Configure<HostOptions>(
opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(10));
})
.ConfigureLogging(...)
.UseConsoleLifetime()
.Build();
try
{
await host.RunAsync();
}
catch(OperationCanceledException)
{
; // suppress
}
}
To make the this work, here is the StopAsync method in my IHostedService class:
public async Task StopAsync(CancellationToken cancellationToken)
{
try
{
await Task.Delay(Timeout.Infinite, cancellationToken);
}
catch(TaskCanceledException)
{
_logger.LogDebug("TaskCanceledException in StopAsync");
// do not rethrow
}
}
See Graceful shutdown with Generic Host in .NET Core 2.1 for more details about this.
Btw, the catch block in Program.Main is necessary to avoid an unhandled exception, even though I am catching the exception generated by awaiting the cancellation token in StopAsync; because it seems that an unhandled OperationCanceledException is also generated at expiration of the shutdown timeout by the framework-internal version of StopAsync.
A relevant answer for ASP.NET Core 6+:
Solution 1:
var builder = WebApplication.CreateBuilder(args);
//...
builder.WebHost.UseShutdownTimeout(TimeSpan.FromSeconds(30));
//...
var app = builder.Build();
Solution 2:
var builder = WebApplication.CreateBuilder(args);
//...
builder.Services.Configure<HostOptions>(
opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(30));
//...
var app = builder.Build();
Solution 3:
var builder = WebApplication.CreateBuilder(args);
//...
builder.Services.PostConfigureAll<HostOptions>(opts =>
opts.ShutdownTimeout = TimeSpan.FromSeconds(30));
//...
var app = builder.Build();
Solution 3 will be applied after all others.
In ASP.NET Core 2 logging in to Azure AD is fairly easy, in ConfigureServices(IServiceCollection services) just add the following
// Azure AD login
services.AddAuthentication(a =>
{
a.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
a.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
a.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(o => o.LoginPath = new PathString("/Account/SignIn"))
.AddOpenIdConnect(o =>
{
o.ClientId = Configuration["Authentication:AzureAd:ClientId"];
o.ClientSecret = Configuration["Authentication:AzureAd:ClientSecret"];
o.Authority = Configuration["Authentication:AzureAd:AADInstance"] +
Configuration["Authentication:AzureAd:TenantId"];
o.CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"];
o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
o.Events = new OpenIdConnectEvents
{
OnRemoteFailure = RemoteFailure,
OnTokenValidated = TokenValidated
};
});
and everything works fine. Then I can add Claims in TokenValidated and that works fine aswell:
private Task TokenValidated(TokenValidatedContext context)
{
var claims = new List<Claim>();
var claim = new Claim(ClaimTypes.Role, "Test", ClaimValueTypes.String, "Issuername")
context.Principal.AddIdentity(new ClaimsIdentity(claims));
return Task.FromResult(0);
}
However, it's never quite that easy. The Claims I want are dependent on a external calls to a service, and the address is stored in the configuration.
In ConfigureServices I also have various classes added for dependency injection with works fine for the controllers.
services.AddTransient<IRoleClaims, RoleClaims>();
This RoleClaims is a class I want to call from the TokenValidated method, but as far as I can see I cannot use DI here. Nor can I access the ServiceCollection to get it via ActivatorUtilities.CreateInstance.
The constructor to RoleClaims looks like this:
public RoleClaims(IOptions<EmployeeConfiguration> configuration)
So, the big question:
How is this supposed to work? Can I somehow use dependency injection in the TokenValidated method? Am I trying to add my own claims in the wrong place?
In ASP.NET Core 2.0, you can get a service from the contain using:
private async Task TokenValidated(TokenValidatedContext context)
{
var widget = ctx.HttpContext.RequestServices.GetRequiredService<Widget>();
...
}
I succeeded in authenticating against IdentityServer4 in a multi-tenancy scenario where I needed to inject client credentials and other stuff on a per-request basis. That's why I also "messed up" my code with custom OpenIdConnectEvents.
The OnTokenValidated func is the right spot. The goal is to assign a value to the TokenValidatedContext.Result (whose setter is unfortunately protected).
You can however call the .Success() method, which sets the property accordingly to what available:
Task TokenValidated(TokenValidatedContext context)
{
//[...] gathering claims
var ci = new ClaimsIdentity(context.Scheme.Name, "name", "role");
ci.AddClaims(my_previously_gathered_Claims);
context.Principal = new ClaimsPrincipal(ci);
// .Success() uses
// 1. the principal just set above
// 2. the context properties
// 3. the context scheme
// to create the underlying ticket
context.Success();
}
That should do the trick.
I personally would have preferred a public setter for .Result.
Found a way to do it. It might not be pretty, but it seems to work.
If anyone have any better way to do it, of if some of this is bad practice I would like to hear it.
public class Startup
{
private IServiceCollection _serviceCollection;
public void ConfigureServices(IServiceCollection services)
{
_serviceCollection = services; // Hacky way to access services in other methods :s
// services.AddStuff() down here, including the AzureAD OIDC
}
private async Task TokenValidated(TokenValidatedContext context)
{
IRoleClaims roleClaims; // My class for reading from services/database
// and create claims
// This is the magic DI workaround I was looking for
var scopeFactory = _serviceCollection.BuildServiceProvider()
.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
var provider = scope.ServiceProvider;
roleClaims = provider.GetRequiredService<IRoleClaims>();
}
// Getting the ObjectID for the user from AzureAD
var objectId = context.SecurityToken.Claims
.Where(o => o.Type == "oid")
.Select(o => o.Value)
.SingleOrDefault();
var claims = await roleClaims.CreateRoleClaimsForUser(objectId);
context.Principal.AddIdentity(new ClaimsIdentity(claims));
}
// Rest of the methods not shown
}