Choosing extension over instance method
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:
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.