Skip to content

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

@yaakov-h

Description

@yaakov-h

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:

  • WebHostBuilderIISExtensions.UseIIS will set this key via IWebHostBuilder.UseSetting.
  • Microsoft.AspNetCore.Server.IIS.Core.IISConfigurationData will get a new string field to transfer this value from the native IIS module to the managed ASP.NET Core runtime.
  • http_get_application_properties will copy the config path from unmanaged code into the struct to be marshalled back to managed code.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-networkingIncludes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions