Last Updated: May 22, 2026
Before C# 8, an interface was a pure contract: signatures only, no bodies. Adding a method to a shipped interface broke every class that implemented it. C# 8 changed that by letting you put a method body directly inside the interface, so older implementers can simply inherit the new behavior. This chapter covers what default interface methods are, the runtime they require, the syntax, the access modifiers they unlock, and the quirks that surface on first use.
The Interfaces chapter introduced interfaces as contracts that a class must fulfill. That model has one painful limitation: you can never grow a published interface. If a library ships INotifier with one method and a hundred customer projects implement it, adding a second method to INotifier is a breaking change. Every existing implementer fails to compile until they add the new method, even when a sensible default exists.
Default interface methods solve that exact problem. The interface author can ship a new method with a body, and existing implementers pick up the default automatically. Implementers who want different behavior can still override it. Nothing breaks.
The left half is the world before C# 8: any new interface member detonates downstream code. The right half is the world after: the interface ships a body, and implementers opt in to override only when they have a better implementation.
Runtime requirement: Default interface methods are a runtime feature, not a compiler trick. You need .NET Core 3.0 or .NET 5+ (so .NET 6, 7, 8, and beyond). They will not run on .NET Framework 4.x because that runtime predates the feature. The language version must be C# 8 or later.
The syntax is exactly what you would guess: declare the method, give it a body, done.
SeasonalSale implements only GetDiscountPercent. The second method, GetDiscountedPrice, is inherited from the interface's default body. The interface itself supplied the implementation, the class didn't have to.
A few things to note in that code:
GetDiscountPercent). That call dispatches virtually to whatever the implementing class returned, so the default body uses the concrete class's behavior without knowing the class.public by default, just like before.virtual keyword, even though the method is effectively virtual. C# 8 treats default interface methods as implicitly virtual.Here's the part of default interface methods that confuses everyone the first time they see it. A default method body lives on the interface, not on the class. The class doesn't inherit it as a member, the way it would inherit a method from a base class. To call the default method, you need an interface-typed reference.
The class reference package doesn't see GetShippingLabel as a member, even though Package implements IShippable. The method body lives on the interface, and class lookup never walks into interface bodies. To reach it, you cast or assign to the interface type first.
If you want callers to use the default method from a class reference too, the class has to declare its own member that forwards to the interface:
That feels backwards at first. The rule to remember: default interface methods are interface-facing. They exist so that code working against the interface contract keeps compiling when the interface grows. They are not a shortcut for sharing implementations between unrelated classes; that's what a base class is for.
An implementing class can supply its own version of a default method, the same way it would implement a regular interface method. The class's version wins over the default.
StandardDiscount accepts the default. TaxAwareDiscount declares its own GetDiscountedPrice and runs that instead. The interface reference picks the right body at runtime, because default interface methods participate in virtual dispatch just like normal interface methods.
A small layout of how that dispatch resolves:
The runtime checks the most-derived implementer first. If the class provides a body, that's what runs. If not, the call falls through to the interface's default body.
Before C# 8, interface members were always public and you couldn't write a modifier at all. C# 8 opened that up. With default bodies came the need to scope helpers, hide details, and mark members as abstract, virtual, or sealed explicitly.
The modifiers you can now write on interface members:
| Modifier | Meaning |
|---|---|
public | Default. Member is part of the contract, callable by anyone. |
private | Helper inside the interface. Only other members of the same interface can call it. Must have a body. |
protected | Callable only from types that implement the interface, including derived interfaces. |
internal | Callable only within the same assembly. |
abstract | Explicit form for the traditional "signature only, no body" interface method. |
virtual | Explicit form of the implicit virtual on a default method with a body. |
sealed | Prevents an implementer from overriding the default. |
static | Belongs to the interface itself, not to instances. Can have a body. |
The point of opening up these modifiers wasn't to give interfaces every feature of classes. It was to make default methods practical. A default body often needs a helper, a guard, or a way to forbid further overrides, and the modifiers above are the minimum vocabulary for that.
private HelpersOnce an interface has bodies, repeated logic across default methods needs a place to live. private interface members are exactly that place, helpers that the default methods share, invisible to implementers and callers.
ApplyDiscount is private static on the interface. Implementing classes can't call it, derived interfaces can't call it, only members of IDiscountable itself can. The defaults share the math without exposing it on the public surface.
A private interface member must have a body. The compiler reports CS0501 if you declare private decimal Foo(); with no body, because a private signature with no implementation could never be called.
static MembersStatic members on an interface look like static members on a class: one body, no instance, shared by every caller. They're useful for constants, factory methods, and shared helpers that don't depend on instance state.
You call static interface members the same way you'd call static class members: IShippable.MaxWeightKg, IShippable.CreateExpress(...). They have no this, so they can't reach instance methods directly; they take whatever they need as parameters.
C# 11 update: Static abstract members and static virtual members on interfaces shipped in C# 11 (.NET 7+). They support generic math and other patterns where a generic constraint needs to call a static method on the type. Static fields and properties on interfaces are also legal there. This chapter sticks to plain static members, which are enough for most cases.
abstract and virtual ModifiersThe abstract and virtual keywords are usually implicit, but writing them explicitly is allowed and sometimes helps readability.
The implicit forms are the convention. The explicit forms exist because, with default bodies in the picture, it's sometimes clearer to say abstract outright when a method has no body, so a reader doesn't wonder whether someone forgot to write one.
sealed to Forbid OverridesWhen an interface method has a default body, implementers can override it. Sometimes you don't want that. sealed on a default method locks the body so no class can change it.
sealed on an interface member means "the default is the implementation, nobody changes it." Use it when the format, behavior, or invariant the default enforces is part of the contract, not a starting point.
A derived interface can do the opposite: take a default method from its parent and turn it back into a method that must be implemented by classes. This is called reabstracting, and the keyword is abstract.
ISecureNotifier says: "I extend INotifier, but I refuse to let the default handle SendUrgent. Anyone implementing me must supply their own." A class that implements ISecureNotifier and forgets to provide SendUrgent fails to compile with CS0535.
The syntax void INotifier.SendUrgent(string message) uses explicit interface implementation. For now, treat it as the shape used when a derived interface or class wants to talk about a member from a specific parent interface by name.
A derived interface can override a default method while still calling the base interface's version, with the syntax BaseInterface.Method(). This mirrors base.Method() in class inheritance.
C# doesn't expose a base. syntax for interfaces directly. The common workaround is to keep the parent's body in a separate helper (often private static) so both the parent default and any derived override can reach it. In the example above, Send_DefaultImpl plays that role. If the parent isn't yours to modify, you can also use ((INotifier)this).Method() from a non-override context, though that triggers virtual dispatch and can recurse, so the static-helper pattern is safer.
Here's a complete walkthrough of the API-evolution scenario default methods were built for. Version 1 of the library ships a small INotifier. Six months later, the library author wants to add bulk notifications without breaking customers.
Version 1 of the library:
Version 2 adds a new method with a default body:
Two payoffs from this v1-to-v2 change:
EmailNotifier didn't touch its source. It inherits SendMany, looping over Send exactly as you'd write by hand.SmsNotifier did override SendMany, because batching is genuinely cheaper for SMS. The class's version wins for any INotifier sms reference.If the new method had been added the pre-C# 8 way, with an abstract signature, both classes would have failed to compile until they added their own SendMany. Customers using the library would get a broken build on the next package update, for a method most of them didn't need a custom version of.
Default interface methods solve API evolution. They're not a general-purpose replacement for base classes, and using them as one tends to produce code that's harder to read than the abstract-class version of the same idea.
A few warning signs:
The Microsoft team's own guidance lines up with this. Default interface methods exist primarily to evolve published interfaces. Use them for that. For other goals, classes still win.
A few mistakes show up repeatedly when teams adopt default interface methods. They're worth seeing once.
This is the trap from earlier, restated as a mistake pattern.
What's wrong with this code?
GetDiscountedPrice is a default interface method. The class reference sale doesn't expose it as a class member.
Fix:
Or, if class-reference callers also need it, the class can declare its own forwarder.
A second sneaky one: the code compiles on your machine, and crashes on the customer's. If a library targets netstandard2.0 for compatibility with older runtimes, default interface methods will be a runtime error on those older runtimes, because .NET Framework 4.x and older Mono builds don't support the feature.
Fix: Multi-target the library. Provide a netstandard2.0 build with abstract signatures (and a base class for the shared code), and a net8.0 build that uses default interface methods. Or, simpler, only ship the net6.0+ build if your customers are all on modern .NET.
What's wrong with this code?
ICriticalNotifier reabstracted Send, so the default no longer applies to anyone implementing ICriticalNotifier. ConsoleNotifier fails to compile with CS0535: 'ConsoleNotifier' does not implement interface member 'INotifier.Send(string)'.
Fix: Either provide an implementation in ConsoleNotifier, or don't reabstract in the derived interface.