You already know that interfaces are meant to be immutable and should not be upgraded once released. Before C# 8.0, the best way to extend an interface was to create extension classes for adding default behaviors. But with default interface methods, you can skip the extension class implementation and implement methods within the interface itself without breaking any existing implementations.

Let’s imagine this scenario: You’re maintaining accounting software for a big bank where you have a simple interface (like below) representing a bank account. There are a few existing implementations of the interface e.g. CheckingAccount, SavingsAccount, and so on.

Sample interface
public interface IBankAccount
{
    int AccountNumber { get; set; }

    decimal Balance { get; set; }

    //More Properties

    void Deposit(decimal amount);

    void Withdraw(decimal amount);

    //More Operations
}

New requirements that have a far-reaching impact

A few years down the road, there may be some new requirements for the IBankAccount interface around easing some repetitive code that your client has to write all over the application. More specifically, these would be convenience methods for existing implementations. You obviously don’t want to change the interface, as there may be a large user base still using it. Before C# 8.0, you would create an extension class for the IBankAccount interface that looks something like this:

public static class BankAccountExtensions
{
    public static bool HasBalance(this IBankAccount account) 
    {
        return account.Balance > 0;
    }

    //More extensions
}

Activating the default interface methods in C# 8

With C# 8.0, the default implementations of the above extension methods can be easily moved into the interface without breaking existing implementations using the default interface methods. Here is how the new code would look:

public interface IBankAccount
{
    int AccountNumber { get; set; }

    decimal Balance { get; set; }

    //More Properties

    void Deposit(decimal amount);

    void Withdraw(decimal amount);

    bool HasBalance() => Balance > 0;
}

This syntax is much more intuitive and concise.  

Extended use cases for default interface methods

A few years more years down the road, you might get a new requirement to add a default functionality to give 2% cashback on grocery transactions, but the implementer can also choose to give cashback on multiple user-chosen categories. Let’s see what that code would look like:

public interface IBankAccount
{
    int AccountNumber { get; set; }

    decimal Balance { get; set; }

    //More Properties

    void Deposit(decimal amount);

    void Withdraw(decimal amount);

    public IList<ITransaction> Transactions {get;set;}

    private static TransactionCategory DefaultCashBackCategory = TransactionCategory.Groceries;

    protected static decimal DefaultComputeCashBack(IBankAccount bankAccount)
    {
        decimal cashbackAmount = 0;

        if(bankAccount.Transactions != null)
        {
            foreach (ITransaction transaction in bankAccount.Transactions)
            {
                if (transaction.Category == DefaultCashBackCategory)
                {
                    //calculate cashback amount
                }
            }
        }
            
        return cashbackAmount;
    }

    public decimal ComputeCashBack() => DefaultComputeCashBack(this);

    bool HasBalance() => Balance > 0;
}

Above, we’ve set the default cashback category as Groceries and added a shared method called “DefaultComputeCashBack” that calculates the cashback on transactions when the category is Groceries. However, we also provide another method called ComputeCashBack, which can be overridden by the implementer to add logic for calculating cashback on custom user-chosen categories.

The default implementation of ComputeCashBack calls the shared method as well. The implementing classes of the above interface can now choose to:

  1. Not override ComputeCashBack, in which case the default cashback on groceries will be computed.
  2. Override ComputeCashBack, check if the user has chosen a cashback category, and based on that, use a mix of shared method and custom calculation logic. Here’s what that would look like:
public class CheckingAccount : IBankAccount
{ 
    public decimal ComputeCashBack()
    {
        decimal cashbackAmount = 0;
        if (IsUsingDefaultCashBack)
        {
            cashbackAmount = IBankAccount.DefaultComputeCashBack(this);
        }
        else
        {
            //cashbackAmount = Compute Cash back based on user choosen categories
        }

        return cashbackAmount;
    }

    //...
}

Thus, with the new feature, you can not only eliminate the need to implement extension classes where possible, but also add upgrades to your interfaces when new requirements are discovered for the same functional idea.

Key Takeaways

So what are the key things you need to know about default interfaces?

  • If you are on .NET Core 3.0, use default interface methods in place of extension classes. The syntax is much more intuitive and concise.
  • When requirements demand having a default functionality on an interface, irrespective of the implementations, consider using static methods on the interface itself with the required functionality.

Let's Talk

Have a tech-oriented question? We'd love to talk.