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.
Related
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;
}
}
I'm follwoing this guide: https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration and I don't understand how to make my application work as an async Main method.
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, configuration) =>
{
configuration.Sources.Clear();
ILogger logger = NullLogger.Instance;
IHostEnvironment env = hostingContext.HostingEnvironment;
configuration
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);
IConfigurationRoot configurationRoot = configuration.Build();
ApiConfig options = new();
configurationRoot.GetSection(nameof(ApiConfig)).Bind(options);
var serviceCollection = new ServiceCollection();
serviceCollection.AddHttpClient();
serviceCollection.AddMemoryCache();
serviceCollection.AddSingleton<IMyClient, MyClient>();
IServiceProvider provider = serviceCollection.BuildServiceProvider();
IHttpClientFactory clientFactory = provider.GetRequiredService<IHttpClientFactory>();
})
.Build();
// Application code should start here.
Result result = await myClient. Get(ApiConfig);
// If i put it here, the client is not available since its inside the //ConfigureAppConfiguration.
If I put the client inside the ConfigureAppConfiguration, is fails saying: The await operator can only be used within an async labmda expression.
How do I make my console app to be async?
Now I want to instantiate MyClient, and use it to make query, however, since its need so to await the result, I fails. I don't understand how to make the app async. How should I solve this?
So i found the problem, i copied the .net 6 code example into a net5.0 existing application. After i recreated everything targeting .net 6, things work as expected.
Im failing to build the following service, since configStoreService need to be injected to the AddInMemoryConfiguration, ConfigurationBuilder. So, is there a way to retrieve configStoreService and use it before building the service provider.
var services = new ServiceCollection();
var configStoreService = services.AddSingleton<ConfigurationStore>();
var configuration = new ConfigurationBuilder()
.AddInMemoryConfiguration(configStoreService)
.Build();
services.AddOptions();
services.Configure<Temp>(configuration.GetSection(typeof(Temp).Name));
services.RegisterOptionsType<Temp>(configuration);
_serviceProvider = services.BuildServiceProvider();
Foreword:
This seems a problematic request. You need configuration to configure services, but you need to configure services to get configuration. A chicken and egg problem.
What to do:
Create an instance of configuration use it to configure your services and also add it to your service collection
var configStoreService = new ConfigurationStore(location, sku);
services.AddSingleton(configStoreService);
var configuration = new ConfigurationBuilder()
.AddInMemoryConfiguration(configStoreService)
.Build();
services.AddOptions();
services.Configure<Temp>(configuration.GetSection(typeof(Temp).Name));
services.RegisterOptionsType<Temp>(configuration);
_serviceProvider = services.BuildServiceProvider();
FYI:
I also have to add you can't get anything from a IServiceCollection. You can get things from IServiceProviderwhich, you get AFTER registration is done and BuildServiceProvider called. So before that you can't access items in the collection.
I need to change parameter value for IdentityOptions dynamically from db. So, in my ConsigureServices(...) method in Startup.cs:
services.AddIdentity<AppUser, IdentityRole>(option =>
{
option.Lockout.MaxFailedAccessAttempts = 3; // I need to set this value dynamically from database when server starts
}).AddEntityFrameworkStores<DataContext>()
.AddDefaultTokenProviders();
I have tried to inject IdentityOptions in my Configure(...) method but with no success:
public void Configure(
IApplicationBuilder app,
DataContext dataContext,
IdentityOptions identityOptions)
{
var sysPolicy = dataContext.SysPolicy.FirstOrDefault();
identityOptions.Lockout.MaxFailedAccessAttempts = sysPolicy.DisablePwdLoginFail;
}
It throws an exception like this (it seems that I can't inject it on my Configure):
System.Exception: Could not resolve a service of type 'Microsoft.AspNetCore.Identity.IdentityOptions' for the parameter 'identityOptions' of method 'Configure' on type 'App.Startup'.
You can try out this:
services.AddIdentity<AppUser, IdentityRole>(
options =>
{
var scopeFactory = services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>();
using var scope = scopeFactory.CreateScope();
var provider = scope.ServiceProvider;
using var dataContext = provider.GetRequiredService<DataContext>();
options.Lockout.MaxFailedAccessAttempts = dataContext.SysPolicy.FirstOrDefault();
})
.AddEntityFrameworkStores<DataContext>()
.AddDefaultTokenProviders();
NOTE: Building a service provider is an antipattern and will result in creating an additional copy of singleton services. I would suggest reading the configs from appsettings.json for example, then you can implement it without building the service provider
I am writing a test to get a token from identity server4 using Microsoft.AspNetCore.TestHost
var hostBuilder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers())
;
})
.Configure(app =>
{
app.UseIdentityServer();
});
var server = new TestServer(hostBuilder);
var client = server.CreateClient();
client.BaseAddress = new Uri("http://localhost:5000");
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
Then disco.Error comes up with the following error
Error connecting to
http://localhost:5001/.well-known/openid-configuration: An error
occurred while sending the request.
What am i missing?
The discovery client is obviously doing an external call to that actual address. You want it to call the test server that happens to "live" InMemory.
Take a look at these tests here for IdentityServer4 that tests the discovery document.
To answer your question though you need to use one of the overloaded methods for the DiscoveryClient that takes in a handler that would make the correct "call" to your InMemory test server. Below is an example of how this could be done.
var server = new TestServer(hostBuilder);
var handler = server.CreateHandler();
var discoveryClient = new DiscoveryClient("http://localhost:5000", handler);
var discoveryDocument = await discoveryClient.GetAsync();
Also I highly recommend going over the IdentityServer4 integration tests if youre going to be doing some of your own tests like this.