Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolving packaged assembly path fail when published #3033

Closed
per-samuelsson opened this issue Feb 5, 2018 · 11 comments
Closed

Resolving packaged assembly path fail when published #3033

per-samuelsson opened this issue Feb 5, 2018 · 11 comments
Assignees
Milestone

Comments

@per-samuelsson
Copy link

I'm using TryResolveAssemblyPaths on a composite assembly resolver to resolve path for a reference in the NuGet cache and I observe it fail only when resolving application is published.

On the contrary, running the same code using dotnet run my.csproj successfully resolve the reference.

As short working sample is provided to predictably reproduce. Basic setup is:

  • Target.csproj
    • Project that has a package reference (Ninject in this sample).
  • SampleApp.csproj
    • Application that use AssemblyContext.LoadFromAssemblyPath to load target and try resolving reference to Ninject package using composite resolver.

Program.cs should be pretty straightforward to study.

Steps to reproduce

dotnet build .\src\Target
dotnet msbuild .\src\SampleApp\SampleApp.csproj /t:Publish
dotnet .\src\SampleApp\bin\Debug\netcoreapp2.0\publish\SampleApp.dll ..\..\..\..\..\Target\bin\Debug\NetStandard2.0\Target.dll

Expected behavior

Sample Program.cs output 1, i.e. finding the path to the package.

Actual behavior

Sample sample Program.cs output 0, i.e. assembly path not found.

Environment data

.NET Command Line Tools (2.0.0)

Product Information:
 Version:            2.0.0
 Commit SHA-1 hash:  cdcd1928c9

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.16299
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\2.0.0\

Microsoft .NET Core Shared Framework Host
  Version  : 2.0.0
  Build    : e8b8861ac7faf042c87a5c2f9f2d04c98b69f28d

Extra

Running the exact same thing without publishing produce expected result. There is a run.bat in sample that illustrate this.

@per-samuelsson
Copy link
Author

Is this issue tracker at all read, and are issues considered? Don't see a single comment on any issue posted last couple of weeks... 😕

@Petermarcu
Copy link
Member

My guess is that you have to preserve the compilation assets when you publish or they won't be there. I don't think published apps probe the actual Nuget cache.

@per-samuelsson
Copy link
Author

I don't think published apps probe the actual Nuget cache.

Yeah, that is kind of what I observe here, but I can't get my head around is why it would make a difference if the application is published or not.

Just to be clear, here's the pseudo-code summary of what this issue is about:

void Main() {

  var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath("path\to\assembly.dll");

  var dependencyContext = DependencyContext.Load(assembly);
  var assemblyResolver = new CompositeCompilationAssemblyResolver(
    new ICompilationAssemblyResolver[] {
      new AppBaseCompilationAssemblyResolver(Path.GetDirectoryName("path\to\assembly.dll")),
      new ReferenceAssemblyPathResolver(),
      new PackageCompilationAssemblyResolver()
    });
    
  assemblyResolver.TryResolveAssemblyPaths(
    new CompilationLibrary(/*package reference*/), 
    assemblies
  );
}

I just can't see anywhere in there where there would be something that makes it dependent on if that running entrypoint would execute in a development- or published context? After all, it's not the reference of the published application I'm trying to resolve, but for a given, dynamically loaded assembly.

@steveharter
Copy link
Member

@per-samuelsson any update? Did you ensure the asset is copied locally during publish?

@per-samuelsson
Copy link
Author

@steveharter,

Appreciate you trying to help here - thanks!

@per-samuelsson any update?

I'm afraid not, no. Mainly because ...

Did you ensure the asset is copied locally during publish?

... I don't even know what that mean? What asset is that, how do I copy it in a predictable manner when publishing, and of most interest: is that something that should be done during publishing apps normally?

@eerhardt
Copy link
Member

This should be investigated for 2.1.

@eerhardt eerhardt self-assigned this Mar 27, 2018
@eerhardt
Copy link
Member

Ok, I finally got back to this.

The reason this doesn't work is exactly what @Petermarcu said above:

I don't think published apps probe the actual Nuget cache.

To give a little bit of background information:

When .NET Core apps are run during dotnet run, all the dependent assemblies are not copied to the bin directory like they are for .NET Framework apps. Instead, dependencies that come from NuGet packages are actually loaded out of the NuGet cache, ex. C:\Users\eerhardt\.nuget\packages. Reasons for this are varied, but mostly it dates back to the pre-project.json world of dnx and dnu, where there was no shared framework. So all the .net core framework assemblies were copied to your bin directory, and it was a inner-loop dev performance hit. But even today, you don't need to copy local 3rd party assemblies to the bin directory, so there is some perf gain.

The way this works is there is a runtimeconfig.dev.json file in the bin directory that directs the dotnet.exe host where it can probe for the dependencies. Here's an example:

{
  "runtimeOptions": {
    "additionalProbingPaths": [
      "C:\\Users\\eerhardt\\.dotnet\\store\\|arch|\\|tfm|",
      "C:\\Users\\eerhardt\\.nuget\\packages",
      "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackagesFallback",
      "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder"
    ]
  }
}

You can see my NuGet cache is in this list. Of course, when I dotnet publish my app and move it to a production machine, this list is no longer valid because the production machine doesn't have a folder C:\\Users\\eerhardt. This is why these paths are in a dev.json file - it is for dev-time only.

When you dotnet publish, it doesn't produce this runtimeconfig.dev.json file, and instead ALL the dependencies are copied to the publish directory. So there is no need to resolve assemblies from the NuGet cache - they are in the local folder.

Now, to see why your code works during dev-time scenarios, know that this list of probing directories gets passed from the dotnet.exe host into the AppContext class. One of the resolvers you list in your code: PackageCompilationAssemblyResolver is responsible for searching all the "probing paths" that get passed on the AppContext. Since at dev-time, the NuGet cache is passed into the AppContext, the Ninject assembly can be resolved from there.

At publish-time, the NuGet cache location is no longer passed into AppContext, because there is no runtimeconfig.dev.json file with the path in it. So your code no longer resolves assemblies from the NuGet cache - it doesn't know where to look for them.

So to solve this, you need to think about how you want to resolve these dependent assemblies. Options I can see are:

  1. You could publish "Target.csproj", in order to get all its dependencies next to it. Then the AppBaseCompilationAssemblyResolver will resolve it on the path you passed into it.
  2. You could write your own resolver by implementing ICompilationAssemblyResolver yourself.
  3. You could pass the NuGet location in to --additionalprobingpath <path> when invoking the app.

Either way, the resolvers need to know where to resolve the Ninject assembly from, they can't do it without the required information.

Closing as this is by design.

@per-samuelsson
Copy link
Author

@eerhardt

Wow, best answer ever. With historic design considerations even. Thanks!

WTBS, I need to consider this some more, because I do get how published apps necessarily must differ (after all - that's what publishing kind of is about), but what I found so weird here is that it's the loading assembly that is published, not the loaded one. The loaded one still lives as a non-published app, and I still can't get why the dependency context for that should be affected by the fact that the app doing LoadFromAssemblyPath("path\to\assembly.dll"); is published. I wonder if it could be that I'm loading it into the context of loading one, and it would work if I instead used a custom context.

Anyway, it very well might be something I'm missing, so let me consider it all again before I give some final comment. Again, thanks.

@per-samuelsson
Copy link
Author

I think I finally see the glitch now. Take a look at this simplified (pseudo) sample:

Main() {
  // NOTE, important:
  Console.Write("Hello, I'm the published app");

  // New custom context in where I want to load other assemblies, plug-in stylish
  // Basically just extend AssemblyLoadContext and provide no custom loading.
  var context = new CustomContext();

  // Loading an application that is NOT published. This assembly is now
  // loaded in the custom context. Next to this assembly is a `.dev.json`.
  var assembly = context.LoadFromAssemblyPath("path\to\appassembly.dll");

  // Loading dependency context for the custom loaded assembly. And repeating,
  // that assembly is NOT one that is published.
  var depContext = DependencyContext.Load(assembly);

  // Here's the glitch: the ICompilationAssemblyResolver I need here is one that
  // resolved based on the CUSTOM loaded assembly. While instead resolver
  // I get using this
  var assemblyResolver = new PackageCompilationAssemblyResolver();  

  // ... is a resolver based on the CURRENT application, i.e. the one published.
  // And hence when I later do sequence such as
  var compilationLibrary = depContext.GetCompilationLibraryFor("Ninject");
  resolver.TryResolveAssemblyPaths(compilationLibrary, out paths);

  // ... that resolving operate on the premises of this application, not the one
  // I have loaded. I got tricked by that we got the compilation library from the
  // DependencyContext we got from the custom loaded application, but that
  // don't mean resolving happen in that context.
}

So, what I'm after is a way to resolve references based on a dependency context loaded from a custom assembly. If that one is published, I'm fine not finding package references (make total sense).

@eerhardt
Copy link
Member

If you really want to make this work as you describe, then you probably want to go with my option 2 above - write your own ICompilationAssemblyResolver that does it. You could read the runtimeconfig.dev.json file of the loaded assembly and get the extra probing paths that way.

PackageCompilationAssemblyResolver (and pretty much all in-box resolvers) is designed and intended for loading dependencies of the current app, which is why it reads the current AppContext to get the probing paths. If your intention is to load other app's dependencies and resolve them, you will need to do some custom logic yourself.

@per-samuelsson
Copy link
Author

If your intention is to load other app's dependencies and resolve them, you will need to do some custom logic yourself.

Yes, copy that, and expected that already from the start. But think I got fooled by DependencyContext.Load(anyAssembly) that got me thinking doing that kind of resolving would be pretty straightforward with types already part of DependencyModel assembly.

You could read the runtimeconfig.dev.json file of the loaded assembly and get the extra probing paths that way.

I probably will need to do something like that, yeah. Thanks. I have some memory reading some article somewhere some year or so ago that there are ready-to-use readers for those runtime- and deps JSON files, so I find that, maybe it's not going to be super-tricky after all.

Again, really appreciate your feedback and help. Thanks!

@msftgits msftgits transferred this issue from dotnet/core-setup Jan 30, 2020
@msftgits msftgits added this to the 2.1.0 milestone Jan 30, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 18, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants