
Guide: Updating Interfaces with Default Members in C # 8.0
- Transfer
- Tutorial
In this guide you will learn how to:
- It is safe to extend interfaces by adding methods with implementations.
- Create parameterized implementations for greater flexibility.
- Obtain the right to implement more specific implementations with the possibility of manual control.

Where to begin?
First you need to configure the machine to work with .NET Core, including the compiler from the C # 8.0 preview. Such a compiler is available starting with Visual Studio 2019 , or with the newer .NET Core 3.0 preview SDK . Default interface members are available starting with .NET Core 3.0 (Preview 4).
Scenario overview
This tutorial starts with the first version of the customer relationship library. You can get the starter app in our repository on GitHub . The company that created this library assumed that customers with existing applications would adapt them to this library. The users were presented with the minimum interface definitions for implementation:
public interface ICustomer
{
IEnumerable PreviousOrders { get; }
DateTime DateJoined { get; }
DateTime? LastOrder { get; }
string Name { get; }
IDictionary Reminders { get; }
}
A second interface was also defined that shows the order:
public interface IOrder
{
DateTime Purchased { get; }
decimal Cost { get; }
}
Based on these interfaces, the team can build a library for its users to create the best experience for customers. The goal of the team was to increase the level of interaction with existing customers and develop relationships with new ones.
It's time to update the library for the next release. One of the most popular features is the addition of discounts to loyal customers who make a large number of orders. This new individual discount is applied every time a customer places an order. With each implementation of ICustomer, different rules can be set for a discount for loyalty.
The most convenient way to add this feature is to expand the interface ICustomer
with any discounts. This proposal has caused concern among experienced developers. “Interfaces are immutable after release! This is a critical change! ” In C # 8.0, default interface implementations for updating interfaces have been added. Library authors can add new members and implement them by default
The default implementation of interfaces allows developers to update the interface, while still allowing other developers to override this implementation. Library users can accept the default implementation as a non-critical change.
Update using default interface members
The team agreed with the most likely default implementation: a discount on customer loyalty.
The update must be functional for setting two properties: the number of orders required to receive a discount, and the percentage of the discount. This makes it an ideal script for default interface members. You can add a method to the ICustomer interface and provide its most likely implementation. All existing and any new implementations can be implemented by default or have their own settings.
First add a new method to the implementation:
// Версия 1:
public decimal ComputeLoyaltyDiscount()
{
DateTime TwoYearsAgo = DateTime.Now.AddYears(-2);
if ((DateJoined < TwoYearsAgo) && (PreviousOrders.Count() > 10))
{
return 0.10m;
}
return 0;
}
The author of the library wrote the first test to verify the implementation:
SampleCustomer c = new SampleCustomer("customer one", new DateTime(2010, 5, 31))
{
Reminders =
{
{ new DateTime(2010, 08, 12), "childs's birthday" },
{ new DateTime(1012, 11, 15), "anniversary" }
}
};
SampleOrder o = new SampleOrder(new DateTime(2012, 6, 1), 5m);
c.AddOrder(o);
o = new SampleOrder(new DateTime(2103, 7, 4), 25m);
c.AddOrder(o);
// Проверка скидки:
ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");
Pay attention to the following part of the test:
// Проверка скидки:
ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");
This part, starting with SampleCustomer
and ending with ICustomer
is important. A class SampleCustomer
should not provide an implementation for ComputeLoyaltyDiscount
; this is provided by the interface ICustomer
. However, the class SampleCustomer
does not inherit members from its interfaces. This rule has not changed. To call any method implemented in an interface, the variable must be an interface type, in this example - ICustomer
.
Parameterization
This is a good start. But the default implementation is too limited. Many consumers of this system can choose different thresholds for the number of purchases, different membership durations or different discounts in percent. You can improve the upgrade process for more customers by providing a way to set these parameters. Let's add a static method that sets these three parameters that control the default implementation:
// Версия 2:
public static void SetLoyaltyThresholds(
TimeSpan ago,
int minimumOrders = 10,
decimal percentageDiscount = 0.10m)
{
length = ago;
orderCount = minimumOrders;
discountPercent = percentageDiscount;
}
private static TimeSpan length = new TimeSpan(365 * 2, 0,0,0); // two years
private static int orderCount = 10;
private static decimal discountPercent = 0.10m;
public decimal ComputeLoyaltyDiscount()
{
DateTime start = DateTime.Now - length;
if ((DateJoined < start) && (PreviousOrders.Count() > orderCount))
{
return discountPercent;
}
return 0;
}
This small piece of code shows a lot of new language features. Interfaces can now include static members, including fields and methods. Various access modifiers are also included. Additional fields are private, and the new method is public. Any of the modifiers are allowed for interface members.
Applications that use the general formula to calculate loyalty discounts, but with different parameters, should not provide a custom implementation; they can set arguments by the static method. For example, the following code sets up “customer appreciation,” which rewards any customer with a membership of more than one month:
ICustomer.SetLoyaltyThresholds(new TimeSpan(30, 0, 0, 0), 1, 0.25m);
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");
Extend the default implementation
The code you added earlier provided a convenient implementation for those scenarios where users want something like the default implementation or provide an unrelated set of rules. For the final version, let's reorganize the code a bit to include scenarios in which users may want to rely on the default implementation.
Consider a startup that wants to attract new customers. They offer a 50% discount on the first order of a new customer. Existing customers receive a standard discount. The author of the library needs to move the default implementation to a method protected static
so that any class that implements this interface can reuse the code in its implementation. The default implementation of an interface member also calls this generic method:
public decimal ComputeLoyaltyDiscount() => DefaultLoyaltyDiscount(this);
protected static decimal DefaultLoyaltyDiscount(ICustomer c)
{
DateTime start = DateTime.Now - length;
if ((c.DateJoined < start) && (c.PreviousOrders.Count() > orderCount))
{
return discountPercent;
}
return 0;
}
In the implementation of the class that implements this interface, you can manually call the static helper method and extend this logic to provide a discount to the “new client”:
public decimal ComputeLoyaltyDiscount()
{
if (PreviousOrders.Any() == false)
return 0.50m;
else
return ICustomer.DefaultLoyaltyDiscount(this);
}
You can see all the finished code in our repository on GitHub .
These new features mean that interfaces can be safely updated if there is an acceptable default implementation for new members. Design interfaces carefully to express individual functional ideas that can be implemented by several classes. This makes it easy to update these interface definitions when new requirements are found for the same functional idea.