In the vast majority of cases when you are using Blazor you will never need to worry about assembly loading. However in a modular framework like Oqtane, where you need to be able to create independent components which are loaded dynamically at run-time, assembly loading becomes a critical requirement. And although this blog is primarily focused on Blazor, there are some fundamental aspects of assembly management in .NET Core which needs to be explained as there are obviously some dependencies in common.
When you create a Blazor application you really need to pay careful attention to where the workloads are being executed. Unless you are simply building a "Hello World" application, any real world Blazor application logically has both client and server workloads - with Blazor components powering the client UI, and back-end services doing processing and dealing with data. This logical separation of concerns is important to recognize, as it directly relates to the run-time execution of the application.
First, let's focus on the server workload. Unlike the .NET Framework, .NET Core does not automatically load all assemblies that are in the /bin folder of your application into the App Domain. This is actually a very good thing as it significantly reduces the start-up time and run-time overhead of an application. However it also means that you need to take responsibility for assembly loading yourself. In most .NET Core applications this is not an issue as .NET Core will load all assemblies that are statically linked to your main application by default. However in a modular application, assemblies are not statically linked to the main application, they are fully independent. So in order for the application to be able to reference these assemblies at run-time, they need to be manually loaded. Luckily .NET Core has some convenient classes which expose a variety of methods for dealing with this problem.
Your modular assemblies can be located anywhere you wish; however, the most common location is to deploy them to the /bin folder of your main application. Once the assemblies are deployed you can use the LoadFromAssemblyPath method which is part of the System.Runtime.Loader.AssemblyLoadContext class. This method accepts a full path to the assembly and loads it into the App Domain.
assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
One of the convenient aspects of this method is that it will deal with dependencies automatically. If your module is dependent upon other assemblies, as long as those assemblies are deployed to the same folder, .NET Core will load them as well. Once the assembly is loaded you are then able to dynamically manage it using standard Reflection techniques.
Type types = assembly.GetTypes();
Now, let's focus on the client workload. When you run a Blazor application in client-side mode, assemblies must first be downloaded to the client browser. Once again, the system is only aware of any statically linked assemblies that are part of your application - it is not aware of any independent assemblies. The statically linked assemblies are copied to a special folder which Blazor uses for deployment purposes. If you look at the file system for your Blazor development project you will see a \bin\Debug\netstandard2.0\dist\_framework\_bin folder.
In theory, this folder contains all of the assemblies which will be downloaded to the client browser by Blazor. However if you manually copy another assembly into this folder and run your application you will find out that it is not downloaded to the client browser. So obviously Blazor must have its own internal logic for determining which assemblies to download. But the end result is that your application will need to take responsibility for downloading any extra assemblies.
It turns out we can download assemblies quite easily. We can use the HttpClient which is part of Blazor to invoke a call to the server and download a byte array which can then be loaded into the App Domain.
// download assembly from server and load
var bytes = await http.GetByteArrayAsync("_framework/_bin/" + assemblyName);
The above code works perfectly for assembly loading. The only challenge is that if the assembly you are loading has any dependencies which do not yet exist on the client then the load operation will fail. So you need to implement a mechanism for informing the client about an assemblies dependencies so that they can all be downloaded in the appropriate order. Oqtane contains a solution to deal with this issue which is specific to the characteristics of its own modular architecture.
Modular applications have unique requirements which sometimes require a more advanced technical approach than standard applications. Hopefully other developers will find these insights about assembly loading in .NET Core and Blazor to be helpful.