5

I have an ASP.NET Core 3 application, and I'm using AzureAD for authentication. I have the following lines in my Startup.ConfigureSerivces method, whose purpose is to perform some business rules when the cookie is appended.

services.Configure<CookiePolicyOptions>(options => {
    options.CheckConsentNeeded = ctx => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;
    options.OnAppendCookie = ctx => {
        var svc = ctx.Context.RequestServices.GetRequiredService<IUserInformationService>();
        // do something with svc        
    };
});
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
    .AddAzureAD(options => {
        Configuration.Bind("AzureAd", options);
    });

This works well, and I get the injected IUserInformationService object as expected in the OnAppendCookie method and the AzureAD info is taken from appsettings.json.

Recently however, this information about the AzureAD tenant must not reside in appsettings.json, but rather I now must consult a database. I have a service that already queries the database and gets the AD settings. Something like:

public interface IAzureADConfiguration {
    void Configure(AzureADOptions options);
}

However, I can't find a way to retrieve the injected service when calling AddAzureAD. What I want is something like this:

services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
    .AddAzureAD(options => {
        var svc = ???
        svc.Configure(options);
        Configuration.Bind("AzureAd", options);
    });

Since I have no access to HttpContext in AddAzureAD method as I did in OnAppendCookie. Is there a way to get injected objects at this stage? I don't want to hard-code an instance because this requirement is likely to change in the future (i.e. add another mean to configure where I get the Azure Ad settings). Thanks in advance!

Nkosi
  • 235,767
  • 35
  • 427
  • 472
Fernando Gómez
  • 464
  • 1
  • 5
  • 19

2 Answers2

2

Option 1: Configuration callback

Credit to @Nkosi for pointing out the relevant documentation: Use DI services to configure options

Their answer got me started on the right track, but the code sample provided didn't work for me because it didn't account named options (which the AzureAD configuration uses). After figuring that out, and simplifying a bit, I ended up with the following...

For AzureADOptions, the best option seems to be to add a post-configure to run after the existing (required) configuration callback provided to AddAzureAD. Both callbacks will be passed the same AzureADOptions instance, so the existing callback can be left as-is to load "base" values, and the post-config callback can override them as needed.

// Configure Azure AD to load settings from the app config.
// This is unchanged (apart from syntax) from the original question.
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
    .AddAzureAD(options => Configuration.Bind("AzureAd", options));

// Add a post-configure callback to augment the app config-provided values
// with service-provided values.
services.AddOptions<AzureADOptions>(AzureADDefaults.AuthenticationScheme)
    .PostConfigure<IAzureADConfiguration>((options, service) =>
        service.Configure(options));

If you don't want to load base values from the app config, just replace Configuration.Bind("AzureAD", options) with an empty block ({ }).

AddAzureAD uses a named options instance, so be sure to pass the same name to both PostConfigure and AddAuthentication. If the same name isn't used in both places, the post-configure callback won't configure the right options instance (or won't run at all).

Note: In case anyone wants to use this answer to configure things other than Azure AD... When there is no existing options instance or callback registered, you should use the Configure method instead of PostConfigure, and you might not need to provide a name.

services.AddOptions<MyOptionsType>()
    .Configure<IMyConfigurationService>((options, service) =>
        service.Configure(options));

Option 2: Custom configuration provider

While this isn't a direct answer to your question, it might be a viable solution to the problem that motivated your question.

Rather than writing a configuration service, you could implement a custom configuration provider to load the settings from your database. Then, you should be able to leave the AddAzureAD call as it is--using Configuration.Bind.

The example in the documentation news up an EF data context rather than having it supplied by the DI container. That goes against DI instincts, but I think it's fine in this context. The example provider is a small and well-contained subsystem. Its database configuration is still injected (albeit not by the DI container), and it can still be unit-tested by injecting the configuration for an EF in-memory database. It doesn't have any other dependencies to swap out. The DI container wouldn't add much value--the system has all the dependency injection it needs already.

xander
  • 1,689
  • 10
  • 18
  • Thanks, I'll take a look. However, even if this solves the issue, how could I access injected objects? – Fernando Gómez Nov 29 '19 at 01:04
  • The edit you made is most helpful, thanks again. Are you implying that there's no way to access DI services from ConfigureServices method, though? – Fernando Gómez Nov 29 '19 at 19:56
  • It's possible (see [this answer](https://stackoverflow.com/a/32461831/2933946)), but I don't think it's a good idea. Accessing services from the DI container before the container is fully configured creates a circular dependency that will be a maintenance headache over time. As registrations evolve, you'll always have to pay careful attention to the order in which services are registered, and the timing of when you access them, or you might end up trying to resolve a service that isn't configured yet. – xander Nov 29 '19 at 20:47
2

Internally AddAzureAD will eventually call Configure on the AzureADOptions.

To tap into that process, create a custom IConfigureOptions<T> that depends on your service

public class AzureADOptionsSetup : IConfigureOptions<AzureADOptions> {
    private readonly IAzureADConfiguration service;

    public AzureADOptionsSetup(IAzureADConfiguration service) { 
        this.service = service;
    }
    
    public void Configure(AzureADOptions options) {
        service.Configure(options);
    }
}

And make sure to register it with service collection

services.AddTransient<IConfigureOptions<AzureADOptions>, AzureADOptionsSetup>();

This will ensure that your custom options configuration gets a chance to operate on the options class.

Reference Use DI services to configure options

Community
  • 1
  • 1
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Thanks for the answer. But what I'm asking is how to access DI objects from ConfigureServices method. While implementing an IConfigureOptions method seems nice, I still would like to know the answer to said question, if possible. Thanks again! – Fernando Gómez Nov 29 '19 at 19:57
  • @FernandoGómez it is not possible without building the service collection, which is usually not advised. – Nkosi Nov 29 '19 at 20:09