natemcmaster / DotNetCorePlugins

.NET Core library for dynamically loading code
Apache License 2.0
1.58k stars 227 forks source link

Unable to resolve services using depenency injection for plugin dependency on another plugin #60

Closed ShaneYu closed 3 years ago

ShaneYu commented 5 years ago

Is your feature request related to a problem? Please describe. I want to be able to have plugins that provide services to be registered with dependency injection and can be resolved and used by other plugins. This means that Plugin1 might register IMyService with the DI and Plugin2 wants to resolve this and use it.

The issue I have is that once all plugins are loaded and each one has registered the services they want to register, I can only resolve services in a plugin that the same plugin registered. I believe this is because each plugin when published to the plugins folder has their own copy of the Plugin1.Abstractions dll...when plugin 1 registered IMyService it's type refers to the assembly loaded from that plugin folder, whereas plugin2 tries to resolve IMyService that's type refers to the assembly under it's plugin folder.

This is an issue because one plugin may be providing a service with caching or something else for argument's sake, if the same service is registered twice because it's type came from different assembly file locations then you have duplicated caches in the application using up more memory. This is just a simple example, I only need the service instance once!

Describe the solution you'd like Is there any way to have plugins depend on other plugins etc or libraries from nuget where the DI can resolve the types even if they are from different copies of the same DLL? Is this just something that just cannot be done? Perhaps it is a limitation of the MS DI?

Describe alternatives you've considered I tried several differnet ways of setting up the plugins and looked at all examples and docs provided, I cannot seem to find a solution for this issue.

ShaneYu commented 5 years ago

By the way, meant to say in the above post...your library is bloody brilliant and so handy. I was so simple to setup plugin loading as I currently have it. Just hope I can find a way around the above. 😄

natemcmaster commented 5 years ago

Thanks for giving this project a try! I hope it will be useful.

One of the deliberate design choices I made here is that plugins are isolated from each other. If you want to share information between plugins, you will need to create abstractions which the host application provides.

I wanted to make sure this was possible, so I wrote a sample and pushed it to the repo. Take a look and let me know if it answers your questions. https://github.com/natemcmaster/DotNetCorePlugins/tree/master/samples/dependency-injection

ShaneYu commented 5 years ago

Thank you very much for the reply, I totally get why you'd design it for islolated plugins. I will take a look at the sample you've mentioned and give it a go, it's a shame though that you have to add the abstractions to the host always. 👍

I wonder if it is possible to scan in all of the plugin directories for assemblies that are the same and then load the latest verion of the found assemblies into the host context and add to the shared types via interface or something? So that the host doesn't have to have a physical reference to the abstractions. It's mainly that I was wanting plugins to provide other functionality that the host doesn't that other plugins could then use etc.

Update: Your example worked perfectly, so thank you ever so much.

Just as a test though, I had a go at scanning the plugin directories for DLLs that are found more than once, I then loaded those into the default assembly load context. This worked an absolute treat and everything worked as expected, so I am going to continue playing with this approch for now and see what I can do with it.

My only concern if if a DLL is found in multiple plugin directories but of different versions...so I think it's best to try load the newest version when this occurs, but we'll see.

natemcmaster commented 5 years ago

My only concern if if a DLL is found in multiple plugin directories but of different versions...so I think it's best to try load the newest version when this occurs, but we'll see.

And that's why I made a design choice to keep plugins isolated by default. Assembly version fusion is a hard problem to solve correctly. If you want to see an implementation that handles all the edge cases, checkout the build-time solution of this in the MSBuild task called "ResolveAssemblyReferences". https://github.com/microsoft/msbuild/blob/master/documentation/wiki/ResolveAssemblyReference.md

Sewer56 commented 5 years ago

Oh snap, I missed this one.

I should have set this repository to watch since I do myself also make extensive use of this library in one of my projects.

Anyway, if anyone wants another example, my program Reloaded II (universal "Mod Loader" allowing arbitrary managed code injection in native processes), also exposes such a pattern whereby mods (plugins) can share types between each other.

Here is how I do it:

  1. A specific interface, IExports can be optionally implemented by plugins (mods) which, allows the plugins to declare which types should be shared with other plugins:

https://github.com/Reloaded-Project/Reloaded-II/blob/320f691df51ab12ece83035af54558937e79f324/Source/Reloaded.Mod.Interfaces/IExports.cs#L7-L14

  1. Before initializing any of the plugins, I load all exported types from plugins to be loaded into the current AssemblyLoadContext.

https://github.com/Reloaded-Project/Reloaded-II/blob/320f691df51ab12ece83035af54558937e79f324/Source/Reloaded.Mod.Loader/Mods/PluginManager.cs#L300-L306

  1. When I load each plugin (mod), I load the plugin with both others' and the plugin's own exports (loaded in the current AssemblyLoadContext in Step 2) as shared types. (I.e. The this will make the plugin use the type from the current ALC)

https://github.com/Reloaded-Project/Reloaded-II/blob/320f691df51ab12ece83035af54558937e79f324/Source/Reloaded.Mod.Loader/Mods/PluginManager.cs#L213

I would also advise adding some form of mechanism where a plugin can declare which plugins it can unify with (I use unique string IDs, "ModDependencies" and "OptionalDependencies") to prevent sharing unwanted types. With time you will notice, especially if you share extensively that you will need this. I have this implemented as you may have noticed above.

This should work fine so long as you try avoid exporting 3rd party libraries, if you do, you will probably face problems if two versions export different versions of a given third party library.

The docs of my implementation might also provide you with some ideas of what I found to be good practices: https://github.com/Reloaded-Project/Reloaded-II/blob/master/Docs/InterModCommunication.md#introduction

Sorry for being late to the party :P

Sewer56 commented 5 years ago

Related to the post above.

I actually had a fork at one point where I implemented a feature to allow loading of shared types from an arbitrary ALC (as well as the default one).

The benefit of this would be that you could store each plugin's export in a separate ALC, making them unloadable. This is useful as you could you could move, delete and overwrite the shared DLLs at runtime (after unloading, as no handle is open/file isn't used).

Additional benefit is that multiple versions of same libraries could be shared by the application between plugins (i.e. not sharing exports via the default ALC allows for multiple versions of same library).

I don't remember quite why I never decided to implement this on my end (and nuked the commits), but if the either of you are interested, I'd be happy to re-implement this. This would be a very useful optional safety feature in this kind of plugin sharing scenario.

ShaneYu commented 5 years ago

Thanks very much @Sewer56, I will take a look at what you've done and see if it fits my needs. I am not, in this instance, in need of being able to unload and update DLL's on the fly, I am able to just restart the host service. I've not bothered with the IExport approach has you've done, thus far, but I am doing what @natemcmaster has done with the plugin configuration class.

They way I've currently got it working is to scan all of the plugin directories for their dependencies, figure out which ones are used more than once and then load them into the default/current ACL, if more than one version is found, the latest version is loaded. I've set the option in the plugin loading to prefer shared types as well. This seems to be working quite nicely and because I have a little control over the plugins being created and used for my application...I can make sure that as part of updatting a plugin that any dependency updates are done in other plugins also.

Also, being late to the party with good information like the above is better than not contibuting at all, so thanks very much 😄 😄

natemcmaster commented 5 years ago

@ShaneYu: They way I've currently got it working is to scan all of the plugin directories for their dependencies, figure out which ones are used more than once and then load them into the default/current ACL, if more than one version is found, the latest version is loaded.

I think this is a good approach. It's what I would recommend in general for those who want to share types among plugins which are not present in the host. If you think this would be a generally useful API to have in this library, I think it would be worth adding. It's a fairly specific case, so it probably deserves documentation and a sample too.

@Sewer56: thanks for sharing details on Reloaded II. It looks like a nice framework for handling more complicated plugin compositions. With DotNetCorePlugins, I've chosen to keep the loader fairly simple. Along with your project, there are several others that people use to compose large applications together, such as System.Composition (MEF). Rather than re-invent, I'd rather provide integration and support for those composition frameworks. I'd be happy to add a sample to this repo showing how DotNetCorePlugins can be used with Reloaded II, or links in the docs, if you think it's useful.

Sewer56 commented 5 years ago

I'd be happy to add a sample to this repo showing how DotNetCorePlugins can be used with Reloaded II, or links in the docs, if you think it's useful.

No need in this case, no user of Reloaded would ever require to directly work with DotNetCorePlugins, in fact they couldn't, unless they were directly contributing to the loader code itself. In addition. Reloaded II's documentation is fairly complete and self contained for any developer who wishes to use it.

I believe you may have misunderstood what Reloaded II is. Reloaded II at the core is an application (plugin loader) of its own that can be used for extending existing native applications. It is a loader that loads arbitrary .NET Standard plugins into native applications, which may then use techniques such as hooking/detouring/trampolining to alter the behaviour of a given native application.

It also contains a plugin manager (WPF GUI), which controls which plugins should be loaded into which application among many other features. In other words, Reloaded II can best be described as a tool/standalone application to get .NET code running inside native applications.

I mostly use it personally to tamper with old games, as a hobby for my spare time. You can do a lot with it in the hands of the right person.

(This is Sonic Heroes, a game from 2003 running in flawless widescreen at an 32:9 aspect ratio. Normally this game cannot run in anything else than 4:3 and any of the 4 hardcoded resolutions.)

Reloaded II Plugin

natemcmaster commented 5 years ago

Gotcha, thanks for the clarification.

jzabroski commented 4 years ago

I think the paradigm @natemcmaster chose fits that of Visual Studio and similar "integrated development environments" or what I think of as "Application Shell" Architectural Style.

Application Shell Architectural Style is basically object capability model: Each plugin only has access to resources provided to it by a specific part of the shell. Some tools, like Visual Studio, don't tunnel these resources all the way down and instead expose Resources for Plugins as statics. However, the basic things I see in Application Shell Architectural Style are:

Sorry if this is not clear. It's something I spent over a decade of my life thinking about. I first encountered this architecture reading Charles Petzold's books, but I have since viewed his explanations as baroque and idiosyncratic.

jzabroski commented 4 years ago

An abbreviated version of Ka-Ping Yee's Secure Interaction Design is available here: Guidelines and Strategies for Secure Interaction Design. I've joking referred to this as, "The Definitive Explanation of why Linux's PAM subsystem is deplorable, terrible, and will give you a Horrible, No Good, Very Bad Day".

Once you realize Access Control and Concurrency Control are two sides of the same coin, you will never go back to your old way of building composite applications.*

stale[bot] commented 3 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Please comment if you believe this should remain open, otherwise it will be closed in 14 days. Thank you for your contributions to this project.

stale[bot] commented 3 years ago

Closing because there was no response to the previous comment. If you are looking at this issue in the future and think it should be reopened, please make a comment and mention natemcmaster so he sees it.