3

We developed a generic .NET library for our line-of business applications. This is what our namespace/class structure looked like 5 years ago:

...
AcmeCorp.Tools.ExcelWriter
AcmeCorp.Tools.Code128Barcode
...

5 years later, the number of classes in the generic Tools namespace has grown (e.g. there's now a CSVWriter in addition to an ExcelWriter and a QRBarcode in addition to a Code128Barcode). Thus, I'd like to refactor the namespaces to look like this:

...
AcmeCorp.Export.ExcelWriter
AcmeCorp.Export.CSVWriter
AcmeCorp.Export.SomeUtilityClassUsedByAllExportClasses
AcmeCorp.Barcode.Code128Barcode
AcmeCorp.Barcode.QRBarcode
...

I know how to refactor namespaces in C# - that's not the question. The question is: How do I keep my library backwards-compatible?

Ideally, I'd like to do something like this:

namespace AcmeCorp.Barcode
{
    [AlsoAvailableViaDeprecatedLegacyNamespace("AcmeCorp.Tools")]
    public class Code128Barcode
    {
        ...
    }
}

which would automatically create a proxy class AcmeCorp.Tools.Code128Barcode which (a) redirects all calls to the new class and (b) annotates the proxy class with the [Obsolete("Use AcmeCorp.Barcode.Code128Barcode instead.")] attribute. Alas, no such AlsoAvailableViaDeprecatedLegacyNamespace attribute exists. (Or does it?)

The only other solution I could think of would be to write such a proxy class myself, which would be a lot of useless boilerplate code.

Is there any other solution that I have missed? To reiterate: My goal is to

  • move classes into a more suitable namespaces, but
  • keep the library backwards-compatible (no source-level or binary-level breaks),
  • avoid code duplication and boilerplate code (as far as possible), and
  • (bonus:) mark all uses of the "old" namespace as obsolete.
1

2 Answers 2

5

How do I keep my library backwards-compatible?

The short answer is: you can't.

If you move a type from one namespace to another, your choices are to manually create a delegation class in the old place that internally maps to the new location (and mark it obsolete), or you make a breaking change.

But I'd question what you are proposing anyway. There exists the design warning, CA1020: Avoid namespaces with few types. It exists because Microsoft's guidelines state:

Make sure that each of your namespaces has a logical organization, and that a valid reason exists to put types in a sparsely populated namespace. Namespaces should contain types that are used together in most scenarios. When their applications are mutually exclusive, types should be located in separate namespaces

In other words, "the number of classes in the generic Tools namespace has grown" is not necessarily a good reason to move types out of a namespace.

UPDATE

Ewan shows a practical way of achieving "manually create a delegation class in the old place that internally maps to the new location (and mark it obsolete)". There's one point to note here in relation to your question though. Every developer worth their salt will set the "treat warnings as errors" flag on their code. Those Obsolete attributes will result in warnings, which in turn result in errors. So this approach is still technically a breaking change.

Don't be shy of breaking changes though. That's what semantic versioning is there for.

2
  • 2
    You raise some very valuable points, thanks. I'm less scared about Obsolete being a breaking change, because that use case is exactly what the Obsolete attribute is made for. If developers want to treat obsolete warnings as errors, that's their choice (and it's usually a good choice, just saying), but it shouldn't stop a library developer from using the Obsolete attribute as it has been intended.
    – Heinzi
    Commented Jul 3, 2019 at 8:03
  • "Don't be shy of breaking changes though. That's what semantic versioning is there for." In theory, yes. In practice, going from v1.x.x to v2.0.0 means that, for a considerable amount of time, I'll have to maintain two branches of my library, since not everyone sees incorporating my breaking changes as their top priority.
    – Heinzi
    Commented Oct 11, 2022 at 14:23
2

It seems like a bad idea, just make a new version with the new namespace. But....

namespace oldNameSpace
{
    [Obsolete("use the new namespace")]
    public class origionalClass
    {
    }
}

namespace newNameSpace
{
    public class origonalClass: oldNameSpace.origonalClass
    {
    }
}
4
  • For many, that will work well. In my view though, it's good practice to mark all API classes as sealed, so this approach doesn't work. It becomes necessary to write a delegation class instead. More work, but equally achievable.
    – David Arno
    Commented Jul 3, 2019 at 6:39
  • Nice idea. Yes, it's a hack (abusing inheritance for code reuse), but, so far, it comes closest to what I had in mind. I'd probably reverse the inheritance, though, i.e., I'd move the class into the new namespace and have the old class inherit from the new one. This has a few advantages: (1) The actual code is in the new namespace, where it should be, (2) removing the old class won't require changes in the new class, (3) the ugly additional "compatibility layer" in the inheritance hierarchy is only in the ugly old class, which we don't want to use anymore anyway, not in the shiny new class.
    – Heinzi
    Commented Jul 3, 2019 at 7:47
  • The drawback of my approach (old inherits from new) would be that the new class can't be sealed (as David Arno correctly mentions in his comment), but that's something I can live with.
    – Heinzi
    Commented Jul 3, 2019 at 7:48
  • as you are changing the code you can simply unseal the old class as required. Of course then someone might inherit from it, but you have marked it obsolete and are removing it anyway
    – Ewan
    Commented Jul 3, 2019 at 8:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.