I\'m contemplating adding some extensibility into an existing app by providing a few predefined interfaces that can be implemented by \"plugins\" dropped at a specific locat
You might be able to use Binding redirects.
I have worked quite extensively in this space, and have consistently found that you need to put down some boundaries around exactly what is in, and out, of scope. It is a risk to want the most extensibility, however it all comes at a cost. That cost will either be compile-time, in the form of conventions, best practices, and perhaps automated build checks - or it will be a complicated runtime.
To address the options that you have listed:
Binding redirects are only a partial tool to achieve a solution. They will allow your program to 'slip' one version of a DLL in place of another, however, it will not magically solve the problem of what happens when methods change. MissingMethodException? Something here that you may not have though of aswell, is the dependency chain. You can see that while the application is treating dependency 'A' as a version 1 object, internally it is creating something from a later version which is being passed back to the application and cast to v1.0 - causing an exception. This can be tricky to handle - and is just one of the risks.
Keeping the contracts assembly version the same across builds This can work elegantly, however, it is just deferring the complexity from build-time, to runtime. You will need to be diligent to ensure that your changes to not break compatibility across versions. Not to mention that as your application ages, you will collect a lot of declarations in these contracts that you will want to deprecate. Eventually this file will become large, cumbersome, and confusing for developers - and that is not even accounting for all the entity contracts you have as well!
I'm not too sure what you mean by this one, and how it covers your problem space.
An alternate approach you could take, which we did, is to create a new contract for each major release of the 'SDK'. This has some political advantages in that it fixes major functionality for a period of time, meaning that we can keep feature requests at a reasonable level of expectation, and anything beyond that (requiring a new generation of contract) is deferred until 'next major release'. This does, however, require diligence in the design of your functionalities - write your contracts with some forethought so you can pre-empt the most obvious requirements - however I really feel that this goes without saying anyway... they are called 'contracts' for a reason. Each new contract would exist in a version namespace (Company.Product.Contracts.v1_0, Company.Product.Contracts.v1_1).
I would NOT chain my contracts (each new version of contract inheriting the last). Doing so takes you back to the issues with keeping the version number the same, you can never fully get rid of functionality without completely breaking the chain.
When your plugin loads, it can ask the host what level of functionality it supports (contract version) - and if its an old host/newer plugin scenario: either, program the plugin to reduce its runtime functionality to deal with the lesser host capabilities, or simply refuse to load. You should probably be performing these checks anyway, because there is no magic that will allow your plugin to utilise functionality in the host that simply isn't there! Microsoft's MAF framework attempts to achieve this by using a shim infrastructure, but it leads to a massive amount of complexity for most people.
So, some things you will need to consider are:
Pertaining to your 'core.dll' problem... I think, for you, this is a relatively simple problem. Everything in your Core contracts DLL should exist under a version namespace (see above, Company.Product.v1_0 etc), so it actually makes sense that your DLL also contain a version number. This will remove the problem of your DLLs overwriting each other when deployed to the bin folder. DONT RELY ON THE GAC - this will be a PAIN in the long run. As much of a shame as it is, developers always seem to forget that the GAC overrides everything, and this can become a debugging nightmare - it would also influence your deployment scenarios with regards to permissions.
If you do need to keep the DLL name the same - you can create a 'local gac' within your application which will enable you to store your DLLs in such a way that they dont over-write each other, but are all still resolvable by the runtime. Check out 'binding redirect' in the app.config (see my answer here). This can be combined with a 'pseudo GAC folder structure' under your application's bin-folder. Your app will then be able to locate any version of DLL required without any custom assembly-resolving code-logic. I would deploy all previously supported versions of your core.dll, with your application. If your application gets to version 9, and you have decided to support versions 7 and 8 of plugins as well, then you only need to include Core.7.dll, Core.8.dll and Core.9.dll. Your plugin loading logic should detect dependencies on older versions of Core, and alert the user that the plugin is not compatible.
There is a lot to this topic, if I think of anything else that is pertinent to your cause, I'll check back...
Whatever your reasons may be to avoid your 3 points (which I think are more reliable and apt), you can take advantage of Assembly Resolve Event.
Include code such as following in your core.dll
static Assembly AppDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
//replace the if check with more reliable check such as matching the public key as well
if (args.Name.Contains(Assembly.GetExecutingAssembly().GetName().Name))
{
Console.WriteLine("Resolved " + args.Name + " as " + Assembly.GetExecutingAssembly().GetName());
return Assembly.GetExecutingAssembly();
}
return null;
}
Bind the above handler before attempting to load your plugins. If you load the plugins in current Appdomain then bind it to AssemblyResolve
of current domain.
For e.g.
[SecurityPermission(SecurityAction.Demand, ControlAppDomain = true)]
public static void LoadPlugins()
{
AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve;
Assembly pluginAssembly = AppDomain.CurrentDomain.Load("MyPlugin");
}