Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interface change between versions - how to manage?

Here's a rather unpleasant pickle that we got into on a client site. The client has about 100 workstations, on which we deployed version 1.0.0 of our product "MyApp".

Now, one of the things the product does is it loads up an add-in (call it "MyPlugIn", which it first looks for on a central server to see if there's a newer version, and if it is then it copies that file locally, then it loads up the add-in using Assembly.Load and invokes a certain known interface. This has been working well for several months.

Then the client wanted to install v1.0.1 of our product on some machines (but not all). That came with a new and updated version of MyPlugIn.

But then came the problem. There's a shared DLL, which is referenced by both MyApp and MyPlugIn, called MyDLL, which has a method MyClass.MyMethod. Between v1.0.0 and v1.0.1, the signature of MyClass.MyMethod changed (a parameter was added). And now the new version of MyPlugIn causes the v1.0.0 client apps to crash:

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

The client pointedly does not want to deploy v1.0.1 on all client stations, being that the fix that was included in v1.0.1 was necessary only for a few workstations, and there is no need to roll it out to all clients. Sadly, we are not (yet) using ClickOnce or other mass-deployment utilities, so rolling out v1.0.1 will be a painful and otherwise unnecessary exercise.

Is there some way of writing the code in MyPlugin so that it will work equally well, irrespective of whether it's dealing with MyDLL v1.0.0 or v1.0.1? Perhaps there's some way of probing for an expected interface using reflection to see if it exists, before actually calling it?

EDIT: I should also mention - we have some pretty tight QA procedures. Since v1.0.1 has been officially released by QA, we are not allowed to make any changes to MyApp or MyDLL. The only freedom of movement we have is to change MyPlugin, which is custom code written specifically for this customer.

like image 970
Shaul Behr Avatar asked Apr 01 '12 12:04

Shaul Behr


6 Answers

The thing is that the changes you made have to be basically in addition and not the change. So if you want to be back compatible in your deployment (as much as I understood in current deployment strategy you have this is an only option) you should never change the interface but add a new methods to it and avoid tight linking of your plugin with shared DLL, but load it dynamically. In this case

  • you will add a new funcionality without disturbing a old one

  • you will be able to choose which version of dll to load at runtime.

like image 172
Tigran Avatar answered Nov 08 '22 10:11

Tigran


I have extracted this code from an application I wrote some time ago and removed some parts.
Many things are assumed here:

  1. Location of MyDll.dll is the current directory
  2. The Namespace to get reflection info is "MyDll.MyClass"
  3. The class has a constructor without parameters.
  4. You don't expect a return value
using System.Reflection;

private void CallPluginMethod(string param)
{
     // Is MyDLL.Dll in current directory ??? 
     // Probably it's 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 we do here:

  1. Load dynamically the library and extract the type of MyClass.
  2. Using the type, ask to the reflection subsystem the list of MethodInfo present in that type.
  3. Check every method name to find the required one.
  4. When the method is found build an instance of the type.
  5. Get the number of parameters expected by the method.
  6. Depending on the number of parameters call the right version using Invoke.
like image 20
Steve Avatar answered Nov 08 '22 11:11

Steve


My team has made the same mistake you have 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 maintainability nightmare. The backwards compatibility matrix grows non-linearly with each release. Strict code reviews can provide some relief, but the problem is you always need to know when methods were added or changed to call them in the appropriate way. Unless both the developer and reviewer know exactly when a method was last changed you run the risk of there being a runtime exception when the method is not found. You can NEVER call a new method in MyDLL in the plugin safely, because you may run on a older client that does not have the newest MyDLL version with the methods.

For the time being, you can do something like this 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 make a similar non-static wrapper.

As for long term changes, one thing you can do on your end is to give your plugins interfaces to communicate through. You cannot change the interfaces after release, but you can define new interfaces that the later versions of the plugin will use. Also, you cannot call static methods in MyDLL from MyPlugIn. If you can change things at the server level (I realize this may be outside your control), another option is to provide some sort of versioning support so that a new plugin can declare it doesn't work with an old client. Then the old client will only download the old version from the server, while newer clients download the new version.

like image 3
Mike Zboray Avatar answered Nov 08 '22 10:11

Mike Zboray


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

public interface MyServiceV1 { }

public interface MyServiceV2 { }

Internally you make your engine to use the new interface and you provide an adapter to translate old objects to the new interface.

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

Upon loading an assembly, you scan it and:

  • when you find a class implementing the new interface, you use it directly
  • when you find a class implementing the old interface, you use the adapter over it

Using hacks (like testing the interface) will sooner or later bite you or anyone else using the contract - details of the hack have to be known to anyone relying on the interface which sounds terrible from the object-oriented perspective.

like image 2
Wiktor Zychla Avatar answered Nov 08 '22 10:11

Wiktor Zychla


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

like image 1
John Berberich Avatar answered Nov 08 '22 10:11

John Berberich


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

like image 1
vansimke Avatar answered Nov 08 '22 12:11

vansimke