dotnet / dotnet/systemweb-adapters
Microsoft.AspNetCore.SystemWebAdapters.CoreServices 2.3.0 crashes on Linux with .NET 10 — IIISEnvironmentFeature removed from shared runtime
- Dominant language
- C#
- Stars
- 384
- Forks
- 76
- Avg merge
- 2h 24m
- Merged PRs (30d)
- 1
Description
## What happened
I upgraded my web application from .NET 8 to .NET 10 and deployed it on RHEL 9. The service crashes immediately at startup before handling a single request. It was working fine on .NET 8 with package version 1.4.0.
`IIISEnvironmentFeature` was removed from the Linux copy of `Microsoft.AspNetCore.Server.IIS.dll` in .NET 10. `SystemWebAdapters.CoreServices.dll` still references it. When `AddSystemWebAdapters()` registers the assembly as an MVC application part, `ControllerFeatureProvider` calls `GetDefinedTypes()` on it at startup — and crashes because the type no longer exists in the shared runtime.
## Exception
```text
Unhandled exception. System.Reflection.ReflectionTypeLoadException:
Unable to load one or more of the requested types.
Could not load type 'Microsoft.AspNetCore.Server.IIS.IIISEnvironmentFeature'
from assembly 'Microsoft.AspNetCore.Server.IIS, Version=10.0.0.0,
Culture=neutral, PublicKeyToken=adb9793829ddae60'.
at System.Reflection.RuntimeModule.GetDefinedTypes()
at Microsoft.AspNetCore.Mvc.Controllers.ControllerFeatureProvider.PopulateFeature(...)
at Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager.PopulateFeature[TFeature](...)
at Microsoft.AspNetCore.Mvc.ApplicationModels.ControllerActionDescriptorProvider.GetControllerTypes()
at Microsoft.AspNetCore.Builder.ControllerEndpointRouteBuilderExtensions.MapControllers(...)
at Program.$(String[] args)
```
## How to reproduce
1. Create a `net10.0` ASP.NET Core web app.
2. Reference `Microsoft.AspNetCore.SystemWebAdapters.CoreServices` 2.3.0.
3. Call `builder.Services.AddSystemWebAdapters()` and `app.UseSystemWebAdapters()` unconditionally.
4. Call `app.MapControllers()`.
5. Run on Linux with `Microsoft.AspNetCore.App 10.0.8`.
Process dies at startup with the exception above.
## What I checked
I ran `strings` against the runtime DLL to confirm the type is actually gone — this isn't a load path issue:
```bash
$ strings /usr/lib64/dotnet/shared/Microsoft.AspNetCore.App/10.0.8/Microsoft.AspNetCore.Server.IIS.dll \
| grep IIISEnvironmentFeature
# no output
```
`COREHOST_TRACE` confirms the assembly is coming straight from the shared framework, not a local copy:
```text
Adding tpa entry: /usr/lib64/dotnet/shared/Microsoft.AspNetCore.App/10.0.8/Microsoft.AspNetCore.Server.IIS.dll,
AssemblyVersion: 10.0.0.0, FileVersion: 10.0.826.23019
```
## Workaround
### Option 1 — OS guard
_Sufficient when `SystemWebAdapters.CoreServices` is the only assembly referencing the removed type._
`AddSystemWebAdapters()` is what registers `SystemWebAdapters.CoreServices.dll` as an MVC application part.
Wrapping with an OS check prevents the registration on Linux, so `GetDefinedTypes()` is never called on it:
```csharp
if (OperatingSystem.IsWindows())
{
builder.Services.AddSystemWebAdapters();
}
// ...
if (OperatingSystem.IsWindows())
{
app.UseSystemWebAdapters();
}
```
### Option 2 — Resilient `ControllerFeatureProvider`
_Needed when other assemblies in the project (e.g. pre-compiled DLLs built against .NET 8) also carry a reference to `IIISEnvironmentFeature`._
Replace `ControllerFeatureProvider` with an implementation that catches `ReflectionTypeLoadException` during type scanning.
> **Important:** this must run _after_ all `AddMvc` / `AddControllersWithViews` calls, because each of those internally checks `OfType().Any()` and re-adds the default one if none is found.
```csharp
builder.Services.AddControllersWithViews()
.ConfigureApplicationPartManager(manager =>
{
var toRemove = manager.FeatureProviders
.OfType()
.ToList();
foreach (var fp in toRemove)
manager.FeatureProviders.Remove(fp);
manager.FeatureProviders.Add(new ResilientControllerFeatureProvider());
});
```
`ResilientControllerFeatureProvider` must **inherit** `ControllerFeatureProvider` (so the internal `Any()` guard does not re-add the original) and use **explicit interface implementation** so `ApplicationPartManager` dispatches to the resilient `PopulateFeature` override instead of the base class method:
```csharp
internal sealed class ResilientControllerFeatureProvider
: ControllerFeatureProvider, IApplicationFeatureProvider
{
void IApplicationFeatureProvider.PopulateFeature(
IEnumerable parts, ControllerFeature feature)
{
foreach (var part in parts.OfType())
{
IEnumerable types;
try
{
types = part.Types.ToList();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types
.Where(t => t is not null)
.Select(t => t!.GetTypeInfo());
}
foreach (var typeInfo in types)
{
try
{
if (IsController(typeInfo) && !feature.Controllers.Contains(typeInfo))
feature.Controllers.Add(typeInfo);
}
catch { }
}
}
}
}
```
## The problem
The package lists `net10.0` as a supported TFM on NuGet, but it references a type that no longer exists in the .NET 10 Linux shared runtime. Either:
- the NuGet compatibility metadata should be restricted to `net10.0-windows`, or
- the reference to `IIISEnvironmentFeature` inside the package should be guarded so it does not cause reflection-based scanning to fail on non-Windows platforms.
## Environment
| Field | Value |
|---|---|
| OS | RHEL 9 x64 |
| .NET runtime | `Microsoft.AspNetCore.App 10.0.8` |
| .NET host | `10.0.8`, commit `94ea82652c`, path `/usr/lib64/dotnet/dotnet` |
| Broken package | `Microsoft.AspNetCore.SystemWebAdapters.CoreServices` 2.3.0 |
| Also referenced | `Microsoft.AspNetCore.SystemWebAdapters` 2.3.0 |
| Last known good | `Microsoft.AspNetCore.SystemWebAdapters.CoreServices` 1.4.0 on .NET 8 / Linux |
Contributor guide
Assessment
This issue has not been assessed yet.