Extension Methods

Published on Saturday, July 10, 2021

Extension Methods in C# are a crucial element of modern .NET. They are the foundation of LINQ and used everywhere. However, the are sometimes considered as a bad smell when it comes to code reviewing. In this article I want to discuss the concept of extension methods and its position in the OO patterns & principles.

Note: Extension Methods are often used to build domain specific languages (might it be a SwiftUI-alike, a mocking specification system, ...). Everything goes in this space. The target there is the newly constructed language. In this article, I discuss the integration into the regular C# programming language and not a specialization of it.

SOLID Principles

SOLID principles are a corner stone of object oriented design.

  • S ingle Responsibility Principle (SRP): "There should never be more than one reason for a class to change." In other words, every class should have only one responsibility.
  • O pen Closed Principle (OCP): "Software entities ... should be open for extension, but closed for modification."
  • L iskov Substitution Principle (LSP): "Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it". See also design by contract.
  • I nterface Segregation Principle (ISP): "Many client-specific interfaces are better than one general-purpose interface."
  • D ependency Inversion Principle (DIP): "Depend upon abstractions, not concretions."

-- via Wikipedia

Soft Skills (of Code)

Under the world of patterns and principles there are also soft skills 😀.

  • Readability: Code is read 100 times as often as it is created or modified. It is read in full fledged editors or in a diff tool on the command line without even syntax highlighting. Regular (as in 99% of) code has to be optimized for readability.
  • Usability: When writing function with the intention of other developers using it, usability is a concern. The definition of the function as well as its integration into the workflow of the user in the IDE matters here.

The Evaluation

So are exension methods violating these principles? Are they a bad thing in software design?

Let us dissect different use cases!

  • Extending fundamental base types: Adding an extension method to a base type.

    string foo = "Hello World";
    
    var hash = foo.HashSha1();
    
    // vs. static method
    var has = Sha1.Hash(foo);
    

    ❌ Single Responsibility: There is no space for hashing in strings. .WordCount() or .IsASCII() maybe, because the contribute to the responsibility to represent text. But Hash is a different domain. The responsibility of hashing and the responsibility of text representation does not intersect. This usage only moves the argument from the braces to the context.

    ❌ Readability: There is no gain versus the usage in traditional static methods. The extension method feels like a transformation, the static method invocation like a function call. Both are fundamentally well understood concept when reading code. Both are valid patterns in object oriented programming and functional programming. The significant negative thing the extension method does is hiding the ownership of the function.

    ❌ Usability: There is no benefit in the general usability either. Auto-Completion would be cluttered with suggestions of countless domains. Namespace (and by extend static class names) with using, static using and global using have a rationale to exist, which is de-cluttering the global namespace and resolving potential conflicts in naming. Extension methods are typically attached to more generic namespaces to unfold their availability.

  • Throw helpers: Simplify your parameter guards by using one-line-extension methods.

    public void Execute(string command, int count)
    {
        command.ThrowIfNullOrEmpty();
        count.ThrowIfSmallerThan(0);
        // ...
    

    ❌ Single Responsibility: See above.

    ❌ Readability: See above.

    ❌ Usability: See above. It is even worse here, since the contributed functions are only for a special purpose which is only used at the beginning of a function.

  • Extending the capabilities: Adding new capabilities to a type.

    public interface ILogger { void Log(string text); }
    
    ILogger log = GetLogger();
    log.LogDebug();
    

    ✅ Single Responsibility Principle: Not violated. LogDebug contributes to the responsibility of ILogger.

    ✅ Open-Close Principle: ILogger is not modified (it remains closed). However, C# extension methods make it Open for extensions.

    ✅ Liskov Substitution Principle: Another implementation of ILogger (or another derived class of a default logger) can be provided without being influenced by the extension mehtod. The extension methods, like other consumers expect an unchanged behavior. Another consumer of ILogger interface (old and new implementation) would not be influenced.

    ✅ Interface Segregation Principle: LogDebug and Log are contracted separately from each other (they come from different types), so the interface was segregated and can be modified independently.

    🕵️‍♀️ Dependency Inversion Principle: There is no abstraction here. The extension method cannot be provided by a third party (like a DI container) and abstracted by an interface. It is a static method.

    ✅ Readability: The contributed extended method intents to extend the capability of the original type. It belongs to the type in regards of the SRP. Additionally - in this example and as a recommendation - the extension method naming is adjusted to the target type helping the reader to understand the purpose.

    ✅ Usability: The extension method improves the usability by making the functionality available with Intellisense.

  • Bridging Domains: Adding capabilities to one domain by attaching another domain.

    void Configure(IServiceCollection services)
    {
        services
            .AddLogging()
            .AddMvc();
    }
    

    ✅ Single Responsibility Principle: .AddLogging() contributes to the DI Container building capability of IServiceCollection. It also has the responsbility of initializing the ILogger infrastructure withing the Logging subsystem. Therefore, .AddLogging() has no place in implementation or the interface of IServiceCollection but is an ideal candiate for extension methods (even in the same assembly and direct next to the extended type).

    Assembly: Logging Assembly: DependencyInjection IServiceCollection + AddSingleton + AddTransient + BuildServiceProvider + AddLogging

    ✅ Open-Close Principle: IServiceCollection is not modified as Logging does not contribute to its domain and there is no need to change it since consumers can only use .AddSingleton() or .AddTransient(). The interface remains closed. However, C# extension methods make it Open for extensions.

    ✅ Liskov Substitution Principle: See above.

    ✅ Interface Segregation Principle: See above. Extension methods are an valid method to segregate the interface into smaller parts (here the segregated specialization into the bridged technology). Extensions methods also do not contribute to the single-inheritance limitation (which extension derives in which order and are all known) and effectively form segregated interfaces.

    Assembly: Logging Assembly: DependencyInjection Assembly: AspNetCore IServiceCollection + AddSingleton + AddTransient + BuildServiceProvider + AddLogging + AddMvc

    🕵️‍♀️ Dependency Inversion Principle: See above.

    ✅ Readability: See above.

    ✅ Usability: See above.

Summary

Not all extension method use cases are really good usages when it comes to object-oriented programming. However, there are plenty of good ones.

Note: I will continually update this article when I see proper use cases which are interesting to document.