Loading assemblies and versioning

前端 未结 3 583
有刺的猬
有刺的猬 2021-01-06 06:03

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

3条回答
  •  伪装坚强ぢ
    2021-01-06 06:43

    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:

    1. 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. dependency chains 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.

    2. 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!

    3. 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:

    1. Scope your extensibility requirements! Everything you want to accomplish will cost you in ongoing maintenance.
    2. Think about how you will go about deprecating functionality
    3. Don't forget that your contracts will contain entities as well as logic contracts, these have slightly different considerations to logic contracts because they are often passed around much more.
    4. Carefully consider if each compatibility check would be better performed at compile time instead of run-time (or vice-versa)
    5. Version numbering! Assembly version numbers are great for runtime behavior, file version numbers are there to help you track a DLL back to the source in your version control - make use of both of them!
    6. If you are doing custom DLL resolution, use the app.config to define your custom DLL locations, NOT the assembly resolve events. The config approach is much more predictable, and, hey, it's declared in easily readable Xml! The Fusion Log Viewer will also nicely report where in the probing chain your DLL was inserted, whereas the assembly-resolve event will hide all your logic and rules in code. The only real downside is that using the app.config means changes wont take effect until your application re-reads the config file (typically app restart), but if you are doing AppDomain isolation of your plugins, you can even get around this.

    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...

提交回复
热议问题