Skip to content
New issue

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

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

Already on GitHub? # to your account

Provide API in Microsoft.Extensions.Hosting to allow apps to know when they're being run in the context of a tool #85739

Open
DamianEdwards opened this issue May 3, 2023 · 9 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-Hosting design-discussion Ongoing discussion about design without consensus
Milestone

Comments

@DamianEdwards
Copy link
Member

DamianEdwards commented May 3, 2023

Problem

There are a number of .NET tools & libraries that follow the pattern of loading and executing the application the tool is being invoked in the context of, in order to extract configuration and other details from the application host's DI container, e.g.

  • dotnet ef: Boots the application to the point of the service container being built so that any registered DbContexts can be interacted with to run migrations, generate compiled models, etc.
  • Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>: Hosts the application for the intent of configuring it and executing requests against it in the context of integration tests.
  • Microsoft.Extensions.ApiDescription.Server: Includes MSBuild targets and a command line tool that executes the ASP.NET Core application after injecting a no-op IServer and IHostApplicationLifetime such that details of the app's endpoints can be obtained for the purposes of generating OpenAPI documents.
  • Swashbuckle.AspNetCore.Cli: Functionally similar to Microsoft.Extensions.ApiDescription.Server but customized for Swashbuckle and designed to be used as a CLI tool directly rather than via MSBuild targets.

A common issue with these tools is that it's difficult to condition code in the application such that it doesn't run when the application is booted in the context of one of these tools. For example, imagine your application has code that executes logic on application start to seed a database with initial data, it's very unlikely that one would want that code to run when the application is run in the context of the tool. Another example relates to validation of application configuration, especially secrets that aren't available in the application source code. In "normal" application startup it's desirable to validate that application is correctly configured with non-null values, but when run in the context of a tool these values aren't required and may even not be available if the tool is being executed as part of a CI configuration.

Some approaches that are used in applications today to detect when it's being hosted by a tool:

  • Use a custom configuration value from a source that can be set easily from the same context in which the tool is run, and condition code in the application based on that value, e.g. an environment variable, and then ensure that when the tool is run that configuration value is set.
    • A variation of this pattern is to use the existing environment variable for setting the IHostingEnvironment.EnvironmentName property and then check that via IHostingEnvironment.IsEnvironment(string environmentName), example
  • Check the name of the type implementing IServer and/or IHostApplicationLifetime to see if it matches the name of known private types that tools inject
  • Check the application's parent process name to see if it's something other than that expected when the application is hosted normally
  • EF Core actually sets a flag that can be used by apps to detect when they're being run for the purposes of design-time discovery

Proposal

Provide an API in Microsoft.Extensions.Hosting that would make it easier for an application to detect when it's been loaded in the context of a tool, such that it can perform conditional logic as appropriate. Tools would need to inject/set this API as part of booting the application. If no implementation is registered, the application is not being hosted by a tool. The tools shipped by MS that follow this hosting pattern utilize a shared code package to implement the behavior so implementing this for our own tools would be straightforward.

Note this is just a starting suggestion intended to help kickstart discussion.

namespace Microsoft.Extensions.Hosting;

public interface IHostingTool
{
    /// <summary>
    /// Gets the name of the tool currently hosting the application.
    /// </summary>
    string? ToolName { get; }

    /// <summary>
    /// Gets a value that indicates whether the tool will stop the application after the host is built.
    /// </summary>
    bool StopsApplicationAfterHostBuilt { get; }
}

Note one drawback with this approach is that the app can't evaluate if it's being hosted in the context of a tool until the DI container is built.

Alternatives

Some alternatives and potential drawbacks with them:

  • Passing a known named flag via the application's entry point args parameter
    • This could be problematic as args are often parsed and validated by applications which then fail if an unexpected arg is passed
  • Setting a known named environment variable before the application's entry point is invoked
    • Not sure that all tools actually start a separate process to host the application which might make it difficult to set an process-scoped environment variable
@ghost
Copy link

ghost commented May 3, 2023

Tagging subscribers to this area: @dotnet/area-extensions-hosting
See info in area-owners.md if you want to be subscribed.

Issue Details

Problem

There are a number of .NET tools & libraries that follow the pattern of loading and executing the application the tool is being invoked in the context of, in order to extract configuration and other details from the application host's DI container, e.g.

  • dotnet ef: Boots the application to the point of the service container being built so that any registered DbContexts can be interacted with to run migrations, generate compiled models, etc.
  • Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>: Hosts the application for the intent of configuring it and executing requests against it in the context of integration tests.
  • Microsoft.Extensions.ApiDescription.Server: Includes MSBuild targets and a command line tool that executes the ASP.NET Core application after injecting a no-op IServer and IHostApplicationLifetime such that details of the app's endpoints can be obtained for the purposes of generating OpenAPI documents.
  • Swashbuckle.AspNetCore.Cli: Functionally similar to Microsoft.Extensions.ApiDescription.Server but customized for Swashbuckle and designed to be used as a CLI tool directly rather than via MSBuild targets.

A common issue with these tools is that it's difficult to condition code in the application such that it doesn't run when the application is booted in the context of one of these tools. For example, imagine your application has code that executes logic on application start to seed a database with initial data, it's very unlikely that one would want that code to run when the application is run in the context of the tool. Another example relates to validation of application configuration, especially secrets that aren't available in the application source code. In "normal" application startup it's desirable to validate that application is correctly configured with non-null values, but when run in the context of a tool these values aren't required and may even not be available if the tool is being executed as part of a CI configuration.

Some approaches that are used in applications today to detect when it's being hosted by a tool:

  • Use a custom configuration value from a source that can be set easily from the same context in which the tool is run, and condition code in the application based on that value, e.g. an environment variable, and then ensure that when the tool is run that configuration value is set.
    • A variation of this pattern is to use the existing environment variable for setting the IHostingEnvironment.EnvironmentName property and then check that via IHostingEnvironment.IsEnvironment(string environmentName), example
  • Check the name of the type implementing IServer and/or IHostApplicationLifetime to see if it matches the name of known private types that tools inject
  • Check the application's parent process name to see if it's something other than that expected when the application is hosted normally

Proposal

Provide an API in Microsoft.Extensions.Hosting that would make it easier for an application to detect when it's been loaded in the context of a tool, such that it can perform conditional logic as appropriate. Tools would need to inject/set this API as part of booting the application. If no implementation is registered, the application is not being hosted by a tool. The tools shipped by MS that follow this hosting pattern utilize a shared code package to implement the behavior so implementing this for our own tools would be straightforward.

Note this is just a starting suggestion intended to help kickstart discussion. Please

namespace Microsoft.Extensions.Hosting;

public interface IHostingTool
{
    /// <summary>
    /// Gets the name of the tool currently hosting the application.
    /// </summary>
    string? ToolName { get; }

    /// <summary>
    /// Gets a value that indicates whether the tool will stop the application after the host is built.
    /// </summary>
    bool StopsApplicationAfterHostBuilt { get; }
}
Author: DamianEdwards
Assignees: -
Labels:

area-Extensions-Hosting

Milestone: -

@ghost ghost added the untriaged New issue has not been triaged by the area owner label May 3, 2023
@DamianEdwards
Copy link
Member Author

FYI @eerhardt @davidfowl @ajcvickers

@eerhardt
Copy link
Member

eerhardt commented May 5, 2023

Another potential alternative would be to have well-known IConfiguration value that could be checked by the app (it could be set by an env var, command line arg, or injected into the IConfiguration during startup). We could provide an extension method to get the value and return whether the app is being run in the context of the tool. This would be similar to the current Environment configuration value, but with a different name and a different purpose.

I don't think this alternative is better than the current proposal. Just listing it for other ideas.

@DamianEdwards
Copy link
Member Author

@eerhardt the advantages of your proposal as I see it are:

  • It's provides more flexibility in how the tool can pass the value as configuration will bind from the usual host/app sources, e.g. environment variables, command line args, settings files, etc.
    • RE direct injection of the value by the hosting tool into the app's IConfiguration, what's the mechanism for that exactly? When does the tool get access to the host builder WRT the lifecycle of the app?
  • The app can read configuration at any point after the host builder is created so it's possible to detect before the container is built, which allows the app to include conditional logic much earlier, including service registration, etc.

@eerhardt
Copy link
Member

eerhardt commented May 5, 2023

RE direct injection of the value by the hosting tool into the app's IConfiguration, what's the mechanism for that exactly? When does the tool get access to the host builder WRT the lifecycle of the app?

The same mechanism it uses today to inject DI services. For example:

https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/8f363f7359cb1cb8fa5de5195ec6d97aefaa16b3/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs#L28C18-L56

Instead of calling hostBuilder.ConfigureServices there, it would call hostBuilder.ConfigureAppConfiguration and add the configuration value to the IConfigurationBuilder.

@DamianEdwards
Copy link
Member Author

DamianEdwards commented May 6, 2023

OK so IIUC that means that the configuration value would not be observable in the app until they call the Build() method on the host, meaning they can't use it during their own host-building logic, right? In which case, they can't use it to conditionally register services, validate options, etc.

@eerhardt
Copy link
Member

eerhardt commented May 8, 2023

that means that the configuration value would not be observable in the app until they call the Build() method on the host, meaning they can't use it during their own host-building logic, right? In which case, they can't use it to conditionally register services, validate options, etc.

Correct - it has the same drawback as the IHostingTool service approach.

@DamianEdwards
Copy link
Member Author

Perhaps this is a convenient time to introduce a new event for the HostFactoryResolver then, that fires when the host builder is created, e.g. "HostBuilderCreated", that passes the HostBuilder as data so that the tool can inject app configuration before the app starts manipulating the builder.

@eerhardt
Copy link
Member

eerhardt commented May 9, 2023

The issue is the HostFactoryResolver is based on IHostBuilder, which is the "callback" approach. So even if you got the IHostBuilder early, there is no API to add to the configuration inline right now. You only get ConfigureHostConfiguration and ConfigureAppConfiguration methods, which only get called during .Build().

With #85486, we will have an interface that will be able to add to the configuration directly inline. But HostFactoryResolver would need to be modified to work with the new interface.

@steveharter steveharter added this to the Future milestone May 23, 2023
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label May 23, 2023
@steveharter steveharter added design-discussion Ongoing discussion about design without consensus api-suggestion Early API idea and discussion, it is NOT ready for implementation labels May 23, 2023
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-Hosting design-discussion Ongoing discussion about design without consensus
Projects
None yet
Development

No branches or pull requests

4 participants
@DamianEdwards @steveharter @eerhardt and others