Sdk: Unable to specify a subdirectory for assembly loading

Created on 3 Jul 2019  Â·  21Comments  Â·  Source: dotnet/sdk

Steps to reproduce

Build a project for .NET Core 3. Add some nuget package.
Create a "lib" subdirectory in the output folder.
Move some/all of the .dll files into "lib".
Program will fail to run, as it is unable to locate the moved assemblies.

Details

When building a program using the standard framework, which used app.exe.config for its runtime configuration, you could specify private subdirectories that would be searched when resolving assembly loading:

  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="lib" />
    </assemblyBinding>
  </runtime>

I have been unable to find any directives that would reproduce that behavior in the new app.runtimeconfig.json configuration. The runtime spec documentation only mentions searching the root application directory, and doesn't indicate any means of getting the runtime to search alternate directories (even with the presumed restriction of only being allowed to search private subdirectories).

Use of additionalProbingPaths in app.runtimeconfig.json and app.runtimeconfig.dev.json does not seem to work.

Environment data

dotnet --info output:

.NET Core SDK (reflecting any global.json):
Version: 3.0.100-preview6-012264
Commit: be3f0c1a03

Runtime Environment:
OS Name: Windows
OS Version: 6.1.7601
OS Platform: Windows
RID: win7-x64
Base Path: C:\Program Files\dotnet\sdk\3.0.100-preview6-012264\

Host (useful for support):
Version: 3.0.0-preview6-27804-01
Commit: fdf81c6faf

.NET Core SDKs installed:
1.1.14 [C:\Program Files\dotnet\sdk]
2.1.602 [C:\Program Files\dotnet\sdk]
2.1.604 [C:\Program Files\dotnet\sdk]
2.1.700 [C:\Program Files\dotnet\sdk]
2.1.800-preview-009696 [C:\Program Files\dotnet\sdk]
2.2.202 [C:\Program Files\dotnet\sdk]
2.2.204 [C:\Program Files\dotnet\sdk]
2.2.300 [C:\Program Files\dotnet\sdk]
2.2.400-preview-010219 [C:\Program Files\dotnet\sdk]
3.0.100-preview6-012264 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
Microsoft.AspNetCore.All 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.0.0-preview6.19307.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 1.0.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 1.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 3.0.0-preview6-27804-01 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.0.0-preview6-27804-01 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Most helpful comment

That workaround is ugly and requires too much work.

@Kinematics I've successfully removed all files from the root folder and put them in the bin folder. The only thing remaining are the apphost exe files. Nothing else. I already mentioned it above but here's some more info.

The apphost exe (eg. myapp.exe if your entry point assembly is myapp.dll) has the path to your main dll file. You can patch the apphost exe so instead of having the path myapp.dll in it, it's bin\myapp.dll. When the exe is started, it will load your file from the bin folder. This is the patcher I use. What's left is just moving the output to the bin folder and moving the apphost exe to the root and you're done.

Moving your files to a bin folder is a common request and the SDK should have support for this. All it needs to do is move all files to the bin folder and make sure the apphost exe loads the file from the bin folder.

All 21 comments

I do not believe this is possible at in .Net Core.

@peterhuene to confirm or see if he has an idea here for this.

If you are trying to simplify the output of your application, have you looked at the single executable option?

If you are trying to simplify the output of your application, have you looked at the single executable option?

I've considered it, but a 60 MB release isn't acceptable compared to something that should be less than 1 MB. The issue is the noise as more nuGet packages are added. They're tiny in size, but add huge numbers of extra files that really don't need to be polluting the root directory.

As far as I'm aware, this is currently not a supported feature of the host.

There is a runtimeOptions.additionalProbingPaths option for runtimeconfig.json, but the probing is done by the library (e.g. package) path and then asset's path within the library.

Perhaps the host could support an additional set of probing paths to consider when doing "local" dependency resolution, in addition to the application's root directory.

@vitek-karas would this be something that the host might support for the future?

They're tiny in size, but add huge numbers of extra files that really don't need to be polluting the root directory.

Sounds like you want the exe in the root folder and everything else in the bin folder, similar issue: https://github.com/dotnet/core-setup/issues/5120

The apphost supports loading a DLL file located in another folder, but the msbuild files don't allow you to change the name. it defaults to <asm>.dll with apphost renamed to <asm>.exe.

In general .NET Core has a different model for the apps - the runtime/host is following the .deps.json file to learn where to look for files, it does very little probing.

I'm wondering what is the root problem here and what is the scenario:

  • Is your application self-contained, meaning it has to carry the framework with it? If so, does it really have to do that? Could you potentially use framework dependent app which will have much less files in the output?
  • Single-exe will give you one file, although large. But then again even if you move some files into subfolder the entire app is still the same size, so you're not going to save any size issues with this approach. Why is it important to have small file size in the root folder, but it's OK to have the rest in a subfolder?

The ideal solution would be to modify the .deps.json to point to the files in the subfolder, unfortunately this is currently broken in the host (for searches done in the app folder, the relative path specified is ignored). I don't know the exact reason why the code works that way unfortunately (it's been like that for a long time). If there's enough interest I'm not opposed to somehow tweaking the host to make this work - but it would still require either manual modification of the .deps.json or SDK support (ideally SDK support).

Just to list other options, not that I would recommend either:

  • Excluding those files from the .deps.json (not sure if SDK has a good way to do this though) and implementing a handler for the AssemblyLoadContext.Default.Resolving event where you find them manually.
  • Use a custom native host which can specify additional probing paths to the runtime by modifying the APP_PATHS runtime property.

So in short, currently I'm not aware of a good way to support this...
I would still be interested in the overall scenario and why you're trying to do this - as it will help us decide what to do about this going forward.

  • It is for a framework dependent app already, though 0xd4d mentioned a similar issue (#5120) for someone deploying a self-contained app.
  • As noted, a single-exe deployment is not desirable because of size issues.

Why is it important to have small file size in the root folder, but it's OK to have the rest in a subfolder?

It's not. I'm saying that the individual files are already tiny in size, so I'm not wanting to go a route that negates that (ie: a single exe or self-contained), but that it's not desirable to have them all in the root folder.

The deps.json file wouldn't actually need to be modified. The relative paths in that file (eg: "lib/netstandard2.0/HtmlAgilityPack.dll") are fine as a subdirectory of the root folder. I'd just need to create one additional subdirectory level and move files into that, as well as handle files that go into different subdirectories (eg: "lib/netcoreapp3.0/Microsoft.Extensions.DependencyInjection.dll"). It would be a mild annoyance to make sure everything is in the right place, but the deployment would be clean. If those files could be placed there automatically on build or publish, that would be a nice convenience, but it can be done as a post-build event on my end, so is not necessary for the time being. It's only the loading part on program run that's out of my hands.

Alternatively, the option for a property analogous to the original .config <probing privatePath=""> element seems like a relatively trivial change, but would require the addition of a new property, which is probably less desirable than making use of the existing information in deps.json.

I would still be interested in the overall scenario and why you're trying to do this - as it will help us decide what to do about this going forward.

Primarily it's about the end-user experience for an xcopy deployment. The less noise to sort through in the root folder, the less stress there is on the end user to find the executable, and the less in-your-face complexity is shown for what is (or should be) viewed as a simple program. Dependency injection and configuration abstractions and logging extensions and whatever else may make things easier on the programming side, but they're big scary words to the average end-user who thinks he's getting something simple. They're an aggravation that doesn't need to be there. Sort of like code formatting guidelines — not strictly necessary for functionality, but a way to help reduce stress of the users.

It's likely to only start becoming noticeable now, because of .NET Core 3's addition of WPF and WinForms. Since those necessarily had to be built using the standard Framework prior to this, and that already had the convenience of the <probing> element, there wasn't much need to consider it in Core.

@Kinematics thanks for the response.

I'm looking into some idea which might be a possible work around... meanwhile I'm curious about your single-file statement.

As noted, a single-exe deployment is not desirable because of size issues.
I don't really understand this. Single-file can also be framework dependent in which case its size is nearly identical to your current build output.

OK - I might have a workaround, but it's not pretty.

I tried a simple app created like this:

dotnet new console
dotnet add package Newtonsoft.Json
dotnet build

Then I went into the bin/Debug/netcoreapp3.0 folder (the build output) and I moved the Newtonsoft.Json.dll into a subfolder bin\newtonsoft.json\12.0.2\lib\netstandard2.0 (all under the netcoreapp3.0 folder. Then I modified the .runtimeconfig.dev.json by removing all the paths in it (as that would make it find the library in nuget caches) and just added one path bin.

Then I ran the app with current directory set int he netcorreapp3.0 folder - and it works.

This relies on several things:

  • It uses a relative path in the additionalProbingPaths, so that you can run the app from anywhere, but it means the current directory must be the folder with the app itself. If that is not possible, you would have to specify a full path there, but that might be different on different machines, so you would need some kind of installer to set it up. Without the above bug fix I'm not aware of a way to specify an application relative path.
  • I picked the name bin myself - it can be really anything. Alternatively it's possible to not have a subdirectory, but then the root would contain multiple subdirectories (one for each nuget package basically)
  • The path into which the dependent assembly must be put (in this case newtonsoft.json\12.0.2\lib\netstandard2.0\Newtonsoft.Json.dll) is dictated by the .deps.json. The first part is the path of the library (near the bottom of the file), in this case it's the newtonsoft.json/12.0.2 and typically it will be the name of the NuGet package and the version of it (the NuGet version!). The second part is the relative path in the runtime section which is basically path inside the NuGet package. This may differ package to package, but typically it's something like lib/<tfm>/assemblyname.dll where tfm stands for Target Framework Moniker - so typically netstandard2.0 or netcoreapp2.0 or similar.

Alternatively you can specify the additionalProbingPath on the command line app.exe --additionalprobingpath <path> but it is basically the same thing, has the same limitations.

It's definitely not pretty, but if you need this and can't use single-file it should work (a bit tedious to setup the folder structure though).

It's likely to only start becoming noticeable now, because of .NET Core 3's addition of WPF and WinForms. Since those necessarily had to be built using the standard Framework prior to this, and that already had the convenience of the <probing> element, there wasn't much need to consider it in Core.

I totally agree that before UI apps this problem is much less likely to occur. That said .NET Core doesn't have a simple counterpart to "probing paths":

  • It generally relies on .deps.json and the SDK to produce these artifacts which are consumed at runtime - trying to reduce the complexity of the assembly resolution logic in the runtime.
  • The main reason for the strong bias towards .deps.json is that it can express things which normal probing paths really can't. It's the one thing which enables portable apps - that is applications which can run on any platform - the same binaries. For platform specific components, it can express the dependency, so for example it can say that a native library A is in subfolder win64 when running on Windows and is in subfolder linux when running on Linux.
  • The portable notions is not very interesting for UI apps at the moment (WinForms and WPF are windows only), but such apps may have a dependency on libraries which are portable. One would have to publish the app as runtime specific to get this resolved at build time - otherwise the runtime support in .deps.json is necessary.
  • For plugin-like scenarios it's similar - typically loading a single assembly file will not work well due to its possible dependency resolution complexities - so again we tend to rely on .deps.json for that as well.

I'm not saying this solution is perfect, but it's at least consistent across all application types. That said if the "probing path" paradigm is something common for UI apps, we will definitely look into providing an easier way to achieve the desired outcome. That's why I'm trying to understand the scenario you're facing, because the "probing path" itself is just a technical solution - I want to understand the problem first, before we commit to a solution.

I don't really understand this. Single-file can also be framework dependent in which case its size is nearly identical to your current build output.

OK, attempts that I've made had all generated the self-contained ~60 MB output. I'll take another look at this to see if I can get it done properly as framework dependent.

... OK, took a bit to figure out how (documentation seems a bit scarce), but I managed to get it to compile. Unfortunately it has two problems:

  • Attempting to run it fails, saying it's missing the hostfxr.dll file, which isn't part of the build.
  • It extracts to a temp directory and runs from there. This may have unintended consequences, since the program is intended to be portable — it can be run from a flash drive without needing access to anything on the computer. However it also needs to be able to save configuration data in the directory with the executable (ie: the folder on the flash drive), and I don't know how that's going to work when the 'local' directory is in some temp folder. I can't test this yet because it's failing to run.

Then I went into the bin/Debug/netcoreapp3.0 folder (the build output) and I moved the Newtonsoft.Json.dll into a subfolder bin\newtonsoft.json\12.0.2\libnetstandard2.0 (all under the netcoreapp3.0 folder. Then I modified the .runtimeconfig.dev.json by removing all the paths in it (as that would make it find the library in nuget caches) and just added one path bin.

OK. A bit ugly, but yes, it works. I can also put the "bin" path in the runtimeconfig.template.json file to put it in the normal runtimeconfig.json file on publish, and that works fine. It will require manual maintenance when package versions are updated, and more complicated work in post-build, but it's doable.

And, while it might be a nuisance for me, I can see wanting to keep using the version directories from deps.json for the future possibility of improving versioning resolution (eg: being able to load two different versions of an assembly because of different versions used between an app and one of the app's packages). That fits in with the other reasons to want to keep using the deps.json data.

For a future consideration, maybe have an option to specify a 'local' package repository directory (ie: "bin" subdirectory) that the build process can store the dependent assemblies in, in the same format as the common nuget directories. That would also assist in the potential versioning issues, where you may need access to two (or more) different versions of the same package, which would be a complete mess if you just dumped everything into the root directory. Though that gets into an entirely different mess of issues. (The versioning issue is on my mind because of Jon Skeet's recent blog post on the topic.)

Overall, that would solve the messy root directory problem, while also opening up more flexibility in assembly resolution.

Thanks - could you please file a new issue on the problem with single-file failing to run? That sounds like a bug.

Could you please file a new issue on the problem with single-file failing to run? That sounds like a bug.

While trying to create a testcase, I realized I'd specified the win-x86 RID, since it errored out when trying just win (which is what the RID Catalog docs seem to imply would be generically usable), but it failed because I have the 64 bit version of .NET Core installed, not the 32 bit version. So that's where the error message was coming from. When I use win-x64 it runs fine.

I'm not sure if there's a known issue for not being able to specify a generic Windows build.

That workaround is ugly and requires too much work.

@Kinematics I've successfully removed all files from the root folder and put them in the bin folder. The only thing remaining are the apphost exe files. Nothing else. I already mentioned it above but here's some more info.

The apphost exe (eg. myapp.exe if your entry point assembly is myapp.dll) has the path to your main dll file. You can patch the apphost exe so instead of having the path myapp.dll in it, it's bin\myapp.dll. When the exe is started, it will load your file from the bin folder. This is the patcher I use. What's left is just moving the output to the bin folder and moving the apphost exe to the root and you're done.

Moving your files to a bin folder is a common request and the SDK should have support for this. All it needs to do is move all files to the bin folder and make sure the apphost exe loads the file from the bin folder.

@Kinematics In .NET Core it's not possible to create an executable which would work on both x86 and x64 (and ran natively as 32bit or 64bit). The executable is just like any other native executable and thus has to be tied to a specific platform. The fact that in .NET Framework it was possible to have an .exe which ran on either x86 or x64 was achieved by tight integration with the Windows OS (the OS loader actually knows about managed apps and does special things). For various reasons we didn't want to introduce a similar component for .NET Core.

The error message in this case is not ideal as it doesn't provide guidance of what to do instead unfortunately. I filed https://github.com/dotnet/sdk/issues/3401 for this.

@0xd4d That is definitely another approach although it has its own issues as well. The main problem is that it's somewhat confusing what the application base path going to be (is it the folder with the .exe or the one with the main .dll?). But I agree that in terms of work required it's relatively simpler.

The bug in the host where it ignores the relative path for app directory lookups is tracked here: https://github.com/dotnet/core-setup/issues/5645.

See more customer scenarios described in dotnet/sdk#3405

try this tool NetCoreBeauty

@vitek-karas
Hello, I've been browsing through several of these similar issues and don't see any recent updates. Has this issue been addressed at all in .Net 5?

I am also in a scenario where I have a large number of dll references but single-file is not an option. Essentially, I have a project with a large amount of shared business logic and multiple front-ends. If I use the standard verbose deployment, the separate interfaces can all share the same dlls (which saves space). If I use single-file, then each interface gets its own embedded copy of the shared code, which balloons the install size

Sadly, it has not. But the tools mentioned in this topic are still working.

There's no nice solution in .NET 5. I think that if this should happen it should be an SDK based solution. Basically some way for the SDK to specify that parts of the build output should go into a subdirectory. And then SDK would generate the right .deps.json (and would require a fix in the hosting layer). Fixing just the hosting layer is desirable, but on its own it doesn't make the solution simpler - it would still require custom tool to process the .deps.json.

Was this page helpful?
0 / 5 - 0 ratings