Choosing extension over instance method

March 22nd 2024 C# .NET

Extension methods are a great way to extend classes you don't own with what looks like an instance method. Therefore, you usually don't need to create extension methods for a class from the same assembly because you could simply add the same code to that class as an instance method. However, not everything you can do with an extension method can also be done with an instance method.

Let's start with the following classes:

public partial class ResponseStatus
{
    public string? Code { get; init; }
}

public class Response
{
    public ResponseStatus? Status { get; init; }

    // additional properties of no relevance
}

Imagine that a response is successful when the 3-digit Code starts with a 2 (just like an HTTP response status code). You can write a method in ResponseStatus class to check for this:

public bool IsSuccess() => Code != null && SuccessCodeRegex().IsMatch(Code);

[GeneratedRegex(@"^2\d{2}$")]
private static partial Regex SuccessCodeRegex();

You can call this isSuccess method in your code to make sure the response is successful before you start processing it:

public void ProcessResponse(Response response)
{
    if (response.Status.IsSuccess()) // throws if Status is null
    {
        // Process the response
    }
}

But if the Status property of the Response is null this code will throw (and null-state static analysis will warn you of that). So the correct code`is:

public void ProcessResponse(Response response)
{
    if (response.Status != null && response.Status.IsSuccess())
    {
        // Process the response
    }
}

You can't move that null check into the isSuccess method, because you can't even invoke it if ResponseStatus is null. However, you can do that if you create an extension method instead:

public static partial class ResponseStatusExtensions
{
    public static bool IsSuccess(this ResponseStatus? status) =>
        status?.Code != null && SuccessCodeRegex().IsMatch(status.Code);

    [GeneratedRegex(@"^2\d{2}$")]
    private static partial Regex SuccessCodeRegex();
}

You can now safely remove the extra null check before calling this extension method. It's not going to throw even if ResponseStatus is null:

public void ProcessResponse(Response response)
{
    if (response.Status.IsSuccess()) // doesn't throw if Status is null
    {
        // Process the response
    }
}

Unfortunately, the null-state static analysis doesn't know that Status is not null if IsSuccess returns true although we can be sure that's the case. So, you'll get a bogus warning if you try to access its members inside the if block:

Bogus null reference warning

By adding a post-condition NotNullWhen attribute to the IsSuccess extension method, you can solve this issue. It will inform the static analysis that the value of Status can't be null if the method returns true and make the warning inside the if block go away:

public static bool IsSuccess([NotNullWhen(true)] this ResponseStatus? status) =>
    status?.Code != null && SuccessCodeRegex().IsMatch(status.Code);

Now, you really don't need to check if ResponseStatus for null anymore, before calling this extension method.

You can find full source code for a working example in my GItHub repository. Individual commits show a step by step progress from the instance method to the final extension method.

Yes, usually there is no need for extension methods when you have the option of adding a method to the class directly. However, extension methods can be invoked even on a null value, while instance methods can't. In certain scenarios, you can make your code simpler if you do that. With null reference types, the method signature will clearly tell whether a method can be called on a null or not. And you'll get a warning when you call a method incorrectly, so you can be sure when your code is safe.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License