10

I have a nuget package, in it I have a method which has optional args (v1.0)

public void MyMethod(int a = 0){}

I later created a new version of my package with another optional arg (thinking it wasnt a breaking change) (v1.1)

public void MyMethod(int a = 0, int b = 1){}

This package is consumed by another nuget package which references v1.0 I have both v1.1 and the other nuget package included in my project. This means that at a binding level Im using v1.1 and the package using v1.0 is redirecting to the 1.1 dll.

This causes a missing method exception because of the following: https://stackoverflow.com/a/9884700/1070291

I want to fix my library so that either signature will function, my first thought was this:

public void MyMethod(int a = 0, int b = 1){}
public void MyMethod(int a = 0){ MyMethod(a,1); }

However this causes an ambiguous method when its used elsewhere.

I'm wondering if there is some way to fill in the old method for backward compatibility without creating something ambiguous going forward.

I almost want to mark the old signature with something to instruct the compiler to include this in the assembly but not link any new methods to it.

Community
  • 1
  • 1
undefined
  • 33,537
  • 22
  • 129
  • 198
  • I think this might solve your problem? https://learn.microsoft.com/en-us/dotnet/articles/csharp/language-reference/keywords/new-modifier – wannadream May 22 '17 at 03:53
  • Renaming your new `MyMethod` should help. Something like `MyMethod(int a = 0)` and `MyMethodExtended(int a = 0, int b = 1)`. I'm not posting it as an answer because it is ugly :) – Yeldar Kurmangaliyev May 22 '17 at 03:57
  • @wannadream, not sure how you mean I should use new? These methods are side by side and my understanding is that new just hides a method in extension – undefined May 22 '17 at 03:59
  • @YeldarKurmangaliyev that would work but it is really ugly – undefined May 22 '17 at 04:00

4 Answers4

6

Provide a method with explicitly no arguments. For example:

public void MyMethod(int a = 0, int b = 1) { }
public void MyMethod(int a = 0) { MyMethod(a, 1); }
public void MyMethod() { MyMethod(0, 1); }

This removes the ambiguity of having no arguments passed to MyMethod.

Extending for more arguments means you can write something like:

public void MyMethod(int a, int b, int c) { }
public void MyMethod(int a, int b) { MyMethod(a, b, 2); }
public void MyMethod(int a) { MyMethod(a, 1); }
public void MyMethod() { MyMethod(0); }

Note that we can completely remove optional arguments by explicitly implementing each overload. We do lose some intellisense help with default arguments, but this can be somewhat replaced with code comments.

Rob
  • 26,989
  • 16
  • 82
  • 98
  • I thought about this, but in real life I have 3 optional which would make for 8 separate methods which isn't ideal. Do you think theres another way? – undefined May 22 '17 at 03:59
  • You could remove MyMethod(int a = 0) `public void MyMethod(int a = 0, int b = 1) { } public void MyMethod() { MyMethod(0, 1); }`. You're always going to use new method. – Miguel May 22 '17 at 04:01
  • 1
    @LukeMcGregor You shouldn't need 8 separate methods - you merely need to implement the no-argument method to remove the ambiguity, regardless of how many parameters you have. – Rob May 22 '17 at 04:01
  • If someone calls `MyMethod(3)` which method would be invoked in this scenario? – Alisson Reinaldo Silva May 22 '17 at 04:03
  • Just wow. I'm not going to use optional arguments in my public assemblies anymore :) – Yeldar Kurmangaliyev May 22 '17 at 04:03
  • @Alisson In the answer I posted, `MyMethod(3)` would bind to the second definition – Rob May 22 '17 at 04:06
  • @LukeMcGregor See update; if you're willing to remove some the optional arguments (behaves the way in code, you'll lose some intellisense over the default value, but you can replace that with code comments) you can bring the number of methods down to 4 for 3 arguments. – Rob May 22 '17 at 04:10
  • 2
    I wish i could remove some of the optionals but what you have above will have to do I think. I actually have `[CallerMemberName]string caller = null` as one of the args and its only sorta optional as it will always be populated by the compiler with the correct default – undefined May 22 '17 at 04:14
  • I do not agree. It's a quick fix but it won't solve the true problem (free usage of optional parameters) and 1) it will **quickly become a mess** if you have too many parameters 2) **callers will be confused** by too many overloads 3) each new public method is a **new contract you have to respect** in future 4) **next time** you will have exactly the same problem. Not something you want to deal with when **writing a library** (where compatibility is more often than not the master). – Adriano Repetti May 22 '17 at 09:43
  • @AdrianoRepetti No, it's not ideal, but the proper approach violates the constraint in the question: `without creating a breaking change` – Rob May 23 '17 at 01:47
  • No, you don't need to "drop" the old method, just add a new one with a new name (and if you have a method with so many parameters you should consider to group them in a class like ProcessStartInfo, in this case you just add another overload). Then you declare the old method obsolete and after few versions you stop its usage at compile-time. Few versions more and you remove it from binaries. – Adriano Repetti May 23 '17 at 08:37
4

I think that trying to solve the issue introduced by default parameters using other overloads with default parameters is just booking for more troubles in future...

Non-breaking change: what I'd do is to add a new method with a different name and keep the old method as-is. Callers of the old interface won't be affected by this change and new users will have the fully featured new method available.

public void MyMethod(int a = 0) {
    MyBetterMethod(a, 1);
}

public void MyBetterMethod(int a, int b) { }

Migrate from old syntax: mark the old method [Obsolete] (and remove it in a future version). Do not forget to suggest the right method to use in the warning message.

Keep it as warning for few versions:

[Obsolete("This method is obsolete, use MyBetterMethod() instead.")]
public void MyMethod(int a = 0){}

With your next major release make it a compile-time error:

[Obsolete("This method is obsolete, use MyBetterMethod() instead.", true)]
public void MyMethod(int a = 0){}

Later you may consider to definitely remove that code and break binary compatibility. Life-cycle is better described in this Eric Lippert's SE answer (note that for simple deployments, if you do not need to keep binary compatibility, he suggest to remove code as soon as the deprecation became a compile-time error.)

Favor usage of new syntax: hide the obsolete method to minimize the chances that new users will call it instead of the new one:

[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method is obsolete, use MyBetterMethod() instead.")]
public void MyMethod(int a = 0){}

Learn from this: do not use optional parameters, this is just one of the issues you will have (of course there are proper use-cases but IMO they're an exception, not the rule).

In general when writing a library you have a different approach to your public interface, be very careful about your class contract (to read as: quick fixes are an heritage you do not want to have).

When your method has too many parameters and you want to make callers' life easier then you probably have a design issue and for the rare cases where a default is really required then you should strongly consider to use an overloaded version. That said, I can't judge because I do not see your real code, there are chances that new method is doing something different or that it is doing too much.

When many parameters are unavoidable you should consider to group them all in a separate class that will be the only parameter of the method. Any time in future you can add more properties to that class and you won't break compatibility. See, for example, Process.Start() and its ProcessStartInfo. In this case you do not even need to rename new method:

public sealed class MyMethodInfo {
    public int A { get; set; } = 0;
    public int B { get; set; } = 1;
}

public void MyMethod(MyMethodInfo info) {
}

[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method is obsolete, use MyMethod(MyMethodInfo) instead.")]
public void MyMethod(int a = 0) {
    MyMethod(new MyMethodInfo { A = a });
}

One important note: default values are an important part of your interface contract, use them cum grano salis, and only when they really make sense, or force callers to specify a value. Always.

Adriano Repetti
  • 65,416
  • 20
  • 137
  • 208
  • Yeah going to be much more careful about optionals now, i hadnt thought of this issue before it found me out. – undefined May 22 '17 at 10:23
2

I would define 1 non-optional parameter to make it non-ambiguous.

public void MyMethod(int a, int b){} // first new method: this will be your main functioning method
public void MyMethod(int a = 0){ MyMethod(a,1); } // second old method: calls the main method above

There should not be ambiguity here, because:

  • 0 parameter will call the second old method, which will call the first new method
  • 1 parameter will call the second old method, which will call the first new method
  • 2 parameters will call the first new method

In short, every methods are directed to the first new method.

kurakura88
  • 2,185
  • 2
  • 12
  • 18
-1

Just for completeness here is how I eventually solved this issue.

public void MyMethod(int a = 0){ MyMethod(1,a); }
public void MyMethod(int b, int a = 0){}
  • Reinstated the origional method signiture in place
  • Added the new parameter in a new overload as a reqired parameter (which forced it before the existing optionals in the args list)
  • Called through with the default value from the old signiture to the new

The net effect is an 'optional' parameter done with an overload.

This works because the new overload now can only be called with the new argument making it un-ambiguous with the old implementation.

There are things I dislike about having to do this

  • Parameter order isnt as logical
  • If you repeat this process things get messier and you have to deal with permutations

Rules for Nuget and optional

Heres my psudo rules to help do this better in the future.

  • If you add a parameter (regardless of if there are optionals or not) it must be done in a new overload
  • New paramaters cannot be optional (ever)
  • If you need to add an optional (like something with [CallerMemberName]) you have to do so with an overload which also changes the signiture (either by adding another non-optiona parameter, changing method name ect)

Its important to note this only applies to libraries which are shipped pre-compiled (like nuget) if you are project referencing things you wont run into this problem.

Thanks to all who answered and helped me think this through

undefined
  • 33,537
  • 22
  • 129
  • 198