'Additional probing paths for .NET Core 3 migration

Short version of the question:

Is there any way in .NET Core 3 to specify a local probing path, using the same rules as the <probing> element from app.config? additionalProbingPaths does not seem to work.


Long version of the question:

I'm migrating a project from .NET Framework to .NET Core 3. In the original project, I kept a number of secondary dlls in a lib/ folder. This worked fine, as I set the probing path in App.exe.config, like so:

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

However after converting the project to .NET Core 3, the program won't run, saying it can't find the dlls. The App.exe.config is still there, and still being read/used, because it also contains info on System.Configuration parameters, and that part of the program still works fine.

I've determined that there is a new json file that stores configuration information for the program, in App.runtimeconfig.json. It's auto-generated, and does not contain additional probing paths by default, but the App.runtimeconfig.dev.json file contains some.

Now, I can't use the paths that were in the .dev.json file because those point to local user directories, and are not acceptable for deployment. However I'm able to add my own version to the main runtimeconfig by using a template file (runtimeconfig.template.json) in the project directory. This adds the property to the runtimeOptions grouping in the main runtimeconfig file. The template code is:

{
  "additionalProbingPaths": [
    "lib"   
  ]
}

And the final output of the App.runtimeconfig.json file is:

{
  "runtimeOptions": {
    "tfm": "netcoreapp3.0",
    "framework": {
      "name": "Microsoft.WindowsDesktop.App",
      "version": "3.0.0-preview6-27804-01"
    },
    "additionalProbingPaths": [
      "lib"
    ]
  }
}

However the relative path that I inserted does not seem to be getting used at all, whether I insert it into the main runtimeconfig file using the template, or just manually edit the dev.json file instead. I've also tried a number of variations on how the directory is specified. The program always generates an error saying that the specified assembly was not found if it's not located in the root program directory. The error says it's looking for lib/netstandard2.0/HtmlAgilityPack.dll (or other similar libraries) that it gets from the App.deps.json file.

The workaround is to let all the libraries live in the root program directory, but since this used to work before, and I'd expect it to work now, I'd like to know what I'm doing wrong. Attempting to use the diagnostic output in Visual Studio for more info fails because the program terminates before any diagnostic information is generated.



Solution 1:[1]

So, based on information gained from this Github issue, I have found that there is no current equivalent to the <probing> element from app.exe.config in .NET Core. Thus there is no simple "drop all these .dlls into a subdirectory and work from there" solution.

It is, however, possible to make use of the additionalProbingPaths directive, as described above, with some additional tweaks.

First, set the additionalProbingPaths directory in the template file to something like "bin". This will define the root of a new assembly storage location, that will be constructed to look like the NuGet repository.

Then set up commands in the post-build event to move the (for example) HtmlAgilityPack.dll file into "$(TargetDir)bin/HtmlAgilityPack/1.11.8/lib/netstandard2.0". The full path is constructed from the two halves of the assembly info provided in the deps.json file: "HtmlAgilityPack/1.11.8", and "lib/netstandard2.0/HtmlAgilityPack.dll" located under the "runtime" subsection. The normal dependency resolution process will then be able to find it, based on what's in the deps.json file, and the bin probing path.

In addition, copy the command that's generated for the post-build, and create another Target element in the .csproj file (<Target Name="PostPublish" AfterTargets="Publish">), using $(PublishDir) instead of $(TargetDir) to define the output. That will let the build system do the same file moving when publishing, as well as building.

This does mean updating the file move command each time you update the package version number, so there will be extra manual work involved to keep it current.

I'm hoping they will improve the build system to do something like this automatically, because aside from cleaning things up, it also opens up options for multiple versions of dependencies, and may help with the ongoing problem of versioning in .NET.


Addendum: A cleaner way to move the various DLLs into a usable directory. Using the post-build code window is a horrible way of going about it, but it's much easier to handle using standard MSBuild commands. It still requires manually updating when package version changes, though.

The following sets things up for both building and publishing. Note that these must be set up separately. You can't refactor to use a single set of move commands after defining the target directory variable in different 'parent' actions, because publishing implicitly builds first, and a given target action can only ever be called once. So once it was called during build, it can't be called again during publish.

<Target Name="CreateBuildBin" AfterTargets="Build">
    <MakeDir Directories="$(TargetDir)bin" Condition="!Exists('$(TargetDir)bin')" />
</Target>

<Target Name="MoveBuildDlls" AfterTargets="CreateBuildBin">
    <Message Importance="high" Text="Build directory = $(TargetDir)" />
    <Copy SourceFiles="$(SolutionDir)LICENSE.txt" DestinationFolder="$(TargetDir)" />
    <Move SourceFiles="$(TargetDir)HtmlAgilityPack.dll" DestinationFolder="$(TargetDir)bin/HtmlAgilityPack/1.11.17/lib/netstandard2.0" />
</Target>

<Target Name="CreatePublishBin" AfterTargets="Publish">
    <MakeDir Directories="$(PublishDir)bin" Condition="!Exists('$(PublishDir)bin')" />
</Target>

<Target Name="MovePublishDlls" AfterTargets="CreatePublishBin">
    <Message Importance="high" Text="Publish directory = $(PublishDir)" />
    <Copy SourceFiles="$(SolutionDir)LICENSE.txt" DestinationFolder="$(PublishDir)" />
    <Move SourceFiles="$(PublishDir)HtmlAgilityPack.dll" DestinationFolder="$(PublishDir)bin/HtmlAgilityPack/1.11.17/lib/netstandard2.0" />
</Target>

Solution 2:[2]

Add

  <Target Name="CreateLib" AfterTargets="AfterBuild">
    <ItemGroup>
      <NugetFiles Include="@(ReferenceCopyLocalPaths->HasMetadata('PathInPackage'))">
        <OutPath>lib\%(ReferenceCopyLocalPaths.NuGetPackageId)\%(ReferenceCopyLocalPaths.NuGetPackageVersion)\%(ReferenceCopyLocalPaths.PathInPackage)</OutPath>
      </NugetFiles>
    </ItemGroup>
    <Copy SourceFiles="@(NugetFiles)" DestinationFiles="@(NugetFiles->'%(OutPath)')" />
  </Target>

at the end of your .csproj (just before </Project>) to copy all dependent nuget dlls into your "lib" folder as the additionalProbingPaths requires.

For deploing the output directly you would rather "move" (not just copy) all the nuget files to lib folder like this:

      <Target Name="CreateLib" AfterTargets="AfterBuild">
        <ItemGroup>
          <NugetFiles Include="@(ReferenceCopyLocalPaths->HasMetadata('PathInPackage'))">
            <OutPath>lib\%(ReferenceCopyLocalPaths.NuGetPackageId)\%(ReferenceCopyLocalPaths.NuGetPackageVersion)\%(ReferenceCopyLocalPaths.PathInPackage)</OutPath>
            <DeletePath>$(OutDir)%(ReferenceCopyLocalPaths.DestinationSubPath)</DeletePath>
          </NugetFiles>
        </ItemGroup>
        <Copy SourceFiles="@(NugetFiles)" DestinationFiles="@(NugetFiles->'%(OutPath)')" />
        <Delete Files="@(NugetFiles->'%(DeletePath)')"/>
      </Target>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 riQQ
Solution 2