Default interface methods (C# 8+) allow you to add new members to an interface and provide a default implementation, so existing classes that implement the interface do not need to change. This solves the “interface evolution problem” โ before C# 8, adding a method to a published interface was a breaking change that forced every implementor to update. Now you can extend interfaces safely. However, default implementations live in the interface, not in implementing classes, which has important implications for how they are called.
Default Interface Methods
public interface INotificationSender
{
// Original contract โ all implementors must provide this
Task SendAsync(string recipient, string message);
// New method added in v2 โ default implementation provided
// Existing implementors do not need to change
Task SendWithRetryAsync(string recipient, string message, int maxRetries = 3)
{
// Default retry logic โ any implementor that does not override this gets it
return RetryHelper.ExecuteAsync(
() => SendAsync(recipient, message),
maxRetries);
}
// Default property โ provides a sensible default, can be overridden
int MaxMessageLength => 1000;
// Static factory method โ creates a null-object implementation
static INotificationSender Null => new NullNotificationSender();
}
// โโ Class that only implements the original interface โโโโโโโโโโโโโโโโโโโโโ
// (written before SendWithRetryAsync was added)
public class SmsNotificationSender : INotificationSender
{
public async Task SendAsync(string recipient, string message)
=> Console.WriteLine($"SMS to {recipient}: {message}");
// SendWithRetryAsync NOT implemented here โ uses the default from the interface
}
// โโ Class that overrides the default โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
public class CriticalAlertSender : INotificationSender
{
public async Task SendAsync(string recipient, string message)
=> Console.WriteLine($"CRITICAL to {recipient}: {message}");
// Override the default with a more aggressive retry
public async Task SendWithRetryAsync(string recipient, string message, int maxRetries = 3)
{
for (int i = 0; i < maxRetries; i++)
{
await SendAsync(recipient, message);
await Task.Delay(500);
}
}
public int MaxMessageLength => 500; // override the default property
}
// โโ Accessing default members โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
INotificationSender sender = new SmsNotificationSender();
await sender.SendWithRetryAsync("user1", "Hello"); // uses default retry logic โ
// Default members only accessible via interface reference โ not class reference
var smsSender = new SmsNotificationSender();
// smsSender.SendWithRetryAsync(...) // โ compile error! use interface ref
IComparable<T> and IEquatable<T> are important to implement on value objects and entities: IComparable<T> enables sorting (List.Sort(), LINQ’s OrderBy with no comparer), and IEquatable<T> provides typed equality comparison that avoids boxing. Records implement both automatically, which is one reason they are preferred for value objects in Domain-Driven Design.this implicit parameter, typed as the interface). They cannot introduce mutable state shared across calls. If your default implementation needs state, it is a sign that the logic belongs in an abstract class (which can have fields) rather than an interface default method.Interface Inheritance
// An interface can extend another interface
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
Task DeleteAsync(int id);
}
// Extended interface adds more members
public interface IWritableRepository<T> : IRepository<T> where T : class
{
Task<T> CreateAsync(T entity);
Task<T> UpdateAsync(T entity);
}
// A class implementing IWritableRepository must implement ALL members
// from both IRepository and IWritableRepository
public class PostRepository : IWritableRepository<Post>
{
public Task<Post?> GetByIdAsync(int id) => throw new NotImplementedException();
public Task<List<Post>> GetAllAsync() => throw new NotImplementedException();
public Task DeleteAsync(int id) => throw new NotImplementedException();
public Task<Post> CreateAsync(Post entity) => throw new NotImplementedException();
public Task<Post> UpdateAsync(Post entity) => throw new NotImplementedException();
}
Common Mistakes
Mistake 1 โ Adding state to a default interface method
โ Wrong โ compile error: interfaces cannot have instance fields:
public interface INotificationSender
{
private int _retryCount = 3; // compile error โ no fields in interfaces!
}
โ Correct โ use an abstract class if you need shared state alongside default methods.
Mistake 2 โ Calling a default interface method through a class reference
โ Wrong โ compile error if the class did not override the method:
var sms = new SmsNotificationSender();
sms.SendWithRetryAsync("r", "m"); // compile error! use interface ref
โ Correct:
INotificationSender sms = new SmsNotificationSender();
sms.SendWithRetryAsync("r", "m"); // โ