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 help 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 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 subsciption 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 Assembly Resolver 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:

image

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 process.

Then, it rechecks if it can load the assembly:

image

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 only 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 it makes your life easier.

7 thoughts on “Dynamically loading assemblies at runtime”

  1. Hi Vjeko,

    great stuff, really!

    I like to add some inspiration for future improvements 🙂

    In our solution we use custom assemblies for Reporting. These assemblies must also be accessible on the client but have to be in the search path for ReportViewer. This can be the GAC or a (named) subfolder in the RoleTailored Client directory.

    For easier automatic deployment, we decided to use the second approach to allow automatic user side deployment for these assemblies without the need for a GAC deployment.

    This can be accomplished by having a user writable (security!) subfolder in the RTC installation directory and having this path configured for searching in Microsoft.Dynamics.Nav.Client.exe.config in section configuration/runtime/assemblyBinding/probing. Simply add the folder to provatePath there (). Because ReportViewer is launched from RTC process, this path is added to the ReportViewer search path.

    Using this approach, you are free to deploy additional DLLs without specific deployments even for Reporting. Keep in mind that the initial deployment has to take care of this config change and making this folder writable for the user.

    Cheers
    Carsten

    1. Well, I had demonstrated something like that at TechDays 2013 (or was it 2014?), it was a little bit different – if a client side requested an assembly that was available on the server side, then server simply serialized the assembly into binary, sent the byte array over to the client, and client stored that into the client add-ins folder. Didn’t do anything of the sort for the reports, though. However – as you point out – it has security implications, so I abandoned the whole idea.

  2. Hi,

    I found a problem in mismathcing number of parameters between Nav objects and the Vjeko.Com.AssemblyResolver-DLL.

    In the DLL the function ResolveAssembly has 3 parameters and looks like this:

    public void ResolveAssembly(string name, byte[] asm, byte[] pdb)
    {…}

    In Codeunit ‘.NET Assembly Resolver’ (76001) inside function ‘ResolveAssembly’ for Server and Client Tier this functions is called with only two parameters:


    WITH Asmbl DO BEGIN
    CASE TargetTier OF
    Tier::Server:
    BEGIN
    MethodInfo := Resolver.GetType().GetMethod(‘ResolveAssembly’);
    Params := Params.CreateInstance(GETDOTNETTYPE(Object),2); Params.SetValue(“Assembly Name”,0);
    Params.SetValue(Bytes,1);
    MethodInfo.Invoke(Resolver,Params);
    END;
    Tier::Client:
    BEGIN
    MethodInfoClient := Resolver.GetType().GetMethod(‘ResolveAssembly’);
    ParamsClient := ParamsClient.CreateInstance(GETDOTNETTYPE(Object),2);
    ParamsClient.SetValue(“Assembly Name”,0);
    ParamsClient.SetValue(Bytes,1);
    MethodInfoClient.Invoke(Resolver,ParamsClient);
    END;
    END;
    END;

    This causes an error if Navision client is started.
    The error shows: Conflict in number of parameters.

    Are there any updated Nav-Objects available which consider the 3rd parameter?

    Thanks!

    1. Hi! First of all, thanks for being so active here, trying to solve this, and sorry for me being unresponsive. It has been a busy conference season for me, so I didn’t have much time for maintaining my blog during this period. Anyway, I didn’t update anything on this, nor do I plan to. I create my posts as-is, primarily to share my thoughts and ideas, and I generally don’t have time to maintain that stuff in the long run. I am glad when my stuff helps somebody out there, but I can’t invest much time in maintaining individual things and ideas. This is my hobby and I do these things out of enthusiasm, and I post them, and then move on to other things that interest me in the world of NAV, Azure, and all… Hope you understand it.

      Best regards,

      /Vjeko

Leave a Reply