Azure / azure-functions-host

The host/runtime that powers Azure Functions
https://functions.azure.com
MIT License
1.94k stars 442 forks source link

Proper way to access ILogger and IConfiguration inside Startup #4577

Open APIWT opened 5 years ago

APIWT commented 5 years ago

Is your question related to a specific version? If so, please specify:

v2 (latest)

What language does your question apply to? (e.g. C#, JavaScript, Java, All)

C#

Question

What is the proper way to get access to IConfiguration and ILogger inside a custom startup class?

PureKrome commented 5 years ago

@APIWT Have you tried experimenting with Azure Functions + Dependency Injection. Those docs show some examples.

Update :

ouch at all the downvotes. Feels like I'm on stack overflow now 😢

espray commented 5 years ago

This looks like a duplicate of #4464

mhoeger commented 5 years ago

@jeffhollan - looks like a good candidate for doc enhancements? I know @brettsam has answered similar questions for ILogger (here)

APIWT commented 5 years ago

The big issue with the ILogger suggestion is that it is not available in Startup. Plus, due to the bugs that currently exist with scoped services we are forced to do our own DI which is the bigger issue with functions as a whole currently.

idg10 commented 4 years ago

The problem with the current design is that there doesn't seem to be a reliable way to get hold of the IConfiguration in your Startup.Configure method.

Apparently the only mechanism by which you can obtain IConfiguration is to have it injected as a dependency, which means it's only available after you've finished configuring all your services, and the service collection has been built. So what are you supposed to do if you need access to configuration settings as part of your service configuration code?

If you look at a typical ASP.NET Core startup class, it takes an IConfiguration as a constructor argument, for precisely this reason. It makes it possible to make configuration-driven decisions about how you configure services at startup.

So what I'd really like is for functions to support the same pattern. The host code that looks for an WebJobsStartupAttribute-annotated class would inspect the constructors of the startup class, and if there's a constructor that accepts an IConfiguration it should use that, passing the configuration, just like we get in ASP.NET Core apps.

idg10 commented 4 years ago

Having looked into this a little more, it would appear that the ability to use startup classes at all in functions comes from the WebJobs SDK. So I created a draft PR https://github.com/Azure/azure-webjobs-sdk/pull/2405 to show how this might be fixed. It's not terribly complex, and doesn't appear to require any changes to the Functions SDK itself. But before I go any further with this, can anyone from Microsoft let me know whether there's any interest in pursuing this change?

carlosscastro commented 4 years ago

Functions team, any update on this? The need to use IConfiguration during startup seems like a reasonable one and would align better with the pattern in asp.net core web apps.

mhoeger commented 4 years ago

@brettsam - do you mind weighing in on this?

cygnim commented 4 years ago

The current guidance is to inject IOptions into my services. That's not a great solution and I don't want that dependency nor do I have the ability to change this in pre-built class libraries. Consider this very real and very common scenario that cannot be achieved currently:

services.AddScoped<IDatabaseConnection>(x => new SqlDbConnection(configuration.GetConnectionString("connStringName"));

The fact that I can't access the configuration during Startup means I cannot properly configure services without an undesirable workaround or creating a lot of service adapters. Please make this a priority.

brettsam commented 4 years ago

@cygnim -- IOptions isn't required by any means. IConfiguration is a service you can request in any of your DI factories and access directly. In your example, can you do this? I think that'll unblock you without the need for an additional wrapper class.

services.AddScoped<IDatabaseConnection>(x => new SqlDbConnection(x.GetService<IConfiguration>().GetConnectionString("connStringName")));

That being said, we do have a PR already (https://github.com/Azure/azure-webjobs-sdk/pull/2405) that will start to plumb this through and make it available in the startup class. I will work on prioritizing this -- and the follow-up work needed to make it accessible to Functions.

cygnim commented 4 years ago

@brettsam, thanks for the response. I am basing this off the documentation here. Paraphrased below:

builder.Services.AddOptions<MyOptions>()
                .Configure<IConfiguration>((settings, configuration) =>
                                           {
                                                configuration.GetSection("MyOptions").Bind(settings);
                                           });

This seems to be the only way to get config values from the default configuration provider within the IFunctionsHostBuilder. But this means the only way to inject from this config is to take a dependency on MyOptions, which is not always possible without an adapter.

I've gotten around this by building a separate instance of IConfiguration and adding environment variables. It works, but it would be nice if the builder exposed the configuration object directly and it sounds like you're already on it. I'll just have to be patient. :)

Thanks!

brettsam commented 4 years ago

The code I provided above is the right way to do this -- it's not even a workaround. No need to be patient :-) Let me know if it works for you -- or if it's missing something you need.

One of the reasons we keep punting this issue is because for most scenarios, access to IConfiguration isn't required during startup and other issues with higher priorities keep moving above this one. You can use a factory to get the fully-resolved IConfiguration like I've shown above. That IConfiguration should have all environment variables and app settings applied.

To put it another way -- IConfiguration is a singleton service registered with DI and you can access it from anywhere you could access IOptions<T>. So in the factory you had above, instead of passing in an IConfiguration, you can ask for it from DI.

There are some reasons that you'd need it directly in the startup class, but your scenario doesn't look like one of them. We do this in a couple of places in Functions itself -- mostly to switch between Linux and Windows service registrations, I think. This is why we still want to add this (and we're looking at it) -- but I don't think you need to be blocked on this.

To answer your point about the docs -- that's just one pattern (specifically the .NET Core Options pattern) and is useful when you want to map config settings to your own Options types (which can make testing easier). That code doesn't actually give you access inside the IFunctionsHostBuilder. It's registering a factory to be called later -- same as the code that I suggested.

Maybe these examples will help others as well:

builder.Services.AddOptions<MyOptions>().Configure<IConfiguration>((settings, configuration) =>
  {
    // This is a factory -- this line is not called until IOptions<MyOptions> is needed to fulfill a dependency via DI. Notice that
    // IConfiguration is being passed in and used to bind it's values to this object.
    configuration.GetSection("MyOptions").Bind(settings);
  });

Similar to what I suggested you use above (formatted differently):

services.AddScoped<IDatabaseConnection>(x => 
  {
     // The 'x' passed in here is the IServiceProvider. It gives you access to all DI services -- including IConfiguration. You can use this to get the
     // fully-resolved config and use that to construct your other services. This happens on-demand every time your IDatabaseConnection is needed to
     // fulfill a dependency. If you set a breakpoint in here, you'll notice it isn't called the when startup is run -- it's only called when needed later.
     var config =  x.GetService<IConfiguration>();
     return new SqlDbConnection(config.GetConnectionString("connStringName"))
  });

... all that being said, please do let me know if you're still not able to get this to work. Having some concrete examples of folks being blocked helps move up priorities :-)

espray commented 4 years ago

@brettsam would surfacing the IConfigurationBuilder be on the roadmap or even on option? Basically I am wanting to add configuration from an Azure App Configuration store to the IConfiguration.

brettsam commented 4 years ago

IConfigurationBuilder work is in-progress right now (and higher-priority as that's something that can't be done really easily today). The PRs are out -- but we've had a long backup of our deployments so we're waiting before we add new features at the moment. Those PRs are here:

cygnim commented 4 years ago

The code I provided above is the right way to do this -- it's not even a workaround. No need to be patient :-) Let me know if it works for you -- or if it's missing something you need.

@brettsam, thanks for the response. I realized after I posted that IOptions doesn't solve the challenge I'm experiencing- that I want to be able to use IConfiguration in Startup to configure my services. I appreciate the suggestion of registering IConfiguration as a service. I'm not keen on it, but it is a viable workaround to be sure. :)

I'll just wait for https://github.com/Azure/azure-webjobs-sdk/pull/2407 as it appears to solve the Startup configuration issue.

Thanks!

brettsam commented 4 years ago

Do your services need to be created directly in Startup? Or are they created in factory delegates like you showed above?

phatcher commented 4 years ago

@brettsam I need to register named configuration options to pass to HttpClient configurations, and I don't see away around to do this currently without building my own IConfiguration

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<TopItemSettings>(TopItemSettings.Month,
                                       Configuration.GetSection("TopItem:Month"));
    services.Configure<TopItemSettings>(TopItemSettings.Year,
                                        Configuration.GetSection("TopItem:Year"));

    ...
}

The problem is that the Configure overload does not take the service provider, so I'm stuck until the patch you propose is available - unless I'm missing something?

patrick-steele commented 4 years ago

@brettsam The use case I'm looking at is one-time initialization to hook up Azure Key Vault provider for SQL Server's column encryption. I only need to do it startup. Basically, something like this:

public override void Configure(IFunctionsHostBuilder builder)
{
    InitializeKeyVaultAccess();
}

private void InitializeKeyVaultAccess()
{
    var clientId = "xxx";
    var clientSecret = "yyy";
    _clientCredential = new ClientCredential(clientId, clientSecret);

    var azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider(GetToken);
    IDictionary<string, SqlColumnEncryptionKeyStoreProvider> providers = new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>();

    providers.Add(@"AZURE_KEY_VAULT", azureKeyVaultProvider);
    SqlConnection.RegisterColumnEncryptionKeyStoreProviders(providers);
}

public static async Task<string> GetToken(string authority, string resource, string scope)
{
    var authContext = new AuthenticationContext(authority);
    AuthenticationResult result = await authContext.AcquireTokenAsync(resource, _clientCredential);

    if (result == null)
        throw new InvalidOperationException("Failed to obtain the access token");

    return result.AccessToken;
}

I want to pull the clientId and clientSecret from IConfiguration. I don't need them injected as options into anything.

I suppose I could create a class that has a dependency on an IOptions<KeyVaultSettings> (do the above initialization in the ctor), register it as a singleton and then make one of my services depend on the singleton, but the service won't "need" the singleton so it'll be a ctor argument that is never used by the service (kind of hacky).

StefanSchoof commented 4 years ago

@patrick-steele can you use an Managed identity and Key Vault references? I think those two things can make you task easier.

APIWT commented 4 years ago

IConfigurationBuilder work is in-progress right now (and higher-priority as that's something that can't be done really easily today). The PRs are out -- but we've had a long backup of our deployments so we're waiting before we add new features at the moment. Those PRs are here:

* [Azure/azure-webjobs-sdk#2407](https://github.com/Azure/azure-webjobs-sdk/pull/2407)

* [Azure/azure-functions-dotnet-extensions#33](https://github.com/Azure/azure-functions-dotnet-extensions/pull/33)

* #5484

Does this mean we will be able to access the IConfiguration in our startup Configure function similar to how we can with ASP.NET Core? This seems to be something that is causing our team a lot of grief.

fabiocav commented 4 years ago

@APIWT yes. This is being deployed right now and should be globally available within a couple of weeks. Detailed documentation will be linked from here as well once published.

brettsam commented 4 years ago

Yes. Docs will first go into the https://github.com/Azure/azure-functions-dotnet-extensions repo.

Your code will look like this: https://github.com/Azure/azure-functions-dotnet-extensions/pull/33/files#diff-963fa0079554a9be3de8c62bb88e2695

brettsam commented 4 years ago

The host release is just starting to roll out today and takes roughly a week until it's worldwide. And yeah, best place to watch is probably the Releases here -- https://github.com/Azure/azure-functions-dotnet-extensions/releases.

Also note that we're discussing whether to put this out as a "preview" nuget package at first -- but I'll still create a Release there with details on how to use it.

espray commented 4 years ago

@jackbond @brettsam posted some example for working with IOptions/IConfiguration. Do you have a public repro, the community might be able to give some advice. Personally I found i did not need IConfiguration and ILogger in the startup, but in the setup actions

fabiocav commented 4 years ago

This is still rolling out and available in a few regions. At this point, we expect global deployment to complete by July 31st.

APIWT commented 4 years ago

Does this behave identically to adding the following code to our Configure method?

var workaroundProvider = builder.Services.BuildServiceProvider();
var configuration = workaroundProvider.GetRequiredService<IConfiguration>();

Or is there a difference that I'm overlooking? I understand that it is not ideal to use BuildServiceProvider since the underlying host builder will also do this, but I'm curious if doing this as a workaround has any drawbacks.

CasperWSchmidt commented 4 years ago

@fabiocav Is there any place where I can follow the roll-out? Also, what is the estimated timeline for getting this available in the Microsoft.Azure.Functions.Extensions NuGet package?

fabiocav commented 4 years ago

Custom configuration is supported with the preview package published with this release: https://github.com/Azure/azure-functions-dotnet-extensions/releases/tag/v1.1.0-preview1

Package: https://www.nuget.org/packages/Microsoft.Azure.Functions.Extensions/1.1.0-preview1

You can also find some documentation on how to use the new feature here: https://github.com/Azure/azure-functions-dotnet-extensions/wiki/Configuration-and-FunctionsHostBuilderContext-support

Please try this out and provide any feedback you may have. For problems with the new feature, please open new issues with the details and we'll investigate.

Thank you all for the feedback and patience!

FYI, @brettsam

CasperWSchmidt commented 4 years ago

The new 1.1.0 works perfect 😊

phatcher commented 4 years ago

@brettsam Can I suggest a small change in FunctionsStartup.

From debugging locally I can see that appsettings.json (optional) and the environment variables are already added to the configuration root.

If you also add appsettings.{EnvironmentName}.json then for most people it would work out of the box; this would avoid the custom configuration breaking consumption and premium plans

felipementel commented 3 years ago
[assembly: FunctionsStartup(typeof(Startup))]
namespace xxxxx.Functions.Base
{
    [ExcludeFromCodeCoverage]
    public class Startup : FunctionsStartup
    {
        private static IConfiguration _configuration = null;

        public override void Configure(IFunctionsHostBuilder builder)
        {
            var serviceProvider = builder.Services.BuildServiceProvider();
            _configuration = serviceProvider.GetRequiredService<IConfiguration>();

            *** Now you can use _configuration["KEY"] in Startup.cs ***
        }
CasperWSchmidt commented 3 years ago

@felipementel following the discussion above (and related issues) you would know that the above idea is not optimal. Instead use the new feature in 1.1.0 to retrieve the configuration.

brennfoster commented 3 years ago

How does this change affect local.settings.json? It seems to make it irrelevant.

CasperWSchmidt commented 3 years ago

@brennfoster Why is that? local.settings.json can still be used for settings that otherwise live in the Azure (portal) configuration

brennfoster commented 3 years ago

@CasperWSchmidt Why would I do that when I can stick them all in appsettings.json? Now I'm required to use two configuration files if I want to work locally.

CasperWSchmidt commented 3 years ago

Appsettings.json is committed to git and deployed to azure so it is not the best place to have environment variables that differ from local development to test, staging and production. I know that you can use environment specific Appsettings.json but this requires some manual tweaking AFAIK. Local.settings.json on the other hand, does not leave your machine. We use the configuration page in the Azure portal to set variables for each service (per environment) and Local.settings.json for local development. It works pretty well I think.

PureKrome commented 3 years ago

@CasperWSchmidt out of interest, how would you handle secrets with an ASP.NET Core application? (i know ASP.NET core apps != .NET Core Function apps). The reason I ask this .. is so we can see if that could (should?) be applied to Functions, also?

It's a shame that functions has the local.settings.json file. I thought it was there because of how functions is x-language and appsettings.json was very .NET core centric or something .. and not a x-lang naming convention or something. Still, i think it's a shame because it doesn't follow what I thought was a common ,net core standard of having appsettings.<env>.json files for configuration and to me, i still see functions as a .net based service. Even though it's x-lang ...

Sure, those files shouldn't contain private info -> leverage ENV VARS and/or SECRETS instead, IMO.

phatcher commented 3 years ago

@PureKrome You can use appsettings.json/appsetttings.{env}.json locally in .NET Core Functions app, you just need the slightly changed startup routine...

public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
    var context = builder.GetContext();

    // Trying to solve https://github.com/Azure/azure-functions-host/issues/4577
    // Note that these files are not automatically copied on build or publish. 
    // See the csproj file to for the correct setup.
    // NB appsettings.json/Environment variables already done by the default but not the environment specific one
    // So add them all again so we get the desired order
    builder.ConfigurationBuilder
        .AddJsonFile(Path.Combine(context.ApplicationRootPath, "appsettings.json"), optional: true, reloadOnChange: false)
        .AddJsonFile(Path.Combine(context.ApplicationRootPath, $"appsettings.{context.EnvironmentName}.json"), optional: true, reloadOnChange: false)
        .AddEnvironmentVariables();

    var configuration = builder.ConfigurationBuilder.Build();

    // Now see if we have a key vault and make that the primary source of truth.
    // Since it is last any values in the vault will take precedence.
    var kvn = configuration["KeyVaultName"];
    if (!string.IsNullOrEmpty(kvn))
    {
        builder.ConfigurationBuilder.AddAzureKeyVault($"https://{kvn}.vault.azure.net/");
    }
    ....
}

Note the comment regarding the Function App inclusion of files/environment variables, hence my suggestion in September. Also you don't need to do the AzureServiceTokenProvider dance as it seems to work without it out of the box.

For ASP.NET Core apps we get

public Startup(IHostEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
        .AddEnvironmentVariables();

    Configuration = builder.Build();

    // Now see if we have a key vault and make that the primary source of truth.
    // Since it is last any values in the vault will take precedence.
    var kvn = Configuration["KeyVaultName"];
    if (!string.IsNullOrEmpty(kvn))
    {
        builder.AddAzureKeyVault($"https://{kvn}.vault.azure.net/");
        Configuration = builder.Build();
    }
   ...

We generally favour Key Vaults since you then get an audit trails of when the values were modified and by whom, whereas if you just use environment variables via the console this information is not know.

CasperWSchmidt commented 3 years ago

@PureKrome We don't have any ASP.Net Core apps. We only use Azure Functions. For secrets we use Azure Key Vault and let the Functions Runtime handle the key vault references for us (unfortunately this doesn't work when running locally - or at least didn't when we first started this a year ago).

Azure Functions are cross-language and local.settings.json (instead of appsettings.json) is there for this exact reason. If you are a .Net developer and prefer to keep to the appsettings.json and appsettings.{environment}.json, you can still do that. Just follow the example mentioned by @phatcher

PureKrome commented 3 years ago

@phatcher

You can use appsettings.json/appsetttings.{env}.json locally in .NET Core Functions app, you just need the slightly changed startup routine...

yeah - this was basically what I was doing also. The it's a shame part of my poor opinion here is that it feels like this could have been more of a default setup for Functions, so it aligns nicely with ASP.NET Core. Then again arguments could be made to say that Console apps don't have this by default. But the rebuttal could be that Console apps favour/suggest to use appsettings.<env>.json as the default way (consistency across .NET applications) and then we come to Functions and .. it's a different approach .. breaking some consistency for those used to ASP.NET and .NET, IMO.

The rebuttal to that is that Functions are x-lang, not just x-plat (but only c#).

which segways to @CasperWSchmidt comment:

Azure Functions are cross-language and local.settings.json (instead of appsettings.json) is there for this exact reason.

Yep. I've heard the explanation a few times. I'm not going to argue about it and stuff.

phatcher commented 3 years ago

@PureKrome Hence my comment on 29 September arguing the same 😄 - basically for the case with consumption plans - though I'd also like consumption plans to work nicely with docker containers 😉

PureKrome commented 3 years ago

yes and yes @phatcher !!

Tanzy commented 3 years ago

@PureKrome You can use appsettings.json/appsetttings.{env}.json locally in .NET Core Functions app, you just need the slightly changed startup routine...

public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
    var context = builder.GetContext();

    // Trying to solve https://github.com/Azure/azure-functions-host/issues/4577
    // Note that these files are not automatically copied on build or publish. 
    // See the csproj file to for the correct setup.
    // NB appsettings.json/Environment variables already done by the default but not the environment specific one
    // So add them all again so we get the desired order
    builder.ConfigurationBuilder
        .AddJsonFile(Path.Combine(context.ApplicationRootPath, "appsettings.json"), optional: true, reloadOnChange: false)
        .AddJsonFile(Path.Combine(context.ApplicationRootPath, $"appsettings.{context.EnvironmentName}.json"), optional: true, reloadOnChange: false)
        .AddEnvironmentVariables();

    var configuration = builder.ConfigurationBuilder.Build();

    // Now see if we have a key vault and make that the primary source of truth.
    // Since it is last any values in the vault will take precedence.
    var kvn = configuration["KeyVaultName"];
    if (!string.IsNullOrEmpty(kvn))
    {
        builder.ConfigurationBuilder.AddAzureKeyVault($"https://{kvn}.vault.azure.net/");
    }
    ....
}

I've been trying to get this to work and does not seem to get the values. The function is a C# Core 3.1

If I view the configuration object in the variable watcher it has the values, however, when getting the value in the code

var kvn = configuration["KeyVaultName"];

It does not get the value? Don't suppose you have any idea?

phatcher commented 3 years ago

@Tanzy Only idea I have is to run locally and then have a breakpoint on that line - you can then see which values are defined in each configuration provider.

Tanzy commented 3 years ago

@phatcher thanks for the quick reply. I created a sample project that I was going to use to show the issue. Strange thing is that it works in that my sample, so need to look at the real project to see if I can see a difference.

Tanzy commented 3 years ago

@phatcher sorted it, it was a PEBCAK issue. I did not realise that I had overridden the value I wanted in a later config :(

Again, thanks for the help

StefanSchoof commented 3 years ago

A just failed to use AddServiceBusClient inside of a Azure Function Startup. The method takes a connection string, that I have in the configuration. But I found no solution to get the connection string and pass it. Do I miss some documentation or is this currently not possible?

joeyeng commented 3 years ago

Not sure if this was stated earlier in this thread, but ConfigureAppConfiguration runs before Configure in Startup.cs. Therefore, if you need to access IConfiguration (after it's been setup) in Configure you can access it via the FunctionsHostBuilderContext like this:

var config = builder.GetContext().Configuration;

erionpc commented 2 years ago

This worked for me. I had to override ConfigureAppConfiguration and add an appsettings.json configuration file because I found that local.settings.json doesn't support nesting (for local testing). I'm not sure if there's a workaround for that.

public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
    if (builder == null) throw new ArgumentNullException(nameof(builder));

    var context = builder.GetContext();

    builder
        .ConfigurationBuilder
        .AddJsonFile($"{context.ApplicationRootPath}{Path.DirectorySeparatorChar}appsettings.json", true, true)
        .AddEnvironmentVariables();
}

public override void Configure(IFunctionsHostBuilder builder)
{
    if (builder == null)  throw new ArgumentNullException(nameof(builder));

    builder.Services.AddSingleton(x =>
    {
        MyConfigType myConfig = new();
        builder.GetContext().Configuration.Bind("MyConfigSection", myConfig);
        return myConfig;
    });
}
PureKrome commented 2 years ago

because I found that local.settings.json doesn't support nesting (for local testing)

Not true @erionpc . It supports nesting by using the : separator character. E.g.:

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "ASPNETCORE_ENVIRONMENT": "Development",
    "Serilog:MinimumLevel:Default": "Information",
    "Serilog:Enrich:1": "FromLogContext",
    "Serilog:WriteTo:1:Name": "Console",
    "Serilog:WriteTo:2:Name": "Seq",
    "Serilog:WriteTo:2:Args:serverUrl": "http://localhost:5301",
    "Serilog:WriteTo:2:Args:restrictedToMinimumLevel": "Debug"
  }

remember, the file format/schema is used to read in some data and translate it to an Configuration Sections/Key-values ... so this is just a custom Azure Functions format. Well, that's how I see it. In the end, following that pattern above, it does the equivalent of

{
   ... snipped ...

    "Serilog": {
        "Enrich" : [ "FromLogContext" ],
        "WriteTo": [
            {
                "Name": "Console"
            },
            {
                "Name": "Seq",
                "Args": {
                    "serverUrl": "http://localhost:5301",
                    "Args:restrictedToMinimumLevel": "Debug"
                }
            }
        ]
    }
}

REF: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-6.0#environment-variables