Dynamically loading assemblies at runtime
When you spend more time in C# than C/AL, and you still tell yourself and the world around you that you are developing for NAV, then this post is for you.
I already wrote a three-article series about "DLL hell" and how to resolve it, and in my last post in the series (http://vjeko.com/sorting-out-the-dll-hell-part-3-the-code) I delivered some code that helps you take control of your .NET assemblies.
This time, I am delivering an updated solution, one that solves all the problems others, and myself, have encountered in the meantime.
So, fasten the seatbelt, and let's embark on another .NET interoperability black belt ride.
First of all - a short disclaimer. On the face of this trick that this post is talking about you, you could say "whadda heck, this is the same as the standard database deployment feature" - but you'd be wrong. The difference is huge, and over the next few posts I'll spend some time to explain in what ways it is different specifically.
I know that at least a few of you have been impatient and asked for code and everything, so I've nicely put all this together and published it on GitHub: https://github.com/vjekob/NAV-Assembly-Resolver
So, what do we have there. First is, the Assembly Resolver assembly code. Many of you have asked about the "dll" to put into the Add-ins folder. You can take this C# solution, compile it and you get your assembly that you can put into the Add-ins folder for the development environment. You don't need it for the service tier (at least not if you are on 2016).
These are the differences from the first version:
There is a shared static instance of the assemblies. This makes sure there is no "memory leak" that Jaap Mosselman has identified. This static instance is a ConcurrentDictionary so it makes sure multiple threads can safely read from it and add to it, and that all sessions of one NST running instance will have one single collection of resolved assemblies. No more memory leaks.
There is still OnResolveAssembly event in C/AL, but it doesn't fire for all sessions (another issue Jaap identified). It is enough that this event fires in only one session, so the AssemblyResolver class keeps track of all living instances of itself, and when one is disposed, it binds the event to the next session in queue. This makes sure that only one session will receive OnResolveAssembly event and is also thread safe. This makes sure that even if you deploy new assemblies to the database at runtime, you still get them properly loaded by the NST.
C/AL doesn't attempt to be unnecessarily smart. Last version was doing some crazy .NET interop to compile itself and then bind itself to an instance of System.Object, but then to assign itself to an instance of non-existing self for the purpose of event listening. Crazy, but it worked, however it caused troubles at client-side compilation (C/AL compiler couldn't check the AssemblyResolver type at compile time). It's a little less crazy this time, more about this later.
The C/Al code deployed with the solution is now compatible only with 2016, however this dependency is very shallow: it's the subscription to the OnAfterCompanyOpen event of Codeunit 1. You simply remove this function and run codeunit 76001 from CompanyOpen trigger in Codeunit 1 and you are good to go. Also, for 2015 and earlier versions, you'll have to deploy the AssemblyResolver assembly to the Add-ins folder of the NST.
Now, if you run NAV 2016, this functionality has zero footprint on your NST, which makes it particularly useful for deployments in Managed Service - you don't need to deploy anything to the NST, everything is contained in C/AL.
So, how does it work?
First - it checks whether the AssemblyResolver type is available, and if not, then it installs itself:
This "installs itself" does the following:
It compiles the AssemblyResolver assembly from C# code embedded in C/AL.
It zips the assembly into a zip file.
It creates a line in the Add-ins table for the AssemblyResolver assembly, and imports the zip file created earlier as its resource file.
It cleans up the compiled assembly, zip file, and temporary folder used for this proces
Then, it rechecks if it can load the assembly:
At this point, if everything is okay, NST will locate the assembly in the Add-ins table, and deploy that assembly from the database and dynamically load it. If something is not okay, you get a message about it, and the codeunit exits.
Everything else is already described.
Now, you may think again: what the heck - he is using database deployment and is providing a feature that's purported to replace it.
As a matter of fact, yes.
Database deployment is - as I said earlier, and as I will show again - not working correctly. I can't know for a fact exactly what it does at execution level, but I am sure I've been able to pretty accurately figure it out from its behavior, and having worked with .NET since 2000 when it was still in beta, you can take my word for it: it's doing it all wrong. This little assembly that I provide here takes care of all the wrong in there, and fixes it.
The end result is this:
NAV database deployment feature is used to deploy my assembly from the database and load it.
From that moment on, my assembly essentially takes over the functionality that would be done by the NAV database deployment feature.
And the reason why I use database deployment is simple: I want to avoid any kind of deployment of assemblies to the file system. With this little trick (having a self-compilable self-deployable assembly) I take care of that. So, for your live environments, you only need the objects in the application database (which will include the assemblies you upload to the .NET Assembly table) and all of your .NET interop will nicely work, as if assemblies were deployed in the Add-ins folder.
One small limitation in multi-tenant environments is that the InstallResolverAssembly function will onlly execute successfully when ran from a session of a tenant mounted with "allow application database write" setting.
And last, but not least, this works in Managed Service - I had problems making the version that binds itself during the OnAfterCompanyOpen works, but if you avoid binding from there, and load it afterwards, then this works nice.
Good luck with this, and let me know if this makes your life easier.