Closed yaakov-h closed 1 year ago
@davidfowl I've re-filed here.
We've run into similar problems (IIS site name in our case), and the way we managed to work around this limitation, was to introduce a well-known environment variable via system.webServer/aspNetCore/environmentVariables in applicationHost.config for each site manually.
Getting the application virtual path during startup can currently be done by reading the ASPNETCORE_APPL_PATH environment variable; although that does not appear to be documented. But .NET 7 and below depend on it too, so it's not likely to go away soon (just plan for contingencies for when it does, like juggling aspNetCore/environmentVariables manually).
Triage: we can consider adding something like this, though we'd need to think about the API design more. Once we've agreed on the design, we would take a PR to add this.
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
Marking ready for review so we remember to discuss it.
For reference - this might be something that would overlap with the work being done in https://github.com/dotnet/systemweb-adapters and specifically HttpRuntimeFactory which allows mocking/testing the properties.
API review notes:
@halter73 are there any further details from API review?
Personally a static property (like in ASP.NET / .NET Framework) would also work, and I could then wrap it up in something more DI-able with private code or a NuGet package.
I don't think a server feature would be appropriate as (AFAIK) that cannot be accessed before calling WebApplicationBuilder.Build()
/WebHostBuilder.Build()
.
The main takeaway is that we want to have someone look at this closely and try implementing a static API and see if there are any implementation issues before we approve the final API shape.
@halter73 Can you clarify? This API should not exist on anything that isn't IIS specific and it would couple the code in a way that was undesirable. This is why it's a configuration property:
Let's try avoid the static API here please.
EDIT: I see, the configuration API as a property. That's fine 😄
It was @Tratcher's idea to make this a static API, but it makes sense. This is environmental data that's either available immediately when the process starts or it isn't. .NET also ships static APIs to read environment variables. I see no reason to make you initialize the host before reading it
If you care about testing it, abstract it yourself, just like you would for environment variables. I suppose we could also put it in config like we do with environment variables, but it should come from a static source.
I think it's a bad idea to expose it as a static API. We don't do anything else like this in ASP.NET Core (it's one of the original design principles). Because this is so special, and IIS specific, it should not exists as a first class static API. If it wasn't tied to IIS I'd put couple it to the IWebHostEnvironment
. Maybe we could add an IFeatureCollection or property bag there to expose static state like this in a non-static way.
Let me try to propose some alternatives. We could have an IISWebHostEnvironment that exposes all of the static state understood by IIS as first class properties:
class IISWebHostEnvironment : IWebHostEnvironment
{
public string ConfigurationPath { get; }
// More properties from IWebHostEnvironment
}
When you call the IIS configuration logic would replace the IHostEnvironment
with this implementation (probably with a wrapper) and code in the application can do this:
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment is IISWebHostEnvironment env)
{
var configPath = env.ConfigurationPath; // "/LM/W3SVC/3/ROOT"
var siteID = configPath.Split('/')[3];
builder.Configuration.AddJsonFile($"MyAppConfig-Site{siteID}.json");
}
var app = builder.Build();
app.Run();
Problems:
IWebHostEnvironment
flows via the DI Container after having replaced it on the WebHostBuilderContext
(does that work today?)IWebHostEnvironment
? This isn't composable.Another approach would be a feature based one:
interface IISEnvironmentFeature
{
string ConfigurationPath { get; }
}
We add an IFeatureCollection to IWebHostEnvironment:
interface IWebHostEnvironment
{
+ IFeatureCollection Features { get; }
}
When you call the IIS configuration logic would add the feature to the collection.
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.Features.Get<IISEnvironmentFeature>() is { } env)
{
var configPath = env.ConfigurationPath; // "/LM/W3SVC/3/ROOT"
var siteID = configPath.Split('/')[3];
builder.Configuration.AddJsonFile($"MyAppConfig-Site{siteID}.json");
}
var app = builder.Build();
app.Run();
This approach follows the same pattern as Server features and even Http features.
When originally trying to find an API shape for this feature in #43631, I also considered a special subclass of IWebHostEnvironment
but when looking at the implementation of that today, replacing it with a subclass seemed like a particularly invasive change just to pass around a single, small, rarely-used string. It also has the same problem @davidfowl mentioned of not being able to have multiple features each follow the same pattern and replace the host environment.
A builder-level or environment-level feature collection is interesting. (I assume you meant builder.Environment.Features in the example snippet?)
Just on this comment, because it's sticking in the back of my head:
If you care about testing it, abstract it yourself, just like you would for environment variables.
TBH nowadays I mostly use environment variables that map to IConfiguration
/IOption[Monitor|Snapshot]<T>
anyway.
The out-of-box abstractions are flexible and powerful enough that I haven't had to write my own abstractions for ASP.NET Core applications in quite a long time, and I'd expect that to continue for fairly standard things.
Not to hijack this at all, but it might be nice to surface a couple of other IIS properties in the IISEnvironmentFeature
AppPoolId
, AppPoolConfig
, AppDomainAppId
, IISVersion
, InstanceID
, and InstanceMetaPath
would be nice.
The first two can be got via environment variables - APP_POOL_ID
and APP_POOL_CONFIG
, and the others can be found via server variables - APPL_MD_PATH
, SERVER_SOFTWARE
, INSTANCE_ID
, and INSTANCE_META_PATH
, but only during a request.
I think most of these can be surfaced via the existing interface, perhaps with the exception of the version number.
I couldn't find any reference source for webengine4.dll to see how it does it and perhaps have a go at adding it to ANCM.
[DllImport(_IIS_NATIVE_DLL)]
internal static extern void MgdGetIISVersionInformation(
[Out] out uint pdwVersion,
[Out] out bool pfIsIntegratedMode);
For properties that are only available during a request, the existing API should be able to retrieve them, and I would expect code being ported from ASP.NET to ASP.NET Core to already only be handling them during a request.
(If they can also be fetched outside of a request then I'd be happy to scope-creep this somewhat to include them, if doing so is reasonable.)
The issue I have with the IIS config path specifically is that in server variables it is only available during a request, but it is available in ASP.NET from other sources outside of the request and for the entire application lifetime, and I have existing code and design patterns that rely on this.
INSTANCE_META_PATH
looks like it's exactly the same thing as HttpRuntime.AppDomainAppId
, as does APPL_MD_PATH
.
INSTANCE_ID
looks like its the same derived value that I'm ultimately after (the site ID).
Perhaps this proposal can solve all four at once.
We already have a FeatureCollection on IServer and IApplicationBuilder to convey server specific information. Is that available early enough?
Nope, it's not early enough. That's the problem.
@CZEMacLeod I think it makes sense to expose a bunch of properties as well.
Coming back to this, I had a closer look again and I seem to have mixed up Configuration Path and Application ID. Oops
Application ID is what I'm after here, Configuration Path is slightly different, but we could expose that as well if neccesary.
I think this would be the best alternative to a static API:
namespace Microsoft.AspNetCore.Hosting;
public interface IWebHostEnvironment : IHostEnvironment
{
+ IFeatureCollection Features { get; }
string WebRootPath { get; set; }
IFileProvider WebRootFileProvider { get; set; }
}
namespace Microsoft.AspNetCore.Server.IIS;
+public class IISApplicationFeature
+{
+ string ApplicationId { get; }
+}
However it comes with the caveat that we're adding a new member to an interface, which is a breaking change for any implementers.
Is that acceptable to do in a major version (hopefully .NET 8), or would we need to mitigate it somehow (IWebHostEnvironment2
, Default Method Implementation, new interface, etc.)?
@adityamandaleeka Can the above be presented for API review or is it a non-starter to make that kind of breaking change?
Some of the APIs have been requested in the dotnet/systemweb-adapters project and it would be great to enable them here (not sure how to do some without access to the native module). I have a prototype of what this would look like here.
As @yaakov-h mentioned, it does require changes to native code side. For the most part, it can be isolated to changes in InProcessRequestHandler
which probably has a smaller compat bar. However, to retrieve the version information, a change must be made to ANCM itself (the version is passed into the initial RegisterModule
function export).
I added all the properties I found mentioned above and came up with the following:
namespace Microsoft.AspNetCore.Hosting
{
public interface IWebHostEnvironment : IHostEnvironment
{
+ IFeatureCollection Features { get; }
string WebRootPath { get; set; }
IFileProvider WebRootFileProvider { get; set; }
}
}
namespace Microsoft.AspNetCore.Server.IIS
+{
+ /// <summary>
+ /// This feature provides access to IIS application information
+ /// </summary>
+ public interface IISEnvironmentFeature
+ {
+ /// <summary>
+ /// Gets the version of IIS that is being used.
+ /// </summary>
+ Version IISVersion { get; }
+
+ /// <summary>
+ /// Gets the AppPool Id that is currently running
+ /// </summary>
+ string AppPoolId { get; }
+
+ /// <summary>
+ /// Gets path to the AppPool configuration that is currently running
+ /// </summary>
+ string AppPoolConfig { get; }
+
+ /// <summary>
+ /// Gets the path of the application.
+ /// </summary>
+ string ApplicationPath { get; }
+
+ /// <summary>
+ /// Gets the virtual path of the application.
+ /// </summary>
+ string ApplicationVirtualPath { get; }
+
+ /// <summary>
+ /// Gets ID of the current application.
+ /// </summary>
+ string ApplicationId { get; }
+
+ /// <summary>
+ /// Gets the name of the current site.
+ /// </summary>
+ string SiteName { get; }
+
+ /// <summary>
+ /// Gets the id of the current site.
+ /// </summary>
+ uint SiteId { get; }
+ }
}
Example:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Server.IIS;
var builder = WebApplication.CreateBuilder();
// Can access it once the builder is created
var feature = builder.Environment.Features.Get<IISEnvironmentFeature>();
builder.Build().Run();
High level overview of changes:
IISConfigurationData
struct to transfer the dataInProcessRequestHandler
so the existing managed exports can be usedUseIIS
to add the IISEnvironmentFeature to IWebHostEnvironment.Features
Questions:
IISEnvironmentFeature.IISVersion
property be nullableIWebHostEnvironment.Features
be accessible via the IServer.Features
? i.e. IServerFeatures.Features = new FeatureCollection(IWebHostEnvironment.Features)
Could this be simplified by making IISEnvironmentFeature available as a DI service instead of a feature? We wouldn't need to change IWebHostEnvironment.
I think the main driving force for the IWebHostEnvironment change was that it should be available before IServiceProvider is available. One of the example scenarios is using one of the values (such as the SiteId) as a key for which config file should be added.
It still wouldn't be available until some time after UseIIS is called, and UseIIS doesn't have direct access to IWebHostEnvironment. It would have to register for one of the Configure callbacks, where it would still run after most other registrations. https://github.com/dotnet/aspnetcore/blob/bf52af43b0f77e1ee66096097554c3c36c04c6e6/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs#L23
Yes, on the branch I linked I registered a configure callback, and it's available for use in this pattern (which covers the scenarios described originally):
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Server.IIS;
var builder = WebApplication.CreateBuilder();
// Can access it once the builder is created
var feature = builder.Environment.Features.Get<IISEnvironmentFeature>();
var app = builder.Build();
app.Run();
There may be two issues at play here that we may address separately:
IServer.Features
)IWebHostEnvironment.Features
)Thanks, @twsouthwick!
Just to note that I've been bitten in the past by using environment variables in this manner (particularly by MSBuild). If one ASP.NET Core site in IIS launches another in a subprocess, then it would by default inherit this piece of version data...
ConfigureServices is too late to be specifying environment data, especially for the example you gave of using it to load config files. You'd want to do this in ConfigureHostConfiguration, but even then it will be order dependent on if it's available to other callers of ConfigureHostConfiguration.
We'd also need a way to avoid the API breaking change of adding a member to IWebHostEnvironment. Do Default Interface Methods work on properties, and can they store state?
I hate to say it, but this information might have to be made available from a static method for people that need it ASAP, like in Program.cs before the host is constructed.
Just to note that I've been bitten in the past by using environment variables in this manner
I've updated my POC to dynamically load aspnetcorev2.dll
- not sure if that's better or worse, but works
We'd also need a way to avoid the API breaking change of adding a member to IWebHostEnvironment. Do Default Interface Methods work on properties, and can they store state?
AFAIK they can't store state. However, what about the default implementation be a read only IFeatureCollection? I pushed a change that does that.
ConfigureServices is too late to be specifying environment data, especially for the example you gave of using it to load config files.
I may be missing something here as the following is working (which is adapted from original scenario to use an in-memory collection rather than a json for simplicity):
using Microsoft.AspNetCore.Server.IIS;
var builder = WebApplication.CreateBuilder();
var feature = builder.Environment.Features.Get<IISEnvironmentFeature>();
builder.Configuration.AddInMemoryCollection(new[] { new KeyValuePair<string, string>("IISSite", feature.SiteName) });
var site = builder.Configuration["IISSite"];
var app = builder.Build();
app.Run();
The value is available in the environment after creating the builder, which, from the thread, appears to be early enough. I'm not familiar with how the builders here work, so maybe it happens to work with the WebApplication builder? If you open IISIntegration.slnf, set NativeIISSample as the startup project, select 'ANCM IIS Express' as the profile and hit F5, you can see that it's surfacing the information at the time the original issue was made available.
I might be being naive here, but wouldn't it be possible to do this with a simple callback from UseIIS
with the feature data?
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseIIS(env =>
{
builder.Configuration.AddInMemoryCollection(env.AsKeyValuePairs());
builder.Configuration.AddJsonFile($"appsettings.{env.SiteName}.json");
});
namespace Microsoft.AspNetCore.Hosting
{
public static class IISExtensions
{
public static IWebHostBuilder UseIIS(this IWebHostBuilder hostBuilder, Action<IISEnvironmentFeature> action)
{
if (hostBuilder == null)
{
throw new ArgumentNullException(nameof(hostBuilder));
}
// Check if in process
if (OperatingSystem.IsWindows() && NativeMethods.IsAspNetCoreModuleLoaded())
{
var iisConfigData = NativeMethods.HttpGetApplicationProperties();
action(iisConfigData);
return hostBuilder.ConfigureServices(
services =>
{
services.AddSingleton(new IISNativeApplication(new NativeSafeHandle(iisConfigData.pNativeApplication)));
services.AddSingleton<IServer, IISHttpServer>();
services.AddTransient<IISServerAuthenticationHandlerInternal>();
services.AddSingleton<IStartupFilter, IISServerSetupFilter>();
services.AddAuthenticationCore();
services.AddSingleton<IServerIntegratedAuth>(_ => new ServerIntegratedAuth()
{
IsEnabled = iisConfigData.fWindowsAuthEnabled || iisConfigData.fBasicAuthEnabled,
AuthenticationScheme = IISServerDefaults.AuthenticationScheme
});
services.Configure<IISServerOptions>(
options =>
{
options.ServerAddresses = iisConfigData.pwzBindings.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
options.ForwardWindowsAuthentication = iisConfigData.fWindowsAuthEnabled || iisConfigData.fBasicAuthEnabled;
options.MaxRequestBodySize = iisConfigData.maxRequestBodySize;
options.IisMaxRequestSizeLimit = iisConfigData.maxRequestBodySize;
}
);
});
}
return hostBuilder;
}
public static IEnumerable<KeyValuePair<string,string>> AsKeyValuePairs(this IISEnvironmentFeature feature, string? prefix = null)
{
prefix ??= "IIS_";
yield return new KeyValuePair<string, string>(prefix + nameof(feature.AppPoolId), feature.AppPoolId);
yield return new KeyValuePair<string, string>(prefix + nameof(feature.AppPoolConfig), feature.AppPoolConfig);
yield return new KeyValuePair<string, string>(prefix + nameof(feature.ApplicationId), feature.ApplicationId);
yield return new KeyValuePair<string, string>(prefix + nameof(feature.ApplicationPath), feature.ApplicationPath);
yield return new KeyValuePair<string, string>(prefix + nameof(feature.ApplicationVirtualPath), feature.ApplicationVirtualPath);
yield return new KeyValuePair<string, string>(prefix + nameof(feature.SiteName), feature.SiteName);
yield return new KeyValuePair<string, string>(prefix + nameof(feature.SiteId), feature.SiteId.ToString());
yield return new KeyValuePair<string, string>(prefix + nameof(feature.IISVersion), feature.IISVersion.ToString());
}
}
}
I know that normally the callback allows you to configure options etc. and in this case the 'options' is the readonly feature, but it would seem to fit the bill.
It looks like all the data is loaded immediately in the UseIIS
method by NativeMethods.HttpGetApplicationProperties()
I don't know if it would be possible to cache that somewhere in the WebApplicationBuilder
and/or to surface the callback in another way.
I understand that it might be more disruptive, but you could make the default UseIIS call copy the settings into the WebHostBuilder.
var iisConfigData = NativeMethods.HttpGetApplicationProperties();
hostBuilder.UseIISSettings(iisConfigData,"ASPNETCORE_IIS_");
public static IWebHostBuilder UseIISSettings(this IWebHostBuilder builder, IISEnvironmentFeature feature, string? prefix = null)
{
prefix ??= "IIS_";
builder.UseSetting(prefix + nameof(feature.AppPoolId), feature.AppPoolId);
builder.UseSetting(prefix + nameof(feature.AppPoolConfig), feature.AppPoolConfig);
builder.UseSetting(prefix + nameof(feature.ApplicationId), feature.ApplicationId);
builder.UseSetting(prefix + nameof(feature.ApplicationPath), feature.ApplicationPath);
builder.UseSetting(prefix + nameof(feature.ApplicationVirtualPath), feature.ApplicationVirtualPath);
builder.UseSetting(prefix + nameof(feature.SiteName), feature.SiteName);
builder.UseSetting(prefix + nameof(feature.SiteId), feature.SiteId.ToString());
builder.UseSetting(prefix + nameof(feature.IISVersion), feature.IISVersion.ToString());
return builder;
}
I think this could also replace builder.Configuration.AddInMemoryCollection(env.AsKeyValuePairs());
with builder.WebHost.UseIISSettings(env);
if using the callback method.
I don't know the (performance) implications of calling HttpGetApplicationProperties
twice, but would the following overload for UseIISSettings
not bypass the problem of replacing/creating multiple IISNativeApplication
objects etc. due to multiple calls to UseIIS
.
public static IWebHostBuilder UseIISSettings(this IWebHostBuilder hostBuilder, string? prefix = null, Action<IISEnvironmentFeature>? action=null)
{
if (hostBuilder == null)
{
throw new ArgumentNullException(nameof(hostBuilder));
}
// Check if in process
if (OperatingSystem.IsWindows() && NativeMethods.IsAspNetCoreModuleLoaded())
{
var iisConfigData = NativeMethods.HttpGetApplicationProperties();
hostBuilder.UseIISSettings(iisConfigData);
action?.Invoke(iisConfigData);
}
return hostBuilder;
}
Then the main program would strip down to
builder.WebHost.UseIISSettings(null, env =>
{
builder.Configuration.AddJsonFile($"appsettings.{env.SiteName}.json");
});
and still work as previously, but with the additional features requested.
If you need access to IISEnvironmentFeature
elsewhere in code, and you don't want to bind via settings, you could always use
builder.WebHost.UseIISSettings(null, env =>
{
builder.Configuration.AddJsonFile($"appsettings.{env.SiteName}.json");
builder.Services.AddSingleton(env);
});
instead.
I leave this to the greater minds than mine, but hope that this gets added in some form, as this would really match up with one of our workflows.
I like this approach more. It's simpler and lets us add more APIs in the future. Plus we can use a DIM to implement this on IWebHostEnvironment. @twsouthwick Do you have a branch?
Oh, WebApplication runs some calls like ConfigureServices immediately, but other hosts like WebHost delay them until Build. Make sure to try this on all of the hosts to see if the ordering works as expected. Host, WebHost, WebApplication, etc.
Oh, WebApplication runs some calls like ConfigureServices immediately, but other hosts like WebHost delay them until Build. Make sure to try this on all of the hosts to see if the ordering works as expected. Host, WebHost, WebApplication, etc.
Yup - you're right. My initial POC only worked with WebApplication and the Startup pattern. I've swapped it to use .ConfigureAppConfiguration
and it now works with the following patterns:
using Microsoft.AspNetCore.Server.IIS;
Host.CreateDefaultBuilder()
.ConfigureWebHost(builder =>
{
// Must be called before needing the feature
builder.UseIIS();
builder.ConfigureAppConfiguration((context, builder) =>
{
var feature = context.HostingEnvironment.Features.Get<IISEnvironmentFeature>();
});
})
.Start();
using Microsoft.AspNetCore.Server.IIS;
var builder = WebApplication.CreateBuilder();
var feature = builder.Environment.Features.Get<IISEnvironmentFeature>();
builder.Configuration.AddInMemoryCollection(new[] { new KeyValuePair<string, string>("IISSite", feature.SiteName) });
var site = builder.Configuration["IISSite"];
var app = builder.Build();
app.Run();
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Server.IIS;
var builder = WebHost.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, builder) =>
{
var feature = context.HostingEnvironment.Features.Get<IISEnvironmentFeature>();
})
.Build();
builder.Start();
To make this simpler, a general purpose way to configure the environment would be nice. Something like:
public static IWebHostBuilder ConfigureEnvironment(this IWebHostBuilder hostBuilder, Action<IWebHostEnvironment> configure)
If we want to include it, I've got it wire up here: https://github.com/dotnet/aspnetcore/commit/11cf8f0a73793e269d9e634342a458d97453fb61
public static IWebHostBuilder ConfigureEnvironment(this IWebHostBuilder hostBuilder, Action
configure)
I don't think we want that.
Better, thanks. The order dependence is still unfortunate, but at least it's more understandable. E.g. this breaks:
using Microsoft.AspNetCore.Server.IIS;
Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, builder) =>
{
var feature = context.HostingEnvironment.Features.Get<IISEnvironmentFeature>();
})
.ConfigureWebHost(builder =>
{
// Must be called before needing the feature
builder.UseIIS();
})
.Start();
@Tratcher Are you sure that breaks? Pretty sure the ConfigureWebHost
lambda is called before ConfigureAppConfiguration
in build/start. The feature is available immediately on calling `UseIIS (does not require services to be built), and should be available in ConfigureAppConfiguration.
I expect it to break. UseIIS uses ConfigureAppConfiguration to set IISEnvironmentFeature, so any calls to ConfigureAppConfiguration registered before UseIIS is called won't see it. This however would work:
using Microsoft.AspNetCore.Server.IIS;
Host.CreateDefaultBuilder()
.ConfigureWebHost(builder =>
{
// Must be called before needing the feature
builder.UseIIS();
})
.ConfigureAppConfiguration((context, builder) =>
{
var feature = context.HostingEnvironment.Features.Get<IISEnvironmentFeature>();
})
.Start();
@Tratcher Thanks for the clarification - I hadn't noticed the implementation detail of using ConfigureAppConfiguration
to add the Feature.
This is why I suggested an overload for UseIIS
with an Action<IISEnvironmentFeature>
.
The following does work (I used the existing FullApplicationPath
property as I didn't have the full extended version of IISConfigurationData
/IISEnvironmentFeature
).
Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, builder) =>
{
var appPath = context.Configuration["IIS:FullApplicationPath"];
if (appPath is not null)
{
var last = appPath.Split('\\', StringSplitOptions.RemoveEmptyEntries).Last();
builder.AddJsonFile($"appsettings.{last}.json", true, true);
}
})
.ConfigureWebHost(builder =>
{
builder
.UseStartup<Startup>()
.UseIIS(iis =>
{
builder.UseSetting("IIS:FullApplicationPath", iis.FullApplicationPath);
});
})
.Build()
.Run();
I created all 3 hosting versions here: https://github.com/CZEMacLeod/HttpRuntime-Equiv
@Tratcher You're example doesn't work for a different reason: that call to ConfigureAppConfiguration
provides a HostBuilderContext
which doesn't have the .Features
property; that's on the WebHostBuilderContext
. But your point about ordering being an issue is real.
However, in practice, will it be an issue? Seems like something to just make sure you order correctly (similar to how middleware need to be ordered correctly). It seems like it will solve the main issue of accessing it early enough in configuration scenarios, just requires a little bit of care if used there; other uses that need it will be run after all these callbacks are finished and it's a non-issue.
After chatting with @Tratcher, it may be easier to first get the IIS details plumbed through and then figure out the ordering issue. Here's an updated proposal:
A number of IIS parameters were exposed in ASP.NET via various APIs (System.Web.HttpRuntime
, System.Web.Hosting.HostingEnvironment
, etc) but are not available in ASP.NET Core. These are available on the native side of the IIS module, but are not surfaced to the application in a usable way. Some of them are currently used internally and are available as-is, but others are not and require a change to the IIS module in order to access them. There's no great way currently to access these at the application level and would benefit from a first class API to get at them.
namespace Microsoft.AspNetCore.Server.IIS;
/// <summary>
/// This feature provides access to IIS application information
/// </summary>
public interface IIISEnvironmentFeature
{
/// <summary>
/// Gets the version of IIS that is being used.
/// </summary>
Version IISVersion { get; }
/// <summary>
/// Gets the AppPool name that is currently running
/// </summary>
string AppPoolName { get; }
/// <summary>
/// Gets the path to the AppPool config
/// </summary>
string AppPoolConfigFile { get; }
/// <summary>
/// Gets path to the application configuration that is currently running
/// </summary>
string AppConfigPath { get; }
/// <summary>
/// Gets the physical path of the application.
/// </summary>
string ApplicationPhysicalPath { get; }
/// <summary>
/// Gets the virtual path of the application.
/// </summary>
string ApplicationVirtualPath { get; }
/// <summary>
/// Gets ID of the current application.
/// </summary>
string ApplicationId { get; }
/// <summary>
/// Gets the name of the current site.
/// </summary>
string SiteName { get; }
/// <summary>
/// Gets the id of the current site.
/// </summary>
uint SiteId { get; }
}
IServer.Features
: var envFeature = context.RequestServices.GetService<IServer>().Features.Get<IIISEnvironmentFeature>();
await context.Response.WriteAsync(Environment.NewLine);
await context.Response.WriteAsync("IIS Environment Information:" + Environment.NewLine);
await context.Response.WriteAsync("IIS Version: " + envFeature.IISVersion + Environment.NewLine);
await context.Response.WriteAsync("ApplicationId: " + envFeature.ApplicationId + Environment.NewLine);
IWebHostEnvironment
If we want to make it available early in the hosting, we could surface it via a DIM on IWebHostEnvironment
:
public interface IWebHostEnvironment : IHostEnvironment
{
+ public IFeatureCollection Features => EmptyFeatureCollection.Instance;
}
+internal sealed class EmptyFeatureCollection : IFeatureCollection
+{
+ private EmptyFeatureCollection()
+ {
+ }
+
+ public static IFeatureCollection Instance = new EmptyFeatureCollection();
+
+ public object? this[Type key]
+ {
+ get => null,
+ set => throw new NotImplementedException();
+ }
+
+ public bool IsReadOnly => true;
+
+ public int Revision => 0;
+
+ public TFeature? Get<TFeature>() => default;
+
+ public IEnumerator<KeyValuePair<Type, object>> GetEnumerator() => Enumerable.Empty<KeyValuePair<Type, object>>().GetEnumerator();
+
+ public void Set<TFeature>(TFeature? instance) => throw new NotSupportedException();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+}
This adds an ordering issue in which it is only available after .UseIIS()
is called. It also doesn't cover scenarios where the values may be needed before a host is even started. This was tabled for now to first get the data plumbed through, and then we can decide if we need to add the DIM for environment features.
I'm currently using environment variables to share the values from the module. However, a concern was raised about some issues encountered in the past with similar patterns. An alternative would be add an exported function that we call into via interop. This would not be as easy for downlevel to pick up on, but could be self-contained reasonably enough.
There is a need to access it early on before IServer
is available (and potentialy before any hosting is started). We could add a static API that would enable this:
namespace Microsoft.AspNetCore.Server.IIS;
public static class IISUtility
{
/// <summary>
/// Gets the <see cref="IIISEnvironmentFeature"/> for the current application if available.
/// If possible, prefer <see cref="IServer.Features"/> to access this value.
/// </summary>
public static IIISEnvironmentFeature? GetEnvironmentFeature();
}
However, if we use environment variables, it's probably sufficient to tell people to use those.
ANCM_{PropertyName}
Regarding risk (2), what happens when, for example, a .NET 6 app is running on a server that has both .NET 6 Hosting Bundle and .NET 8 Hosting Bundle installed?
Regarding risk (2), what happens when, for example, a .NET 6 app is running on a server that has both .NET 6 Hosting Bundle and .NET 8 Hosting Bundle installed?
@Tratcher I got it working with only updating the aspnetcorev2.dll library. Currently using your recommendation of environment variables, although we could maybe export a function to retrieve the values
@yaakov-h you've had issues with environment variables like this. Can you expound on that?
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
API Review Notes:
Who needs the IIS variable before they can get the IServer.Features
?
Will this be available for out-of-proc or HttpSys?
Are environment variables better than static properties?
Should we duplicate environment variables already set by IIS?
IIISEnvironmentFeature
.AppPoolName
(APP_POOL_ID
) and AppPoolConfigFile
(APP_POOL_CONFIG
) should not be duplicated.AppPoolName
to AppPoolId
on the feature.Why Path
in some places and File
in others?
IIIS
makes us sad, but it's consistent.
Do we care about uint
not being CLS compliant?
int
despite being unsigned natively.int
, but otherwise we think uint
is more straightforward.Let's use ANCM_{PROPERTY_NAME}
for the environment variables.
API Approved!
namespace Microsoft.AspNetCore.Server.IIS;
public interface IIISEnvironmentFeature
{
Version IISVersion { get; }
string AppPoolId { get; }
string AppPoolConfigFile { get; }
string AppConfigPath { get; }
string ApplicationPhysicalPath { get; }
string ApplicationVirtualPath { get; }
string ApplicationId { get; }
string SiteName { get; }
uint SiteId { get; }
}
@Tratcher @halter73 It was mentioned using configuration, but with the "ANCM" prefix, it doesn't show up in the IConfiguration. What about changing it to ASPNETCORE_IIS_{PROPERTY_NAME}
? This already exists actually for two values (including one here) so appears to be a pattern:
ASPNETCORE_IIS_HTTPAUTH: anonymous;
ASPNETCORE_IIS_PHYSICAL_PATH: W:\aspnetcore\src\Servers\IIS\IIS\samples\NativeIISSample\
If we do this, then we will need to duplicate the APP_POOL_ID
and APP_POOL_CONFIG
to have the prefix we need. Then they'll show up in IConfiguration:
@twsouthwick we add all environment variables to configuration by default.
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 viaIServerVariablesFeature
, 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
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 viaIWebHostBuilder.UseSetting
.Microsoft.AspNetCore.Server.IIS.Core.IISConfigurationData
will get a newstring
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
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:
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.