dotnet / sdk

Core functionality needed to create .NET Core projects, that is shared between Visual Studio and CLI
https://dot.net/core
MIT License
2.59k stars 1.03k forks source link

EnableDynamicLoading causes multiple loading of assemblies at runtime #41805

Open G-MW opened 1 week ago

G-MW commented 1 week ago

Describe the bug

Compile time

When adding <EnableDynamicLoading>true</EnableDynamicLoading> a copy of Microsoft.Extensions.DependencyInjection.Abstractions.dll is included in the output directory.

Runtime

At runtime (loading using the process described here) the assembly <output_directory>/Microsoft.Extensions.DependencyInjection.Abstractions.dll is loaded first and later another version of Microsoft.Extensions.DependencyInjection.Abstractions.dll is loaded from the dotnet installation directory. Then when trying to call the method BuildServiceProvider this exception is thrown:

Unhandled exception. System.MissingMethodException: Method not found: 'Microsoft.Extensions.DependencyInjection.ServiceProvider Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(Microsoft.Extensions.DependencyInjection.IServiceCollection)'.

Maybe this bug is related

List of Assemblies

These assemblies show this behaviour:

To Reproduce

  1. Create a new C# project containing:

    
    <Project Sdk="Microsoft.NET.Sdk">
    
    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <EnableDynamicLoading>true</EnableDynamicLoading>
    </PropertyGroup>
    
    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
    </ItemGroup>

2. Build using `dotnet build`
3. Now a copy of `Microsoft.Extensions.DependencyInjection.Abstractions.dll` is included in the output directory (usually `<project_dir>/bin/Debug/net8.0`)

### Further technical details
Output of `dotnet --info`:

.NET SDK: Version: 8.0.302 Commit: ef14e02af8 Workload version: 8.0.300-manifests.00402117 MSBuild version: 17.10.4+10fbfbf2e

Runtime Environment: OS Name: Windows OS Version: 10.0.22621 OS Platform: Windows RID: win-x64 Base Path: C:\Program Files\dotnet\sdk\8.0.302\

.NET workloads installed: There are no installed workloads to display.

Host: Version: 8.0.6 Architecture: x64 Commit: 3b8b000a0e

.NET SDKs installed: 7.0.410 [C:\Program Files\dotnet\sdk] 8.0.202 [C:\Program Files\dotnet\sdk] 8.0.302 [C:\Program Files\dotnet\sdk]

.NET runtimes installed: Microsoft.AspNetCore.App 5.0.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.28 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.31 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 7.0.17 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 7.0.20 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 8.0.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 8.0.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 5.0.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.28 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.31 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 7.0.17 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 7.0.20 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 8.0.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 8.0.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 5.0.4 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.28 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.31 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 7.0.17 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 7.0.20 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 8.0.3 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 8.0.6 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found: x86 [C:\Program Files (x86)\dotnet] registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables: Not set

global.json file: Not found

Learn more: https://aka.ms/dotnet/info

Download .NET: https://aka.ms/dotnet/download


### Workaround
Add this to your `.csproj` file to delete the assemblies from the output directory:
```xml
<Target Name="DeleteAssembliesFromOutoutPath" AfterTargets="PostBuildEvent">
    <Delete Files="$(OutputPath)\Microsoft.Extensions.DependencyInjection.Abstractions.dll" />
    <Delete Files="$(OutputPath)\Microsoft.Extensions.Logging.Abstractions.dll" />
</Target>
dotnet-issue-labeler[bot] commented 1 week ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

dotnet-issue-labeler[bot] commented 1 week ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

vitek-karas commented 1 week ago

@elinor-fung

elinor-fung commented 1 week ago

When adding <EnableDynamicLoading>true</EnableDynamicLoading> a copy of Microsoft.Extensions.DependencyInjection.Abstractions.dll is included in the output directory.

In the repro, this is expected. Microsoft.Extensions.DependencyInjection.Abstractions is part of the ASP.NET runtime, not the core runtime. Since the project uses Microsoft.NET.Sdk (as opposed to Microsoft.NET.Sdk.Web), the build determines that the library needs to carry Microsoft.Extensions.DependencyInjection.Abstractions with it. You can observe the same thing building an app (as in OutputType=Exe, not setting EnableDynamicLoading)

At runtime (loading using the process described here) the assembly <output_directory>/Microsoft.Extensions.DependencyInjection.Abstractions.dll is loaded first and later another version of Microsoft.Extensions.DependencyInjection.Abstractions.dll is loaded from the dotnet installation directory.

Are there other .NET components that get loaded by the process? Perhaps something that does depend on the ASP.NET runtime?

Using the load_assembly_and_get_function_pointer hosting option will load the assembly in a new, separate AssemblyLoadContext (ALC). My guess here is that <output_directory>/Microsoft.Extensions.DependencyInjection.Abstractions.dll is being loaded into that new ALC for your library, but something else causes Microsoft.Extensions.DependencyInjection.Abstractions.dll from the install to be loaded in the default ALC. And then the IServiceCollection is a different type in those two instances.

Is the MissingMethodException coming from a call your library makes? Do you also see Microsoft.Extensions.DependencyInjection.dll (which has the method called out in the exception you see) loaded from the install directory?

Add this to your .csproj file to delete the assemblies from the output directory:

If you are expecting those dependencies to be coming from a shared place (such that your library should not carry them with it), setting Private=false and ExcludeAssets=runtime on the PackageReference indicates that they should not be part of the library's output - this is what we recommend for plugins with shared dependencies.

<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0">
  <Private>false</Private>
  <ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
elinor-fung commented 1 week ago

Using the load_assembly_and_get_function_pointer hosting option will load the assembly in a new, separate AssemblyLoadContext

If you don't actually want your library loaded into a separate ALC, we have an API in .NET 8+ that allows loading a component into the default ALC (and then calling a function on it). That would make it so that the library lives in the default ALC, so dependencies like Microsoft.Extensions.DependencyInjection.Abstractions wouldn't be loaded into two different ALCs.