dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.44k stars 10.02k forks source link

Add equivalent of HttpRuntime.AppDomainAppId available at all times when hosted in IIS #43632

Closed yaakov-h closed 1 year ago

yaakov-h commented 2 years ago

Moved from #43631.

Background and Motivation

When an ASP.NET Core application is hosted in IIS, IIS gives it an application ID, the same way it gives every other application an application ID. This contains the site ID (numeric) and virtual directory path, and is already used in ASP.NET Core to initialize ANCM and I believe to determine the value of HttpRequest.PathBase.

The particular scenario I have at the moment is a web application that runs multiple instances on different domains from the same physical path on disk. The only way the application can differentiate between instances e.g. for additional config or cache directories) is the IIS site ID.

In .NET Framework / ASP.NET applications, this value is available via HttpRuntime.AppDomainAppId which reads it from the current AppDomain's data. In ASP.NET Core, this value is currently only available during the lifetime of a request via IServerVariablesFeature, but outside of a single request there is no API for it.

I believe that applications that are hosted in IIS and rely on this value outside of a request lifetime (e.g. during startup or during background work) should be able to be ported from ASP.NET / .NET Framework to ASP.NET Core on modern .NET without needing to completely rework any logic that uses the AppDomainAppId.

The value is, however, already known internally to the ASP.NET Core Module and can be reached via reflection and pointer arithmetic. Making it available to the rest of the application is a question of "how", not "if".

Proposed API

namespace Microsoft.AspNetCore.Hosting;

+public static class IISWebHostDefaults
+{
+    public static readonly string IISConfigPathKey = "iis:configpath";
+}

This API will depend upon the following additional behavioural changes, though they are not direct changes to the public API surface:

Usage Examples

var builder = WebApplication.CreateBuilder(args);
var configPath = builder.Configuration[IISWebHostDefaults.ConfigPathKey]; // "/LM/W3SVC/3/ROOT"
var siteID = configPath.Split('/')[3];
builder.Configuration.AddJsonFile($"MyAppConfig-Site{siteID}.json");

var app = builder.Build();
app.Run();
var builder = WebApplication.CreateBuilder(args);
var configPath = builder.Configuration[IISWebHostDefaults.ConfigPathKey];
builder.Services.AddSomeModule(options => options.Category = $"MyCategory-{configPath}");

var app = builder.Build();
app.Run();
var builder = WebApplication.CreateBuilder(args);
app.AddSomeModule();

var app = builder.Build();
app.UseSomeModuleThatNeedsSiteID(app.Configuration[IISWebHostDefaults.ConfigPathKey]);

app.Run();
class MyDependencyInjectedClass
{
    public MyDependencyInjectedClass(IConfiguration configuration)
    {
        var siteSpecificDirectory = Sanitize(configuration[IISWebHostDefaults.ConfigPathKey]);
        cacheDirectory = Path.Combine(Path.GetTempPath(), "MyApplication", siteSpecificDirectory );
    }

    readonly string cacheDirectory;

    static string Sanitize(string input)
    {
        // imagine there's something cool here, its not relevant
    }
}

Alternative Designs

I did consider a method similar to .NET Framework of exposing a static function/property, as http_get_application_properties can be called from anywhere, however this does not easily allow the consuming code to be tested or for stub/mock values to be provided. Every consumer that wished to do so would immediately create their own interface and implementation to simply wrap this static call.

Risks

This will require changes to ANCM, and I do not know what the forwards-compatibility and backwards-compatibility concerns here are and how to mitigate them.

Looking at the most recent change which added a field to the IISConfigurationData struct, there appear to be no compatibility mitigations / versioning / traditional dwSize fields etc., so this may not be a concern?

Additional Notes

This can be done already, albeit extremely hackily, by reflection and pointer arithmetic to access values stored within the IIS integration memory:

var builder = WebApplication.CreateBuilder(args);
var nativeApplicationType = Type.GetType("Microsoft.AspNetCore.Server.IIS.Core.IISNativeApplication, Microsoft.AspNetCore.Server.IIS");
var iisNativeApplication = builder.Services.Where(s => s.ServiceType == nativeApplicationType)
    .Select(x => x.ImplementationInstance)
    .FirstOrDefault();

if (nativeApplicationType?.GetField("_nativeApplication", BindingFlags.NonPublic | BindingFlags.Instance) is { } field &&
    field.GetValue(iisNativeApplication) is SafeHandle nativeApplicationHandle)
{
    var nativeApplicationPointer = nativeApplicationHandle.DangerousGetHandle();

    var objectPointer = IntPtr.Add(nativeApplicationPointer, 136); // Assuming x64, recalculate for x86
    var configPathPointer = Marshal.ReadIntPtr(objectPointer);

    // Got the config path!
    var configPath = Marshal.PtrToStringUni(configPathPointer);
}

I would much rather this be exposed as an API as this relies heavily on not just ASP.NET internals but also the memory layout of underlying native types.

I am happy to drive the implementation myself with a PR, once a design is approved.

CZEMacLeod commented 1 year ago

@twsouthwick we add all environment variables to configuration by default.

Perhaps for application configuration, but fir host config only dotnet and aspnetcore according to https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-7.0#host-configuration

Since one use case is to use the app pool name to load appsettings.{apppoolname}.json for the application, it would be useful to be able to use the host configuration rather than system.environment to manually grab them.

davidfowl commented 1 year ago

I see, it’s reosnable to make them ASPNETCORE prefixed then

yaakov-h commented 1 year ago

@twsouthwick

@yaakov-h you've had https://github.com/dotnet/aspnetcore/issues/43632#issuecomment-1656640252 with environment variables like this. Can you expound on that?

I've had issues particularly with MSBuild, which used environment variables in a similar manner. Specifically:

  1. MSBuild in Visual Studio would have a bunch of environment variables set, which would be inherited by any app being debugged/run from VS. If that app launched a different version of MSBuild or Visual Studio as a child process, the child would get conflicting settings and spit strange errors.
  2. Microsoft.Build DLLs for modern .NET set a bunch of environment variables on the current process, which similarly get inherited by child processes. If I have a .NET Core app that loads MSBuild and then launches Visual Studio as a child process, the child gets conflicting settings and spits strange errors, even more so as it is a Framework app that thinks its configured to load a Core version of MSBuild.

I don't see the same situation happening here as easily, but it still could happen - if an IIS-hosted ASP.NET Core app launches a child or grandchild ASP.NET Core app, that child process could think it is being hosted in IIS but the settings it is seeing is that of the parent process and not its own.

twsouthwick commented 1 year ago

@yaakov-h I believe we went over these scenarios in the API review and we should include them in any documentation to be aware of.

yaakov-h commented 1 year ago

@twsouthwick yeah, I noticed it in the API review notes above and tried to see if there was more detail somewhere, but this API review doesn't seem to have been YouTubed. 🙃

As long as it's something that's solvable, then it should be fine. FWIW in the MSBuild case I had to move all of my project evaluation API calls to a subprocess, but here you can't exactly move "be hosted in IIS" to a subprocess.

twsouthwick commented 1 year ago

For those wanting to try it out, rc2 has the feature, and installing the rc2 hosting bundle will enable it in IIS. However, IIS Express is managed by VS and will require an update to the ANCM it ships to enable the feature from being inserted as expected. I'll update here when that's available