For a new project of mine, I wanted to use a plugin architecture. Since it had been a while since I did plugins in .NET the last time, I wanted to see what’s new in .NET 8. Well, no news at all - plugins for .NET application are literally the same as they always were.
Don’t misunderstand me, there were some steps forward. For one, there is a working example showing you exactly how it’s done. And honestly, that is an example you should use. However, this is the same way we did it back in .NET 2.0 days.
And yes, I am a bit unfair since there were a lot of upgrades in the backend and .NET 8 will give you more options on how to load stuff. Let’s not even get into performance improvements. However, I still have to create a common assembly that inevitably becomes a hell to maintain. And what about single-file publishing? Nope, still not supported.
While I was ok doing it the classical-style, I really hated not having a single-file, self-contained deployment. They are just so freeing when it comes to actual deployments and worth every additional byte they consume. And, since the whole framework is bundled in a single package, there is no real reason why it cannot be done. Is there?
Well, I decided to give it a try.
But before dealing with single-file deployments, what about removing the need for common assembly? Well, if you can get away with simple Get/Set interface that returns objects that can then use other standard interfaces; or, said plainly, if you can get away with forwarding standard .NET classes/interfaces, answer as always lies in good old IDesignerOptionService.
While quite a lot of interfaces you could use for plugins got trimmed with time (especially during the Windows Forms exodus), this one somehow survived. And it’s almost perfect for lously-coupled plugins. It gives you two methods: get and set. Thus, you can simply use something like that:
public class MyPlugin : IDesignerOptionService {
public object GetOptionValue(string pageName, string valueName) {
switch (pageName) {
}
}
public void SetOptionValue(string pageName, string valueName, object value) {
switch (pageName) {
}
}
}
As long as you stick to built-in objects (or you’re willing to do a lot of reflection), you’re golden. I agree, there is a performance impact and design is not as clean as it could be, but I would argue it’s quite often worth it since we don’t have to deal with common assembly versioning and all the fun that can cause.
Thus, that only leaves single-file deployment as our goal. Is it really not supported?
Indeed, if you try to make a single-file deployment of the plugin dll, it will say that you cannot do that unless OutputType is Exe. And, if you try to combine that with common PluginBase assembly, it will not be able to load anything because PluginBase as a separate assembly is not the same as PluginBase that got packed. However, if you are ok with this janky IDesignerOptionService
setup, you can make your host a single-file application.
And remember, the whole .NET is essentially packed there so this application (assuming you didn’t trim it), will have no issues loading our plugin DLLs.
So, to summarize, you can have your host application deployed as a single file (only the executable is needed) and then load any class from plugin dll that implements IDesignerOptionService
interface. Such class will then use .NET from a host itself to run without .NET being installed separately.
To see it in action, download the example. Don’t forget to run Make.sh
in order to copy files around.