diff --git a/.gitignore b/.gitignore index c0f127435..c72620e9d 100644 --- a/.gitignore +++ b/.gitignore @@ -354,3 +354,4 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ +.nuget/ diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/IIISEnvironmentFeature.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/IIISEnvironmentFeature.cs index 05726be03..3efeaced7 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/IIISEnvironmentFeature.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/IIISEnvironmentFeature.cs @@ -1,14 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if !NET8_0_OR_GREATER using System; -namespace Microsoft.AspNetCore.Server.IIS; +namespace Microsoft.AspNetCore.SystemWebAdapters.Features; /// /// This feature provides access to IIS application information. This is available in-box -/// on .NET 8, but we have an internal copy so we can utilize its features on downlevel versions. +/// on newer .NET versions, but we have an internal copy so we can use this shape regardless of runtime packaging. /// internal interface IIISEnvironmentFeature { @@ -57,4 +56,3 @@ internal interface IIISEnvironmentFeature /// uint SiteId { get; } } -#endif diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HostingRuntimeExtensions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HostingRuntimeExtensions.cs index 726d7ba91..fa17951fb 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HostingRuntimeExtensions.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HostingRuntimeExtensions.cs @@ -2,13 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Web; using System.Web.Hosting; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Server.IIS; +using Microsoft.AspNetCore.SystemWebAdapters.Features; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -18,6 +20,33 @@ namespace Microsoft.AspNetCore.SystemWebAdapters; internal static class HostingRuntimeExtensions { + private static readonly Type? RuntimeIIISEnvironmentFeatureType = Type.GetType("Microsoft.AspNetCore.Server.IIS.IIISEnvironmentFeature, Microsoft.AspNetCore.Server.IIS"); + + private static readonly MethodInfo? RuntimeFeatureProxyCreateMethod = CreateRuntimeFeatureProxyCreateMethod(); + + private static MethodInfo? CreateRuntimeFeatureProxyCreateMethod() + { + if (RuntimeIIISEnvironmentFeatureType is null) return null; + + try + { + var createMethod = typeof(DispatchProxy) + .GetMethod(nameof(DispatchProxy.Create), BindingFlags.Public | BindingFlags.Static); + + if (createMethod is null) return null; + + return createMethod.MakeGenericMethod(RuntimeIIISEnvironmentFeatureType, typeof(RuntimeFeatureProxy)); + } + catch (ArgumentException) + { + return null; + } + catch (InvalidOperationException) + { + return null; + } + } + public static void AddHostingRuntime(this IServiceCollection services) { services.TryAddSingleton(); @@ -39,7 +68,7 @@ private sealed class EnvironmentFeatureConfigureOptions(IServer server) : IConfi { public void Configure(SystemWebAdaptersOptions options) { - if (server.Features.Get() is { } feature) + if (TryGetEnvironmentFeature(server, out var feature)) { options.ApplicationPhysicalPath = feature.ApplicationPhysicalPath; options.ApplicationVirtualPath = feature.ApplicationVirtualPath; @@ -93,12 +122,29 @@ public HostingEnvironmentStartupFilter(HostingEnvironmentAccessor accessor) public Action Configure(Action next) => builder => { - // We ensure this feature is available for both pre-.NET 8 as well as .NET 8+ on non-IIS systems if the right environment variables are set - if (builder.ApplicationServices.GetService() is { } server && server.Features.Get() is null) + if (builder.ApplicationServices.GetService() is { } server) { - if (IISEnvironmentFeature.TryCreate(builder.ApplicationServices.GetRequiredService(), out var feature)) + // Ensure our internal feature type is set so it can be accessed regardless of runtime packaging + if (server.Features.Get() is null) + { + if (RuntimeIIISEnvironmentFeatureType is { } runtimeType && server.Features[runtimeType] is { } runtimeFeature) + { + // Runtime IIS type is set (e.g., running on IIS) - bridge it to our internal type + server.Features.Set(new RuntimeIIISEnvironmentFeature(runtimeFeature)); + } + else if (IISEnvironmentFeature.TryCreate(builder.ApplicationServices.GetRequiredService(), out var feature)) + { + // No IIS server feature - populate from config environment variables + server.Features.Set(feature); + } + } + + // Ensure the runtime type is also set when our internal type is populated and the runtime type exists + if (RuntimeIIISEnvironmentFeatureType is { } rType + && server.Features[rType] is null + && server.Features.Get() is { } internalFeature) { - server.Features.Set(feature); + TrySetRuntimeFeature(server, rType, internalFeature); } } @@ -106,6 +152,51 @@ public Action Configure(Action next) }; } + private static bool TryGetEnvironmentFeature(IServer server, [NotNullWhen(true)] out IIISEnvironmentFeature? feature) + { + if (server.Features.Get() is { } existing) + { + feature = existing; + return true; + } + + if (RuntimeIIISEnvironmentFeatureType is { } type && server.Features[type] is { } runtimeFeature) + { + feature = new RuntimeIIISEnvironmentFeature(runtimeFeature); + return true; + } + + feature = null; + return false; + } + + private static void TrySetRuntimeFeature(IServer server, Type runtimeType, IIISEnvironmentFeature source) + { + if (RuntimeFeatureProxyCreateMethod is not { } createMethod) return; + + try + { + var proxy = createMethod.Invoke(null, null); + + if (proxy is not RuntimeFeatureProxy runtimeProxy) return; + + runtimeProxy.Source = source; + server.Features[runtimeType] = proxy; + } + catch (TargetInvocationException) + { + // If proxy creation fails for any reason, continue without surfacing the runtime type + } + catch (MethodAccessException) + { + // If proxy creation fails for any reason, continue without surfacing the runtime type + } + catch (InvalidOperationException) + { + // If proxy creation fails for any reason, continue without surfacing the runtime type + } + } + /// /// Copy of https://github.com/dotnet/aspnetcore/blob/4218bd758012820a955b0185e5b1824168d00c6a/src/Servers/IIS/IIS/src/Core/IISEnvironmentFeature.cs /// @@ -164,4 +255,82 @@ private IISEnvironmentFeature(IConfiguration configuration) public uint SiteId { get; } } + + private sealed class RuntimeIIISEnvironmentFeature : IIISEnvironmentFeature + { + public RuntimeIIISEnvironmentFeature(object feature) + { + var properties = new Dictionary(StringComparer.Ordinal); + + foreach (var property in feature.GetType().GetProperties()) + { + properties[property.Name] = property; + } + + IISVersion = GetVersionProperty(feature, properties, nameof(IISVersion)); + AppPoolId = GetStringProperty(feature, properties, nameof(AppPoolId)); + AppPoolConfigFile = GetStringProperty(feature, properties, nameof(AppPoolConfigFile)); + AppConfigPath = GetStringProperty(feature, properties, nameof(AppConfigPath)); + ApplicationPhysicalPath = GetStringProperty(feature, properties, nameof(ApplicationPhysicalPath)); + ApplicationVirtualPath = GetStringProperty(feature, properties, nameof(ApplicationVirtualPath)); + ApplicationId = GetStringProperty(feature, properties, nameof(ApplicationId)); + SiteName = GetStringProperty(feature, properties, nameof(SiteName)); + SiteId = GetUIntProperty(feature, properties, nameof(SiteId)); + } + + public Version IISVersion { get; } + + public string AppPoolId { get; } + + public string AppPoolConfigFile { get; } + + public string AppConfigPath { get; } + + public string ApplicationPhysicalPath { get; } + + public string ApplicationVirtualPath { get; } + + public string ApplicationId { get; } + + public string SiteName { get; } + + public uint SiteId { get; } + + private static string GetStringProperty(object feature, IReadOnlyDictionary properties, string propertyName) + => GetPropertyValue(feature, properties, propertyName) as string ?? string.Empty; + + private static Version GetVersionProperty(object feature, IReadOnlyDictionary properties, string propertyName) + => GetPropertyValue(feature, properties, propertyName) as Version ?? new Version(0, 0); + + private static uint GetUIntProperty(object feature, IReadOnlyDictionary properties, string propertyName) + => GetPropertyValue(feature, properties, propertyName) is uint value ? value : 0; + + private static object? GetPropertyValue(object feature, IReadOnlyDictionary properties, string propertyName) + => properties.TryGetValue(propertyName, out var property) ? property.GetValue(feature) : null; + } + + /// + /// A DispatchProxy-based adapter that implements the runtime IIS feature type by delegating to our internal . + /// This allows consumers that use Microsoft.AspNetCore.Server.IIS.IIISEnvironmentFeature directly to find the feature on the server + /// even when it was populated via configuration environment variables rather than the IIS server. + /// + private sealed class RuntimeFeatureProxy : DispatchProxy + { + internal IIISEnvironmentFeature? Source { get; set; } + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + => targetMethod?.Name switch + { + "get_IISVersion" => Source?.IISVersion, + "get_AppPoolId" => Source?.AppPoolId, + "get_AppPoolConfigFile" => Source?.AppPoolConfigFile, + "get_AppConfigPath" => Source?.AppConfigPath, + "get_ApplicationPhysicalPath" => Source?.ApplicationPhysicalPath, + "get_ApplicationVirtualPath" => Source?.ApplicationVirtualPath, + "get_ApplicationId" => Source?.ApplicationId, + "get_SiteName" => Source?.SiteName, + "get_SiteId" => Source?.SiteId, + _ => null, + }; + } } diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/HttpRuntimeIntegrationTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/HttpRuntimeIntegrationTests.cs index 8b1468e4d..c0cf17ba7 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/HttpRuntimeIntegrationTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/HttpRuntimeIntegrationTests.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Server.IIS; +using Microsoft.AspNetCore.SystemWebAdapters.Features; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection;