Hi Govert
I am trying to run a small .net 8.0 kestrel sever from an Excel Dna addin. I can get it to successfully run, but there are a lot of issues related to which assembly load context the asp net core components are loaded into. The asp net core hosting seems to get loaded into the Default ALC, which are then not able to discover any controllers in my assembly. It is possible to work around this specific issue by explicitly adding the loaded assembly and disabling the auto load (the commented code in AutoOpen).
Microsoft.AspNetCore.Hosting.Diagnostics: Critical: Hosting startup assembly exception
System.InvalidOperationException: Startup assembly ExcelDnaAspNetCore failed to execute. See the inner exception for more details.
---> System.IO.FileNotFoundException: Could not load file or assembly 'ExcelDnaAspNetCore, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.
...
There are also then further issues if direct or transient reference targets a newer version of framework assemblies (e.g. System.Text.Json 8.0.5 which isn't vulnerable). In my example you can make this occur by uncommenting the reference to Logging.Abstractions. This makes a copy of the newer DLL appear in the bin folder which the ExcelDnaAssemblyLoadContext will load. You can see in the Debug->Modules window that 2 different versions of Logging.Abstractions are loaded (the ref assembly in the Default ALC, and the bin's assembly in the ExcelDnaAssemblyLoadContext ALC). I can also work around this by explicitly adding a PackageReference with ExcludedAssets=all (or explicitly downgrading them) to avoid the DLLs being copied to the bin, but it is hard to know exactly which transitive references may end up being used by referenced projects. Also depending on the SDK version installed, it might not copy the DLL to the bin folder if you have a new enough patched version. E.g. donet SDK 8.0.7 copied System.Text.Json 8.0.5 to the bin, but 8.0.10 did not.
To prevent this from accidentally occurring, I might have to add in a custom msbuild target to detect when a reference assembly ends up copied to the bin folder and fail the build.
Is there a better way of getting all of this to work? I would have ideally liked one of the following:
- Expand
ExcelDnaAssemblyLoadContext so that the ref app/aspnetcore assemblies can be found and loaded into the addin's ALC (not Default)
- No ALC and just add our xll directory to APP_PATHS
- Return the Default's ALC already loaded assembly
- This is probably counter to the purpose of the ALC
My minimal example is as follows:
ExcelDnaAspNetCore.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ExcelAddInCustomRuntimeConfiguration>true</ExcelAddInCustomRuntimeConfiguration>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ExcelDna.AddIn" Version="1.8.0" />
<!-- This could have come from a referenced dependency -->
<!-- Uncomment for MissingMethodException -->
<!--<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />-->
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
Addin.cs
using ExcelDna.Integration;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Logging;
using System.Runtime.Loader;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using ExcelDna.Logging;
namespace ExcelDnaAspNetCore;
public class Addin : IExcelAddIn
{
public void AutoOpen()
{
LogDisplay.Show();
var host = Host.CreateDefaultBuilder()
.ConfigureLogging(l => l.AddDebug())
.ConfigureServices((h, s) =>
{
s.AddHttpClient();
s.AddControllers()
//.AddApplicationPart(typeof(Addin).Assembly) // workaround to find controllers in this loaded assembly
;
})
.ConfigureWebHostDefaults(web =>
{
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/platform-specific-configuration?view=aspnetcore-8.0#disable-automatic-loading-of-hosting-startup-assemblies
//web.UseSetting(WebHostDefaults.PreventHostingStartupKey, "true"); // workaround to not 'Assembly.Load(this assembly name)' from GenericWebHostBuilder.cs
web.ConfigureKestrel(options =>
{
options.ListenLocalhost(9999);
});
web.Configure(app =>
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapGet("/", () => JsonSerializer.Serialize("hello world!"));
});
});
})
.Start();
var logger = host.Services.GetRequiredService<ILogger<Addin>>();
logger.LogInformation("Loaded");
foreach (var alc in AssemblyLoadContext.All)
{
logger.LogInformation("### {Name} ###", alc.Name);
foreach (var assembly in alc.Assemblies)
logger.LogInformation("- {Name,-60}{Location}", assembly.GetName().Name, assembly.Location);
}
}
public void AutoClose() { }
}
[ApiController]
[Route("api/[controller]")]
public class TestController
{
[HttpGet]
public string Get() => "hello world";
} Regards
Steven