How to use default interface methods in C#

By FoxLearn 1/3/2025 7:54:35 AM   89
C# 8.0 introduced default interface methods, allowing you to add new methods to an interface without breaking existing implementations.

Prior to C# 8.0, interfaces could only declare methods, and any change (like adding a new method) required updating all implementing classes. With default interface methods, you can now provide method implementations directly in the interface, making it easier to evolve interfaces without breaking existing code.

Additionally, C# 8.0 expands the capabilities of interfaces by allowing members to be private, protected, static, virtual, and abstract. However, virtual members can only be overridden by derived interfaces, not by classes implementing the interface.

Why use default interface methods?

Default interface methods in C# are methods defined in an interface with concrete implementations. If a class that implements the interface does not provide its own implementation for the method, the default implementation from the interface is used instead. This feature allows developers to add new methods to an interface in future versions without breaking existing implementations, ensuring backward compatibility.

Imagine you're building a payment processing system, and you have an interface IPaymentProcessor for processing different types of payments.

public interface IPaymentProcessor
{
    void ProcessPayment(decimal amount);
}

public class CreditCardProcessor : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        // Process payment through credit card
        Console.WriteLine($"Processing credit card payment of {amount:C}");
    }
}

public class PayPalProcessor : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        // Process payment through PayPal
        Console.WriteLine($"Processing PayPal payment of {amount:C}");
    }
}

At this point, both CreditCardProcessor and PayPalProcessor are implementing the IPaymentProcessor interface. Now, suppose you want to add a new method to the IPaymentProcessor interface that allows you to process a payment with additional details like a currency type.

If you add a new method ProcessPayment(decimal amount, string currency) directly to the interface like this:

public interface IPaymentProcessor
{
    void ProcessPayment(decimal amount);
    void ProcessPayment(decimal amount, string currency);
}

Now, both CreditCardProcessor and PayPalProcessor must implement the new method ProcessPayment(decimal amount, string currency). If you forget to implement this new method in any of the classes, the compiler will raise an error. This could be problematic, especially if the IPaymentProcessor interface is used in various parts of your system or even across different teams.

Solution with Default Interface Methods

With default interface methods, you can provide a default implementation for the new method right inside the interface itself. This way, you avoid breaking existing implementations because classes that don’t implement the new method will use the default behavior.

Here's how you would modify the IPaymentProcessor interface to include the new method with a default implementation:

public interface IPaymentProcessor
{
    void ProcessPayment(decimal amount);

    // Default implementation for the new method
    void ProcessPayment(decimal amount, string currency)
    {
        // Default behavior: Process payment with the specified currency
        Console.WriteLine($"Processing payment of {amount:C} in {currency}");
    }
}

Now, the existing CreditCardProcessor and PayPalProcessor classes don’t need to implement the new ProcessPayment(decimal amount, string currency) method unless they want to customize the behavior. If they don’t implement it, the default implementation will be used.

public class CreditCardProcessor : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        // Process payment through credit card
        Console.WriteLine($"Processing credit card payment of {amount:C}");
    }
}

public class PayPalProcessor : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        // Process payment through PayPal
        Console.WriteLine($"Processing PayPal payment of {amount:C}");
    }
}

In this case, both CreditCardProcessor and PayPalProcessor will still use the default behavior for the ProcessPayment(decimal amount, string currency) method. The default implementation simply prints out the payment amount along with the currency.

Customizing the New Method

If a class needs custom behavior for the new method, it can override the default implementation:

public class CreditCardProcessor : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        // Process payment through credit card
        Console.WriteLine($"Processing credit card payment of {amount:C}");
    }

    // Override the default method to include specific behavior for currency
    public void ProcessPayment(decimal amount, string currency)
    {
        // Custom behavior for credit card payments in different currencies
        Console.WriteLine($"Processing credit card payment of {amount:C} in {currency}");
    }
}

This CreditCardProcessor class now provides its own version of ProcessPayment(decimal amount, string currency), while the PayPalProcessor can still rely on the default implementation.

By using default interface methods, you can evolve the IPaymentProcessor interface to support new functionality (like accepting a currency) without forcing every implementing class to be updated immediately.

Default Interface Methods Are Not Inherited

In C# 8.0, default interface methods are not inherited by the classes that implement the interface. This means that while an interface can provide a default implementation for a method, the class implementing the interface doesn't automatically "inherit" that method as part of its own members.

For example, let's consider the ILogger interface with the newly added Log method that includes a LogLevel parameter:

public interface ILogger
{
    void Log(string message);

    // Default implementation for the new method
    void Log(string message, LogLevel logLevel)
    {
        // Default behavior: Log the message with the provided log level
        Console.WriteLine($"[{logLevel}] {message}");
    }
}

Now, let’s create an instance of the FileLogger class and try calling the new Log method:

FileLogger fileLogger = new FileLogger();
fileLogger.Log("This is a test message.", LogLevel.Debug);

Although the ILogger interface provides a default implementation for the Log method with two parameters, the FileLogger class doesn't automatically inherit this method. The class doesn't "know" about the default method unless it explicitly implements it.

Abstract classes vs. interfaces in C# 8.0

In C# 8.0, abstract classes and interfaces share some similarities but are not the same. Key differences include that a class can still only inherit from one abstract class, whereas multiple interfaces can be implemented. Additionally, interfaces cannot have instance members, unlike abstract classes.

With default interface methods, developers can use a technique called traits programming, which allows for reusing methods across unrelated types. This is particularly useful when releasing new versions of a library. By adding new members to interfaces with default implementations, you can extend the interface's functionality without requiring changes to the existing codebase, ensuring backward compatibility.