Changing the interface between versions - how to manage?

Here is the rather unpleasant pickle we got on the client site. The client has about 100 workstations on which we deployed version 1.0.0 of our MyApp product.

Now one of the things the product does is download the add-in (name it “MyPlugIn”, which it first searches on the central server to see if there is a newer version, and if it then copies this file locally, then downloads the add-in using Assembly.Load and invokes a certain well-known interface that works well for several months.

Then the client wanted to install v1.0.1 of our product on some machines (but not all). This is due to the new and updated version of MyPlugIn.

But then a problem arose. There is a common DLL referenced by both MyApp and MyPlugIn, called MyDLL, which have a MyClass.MyMethod method. Between v1.0.0 and v1.0.1, the signature MyClass.MyMethod has changed (parameter added). And now, the new version of MyPlugIn crashes client applications v1.0.0:

Method not found: MyClass.MyMethod (System.String)

The client clearly does not want to deploy v1.0.1 on all client stations, since the patch included in v1.0.1 was necessary only for several workstations, and there is no need to roll it back to all clients. Unfortunately, we do not use ClickOnce utilities or other utilities yet mass deployment, so deploying v1.0.1 will be a painful and otherwise unnecessary exercise.

Is there a way to write code in MyPlugin so that it works equally well, regardless of whether it deals with MyDLL v1.0.0 or v1.0.1? Perhaps there is some way of exploring the expected interface using reflection to see if it exists before calling it?

EDIT: I should also mention - we have fairly strict QA procedures. Since v1.0.1 was officially released by QA, we are not allowed to make any changes to MyApp or MyDLL. The only freedom of movement that we have is to change MyPlugin, which is special code written specifically for this client.

+6
source share
7 answers

I extracted this code from an application that I wrote some time ago and deleted some parts.
A lot of things are supposed here:

  • Location MyDll.dll is the current directory
  • The namespace for reflection information is "MyDll.MyClass"
  • The class has a constructor without parameters.
  • You do not expect a return value
 using System.Reflection; private void CallPluginMethod(string param) { // Is MyDLL.Dll in current directory ??? // Probably it better to call Assembly.GetExecutingAssembly().Location but.... string libToCheck = Path.Combine(Environment.CurrentDirectory, "MyDLL.dll"); Assembly a = Assembly.LoadFile(libToCheck); string typeAssembly = "MyDll.MyClass"; // Is this namespace correct ??? Type c = a.GetType(typeAssembly); // Get all method infos for public non static methods MethodInfo[] miList = c.GetMethods(BindingFlags.Public|BindingFlags.Instance|BindingFlags.DeclaredOnly); // Search the one required (could be optimized with Linq?) foreach(MethodInfo mi in miList) { if(mi.Name == "MyMethod") { // Create a MyClass object supposing it has an empty constructor ConstructorInfo clsConstructor = c.GetConstructor(Type.EmptyTypes); object myClass = clsConstructor.Invoke(new object[]{}); // check how many parameters are required if(mi.GetParameters().Length == 1) // call the new interface mi.Invoke(myClass, new object[]{param}); else // call the old interface or give out an exception mi.Invoke(myClass, null); break; } } } 

What are we doing here:

  • Load the library dynamically and extract the MyClass type.
  • Using the type, ask the reflection subsystem for the MethodInfo list present in this type.
  • Check the name of each method to find the required one.
  • When the method is found, instantiate the type.
  • Get the number of expected parameters by the method.
  • Depending on the number of parameters, invoke the correct version using Invoke .
+3
source

The fact is that the changes you made should be basically, not changes. Therefore, if you want to be compatible in your deployment (as I understand it in the current deployment strategy, you have this only option), you should never change the interface, but add new methods to it and avoid tightly linking your plugin with a common DLL, but load it dynamically. In this case

  • you add new functionality without breaking the old

  • You will be able to select the version of the dll to load at runtime.

+4
source

My team made the same mistake as yours more than once. We have a similar plugin architecture, and the best advice I can give you in the long run is to change this architecture as soon as possible. This is a nightmare of repairability. The backward compatibility matrix grows non-linearly with each release. Strict code checks may give some relief, but the problem is that you always need to know when the methods were added or changed in order to invoke them accordingly. If both the developer and the reviewer know exactly when the last method was changed, you risk that there is an exception at runtime when the method is not found. You can never safely call a new method in MyDLL in a plugin, because you can run on an older client that does not have the latest version of MyDLL using these methods.

You can currently do something similar in MyPlugin:

 static class MyClassWrapper { internal static void MyMethodWrapper(string name) { try { MyMethodWrapperImpl(name); } catch (MissingMethodException) { // do whatever you need to to make it work without the method. // this may go as far as re-implementing my method. } } private static void MyMethodWrapperImpl(string name) { MyClass.MyMethod(name); } } 

If MyMethod is not static, you can create a similar non-stationary shell.

Regarding long-term changes, one thing you can do at the end is to give your plugin interfaces a chat. After the release, you will not be able to change the interfaces, but you can define new interfaces that will use later versions of the plugin. In addition, you cannot call static methods in MyDLL from MyPlugIn. If you can change the situation at the server level (I understand that this may be beyond your control), another option is to provide some version support so that the new plugin can declare that it does not work with the old client. Then the old client will download only the old version from the server, and new clients will download the new version.

+3
source

It actually sounds like a bad idea to change a contract between releases. Being in an object-oriented environment, you better create a new contract, possibly inheriting from the old one.

 public interface MyServiceV1 { } public interface MyServiceV2 { } 

Inside, you make your own engine to use the new interface, and you provide an adapter for translating old objects into the new interface.

 public class V1ToV2Adapter : MyServiceV2 { public V1ToV2Adapter( MyServiceV1 ) { ... } } 

After loading the assembly, you scan it and:

  • when you find a class that implements a new interface, you use it directly
  • when you find a class that implements the old interface, you use the adapter through it

Using hacks (like testing an interface) will sooner or later bite you or anyone else using a contract - the details of the hack should be known to anyone who relies on an interface that sounds awful from an object-oriented point of view.

+2
source

In MyDLL 1.0.1, discard the old MyClass.MyMethod(System.String) and reload it with the new version.

+1
source

Could you please overload MyMethod to accept MyMethod (string) (compatible with version 1.0.0) and MyMethod (string, string) (version v1.0.1)?

+1
source

Given the circumstances, I think the only thing you can do is to have two versions of MyDLL working side by side,
and that means something like what Tigran suggested dynamically loading MyDLL - for example, as a side example, not related, but might help you, look at RedemptionLoader http://www.dimastr.com/redemption/security.htm# redemptionloader (that for Outlook plugins, which often have problems related to each other, refer to different versions of the auxiliary dll as a backstory - this is a slightly more complicated reason for COM interaction, but it does not change much) -
this is something you can do, something like that. Dynamically load the dll by its location, name - you can specify this location inside, hard code, or even configure it from a configuration or something like that (or check and do it if you see that MyDll does not match the version), <w > and then "wrap" the objects, the calls form a dynamically loaded dll to match what you usually have, or do some kind of trick (you will have to wrap something or a "plug" during implementation) so that everything works in both cases.
Also add "no-nos" and your doubts about QA :),
they should not violate backward compatibility from 1.0.0 to 1.0.1 - these are usually small changes, corrections - without breaking the changes, this requires the main version #.

+1
source

Source: https://habr.com/ru/post/912171/


All Articles