ICYMI C# 8 New Features: Upgrade Interfaces Without Breaking Existing Code

This is part 7 in a series of articles.

Prior to C# 8, if you add members to an interface, exiting code breaks if you do not implement the new members in any class that implements the interface.

As an example, consider the following interface definition:

public interface ICustomer
{
    int Id { get; }
    string Name { get; set; }
    int MonthsAsACustomer { get; set; } 
    decimal TotalValueOfAllOrders { get; set; }
    // etc.
}

We could implement this interface:

class Customer : ICustomer
{
    public int Id { get; }
    public string Name { get; set; }
    public int MonthsAsACustomer { get; set; }
    public decimal TotalValueOfAllOrders { get; set; }
    public Customer(int id) => Id = id;
}

This will compile without error. We could have multiple classes implementing this interface in the same project or across multiple projects.

What happens now if we wanted to add a new interface method that represents the ability to calculate a discount based on the customer’s previous order value and how long they have been a customer?

We could make the following change:

public interface ICustomer
{
    int Id { get; }
    string Name { get; set; }
    int MonthsAsACustomer { get; set; } 
    decimal TotalValueOfAllOrders { get; set; }
    decimal CalculateLoyaltyDiscount();
    // etc.
}

Now if we try and build, we’ll get the error: 'Customer' does not implement interface member 'ICustomer.CalculateLoyaltyDiscount()'

If we have multiple implementations of ICustomer, they will all break.

Default Interface Methods in C# 8

From C# 8 we can fix this problem by providing a default implementation of an interface method.

If we only had one implementation of ICustomer then we could go and add the implementation of CalculateLoyaltyDiscount. But if we had multiple implementations or we didn’t want to force a breaking change on existing implementers then we can actually add the implementation of the the method in the interface itself.

public interface ICustomer
{
    int Id { get; }
    string Name { get; set; }
    int MonthsAsACustomer { get; set; } 
    decimal TotalValueOfAllOrders { get; set; }
    public decimal CalculateLoyaltyDiscount()
    {
        if (MonthsAsACustomer > 24 || TotalValueOfAllOrders > 10_000)
        {
            return 0.05M;
        }

        return 0;
    }
    // etc.
}

If we build now there will be no error, even though we haven’t implemented the method in Customer. Customer ‘inherits’ the default implementation.

Notice in the preceding code that from C# 8, access modifiers are now allowed on interface members.

We could make use of this new method:

var c = new Customer(42)
{
    MonthsAsACustomer = 100
};

decimal discount = ((ICustomer)c).CalculateLoyaltyDiscount();

Notice in the preceding code that we have to cast Customer to ICustomer to be able to call CalculateLoyaltyDiscount – that’s because the method implementation is in the interface, not the Customer class.

The Customer class can still implement it’s own version of the CalculateLoyaltyDiscount method if the default implementation is not acceptable:

class Customer : ICustomer
{
    public int Id { get; }
    public string Name { get; set; }
    public int MonthsAsACustomer { get; set; }
    public decimal TotalValueOfAllOrders { get; set; }
    public Customer(int id) => Id = id;
    public decimal CalculateLoyaltyDiscount()
    {
        if (TotalValueOfAllOrders > 1_000_000)
        {
            return 0.1M;
        }

        return 0;
    }
}

We could refactor the upgraded interface to allow implementers to still be able to access the default implementation:

public interface ICustomer
{
    public int Id { get; }
    string Name { get; set; }
    int MonthsAsACustomer { get; set; } 
    decimal TotalValueOfAllOrders { get; set; }
    public decimal CalculateLoyaltyDiscount() => CalculateDefaultLoyaltyDiscount(this);
    
    protected static decimal CalculateDefaultLoyaltyDiscount(ICustomer customer)
    {
        if (customer.MonthsAsACustomer > 24 || customer.TotalValueOfAllOrders > 10_000)
        {
            return 0.1M;
        }

        return 0;
    }
    // etc.
}

And then in Customer:

class Customer : ICustomer
{
    public int Id { get; }
    public string Name { get; set; }
    public int MonthsAsACustomer { get; set; }
    public decimal TotalValueOfAllOrders { get; set; }
    public Customer(int id) => Id = id;
    public decimal CalculateLoyaltyDiscount()
    {
        if (TotalValueOfAllOrders > 1_000_000)
        {
            return 0.2M;
        }

        return ICustomer.CalculateDefaultLoyaltyDiscount(this);
    }
}

From C# 8, interfaces can also now have static fields, methods, and properties, e.g.:

private static int MonthsAsACustomerThreshold = 24;
public static int MonthsThreshold
{
    get => MonthsAsACustomerThreshold;
    set => MonthsAsACustomerThreshold = value;
}

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

Add comment

Loading