We have a library that uses ProtectedData.Protect. If we load this library through reflection, we get PlatformNotSupportedException, on Windows. If we add the reference of ProtectedData directly in the app, that loads the library, things work fine.
Let me know if you need any other information.
I couldn't figure out the best area label to add to this issue. Please help me learn by adding exactly one area label.
Tagging subscribers to this area: @bartonjs, @vcsjones, @krwq
Notify danmosemsft if you want to be subscribed.
I can reproduce it but I'm not sure what the right solution is. Gist of what I see:
ServiceApp has a dependency on S.S.C.ProtectedData via PackageReference.dotnet publish on serviceApp. This publishes the library, and among other things, puts a copy of S.S.C.ProtectedData.dll in the publish directory. This assembly is the lib copy of ProtectedData. This is the one that throws PNSE. The "real" windows one is copied in runtimes\win\libConsoleApp loads ServiceApp via reflection.ConsoleApp invokes a method in ServiceApp, that method uses S.S.C.ProtectedData.runtimes\win.It seems there are two fixes
dotnet publish on ServiceApp, specify the runtime using dotnet publish --runtime win and adjust the reflection loaded path from serviceApp\bin\Debug\netcoreapp3.1\publish\ServiceApp.dll to serviceApp\bin\Debug\netcoreapp3.1\win\publish\ServiceApp.dll. Note the win in the path. Specifying the RID will cause the published app to include the specific runtime assets for that platform.It worked, thanks!
Just curious, why couldn't it refer to the correct dll as per runtime in reflection while it could without reflection?
Someone more familiar with how reflection works could probably say, but it's likely because reflection hasn't been taught about runtime paths when probing for assemblies to load. It just finds the first assembly that matches, and in this case it picks the wrong one, the one that is not runtime-specific.
Oh I get that. Do you think this scenario can be a bug on reflection?
Do you think this scenario can be a bug on reflection?
I don't think it is a bug, but perhaps a limitation of how assembly probing works. If you think this behavior could be improved, perhaps a new feature request issue would be useful. I think this behavior can occur with just about any package that has RID-specific assets, not just ProtectedData.
I get that, it doesn't seem like an issue with ProtectedData. Can you tag someone from the reflection group to have a look into this?
/cc @GrabYourPitchforks
I repathed the area to the VM instead of _System.Security_ because I'm not quite sure where else to bucket this.
Tagging subscribers to this area: @vitek-karas, @agocke
Notify danmosemsft if you want to be subscribed.
There are several things at play here:
.NET Core by default doesn't probe on disk for assemblies. It follows the .deps.json which is produced by SDK. In this case, the consoleapp.deps.json doesn't mention S.S.C.ProtectedData, so the runtime will not know anything about that assembly when it starts.
Assembly.LoadFromAssembly.LoadFrom is an API which was intentionally kept to work similarly to .NET Framework. One of its features is that it adds default probing behavior. So calling Assembly.LoadFrom("myasm.dll") will load that file, but also if myasm.dll has additional dependencies those will be looked for next to myasm.dll. This is what happened in your case.
.deps.json for RID specific assetsUnlike .NET Framework which didn't have any notion of RID specific assets, .NET Core adds this capability. It means that "portable apps" can carry multiple copies of an asset (assembly, or native library typically) which are specific to certain RIDs (target OS, version, architecture). There's no logic in the runtime itself to search for the right RID specific asset. Instead this information is stored in the .deps.json file. In this case the consoleapp.deps.json doesn't have anything on S.S.C.ProctedData since it's not a direct static dependency of the app. The right information needed is in sampleservice.deps.json. Assembly.LoadFrom doesn't use .deps.json in any way (it's just how the API works right now), so there was no way for the runtime to know about the RID specific assets.
AssemblyDependencyResolverIn .NET Core 3.0 we've added AssemblyDependencyResolver which is the component to use for dynamically loading additional code which can handle all of the intricacies of .deps.json. You give it the path to the assembly to load, it will look for .deps.json next to it and if it's there it will parse it and use it. Then you can call AssemblyDependencyResolver.ResolveAssemblyToPath to perform the assembly resolution based on the .deps.json. It's typically used to load "plugins" into isolated load contexts and we have an example of that here: https://github.com/dotnet/samples/tree/master/core/extensions/AppWithPlugin.
Combining this in your sample app, we just have to use AssemblyDependencyResolver and hook it up to the assembly resolution pipeline on the default load context. So I took your sample app and modified the Program.cs like this:
``` C#
Console.WriteLine("Calling Service DLL by reflection............");
// Console.WriteLine(GetProtectedData() + "\n");
Assembly asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(@"........serviceApp\bin\Debug\netcoreapp3.1\publishServiceApp.dll"));
var resolver = new AssemblyDependencyResolver(asm.Location);
AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext ctx, AssemblyName asmName) =>
{
string asmPath = resolver.ResolveAssemblyToPath(asmName);
if (asmPath != null)
{
return ctx.LoadFromAssemblyPath(asmPath);
}
return null;
};
Type type = asm.GetType("ServiceApp.SampleService");
if (type != null)
{
```
Things to note:
Assembly.LoadFrom as I don't want the "helpful" automatic probing for dependencies (which is wrong in this case)LoadFrom always loads into Default load context as well, but if you plan on isolating the services, this should load into a secondary ALC per the sample mentioned above).AssemblyDependencyResolver to perform the actual resolution - I don't need to know anything about the actual paths, RIDs and so on.With the above changes I can just run the app and it works.
As mentioned above, if you build the app for a specific RID - typically dotnet publish -r win-x64 for example, this changes things. The app and the sample service component are no longer "portable" and so the SDK knows to only include assets for that specific RID. So the windows specific S.S.C.ProtectedData gets copied to the output, and only that one. There are no subfolders and so on. The .deps.json files are still there and will work, but they basically just all point to "root directory". That's why publishing to specific RID fixes the issues as well, since the "look next to it" probing of LoadFrom will suddenly start to work.
If you're planning to only run on Windows, then publishing to windows will fix the problem. That said I would still suggest for you to not use Assembly.LoadFrom as it's "magic" which might not always work for you. Using the AssemblyDependnecyResolver is obviously more code, but if you might ever move to isolated load contexts, it's effectively required then.
@vitek-karas thanks a lot for all this explanations. It cleared a lot of doubts/confusions. I will try to follow the leads.
Most helpful comment
There are several things at play here:
No default probing in .NET Core
.NET Core by default doesn't probe on disk for assemblies. It follows the
.deps.jsonwhich is produced by SDK. In this case, theconsoleapp.deps.jsondoesn't mention S.S.C.ProtectedData, so the runtime will not know anything about that assembly when it starts.Assembly.LoadFromAssembly.LoadFromis an API which was intentionally kept to work similarly to .NET Framework. One of its features is that it adds default probing behavior. So callingAssembly.LoadFrom("myasm.dll")will load that file, but also ifmyasm.dllhas additional dependencies those will be looked for next tomyasm.dll. This is what happened in your case.Reliance on
.deps.jsonfor RID specific assetsUnlike .NET Framework which didn't have any notion of RID specific assets, .NET Core adds this capability. It means that "portable apps" can carry multiple copies of an asset (assembly, or native library typically) which are specific to certain RIDs (target OS, version, architecture). There's no logic in the runtime itself to search for the right RID specific asset. Instead this information is stored in the
.deps.jsonfile. In this case theconsoleapp.deps.jsondoesn't have anything on S.S.C.ProctedData since it's not a direct static dependency of the app. The right information needed is insampleservice.deps.json.Assembly.LoadFromdoesn't use.deps.jsonin any way (it's just how the API works right now), so there was no way for the runtime to know about the RID specific assets.AssemblyDependencyResolverIn .NET Core 3.0 we've added
AssemblyDependencyResolverwhich is the component to use for dynamically loading additional code which can handle all of the intricacies of.deps.json. You give it the path to the assembly to load, it will look for.deps.jsonnext to it and if it's there it will parse it and use it. Then you can callAssemblyDependencyResolver.ResolveAssemblyToPathto perform the assembly resolution based on the.deps.json. It's typically used to load "plugins" into isolated load contexts and we have an example of that here: https://github.com/dotnet/samples/tree/master/core/extensions/AppWithPlugin.Combining this in your sample app, we just have to use
AssemblyDependencyResolverand hook it up to the assembly resolution pipeline on the default load context. So I took your sample app and modified theProgram.cslike this:``` C#
Console.WriteLine("Calling Service DLL by reflection............");
// Console.WriteLine(GetProtectedData() + "\n");
Assembly asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(@"........serviceApp\bin\Debug\netcoreapp3.1\publishServiceApp.dll"));
```
Things to note:
Assembly.LoadFromas I don't want the "helpful" automatic probing for dependencies (which is wrong in this case)LoadFromalways loads into Default load context as well, but if you plan on isolating the services, this should load into a secondary ALC per the sample mentioned above).AssemblyDependencyResolverto perform the actual resolution - I don't need to know anything about the actual paths, RIDs and so on.With the above changes I can just run the app and it works.
RID specific build
As mentioned above, if you build the app for a specific RID - typically
dotnet publish -r win-x64for example, this changes things. The app and the sample service component are no longer "portable" and so the SDK knows to only include assets for that specific RID. So the windows specific S.S.C.ProtectedData gets copied to the output, and only that one. There are no subfolders and so on. The.deps.jsonfiles are still there and will work, but they basically just all point to "root directory". That's why publishing to specific RID fixes the issues as well, since the "look next to it" probing ofLoadFromwill suddenly start to work.If you're planning to only run on Windows, then publishing to windows will fix the problem. That said I would still suggest for you to not use
Assembly.LoadFromas it's "magic" which might not always work for you. Using theAssemblyDependnecyResolveris obviously more code, but if you might ever move to isolated load contexts, it's effectively required then.