tl;dr – To fully deprecate an extension method when you need to maintain binary compatibility (e.g. plugins), especially if it’s colliding with another extension method, you can simply remove the ‘this’ keyword.
I recently needed to remove a duplicate extension method from the Paint.NET code base. I had two copies of a ServiceProviderExtensions class, one in the PaintDotNet namespace and the other in PaintDotNet.AppModel (which I wanted to remove). They both had a GetService<TService>() extension method:
The second copy was just an absent-minded addition at some point, but of course … some plugins started using the “wrong” one (there’s no way the plugin authors could’ve known). Worse, if you had a using at the top of your C# code file for both namespaces, whether inside the app or out in a plugin, you couldn’t use either of the extension methods because the compiler would give you a “The call is ambiguous between the following methods or properties” error.
Hrumph! So, clearly I wanted to delete one of these. But, how could I do that without breaking plugins? Some were using the first copy, others were using the second copy!
The first solution I came up with was to create a new DLL, let’s call it PaintDotNet.Obsolete.dll. I’d then move the “bad” extension method class into there, add an [assembly: TypeForwardedTo(…)] attribute in the original DLL, and add [Obsolete] onto the methods in the new (err, obsolete) DLL. I couldn’t add [Obsolete] to the class, however, otherwise the TypeForwardedTo attribute would get flagged with an error too. This would mostly work, but was messy and clumsy. I may still need to use this idea in the future to cover other deprecation scenarios.
Finally, it dawned on me: what if I just removed ‘this’ from the extension method signature? This would remove it from IntelliSense, and also remove it as a candidate for extension method resolution at compile time, while still affording binary compatibility with old plugin DLLs.
And … it worked!!!
It works because extension methods and ‘this’ are implemented in the compiler as syntactic sugar. It’s neither a runtime nor a framework feature; once your DLL is compiled, extension methods don’t really exist (at least at the call site).
The ‘this’ keyword turns into an attribute on the method which identifies it as an extension method. When compiling a DLL that consumes the extension method, the code that’s emitted is a normal static method call. Since I wasn’t removing the static method, the old plugin DLLs would still be able to use it.
And thus a years-old conundrum has been solved. Old plugins will continue to work, and new plugins (and new app code!) can use the right extension method without the ambiguity error.
This blog post is based on some tweets I made earlier today: https://twitter.com/rickbrewPDN/status/1146810316887429120