[Update, February 8, 2016: there is a new version of code from this post. Please check https://vjeko.com/dynamically-loading-assemblies-at-runtime]
Okay here we go. In this post I deliver the promised code that handles automatic deployment of all your assemblies to client and server, as needed.
For any of you who haven’t read the last two posts, I am talking about automatically deploying .NET assemblies to clients and server, from the database, on demand, at runtime.
This will be heavy on code, so fasten your seatbelt and brace for impact.
First we need to have that .NET assembly that handles the AssemblyResolve event. Let me show that code first:
Yes, I really mean it. This is C# code, compressed (for conserving space), and stuffed into a C/AL string. You bet – I’ll compile it into an assembly right away:
The reason why I do it this way is simple – to avoid deployment. I don’t want to deploy anything, I want this to be a zero-footprint solution, as far as .NET is concerned. My goal is to enable you to just import C/SIDE objects into the database, and be able to use it right out of the box, without any external dependencies you need to worry about at any point, now, or later.
That’s why I simply create an assembly on the fly. If you can’t bother de-minifying C#, let me explain what this piece of code does:
- It has a private dictionary that keeps a map between fully qualified assembly names and assemblies. When .NET runtime asks my class about an assembly, it will primarily look into this dictionary.
- It has a public method to add an assembly to the map. This method can be called by NAV to feed my class with assemblies from the database.
- It subscribes to the AssemblyResolve event. Inside it it first looks if the assembly exists in the dictionary, and if it does, it returns it. If it doesn’t, then it raises an event in NAV and allows NAV to resolve the assembly and call the ResolveAssembly method to feed the assembly into the dictionary. After calling C/AL, the event handler looks into the dictionary again, and if it finds the assembly there, it returns it. Since calls from .NET to C/AL and back are synchronous, this works as expected.
Good, now let’s take care of the assembly storage first. I don’t want to use the standard Add-ins table for all the reasons I explained in my first post in this series. It’s disregards the only correct way of identifying assemblies in .NET, which is through fully qualified name, so I provided my own table:
The primary key is the first three fields, and let me explain why.
First, I want to identify the assembly through its fully qualified name, that’s the first field. Then, I want to be able to bind an assembly to a user. Maybe, just maybe, I’ll want to load different assemblies for different users. If nothing else justifies it, then testability does – you can bind mock assemblies to user accounts used for testing. And last, I bind it to a tier. It’s an option field with three values: Both, Server, Client. Here I can choose where I want the assembly to be deployed when needed. Client assemblies are deployed to client, Server to server, and Both to both. Duh!
There is one function in this table, as well:
No magic in here. It receives a stream containing the assembly bytes, then a System.Reflection.Assembly by reference, to return the assembly to the caller, and name – if for some reason you want to store an assembly under a different name than the one reflection can look up inside – I’ll explain why a bit later.
This function then loads the assembly from the stream, looks up its name, creates a record for it, and streams the assembly bytes into the BLOB field.
Next thing you need is a page to manage the assemblies:
The Import Assembly action does this:
It shows the import dialog, retrieves the selected assembly loaded in TempBlob, reads the stream from TempBlob and passes it onto the InstallAssembly function in the table.
Okay, now that you’ve seen the code and objects for keeping assemblies in line, let’s go back to the codeunit that manages all this.
One of my design goals was to make it possible to deploy new assemblies live, without having to restart the service tier, and without having to restart the client, if possible. I also want to minimize unnecessary loading of assemblies. So I created a single-instance codeunit to host my assembly resolver assembly I showed you how to compile.
Just after compiling the assembly, the codeunit will also call the InstallAssembly function on the table, to store the compiled assembly for future use. This means that compilation will occur only if needed – if the assembly resolver assembly is stored in the database, it’ll be used from there, otherwise it will be compiled first, then stored for future use:
And of course, this:
This function retrieves the BLOB from the database, and converts it to a byte array.
So, here we have three functions: one that decides whether to load or compile the assembly, and two functions to load and compile, respectively.
From OnRun trigger, I retrieve the assembly, and then use it to instantiate the AssemblyResolver class from it
I assign the reference to the instantiated object to a System.Object variable.
Now, I need to teach this AssemblyResolver how to resolve assemblies. I start by providing this function:
Now, this one is trickier than you expected. It receives a .NET Assembly record, the resolver object by reference, and target tier. Then it loads the assembly bytes into a byte array
Then, based on the desired tier (client or server) it uses reflection to access the ResolveAssembly method and call it. I need to do it twice, once with server-side variables, once with client-side variables. That’s important. The Resolver object received as a parameter does not need to be declared twice – it’s by reference, and it carries its RunOnClient flag too.
Apparently, I’ll need two resolvers – one for client, one fore server, so my OnRun trigger will also have this:
Now, this ResolverNst := Object may seem redundant at this point, but it isn’t. It would be if ResolverNst was of System.Object type, but it isn’t. I won’t reveal just yet what it is, because if I do, it’ll twist your brain.
Finally, I want to pre-feed my server and client-side resolvers with the assemblies from the database. This may not be needed for the service tier, but it is absolutely required for the client tier:
If it isn’t obvious, it iterates through the .NET Assembly table, applies the user filter, and then calls the ResolveAssembly method with either ResolverRtc or ResolverNst, depending on the Tier option setting of each record.
Now, I said you don’t need this for the service tier, and you absolutely do for client. Why?
Simply because you can’t have client-side .NET events in non-page objects in C/AL. Since we are in a codeunit, a client-side event is not allowed, so the client-side resolver must know about all assemblies up front. Server-side, on another hand, can raise events, and we can subscribe to the OnResolveAssembly event.
And now comes the trickiest part of all. First, we assigned the instance to System.Object, and setting WithEvents to Yes won’t do anything because this class has no events, and even if it had any, it must have the OnResolveAssembly event to be able to respond. So how in the earth do you subscribe to an event from a class that you just compiled at runtime?
First, let me show you another function I have here:
This function is hardcoded to resolve an assembly on the server side. I need it to properly apply filters, and to load an assembly using the server-side CASE.
Now, I have this trigger in my codeunit as well:
This event will fire when the AssemblyResolver class fires the OnResolveAssembly event and asks C/AL to provide an assembly for the specified name.
So – how did I get this event into my codeunit, because it certainly doesn’t come from System.Object, and I don’t even have my AssemblyResolver class yet, it will be compiled by runtime.
Well, I did it like this – I first compiled the assembly, then deployed it to my RoleTailored Client Add-ins folder to be visible by the development environment, and then I declared the variables:
ResolverNst has WithEvents set to Yes, and ResolverNst has RunOnClient set to Yes.
This solves the compile-time problem. But how do I trick the runtime to not fail at execution time, because it is running an object that references a type from an assembly that doesn’t even exist yet. To answer that question, let me explain when runtime resolution of assemblies occurs. In C/AL, since all .NET interop is heavily based on reflection, .NET variables are not really of the exact type you declare them at, but of a generic type provided by NAV. It’s NAV runtime that handles actual types through reflection. Thankfully, no assembly is resolved sooner than the variable of a specific type is first accessed. This means, that no matter that assembly is not there yet, everything will work before this line of code is reached:
This will fail, because to properly execute this line, NAV runtime must have access to the assembly containing this type. And since the assembly resolver will not be executed before this line fully executes, this fails at runtime because at this point, type NavHelper.AssemblyResolver.Resolver is unknown.
That’s why I never wrote that line of code. If you remember, I did this:
This is legal, because I am assigning to System.Object variable, so at this point only System.Object type must be known – and it always is. Only at this point it contains a valid reference to an instance of NavHelper.AssemblyResolver.Resolver.
Now, if I attempt to do this:
It will still fail, for exactly the same reason. Even though I have my assembly resolver instance ready – it is not able to raise any events in C/AL, because it’s bound to System.Object variable that doesn’t see any events. Also, the assembly resolver instance has no clue about any assemblies to resolve, so I need to do this:
The first line “teaches” my assembly resolver about resolving this particular assembly that hosts the assembly resolver class itself. Then, at the point when I assign my existing instance (referenced by the Object variable) it works, because at this point, the assembly is known, and variable assignment works.
So, in the end, my whole OnRun trigger will look like this:
In a nutshell, it compiles an assembly and then users that assembly to resolve itself for the purpose of initial assignment to the variable of a type which is otherwise unknown at this point.
Obviously, I could have done it even without pulling this stunt, if I wanted to settle for the please-restart-your-client for my service tier to access any assemblies added to the .NET Assembly table after I first loaded this codeunit. Since I want my codeunit to be able to resolve any assembly, even those added live, during runtime, I wanted to expose the OnResolveAssembly event to C/AL to be able to resolve assemblies that were not in the database when the codeunit was first instantiated. This makes it extra useful.
And finally, last piece of code you need to do, and you’ll need to do it on your own – just run this codeunit from codeunit 1, OnCompanyOpen or something. It will install itself and live there for the duration of your session.
Obviously, this solves only the runtime issues – for design-time, you’ll need to have all your assemblies present, but with this little utility here you can achieve this:
- For development purposes, add all your assemblies to the RoleTailored Client Add-ins folder. This will allow you to compile your code.
- For runtime, you can deploy assemblies to the .NET Assembly table at any point. They will be loaded as needed, and always the latest version will be loaded. In the worst case, you’ll need to restart your client session to pick up new versions of already resolved assemblies (e.g. if you didn’t change the version property of the assembly)
And this solves the NAV DLL hell. I feel it does a far better job than automatic deployment of assemblies in NAV 2016, and it’ll also run under NAV 2013 and newer.
Download the objects by clicking here, and do let me know what you think of this solution.