Accessing Page Data during Startup

Vote:
 

Hello,

I am upgrading a cms and commerce solution from CMS 11 to 12. This site uses Azure AD B2C as one of their authentication schemes. Azure AD B2C has this concept of a ClientID, among other configuration values. Because this is a multi-site setup, the ClientId is stored on the Start Page. In their CMS 11 Startup class, they loaded all of their Start Pages and looped through them, initializing Open Id Connect for each of their Start Pages with a different Client Id.

In CMS 12, I have been unable to access the IContentLoader in any way, during Startup in the place that I need it (the ConfigureServices method). I have tried injecting it into the Startup class, used the ServiceBuilder within the ConfigureServices method, and even tried a Configurable Module that was depending on CMS Initialization, Commerce Initialization, and Service Container Initialization. The initilization moldule approach essentially has the same issue in a different flavor. I need the IContentLoader in the ConfigureContainer method, but can only get it in the Initialize method.

Each time, the site will not start up due to an exception from retrieving the IContentLoader before it is available. I'll paste the CMS 11 code below, if anyone knows how to translate this to CMS 12 or how to inject authentication config values after site startup, let me know. Any help is appreciated!

[assembly: OwinStartup(typeof(ProductPlatform.Startup))]

namespace XXX
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            IdentityModelEventSource.ShowPII = true;
            //AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
            var contentRepository = ServiceLocator.Current.GetInstance<IContentLoader>();
            var siteDefinitionRepository = ServiceLocator.Current.GetInstance<ISiteDefinitionRepository>();
            var siteDefinitions = siteDefinitionRepository.List();
            foreach (var site in siteDefinitions)
            {
                var root = site.StartPage;
                var signInPolicy = contentRepository.Get<BaseStartPage>(root).SSOSettings.LoginPolicy;
                var signUpPolicy = contentRepository.Get<BaseStartPage>(root).SSOSettings.RegisterPolicy;
                var clientId = contentRepository.Get<BaseStartPage>(root).SSOSettings.ClientID;
                var redirectURI= contentRepository.Get<BaseStartPage>(root).SSOSettings.RedirectUri;
                if (clientId != null)
                {
                    app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(signInPolicy, clientId, redirectURI));
                    app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(signUpPolicy, clientId, redirectURI));
                }
            }
        }
    }
}
#301364
Edited, May 08, 2023 16:30
Vote:
 

You can use IConfigureOptions or IPostConfigureOptions if you need to access services while configuring options, see https://andrewlock.net/simplifying-dependency-injection-for-iconfigureoptions-with-the-configureoptions-helper/

#301369
Edited, May 08, 2023 20:24
Zach Graceffa - May 08, 2023 20:31
I happened to find the answer around the same time you commented but this is a really cool concept, will definitely need in in the future.
Vote:
 

Alright, the answer here was to use a delegate. Previously, I was adding the code like so:

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(BuildAzureB2CSection(signInPolicy, clientId, redirectURI))

Where I was trying to get the IContentLoader directly in the Startup.ConfigureServices() method. Whereas the AddMicrosoftIdentityWebApp() method also has an overload where you can use a delegate to supply the app with the config that gets executed after the IContenLoader is registered:

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
            .AddMicrosoftIdentityWebApp(options =>
            {
                var cl = services.BuildServiceProvider().GetRequiredService<IContentLoader>();
                //configure options here
            });
#301370
May 08, 2023 20:28
Johan Petersson - May 08, 2023 20:35
I wouldn't recommend building the whole container, just to access a service here. IConfigureOptions exists for this purpose.
Zach Graceffa - May 08, 2023 21:25
Johan, good catch. However, I could get around building the whole container by using the service locator here, instead. Right?

If I do use IConfigureOptions, what would that look like? I don't understand how building my own IConfigureOptions service and then using it later on in my Startup.ConfigureServices() method would work. Wouldn't I run into the same race condition as before?

The answer here, as I understand it so far, is using the delegate as opposed to passing in the options directly. Calling BuildServiceProvider() is just an expensive call I could optimize, but ultimately, unrelated to the fix.
Vote:
 

Building the container is not just about performance, you might also get unwanted side effects. Not sure I can explain it better than the link I posted. But you something like this:

// In your startup class
services.ConfigureOptions<YourConfigurer>();

public class YourConfigurer : IConfigureOptions<TheOptionsClass>
{
    private readonly IContentLoader _contentLoader;

    public YourConfigurer(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    public void Configure(TheOptionsClass options)
    {
        // Use _contentLoader here to fetch content and set the options below...
        options.TheSetting = xxx;
    }
}

There won't be any race conditions here. This configuration is called when needed and after the container has been built. Using the service locator in your startup class won't work, since it needs the container to be built. The service locator is also considered an anti-pattern.

#301374
Edited, May 08, 2023 21:49
Zach Graceffa - May 08, 2023 22:13
Johan, I really appreciate you sticking with me here. I've posted another comment below where I can use a code block.

In addition, the service locator does work here because it is in a delegate and does not execute inline. I agree, though, that it would not work directly in startup.
Vote:
 
#301375
May 08, 2023 21:53
Vote:
 

As for the options pattern and the blog post you sent, I understand how to implement the interface, I just don't understand how to use it because my authentication methods need to also be called in my configure services method. 

Maybe the following example will help convey my confusion. Say I have the above implementation that you just sent and I register it in my DI container, my startup class would look as follows:

public class Startup
{
    private readonly IConfiguration _configuration;

    public Startup(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<IConfigureOptions<MyOpenIdConnectOptions>, ConfigureMyOpenIdConnectOptions>();

        var myOpenIdConnectOptions = services.BuildServiceProvider().GetRequiredService<IConfigureOptions<MyOpenIdConnectOptions>>();

        var authBuilder = services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme);
        foreach (var clientId in myOpenIdConnectOptions.ClientIds)
        {
            authBuilder.AddMicrosoftIdentityWebApp(clientId);
        }
    }
}

This may give a different error, but I would have the same problem as before, right?

#301376
May 08, 2023 22:12
Vote:
 

Okay, I'm understanding the IConfigureOptions class a little more now. My confusion above was that I didn't understand I was working with my settings directly. This approach will not work for me as my requirements are too dynamic. When you use IConfigureOptions you get the benefit of using DI to compute sections of settings. However, in my scenario, I would need to know the amount of settings that exist prior to initialization, this is not feasible for me as the schema is not a collection. I need to be able to iterate over all of my sites and add authentication for each one. I cannot call my initialization method a single time and get multiple different results in this fashion. At least, I do not understand it enough to do that yet.

#301378
May 08, 2023 22:26
Vote:
 

The idea with IConfigureOptions, was that you should configure the options class 'MicrosoftIdentityOptions' with it. Not the way you did it with your own class 'MyOpenIdConnectOptions'. But there's more to it than just configuring that options class to get this working, I realized when looking through the source code of the Microsoft.Identity.Web library.

You will have a real hard time adding these schemes dynamically. I would recommend configuring OpenID Connect completely manually https://docs.developers.optimizely.com/content-management-system/docs/integrate-azure-ad-using-openid-connect or do it statically. How often does these settings change? Do you really need to do this dynamically?

#301414
May 09, 2023 7:39
Zach Graceffa - May 09, 2023 15:02
Thanks for the additional context Johan. I definitely learned a thing or two as well.

First my goal was to replicate the old setup. However, now that I'm having a lot of trouble with doing so, its time to start thinking about a different architecture.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.