Improve assembly loading to get type matching
We should load the user's assembly into the correct binding context so that we can cast into a Definition type and call Publish or Validate directly.
Not sure if it is possible because we are loading the DLL from some path outside of GAC probably. But if we can make it, the code for publishing would get so much nicer.
Specifically, I think we're having this problem: https://docs.microsoft.com/en-us/dotnet/framework/deployment/best-practices-for-assembly-loading?redirectedfrom=MSDN#avoid_loading_into_multiple_contexts
If the target assembly must remain outside your application path, you can use the LoadFrom method to load it into the load-from context. If the target assembly was compiled with a reference to your application's Utility assembly, it will use the Utility assembly that your application has loaded into the default load context. Note that problems can occur if the target assembly has a dependency on a copy of the Utility assembly located outside your application path. If that assembly is loaded into the load-from context before your application loads the Utility assembly, your application's load will fail.
Some reading:
- https://www.hanselman.com/blog/fusion-loader-contexts-unable-to-cast-object-of-type-whatever-to-type-whatever
- https://docs.microsoft.com/en-us/archive/blogs/suzcook/
- https://stackoverflow.com/questions/8058546/create-object-from-dynamically-load-assembly-and-cast-it-to-interface-net-2-0
- https://stackoverflow.com/questions/3623358/two-types-not-equal-that-should-be?noredirect=1&lq=1
I did more investigations and I was able to make it work if I copy user's DLLs into the folder where the Sharpliner NuGet is expanded (the probing path): https://github.com/premun/sharpliner/commit/457d9129990a8f35b404cf2250e7f1b2ed63c185
Binding contexts
There are 3 ways to load an assembly:
-
Assembly.Load(AssemblyName name)- main (load) context - where we have the right Sharpliner interfaces loaded (context of the Sharpliner MSBuild task) -
Assembly.LoadFrom(string path)- Load-from context - this is where we can load the assemblies to from any path (the current way of things) -
Assembly.LoadFile(string path)- No context - This is where we did this before usingAssembly.LoadFileinstead ofAssembly.LoadFrom) and it is the worst out of the three (but works using the Resolve event)
Goal
We want load everything in the main context so that we can instantiate user's definitions and cast them to ISharplinerDefinition. The resulting code then becomes very straightforward, no reflection magic needed:
private void PublishDefinition(ISharplinerDefinition definition, Type? collection = null)
{
var path = definition.GetTargetPath();
var typeName = collection == null ? definition.GetType().Name : collection.Name + " / " + Path.GetFileName(path);
Log.LogMessage(MessageImportance.High, $"{typeName}:");
Log.LogMessage(MessageImportance.High, $" Validating definition..");
try
{
definition.Validate();
}
catch (TargetInvocationException e)
{
Log.LogMessage(MessageImportance.Normal, "Validation of definition {0} failed: {1}", typeName, e.InnerException);
return;
}
definition.Publish();
}
The problem
- To load stuff in the main context, we need to call
Assembly.Load(AssemblyName)because that one looks in the probing path which is application folder basically. - Since we are running as the MSBuild task of the Sharpliner NuGet package, this points to where the NuGet is installed. So something like this:
E:\NuGet\packages\sharpliner\1.2.8\bin\net5.0\... - User's library references the Sharpliner NuGet - the DLL is in user's bin - so there is
Sharpliner.dllthere, there is alsoSharpliner.dllin the location of the extracted Sharpliner. This is fine, becauseAssembly.Loadwill look at the name and try to find it in the probing path too. Then it will match it and we can use the interfaces.
The root problem is that we are unable to use Assembly.Load with arbitrary path. We can copy user's library inside of the NuGet and then it is in the probing path and we can load it but that is a horrible solution because it will pollute the NuGet.
I don't think we can add new path into the probing paths - apparently this is only possible before you create a new app domain and that's already too late for us.
Loading everything in the load context is a problem because then we cannot cast it.
Ok, apparently I can do better: https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support
I need to:
- Create new context
- Load user DLL there
- Return null when loading
Sharpliner.dllso that the one from the host (MSBuild task) is used - Everything should work then
I can use this for debugging: https://docs.microsoft.com/en-us/dotnet/core/dependency-loading/collect-details
I will re-open if this comes back at me but I'm fairly happy with how it is