Deprecating methods with optional arguments

eduherminio

Eduardo

Posted on December 5, 2020

Deprecating methods with optional arguments

Introduction

Optional arguments

From C# 4 methods, indexers, constructors, and delegates can have both named and optional arguments.

In the following example, int bar is a named argument and bool isZeroIndexArray is an optional argument, which becomes true if no other value is provided.

void Foo(int bar, bool isZeroIndexArray = true) { }
Enter fullscreen mode Exit fullscreen mode

ObsoleteAttribute

Since .NET Framework 1.1 ObsoleteAttribute can be used to "mark program elements as no longer in use". Those program elements include classes, methods, fields, etc.

It's a common practice to place it in those elements that are no longer maintained and/or may disappear in future versions:

[Obsolete("This method is obsolete. Please use xxxx instead.")]
void Foo(bool isZeroIndexArray = true) { }
Enter fullscreen mode Exit fullscreen mode

Semantic versioning

Semantic versioning is a common practice in software development nowadays (or at least it should be!).

Essentially, it's a way of versioning software that sticks to the following rules:

Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes,
  2. MINOR version when you add functionality in a backwards compatible manner, and
  3. PATCH version when you make backwards compatible bug fixes.

Deprecating methods that have optional arguments

Real life scenario

Sometimes one doesn't want their users to continue using a method/class/property because:

  • There are plans to remove it in the future.
  • There is already a working alternative in place that mimics its behavior.

Given that scenario, there are some chances that, sticking to semantic versioning and its advice about handling deprecation, you want to release some minor versions where the legacy part of your code appears as 'obsolete' before actually removing it (and therefore bumping your major version).

And there are also some (smaller) chances that the method that needs to be deprecated contains optional arguments.

That's the scenario we're going to focus on.

Initial implementation and gotcha

Let's go back to one of the examples presented in the introduction:

We've always had the following method in our library:

public class OurClass
{
    public static void Foo(bool isZeroIndexArray = true) { }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

We now want to allow our users to use Foo() method in a more flexible way, so we've implemented:


public class FooConfiguration
{
    public bool IsZeroIndexArray { get; set; }

    public int Verbosity { get; set; }

    public FooConfiguration() { IsZeroIndexArray = true; }
}

public class OurClass
{
    public static void Foo(FooConfiguration configuration? = null) { }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Since we don't want to duplicate or complicate our Foo() implementation, ideally we'd like to get rid of void Foo(bool); but we don't want to bump our major version (yet).

However, we do want to release our new code with void Foo(FooConfiguration?).

The solution, as we previously hinted, is marking the old one as obsolete, adding the new one and releasing the code with a new minor version. But there's a gotcha when doing that:

If we simply do

public class OurClass
{
    [Obsolete("This method is obsolete. Please use Foo(FooConfiguration?) instead.")]
    public static void Foo(bool isZeroIndexArray = true) { }

    public static void Foo(FooConfiguration? configuration? = null) { }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

we will potentially break the following user's code:

OurClass.Foo();
Enter fullscreen mode Exit fullscreen mode

They'll get compiler errors similar to:

error CS0121: The call is ambiguous between the following methods or properties: 'OurClass.Foo(bool)' and 'OurClass.Foo(FooConfiguration?)
Enter fullscreen mode Exit fullscreen mode

That is, we would be violating semantic versioning rules, so 💩.

Correct implementation

Fortunately not all hope is gone and there's a way of keeping the required compatibility while releasing our new functionality:

public class OurClass
{
    [Obsolete("This method is obsolete. Please use Foo(FooConfiguration?) instead.")]
    public static void Foo(bool isZeroIndexArray) { }

    public static void Foo(FooConfiguration? configuration = null) { }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Did you notice the change? Now our legacy void Foo(bool isZeroIndexArray) doesn't have its optional argument any more.

This allows all possible usages of our method to keep compiling:

// Will use Foo(bool FooConfiguration? = null)
OurClass.Foo();

// Will use Foo(bool isZeroIndexArray) and get a compiler warning
OurClass.Foo(true);

// Will use Foo(bool isZeroIndexArray) and get a compiler warning
OurClass.Foo(false);
Enter fullscreen mode Exit fullscreen mode

And if we implement correctly void Foo(FooConfiguration?), we'll manage to keep Foo()'s behavior consistent.

💖 💪 🙅 🚩
eduherminio
Eduardo

Posted on December 5, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related