C# Default Interface Method: Syntax and Compatibility
c# default interface method: Learn how C# default interface methods work, when to use them, and how they affect versioning and compatibility in real projects.
Understanding Default Interface Methods
C# default interface methods, introduced in C# 8, allow an interface to provide a method body. Implementing types no longer have to supply an implementation for every member. This changes how interfaces can evolve without breaking existing implementers. Before this feature, adding a method to an interface forced every implementing class or struct to add the method, which was a breaking change in public APIs. Default interface methods give you a way to add new members with a fallback implementation that existing types can inherit.
The primary use case is API versioning: you can add a method to a public interface and provide a default behavior, so existing consumers continue to compile and run without modification. The default implementation is used only when the implementing type does not define its own version.
Syntax and Basic Implementation
Declaring a default interface method is straightforward. You write the method signature followed by a body inside the interface. Here is a minimal example:
public interface ILogger { void Log(string message); void LogError(string message) { Log($"[ERROR] {message}"); } }
In this example, Log is an abstract member that implementing types must provide. LogError has a default implementation that calls Log. A class implementing ILogger can choose to override LogError or rely on the default. If it does not provide an implementation, the default is used.
public class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine(message); } }
Here, ConsoleLogger only implements Log. When you call LogError on an instance of ConsoleLogger, the default implementation from the interface runs and calls Log.
Overriding and Calling the Base Implementation
An implementing type can override a default interface method just like any other method. The override must be public unless you explicitly use explicit interface implementation. For example:
public class TimestampLogger : ILogger { public void Log(string message) { Console.WriteLine($"{DateTime.UtcNow}: {message}"); } public void LogError(string message) { Log($"[ERROR] {message} at {DateTime.UtcNow}"); } }
Sometimes you want to call the default implementation from the override. You can do that by casting the instance to the interface and invoking the method, because the default implementation is accessible only through the interface type. For instance:
public class PrefixedLogger : ILogger { public void Log(string message) { Console.WriteLine($"LOG: {message}"); } public void LogError(string message) { ((ILogger)this).LogError(message); // Calls the default implementation Console.WriteLine("Error logged."); } }
The cast is necessary because the default method is not part of the class's public surface unless explicitly implemented.
Default Interface Methods vs. Abstract Classes
Default interface methods are often compared to abstract classes, but they serve different purposes. An abstract class can contain state, constructors, and protected members. An interface cannot contain instance fields or constructors. Interfaces with default methods are still interfaces: they define a contract that types must satisfy, but they can now provide a partial implementation.
Abstract classes are best used when you need to share a common base implementation with state. Default interface methods are better when you need to add behavior to a contract without breaking existing implementers. You can combine both, but the decision depends on whether you need shared fields or just shared behavior.
| Feature | Abstract Class | Default Interface Method |
|---|---|---|
| Instance state | Yes | No |
| Constructors | Yes | No |
| Multiple inheritance | No | Yes (a type can implement many interfaces) |
| Versioning | Can break existing subclasses | Designed for non-breaking additions |
When to Use Default Interface Methods
The most compelling use case is adding members to a widely consumed interface. For example, a logging library might ship an ILogger interface with a Log method. Later, they want to add a LogWarning method. Without default interface methods, adding that method would break every consumer. With a default implementation, existing implementers continue to compile, and the new method is available with a sensible fallback.
Another scenario is trait-like composition. You can define small interfaces that provide reusable behavior, such as equality comparison or serialization helpers, and let classes pick them up without writing boilerplate. However, default interface methods are not a replacement for extension methods; they are more appropriate when the behavior needs to be polymorphic and overridable.
Compatibility and Runtime Considerations
Default interface methods are supported only on .NET Core 3.0 and later, .NET 5 and later, and .NET Standard 2.1. They are not available in .NET Framework. If you are building a library that targets .NET Standard 2.0 or .NET Framework, you cannot use this feature. The runtime needs to support the dispatch mechanism that resolves default implementations.
At runtime, the compiler generates a method on the interface and uses a special dispatch table to find the most derived implementation. This adds a small indirection compared to a regular virtual method call. In practice, the overhead is negligible for most applications, but if you are in a hot path and calling default interface methods frequently, you should measure the impact. The cost is a vtable-like lookup, similar to calling an interface method on a struct.
Maintainability and Versioning Implications
Default interface methods give you a way to evolve interfaces without breaking changes, but they also introduce a maintenance burden. A default implementation can become stale if the interface evolves further. For example, if you add a new method that logically should call another default method, you need to ensure the default behavior remains consistent across all implementers.
Also, default interface methods can lead to ambiguity when a type implements multiple interfaces that define the same method with different defaults. The compiler will require the implementing type to provide an explicit implementation to resolve the conflict. This is similar to the diamond problem in multiple inheritance. You should design default interface methods to be independent and avoid overlapping signatures across interfaces that are likely to be implemented together.
When you add a default method to an interface, it is effectively a new public API. You should document the default behavior clearly, because consumers may rely on it. If you later change the default implementation, you can introduce subtle behavioral changes for types that did not override it. Treat default interface methods as a contractual fallback, not as an implementation detail.