In the evolving landscape of web development, modularity has become a cornerstone for building scalable and maintainable applications. Blazor, Microsoft's innovative framework for building interactive web UIs using C#, offers a fresh perspective on web development, blending the boundaries between client and server. But as with any framework, the need for extensibility is paramount and that's where we can examine a Blazor plugin architecture.
A plugin architecture allows developers to extend an application's functionality without altering its core. This is particularly useful for applications that need to adapt to changing requirements or for platforms that aim to support third-party extensions. However, implementing such a system in Blazor can present unique challenges, especially when it comes to dynamically loading plugins and managing their life cycles.
In this article, we'll delve deep into creating a robust plugin system for Blazor applications. We'll explore how to dynamically load plugins, integrate them seamlessly using Autofac for dependency injection, and ensure proper management of their life cycles. Whether you're an experienced Blazor developer or just starting out, this guide will provide valuable insights into enhancing your applications with a flexible plugin system.
When To Load Plugins In Your Blazor Plugin Architecture
Dependency resolution in Autofac happens just in time when you ask the container for the service (dependency) that you want. However, this often leads to a significant amount of dependency resolution at startup time when an application is launching. We have some techniques to help with this, such as Lazy in C#, but when it comes to plugins the requirements may look different from application to application.
In many of the applications I've written, it's perfectly acceptable to expect plugins to exist and be loaded at startup time. Especially when the plugins are created by internal developers and not third parties! In these situations, the plugins might actually be assemblies built and/or deployed with the core application.
However, when you want to consider dynamically loading plugins how would this work? We need to consider the mechanisms that we have available to us for plugin identification and loading and adapt them to our requirements: load them at some time after application startup. Let's see how this can work for our Blazor plugin architecture.
Watching a Directory for Plugins
There are many ways that you may want to implement when you load plugins for a Blazor plugin architecture, including manually signaling your service through some API to actually rescan for plugins. However, I wanted to demonstrate that you could do this by watching for file system changes and doing it mostly automatically. This can work well if your plugins are located in a dedicated directory.
Let's check out the following code:
FileSystemWatcher watcher = new FileSystemWatcher("path_to_plugins_folder");
watcher.Created += OnPluginAdded;
watcher.EnableRaisingEvents = true;
When a new plugin is added, load it into the application. When using Autofac for dependency resolution, once our dependency container is built, we cannot add more registrations. However, we can use a lifetime scope to assist here!
// Assume this is your main application's lifetime scope
private readonly ILifetimeScope _scope;
private void OnPluginAdded(object sender, FileSystemEventArgs e)
{
// NOTE: this does not have any safety checks that you
// may want to consider when loading assemblies
var assembly = Assembly.LoadFrom(e.FullPath);
var pluginType = typeof(IPlugin);
var plugins = assembly.GetTypes()
.Where(p => pluginType.IsAssignableFrom(p) && !p.IsInterface)
.ToList();
var childScope = _scope.BeginLifetimeScope(builder =>
{
foreach (var plugin in plugins)
{
builder.RegisterType(plugin).As<IPlugin>();
}
});
foreach (var plugin in plugins)
{
var instance = childScope.Resolve<IPlugin>();
// Use the plugin instance as needed
}
}
Check out this video on how to load plugins after startup in your Blazor plugin architecture:
Managing Plugin Lifecycle
Resource management is important in all applications, and sometimes the ease of use we have in dotnet makes it easy for us to forget. So much happens under the hood for us but we still need to remember to clean up.
In the example above the child lifetime scope that we create is IDisposable. You'll notice that I did not include any calls to Dispose in the example code though! This is because we never clarified expectations around plugin lifetime. Let's consider some examples:
- Run Once: Consider that we watch the directory for a plugin when it is added. We use the code above to load the plugin and execute some functionality implemented by the plugin one time. In the code above, we could put the child scope into a using statement and ensure that it's disposed of before leaving the method.
- Load and Live Forever: If the expectation changes so that the loaded plugin exists for the remainder of the lifetime of the application (i.e. cannot be removed), then disposing of this child scope is less important. It might feel like a code smell to not consciously handle it, but if the desired behavior is truly that the plugin & associated scope will last for the remainder of the application, you may not need to explicitly dispose of it.
- Load and Unload: If we need to be able to unload plugins, then we need to ensure the child lifetime scope can safely be disposed of... and that we actually do this! You will need to track the child's scope and find an appropriate time to call Dispose() on it.
Summarizing Dynamic Loading & Lifecycle for Blazor Plugin Architecture
Blazor, with its integration into the .NET ecosystem, offers a powerful platform for building dynamic web applications. By leveraging a plugin architecture, developers can introduce a level of modularity and extensibility that's hard to achieve with traditional web development paradigms. However, with this flexibility comes the responsibility of managing plugins effectively, especially when it comes to their dynamic loading and lifecycle.
Autofac, as a dependency injection container, provides tools that can simplify this management process, but it's essential to approach it with a clear understanding of both the opportunities and challenges involved. While I use it as my framework of choice, there's no reason you need to constrain yourself to this decision.
In the end, a well-implemented plugin system can greatly enhance the adaptability of a Blazor application, allowing it to evolve and grow without major overhauls. As developers continue to push the boundaries of what's possible with Blazor and .NET, the principles of effective plugin management will remain a cornerstone of scalable and maintainable application design.