Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ODS-5622] Make the ODSStartup configurable #1207

Merged
merged 9 commits into from
Jan 15, 2025
Merged

[ODS-5622] Make the ODSStartup configurable #1207

merged 9 commits into from
Jan 15, 2025

Conversation

semalaiappan
Copy link
Contributor

@semalaiappan semalaiappan commented Jan 7, 2025

Update EdFi.Ods.WebApi project Appsettings as:

"StartupClassName": "CustomStartup"

EDIT: @gmcelhanon changed the sample code to include "debug" trace statements for visibility in the debug output.

Add the CustomStartup class below for testing this change:

using Autofac;
using EdFi.Ods.Api.Startup;
using EdFi.Ods.Common.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.Diagnostics;

namespace EdFi.Ods.WebApi
{
    public class CustomStartup : OdsStartupBase
    {
        public CustomStartup(IWebHostEnvironment env, IConfiguration configuration)
            : base(env, configuration) { }

        public override void ConfigureServices(IServiceCollection services)
        {
            Debug.Print("CustomStartup: ConfigureServices...");
            base.ConfigureServices(services);
        }

        public override void ConfigureContainer(ContainerBuilder builder)
        {
            Debug.Print("CustomStartup: ConfigureContainer...");
            base.ConfigureContainer(builder);
        }
        
        public override void Configure(
            IApplicationBuilder app,
            IWebHostEnvironment env,
            IOptions<ApiSettings> apiSettingsOptions,
            IApplicationConfigurationActivity[] configurationActivities)
        {
            Debug.Print("CustomStartup: Configure...");
            base.Configure(app, env, apiSettingsOptions, configurationActivities);
        }
    }
}


if (startupType == null)
{
throw new MissingMemberException(string.Format(MissingStartupClassMessage, startupClassName));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use TypeLoadException (see documentation here) rather than MissingMemberException which does not match semantically with what is happening here.

return startupType;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a blank lines at the end of the files.

{
public static class StartupFactory
{

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the blank line here.

private const string DefaultStartupClassName = "Startup";
public static OdsStartupBase GetStartup(WebHostBuilderContext webHostBuilderContext)
{

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the blank line here.

.AddJsonFile($"appsettings.{webHostBuilderContext.HostingEnvironment.EnvironmentName}.json", optional: true)
.Build();

string startupClassName = config.GetValue<string>("StartUpClassName")?? DefaultStartupClassName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Treat "Startup" as one word (as .NET does), not two. This means that this setting should be named StartupClassName not StartUpClassName.

Application/EdFi.Ods.WebApi/StartupFactory.cs Show resolved Hide resolved
@@ -37,7 +38,7 @@ public static async Task Main(string[] args)
webBuilder.ConfigureKestrel(
serverOptions => { serverOptions.AddServerHeader = false; });

webBuilder.UseStartup<Startup>();
webBuilder.UseStartup<OdsStartupBase>(context => StartupFactory.GetStartup(context));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look at Line 26 above. You could add a line after that like the one below to read the StartupClassName setting into a variable which is then used to drive the logic in this section:

string startupClassName = config.GetValue<string>("StartupClassName");

With a value of "Startup" or a call to string.IsNullOrEmpty returning true, you would then just use the original code that was here:

webBuilder.UseStartup<Startup>();

With any other value, you'd then proceed down the path of logic you have, but I'd suggest skipping the rebuild of the configuration there (currently at line 24 of StartupFactory.cs), preferring instead an approach making the StartupFactory class non-static, and passing the startup class name value to its constructor.

Then, the webBuilder.UseStartup method overload that accepts a factory method is passed the public instance method rather than a static method, and all the logic related to building the configuration and extracting the "StartupClassName" setting is removed from the factory.

Here's my suggestion for the first part of the Main method:

string startupClassName;

using (var hostBuilderInitial = Host.CreateDefaultBuilder(args).Build())
{
    var config = hostBuilderInitial.Services.GetService<IConfiguration>();
    startupClassName = config.GetValue<string>("StartupClassName");

    AssemblyLoaderHelper.LoadAssembliesFromFolder(
        AssemblyLoaderHelper.GetPluginFolder(
            config?.GetValue<string>("Plugin:Folder") ?? string.Empty));
}

var hostBuilder = Host.CreateDefaultBuilder(args)
    .ConfigureLogging(ConfigureLogging)
    .UseServiceProviderFactory(new AutofacServiceProviderFactory())
    .ConfigureWebHostDefaults(
        webBuilder =>
        {
            webBuilder.ConfigureKestrel(
                serverOptions => { serverOptions.AddServerHeader = false; });

            if (string.IsNullOrEmpty(startupClassName) || startupClassName.EqualsIgnoreCase("Startup"))
            {
                webBuilder.UseStartup<Startup>();
            }
            else
            {
                var startupFactory = new StartupFactory(startupClassName);
                webBuilder.UseStartup(context => startupFactory.Create(context));
            }
        });

And the instance-based startup factory:

public class StartupFactory
{
    private readonly string _startupClassName;

    public StartupFactory(string startupClassName)
    {
        _startupClassName = startupClassName;
    }

    public OdsStartupBase Create(WebHostBuilderContext arg)
    {
        // Scanning logic here based on _startupClassName
    }
}

Application/EdFi.Ods.WebApi/StartupFactory.cs Show resolved Hide resolved
{

private const string MissingStartupClassMessage =
"Could not find the startup class. No loaded class member could be found matching the configured startup class name '{0}'.";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this message to:
Could not find a startup class named '{0}' that derives from OdsStartupBase.

@@ -12,6 +12,7 @@
},
"Tenants": {
},
"StartUpClassName": "Startup",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename this setting to "StartupClassName", and default it to an empty string (which should cause the existing Startup class to be used by default, as described above). This also reduces the chance of this change breaking someone who inadvertently misses the setting in their configuration.

Copy link
Contributor

@gmcelhanon gmcelhanon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good now ... just one more tweak.

Application/EdFi.Ods.WebApi/StartupFactory.cs Outdated Show resolved Hide resolved
public OdsStartupBase Create(WebHostBuilderContext webHostBuilderContext)
{
var startupType = FindStartupClassType(_startupClassName);
var startup = (OdsStartupBase)Activator.CreateInstance(startupType, webHostBuilderContext.Configuration);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In testing, this is missing an argument from the custom startup class' constructor, which we can easily pass along.

Updated code should appear as follows:

            var startup = (OdsStartupBase)Activator.CreateInstance(
                startupType,
                webHostBuilderContext.HostingEnvironment,
                webHostBuilderContext.Configuration);

.Where(assembly => assembly.GetName().Name?.StartsWith("EdFi.") ?? false)
.SelectMany(assembly => assembly.GetTypes())
.Where(t => t.Name.EqualsIgnoreCase(startupClassName) && typeof(OdsStartupBase).IsAssignableFrom(t))
.FirstOrDefault();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're in this file, simplify the statement by replacing the use of Where with FirstOrDefault (rather than repeating it at the end) since FirstOrDefault also accepts a predicate (so the Where can be eliminated).

}
return startupType;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this blank line.

@@ -12,6 +12,7 @@
},
"Tenants": {
},
"StartUpClassName": "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the capitalization of the name in this setting to StartupClassName (Startup is treated as one word, not two).

@gmcelhanon gmcelhanon merged commit aa741f2 into main Jan 15, 2025
25 checks passed
@gmcelhanon gmcelhanon deleted the ODS-5622 branch January 15, 2025 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants