AlgoMaster Logo

Default Interface Methods

Last Updated: May 22, 2026

Medium Priority
9 min read

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.

Why Default Interface Methods Exist

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.

Basic Syntax

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:

  • The default method calls another interface method (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.
  • The default method has no access modifier. Interface members are public by default, just like before.
  • There's no virtual keyword, even though the method is effectively virtual. C# 8 treats default interface methods as implicitly virtual.

The Canonical Gotcha: Class Reference vs Interface Reference

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.

Overriding a Default Method

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.

Access Modifiers on Interface Members

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:

ModifierMeaning
publicDefault. Member is part of the contract, callable by anyone.
privateHelper inside the interface. Only other members of the same interface can call it. Must have a body.
protectedCallable only from types that implement the interface, including derived interfaces.
internalCallable only within the same assembly.
abstractExplicit form for the traditional "signature only, no body" interface method.
virtualExplicit form of the implicit virtual on a default method with a body.
sealedPrevents an implementer from overriding the default.
staticBelongs 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 Helpers

Once 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 Members

Static 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.

abstract and virtual Modifiers

The 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 Overrides

When 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.

Reabstracting in a Derived Interface

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.

Calling the Base Interface's Default

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.

Putting It Together: Evolving a Real Interface

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.

When Not to Use Default Interface Methods

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:

  • You're using a default method to share implementation across unrelated types. If two classes both want some shared behavior, an abstract base class is usually clearer. Default interface methods are most useful when adding to an interface customers already implement.
  • You find yourself wanting state. Interfaces still can't declare instance fields. If your default body needs to remember anything across calls, you'll end up routing state through interface properties that every implementer must supply, which is awkward. A base class with a private field is cleaner.
  • You're tempted to make the interface deep. Once you start layering several interfaces with default bodies, dispatch becomes harder to predict. Most teams keep interfaces shallow and put complex inheritance in classes, which the next two chapters cover in detail.

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.

Common Mistakes

A few mistakes show up repeatedly when teams adopt default interface methods. They're worth seeing once.

Calling Through a Class Reference

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.

Forgetting the Runtime Requirement

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.

Reabstracting Without Realizing

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.