OK, so I think I got a little ahead of myself here. Costura.Fody is great, but the issue of conflicting assemblies persists. I did some more testing on different packages, and realized that this is not a sure fire solution. Why? Because according to Costura.Fody’s source code, it still checks the AppDomain for the already loaded assemblies before resolving it from the embedded resources:
What a bummer. That kind of sucks. Anyways it was worth a try, and apologies for speaking too early. I am not sure why, but when I was first testing it, my errors about Xceed assemblies went away, but the EO.WebBrowser one persists.
Costura.Fody is still insanely useful tool to have in your arsenal so please do read ahead on how to use it.
- Install Costura.Fody from Nuget Pacakges. You can simply right click on References, and go to Manage Nuget Packages, then search for Costura.Fody, and install it.
- Add FodyWeavers.xml file to your project. This is a settings file for Fody. You can use it to specify any changes to the default settings that Fody ships with. Here’s mine, that specifically excludes all RevitAPI assemblies. Why? Just in case. But it’s redundant. Probably. All you have to do to exclude an assembly from being embedded by Fody, is to set its CopyLocal property to false. However, I have been experiencing some weird issues where these DLLs were still getting embedded, regardless. So just to be safe, I am calling them all out.
- That’s it!
If you build your solution now, it should contain all of the packages that you need. How do I know. You can check out your DLL with ILSpy or dotPeek from JetBrains:
From the image above, you can see all of the references marked CopyLocal = True to all of them being embedded in the HOK.Core.dll under resources. There is also another thing that Fody did for you, that is create an AssemblyLoader and an event handler called ResolveAssembly to handle accessing these resources from withing the file, rather than looking in the “public basket”. Those can be seen under a new Namespace:
OK. So that was it? Anything else?
Maybe. If you have some Post Build Commands, like I usually do in my project, then it might be time to migrate them to a MSBuild Target. I usually have some post build commands that copy built assemblies to a location on a drive, like Revit/Addins folder etc. Well, since Fody, also uses the post built to merge the assemblies into your main assembly, you have to make sure that your post builds get executed after Fody is finished, or they will run into Access Denied issues since files are being used. What I ended up doing was just ditch the Post Build Command, and create a new Target:
Ps. If you insist on using the Command Line, you can easily translate the post build commands to a Exec task. Just replace all quotation marks with character code " like in the code above. I am actually keeping the Command line of signing my DLLs with the signtool.exe.
Ps2. MSBuild tasks take the environmental variables but you have to call them with $() rather than %25%25 like in a command line.
Ps3. The message tasks are there just for debugging. They print a message to the console so I know that my builds didn’t fail.
Ps4. One thing to keep in mind, is that Fody will not embed 2nd level dependencies. Imagine that you have a project1, that references project2. Project2 has a dependancy on Newtonsoft.Json.dll, but that will not be merged into project1 unless you reference it directly into project1. Just keep that in mind.
Ps5. MSBuild Targets are a much better option than using Post Build Commands. I know that I have advertised these commands before in one of my posts, but as is, I realized that with these MSBuilds routines a lot of the work is done for us. There are some nice utility methods setup for us, that ship with MSBuild that we can use to sign plug-ins and copy files around. We can also simply call command line routines so basically we get all of the functionality that Post Build Commands offer and some more.
Hi Konrad, could you not simply omit / comment out those lines from the assembly resolver so that it *never* checks the current app domain? Or perhaps only checks the current app domain if the assembly is on that exclusion list (i.e. the Revit API dlls)?
Since Revit doesn’t support strong naming I feel like overwriting the assembly resolver is still the key to avoiding the duplicated dll issue.
I don’t think that would work. So Microsoft suggests to not load the same assembly twice (different versions) into the same context as that can potentially cause issues. So yeah, in theory you can load it in again, but then, you might have an unstable application, that can throw exceptions that are impossible to debug. It’s a little bit more complicated problem that I don’t have an answer for at the moment.
You’re right, I suppose that if you were to do that, any other assembly that references one with the same name but doesn’t have it’s own overwritten assembly resolver would risk getting delivered your embedded copy instead.
It’s too bad because this is a problem that Microsoft solved with dll Strong Naming, but Autodesk refuses to support it.
Sorry a little late to the party, but this looks really promising. So it loads from embedded correct? It looks like it also changes the name of the assembly to put ‘costura’ in front correct? If it does that wouldn’t you essentially be making it a different assembly then? Wouldn’t it make the distinction between ‘Newtonsoft.Json.dll’ and ‘Costura.Newtonsoft.Json.dll’? If so then you’ve potentially broken the conflict as they are two different assemblies.
I suppose you still have the issue if two addins both use Costura. So along those lines, is there a quick modification to the Fody code that could be made to put a custom ‘tag’ at the start of all your assemblies? So you set it to ‘HOK’ and you would get ‘HOK.Costura.Newtonsoft.Json.dll’ and a user at ABC architects would get ‘ABC.Costura.Newtonsoft.Json.dll’? You may end up with a lot of versions loaded potentially if you have a lot of addins but it may fix the conflict issue…
Going to do some experimentation…
Hi Steve, I am afraid that’s not the case. So I was looking through the Costura source code, and they got a check in there, that skips embedded assembly if one is already loaded into the GAC/Default context. So basically when we embed Json v.11 into our plugin, and Revit loads Json v.9 by default, we would still end up with v.9 available or in some cases worse, we end up with both v.9 and v.11 loaded into the LoadFrom context. That actually happens when we don’t use Costura, and that’s something we can easily verify using Fusion Logs Viewer.
I spent some time looking at this issue when I started running into multiple versions of assemblies issues with Revit plugins and found out that Revit loads all external command plugins via the LoadFrom method dumping all of these assemblies into the same context. Then we got things like Dynamo, that now ships with Revit out of the box, and that is also setup in the same fashion. Basically all of these DLLs are available in the LoadFrom context. That’s very problematic when we have a plugin, that requires different version than the one available. If the method called only exists in one of the versions, then its not likely we will have any issues. However, if we call a method that is available in both versions of the same assembly, its likely that we will hit an exception that “method or operation is not implemented”.
There are some ways to deal with this, but putting all assemblies into the same context which Revit does, is problematic. Binding Redirection is one way, but it pretty much only works well, if we blanket redirect all versions of some assembly via a range ex: 0.0.0.0-126.96.36.199 to 188.8.131.52. When we use this kind of redirection, we are forcing all Revit plugins to always resolve given assembly to a single version, assuming that 184.108.40.206 is the latest available. Then we end up with just one version in the LoadFrom context available and potentially no issues, unless a method used by previous version is not forwards compatible.
As you see, that DLL Hell is real, and Revit sure makes it easy for people to trip themselves up all over the place. Shit, even their own developers fuck this up routinely. Let’s take Dynamo. It recommends using Json v.8.X but it doesn’t do squat in Revit 2019 because Revit ships with an application wide binding redirection to Json v.9.0. If you watch Fusion Logs for Revit 2019 loading, you will see v.6, v.8 and v.9 all getting loaded into the context. That’s some great coordination they are doing.
Let’s keep this conversation going. I really would like to know if you have more success resolving this.