Mert Özen Further With Every Line
Backend Object-Oriented Programming in C# — #07

Interfaces in C#: Multiple and Explicit Implementation, Default Interface Methods

Mert Özen Nov 7, 2026 10 min 6 views
Interfaces in C#: Multiple and Explicit Implementation, Default Interface Methods

An interface is a contract: CS0535 and CS0144, a class implementing several interfaces, explicit implementation for same-named members and its examples in the BCL, default interface methods callable only through the interface, choosing between an interface and an abstract class, and testable code by putting the clock behind an interface.

Version note: The code in this post uses C# 10 and the .NET 6 APIs; it was built and run with the .NET 10 SDK.

Guess First

IShippable is an interface, and it carries the body of the ShippingCost method itself. Book implements this interface but does not write ShippingCost; there is no need, the default body is already there. We call the same book once through the interface and once through its own type. Which line causes trouble?

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Book dune = new Book("Dune", 0.6m);
IShippable shippable = dune;

Console.WriteLine(shippable.ShippingCost());
Console.WriteLine(dune.ShippingCost());

public interface IShippable
{
    decimal WeightKg { get; }

    decimal ShippingCost() => WeightKg <= 1m ? 3.00m : 6.00m;
}

public class Book : IShippable
{
    public string Title { get; }
    public decimal WeightKg { get; }

    public Book(string title, decimal weightKg)
    {
        Title = title;
        WeightKg = weightKg;
    }
}

In the previous post we could easily call a method inherited from the base class through the derived class. Is it the same with an interface? The answer is in the section on default interface methods.

An Interface Is a Contract

An interface is a contract that says what a type can do, but usually not how it does it. The sentence "everything shippable has a weight and can calculate its shipping cost" is an interface. When a class implements the interface, it signs the contract, and the compiler checks that every clause in it is fulfilled:

IShippable shippable = new IShippable();

public interface IShippable
{
    decimal WeightKg { get; }
    decimal ShippingCost();
}

public class Mug : IShippable
{
    public decimal WeightKg => 0.4m;
}
Program.cs(9,20): error CS0535: 'Mug' does not implement interface member 'IShippable.ShippingCost()'

Mug signed the contract but did not fulfill the ShippingCost clause. Once that error is fixed, the first line gets its turn: error CS0144: Cannot create an instance of the abstract type or interface 'IShippable'. Like an abstract class, an interface cannot be instantiated. But you can declare a variable of an interface type; that variable can point to any object that implements the contract.

A few rules: interface members are public by default, so you do not write an access modifier next to them. An interface cannot hold instance fields; state is the job of the class that implements it. Names traditionally start with a capital I and usually describe an ability: IShippable, IComparable<T>, IDisposable, IEnumerable<T>.

Multiple Implementation

A class can derive from only one base class, but it can implement as many interfaces as it likes. In Bookstore some products are shipped, some are discounted, and some are both:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

List<Product> cart = new List<Product>
{
    new Book("Dune", 45.90m, 0.6m),
    new EBook("Clean Code", 29.90m),
    new Mug("Bookstore Mug", 12.50m)
};

foreach (IDiscountable item in cart.OfType<IDiscountable>())
{
    item.ApplyDiscount(10);
}

decimal shipping = 0;
foreach (Product product in cart)
{
    if (product is IShippable shippable)
    {
        shipping += shippable.ShippingCost();
    }

    Console.WriteLine($"{product.Name}: {product.Price}, shippable: {product is IShippable}");
}

Console.WriteLine($"Shipping: {shipping}");

public interface IShippable
{
    decimal WeightKg { get; }
    decimal ShippingCost();
}

public interface IDiscountable
{
    decimal Price { get; }
    void ApplyDiscount(int percent);
}

public abstract class Product
{
    public string Name { get; }
    public decimal Price { get; protected set; }

    protected Product(string name, decimal price)
    {
        Name = name;
        Price = price;
    }
}

public class Book : Product, IShippable, IDiscountable
{
    public decimal WeightKg { get; }

    public Book(string name, decimal price, decimal weightKg) : base(name, price)
    {
        WeightKg = weightKg;
    }

    public decimal ShippingCost()
    {
        return WeightKg <= 1m ? 3.00m : 6.00m;
    }

    public void ApplyDiscount(int percent)
    {
        Price -= Price * percent / 100;
    }
}

public class EBook : Product, IDiscountable
{
    public EBook(string name, decimal price) : base(name, price)
    {
    }

    public void ApplyDiscount(int percent)
    {
        Price -= Price * percent / 100;
    }
}

public class Mug : Product, IShippable
{
    public decimal WeightKg => 0.4m;

    public Mug(string name, decimal price) : base(name, price)
    {
    }

    public decimal ShippingCost()
    {
        return 7.50m;
    }
}
Dune: 41.31, shippable: True
Clean Code: 26.91, shippable: False
Bookstore Mug: 12.50, shippable: True
Shipping: 10.50

In the class declaration the base class comes first, then the interfaces, separated by commas. The discount loop visited only the IDiscountable items; LINQ's OfType<T>() operator filters out of the list those that match that type. The mug was not discounted, and the e-book was not added to the shipping cost. The loops do not know the products' concrete types; they only look at which contract they signed.

A subtle detail: IDiscountable asks for a Price property, and Book did not write one itself. The public Price it inherited from Product fulfills that clause of the contract. An interface does not care where a member comes from; it is enough that the class offers a public member with that name and type.

Explicit Implementation

What happens if two interfaces ask for a member with the same name but a different meaning? For the catalog a book's code is its ISBN; for the warehouse it is the shelf code. With explicit implementation you give each interface its own answer:

Book dune = new Book("978-0441172719", "A-12");

Console.WriteLine(((ICatalogItem)dune).Code);
Console.WriteLine(((IInventoryItem)dune).Code);

IInventoryItem item = dune;
Console.WriteLine(item.Code);

List<int> numbers = new List<int>();
Console.WriteLine(((ICollection<int>)numbers).IsReadOnly);

ICollection<int> readOnly = numbers.AsReadOnly();
Console.WriteLine(readOnly.IsReadOnly);
readOnly.Add(42);

public interface ICatalogItem
{
    string Code { get; }
}

public interface IInventoryItem
{
    string Code { get; }
}

public class Book : ICatalogItem, IInventoryItem
{
    public string Isbn { get; }
    public string Shelf { get; }

    public Book(string isbn, string shelf)
    {
        Isbn = isbn;
        Shelf = shelf;
    }

    string ICatalogItem.Code => Isbn;
    string IInventoryItem.Code => $"SHELF-{Shelf}";
}
978-0441172719
SHELF-A-12
SHELF-A-12
False
True
Unhandled exception. System.NotSupportedException: Collection is read-only.
   at System.Collections.ObjectModel.ReadOnlyCollection`1.System.Collections.Generic.ICollection<T>.Add(T value)

An explicit member has no access modifier, and its name is prefixed with the interface name: string ICatalogItem.Code. These members do not appear on the class's own surface; they are called only through a reference of the interface type. When I tried writing dune.Code, the compiler said, with CS1061, "'Book' does not contain a definition for 'Code'".

The second use of explicit implementation is to offer a member because the contract demands it, without cluttering the type's everyday surface. The BCL does this often. List<T> implements ICollection<T>.IsReadOnly explicitly; writing numbers.IsReadOnly gives you the same CS1061, because for a list the answer is always false and nobody needs to ask. That is also the secret of the ReadOnlyCollection<T> from the third post: it implements Add explicitly and throws an exception when it is called. The name ReadOnlyCollection`1.System.Collections.Generic.ICollection<T>.Add in the stack trace shows the compiled name of the explicit member.

Default Interface Methods

Now the answer to the guess. The first program did not compile:

Program.cs(9,24): error CS1061: 'Book' does not contain a definition for 'ShippingCost' and no accessible extension method 'ShippingCost' accepting a first argument of type 'Book' could be found (are you missing a using directive or an assembly reference?)

Since C# 8, interfaces can carry members with bodies; these are called default interface methods (default interface members). But such a member is not inherited by the class; it stays in the interface. shippable.ShippingCost() works through the interface, dune.ShippingCost() does not. That is exactly the difference from a base class: a base class hands its members to derived classes, while an interface only answers calls made through itself.

The real purpose of this feature is versioning. Adding a new member to an interface implemented by hundreds of classes used to break all of them. If you add it with a default body, the existing classes keep compiling unchanged, and whoever wants to writes their own version:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

List<IShippable> parcels = new List<IShippable>
{
    new Book("Dune", 0.6m),
    new Book("Encyclopedia Set", 7.2m),
    new Mug("Bookstore Mug", 0.4m)
};

foreach (IShippable parcel in parcels)
{
    Console.WriteLine($"{parcel.Name}: {parcel.ShippingCost()}, signature: {parcel.RequiresSignature()}");
}

public interface IShippable
{
    string Name { get; }
    decimal WeightKg { get; }

    decimal ShippingCost() => WeightKg <= 1m ? 3.00m : 6.00m;

    bool RequiresSignature() => WeightKg > 5m;
}

public class Book : IShippable
{
    public string Name { get; }
    public decimal WeightKg { get; }

    public Book(string name, decimal weightKg)
    {
        Name = name;
        WeightKg = weightKg;
    }
}

public class Mug : IShippable
{
    public string Name { get; }
    public decimal WeightKg { get; }

    public Mug(string name, decimal weightKg)
    {
        Name = name;
        WeightKg = weightKg;
    }

    public decimal ShippingCost()
    {
        return 7.50m;
    }
}
Dune: 3.00, signature: False
Encyclopedia Set: 6.00, signature: True
Bookstore Mug: 7.50, signature: False

RequiresSignature was added later, and neither Book nor Mug changed. Because Mug wrote its own ShippingCost, the interface's default was switched off for it. There is no override keyword here; a public method with the same signature in the class is enough. A note: this feature needs runtime support and arrived with .NET Core 3.0; it does not work on .NET Framework. If you start using default methods as a multiple inheritance tool for sharing code, you have reached a place where an abstract class should be doing the job.

Interface or Abstract Class

Both say "provide these members", but they carry different weight. An abstract class builds a family: shared state (fields), constructors, shared code and gaps to fill. A class joins that family through a single door. An interface describes an ability; it gives the same role to unrelated types without creating any family relationship. A book and a mug may not come from the same family, but both can be shipped.

A practical measure: if types share state and code, use an abstract class; if what they share is only a role or a behavioral contract, use an interface. The two also work together, as in this post's Book : Product, IShippable, IDiscountable. When in doubt, start with an interface; moving to a base class later is much cheaper than tearing out a base class and going back to an interface.

Code That Depends on an Interface Is Testable

The biggest everyday benefit of interfaces is that they separate code from concrete dependencies. Bookstore has a 20 percent night sale after 21:00. To test a method that uses DateTime.Now directly, you would have to wait until night. Let's put the clock behind an interface:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Checkout morning = new Checkout(new FixedClock(new DateTime(2026, 11, 7, 10, 0, 0)));
Checkout evening = new Checkout(new FixedClock(new DateTime(2026, 11, 7, 21, 30, 0)));
Checkout now = new Checkout(new SystemClock());

Console.WriteLine(morning.Total(45.90m));
Console.WriteLine(evening.Total(45.90m));
Console.WriteLine(now.Total(45.90m) > 0);

public interface IClock
{
    DateTime Now { get; }
}

public class SystemClock : IClock
{
    public DateTime Now => DateTime.Now;
}

public class FixedClock : IClock
{
    public DateTime Now { get; }

    public FixedClock(DateTime now)
    {
        Now = now;
    }
}

public class Checkout
{
    private readonly IClock _clock;

    public Checkout(IClock clock)
    {
        _clock = clock;
    }

    public decimal Total(decimal price)
    {
        bool isNightSale = _clock.Now.Hour >= 21;
        return isNightSale ? price * 0.80m : price;
    }
}
45.90
36.7200
True

Checkout does not know where the time comes from; it only asks for an IClock and receives it through the constructor. In the application you pass a SystemClock, in a test a FixedClock that returns whatever moment you want. That is the essence of dependency injection; we covered the container in ASP.NET Core in the dependency injection post of the .NET Core Web API series. .NET 8 added a ready-made TimeProvider type to the BCL for exactly this job; we will see it in the .NET 8 post of the asynchronous programming series.

Common Pitfalls

An interface for every class. An IBookService next to BookService, with a single implementation, never tested and never going to change, only doubles the file count. Add an interface when a need creates it: a second implementation, a fake version for tests, or a module boundary.

Fat interfaces. Every class that implements a forty-member IProductOperations starts writing NotImplementedException for thirty methods it does not need. Small interfaces that describe a single role are better; the I in SOLID (interface segregation) says so.

Expecting to call a default method through the class. The CS1061 in the guessing example was the result of that. If the variable is not of the interface type, you cannot reach the default body.

Treating explicit implementation as a hiding tool. Explicit members are only removed from the surface; anyone who casts can reach them. Something that must really be hidden should be private.

A Small Experiment

★☆☆ Write an interface named IGiftWrappable with a decimal WrappingFee() method. Book and Mug should implement it, EBook should not. Calculate the total wrapping fee of the wrappable products in the cart with OfType<IGiftWrappable>().

★★☆ Add a default method string Carrier() => "Standard"; to IShippable. In just one class, write its own version that returns "Freight" for products heavier than 5 kg, and print the result in the parcels loop.

★★★ Instead of FixedClock, write a SteppingClock that moves the time forward by 1 hour every time Now is read. Call Total 24 times in a row with the same Checkout and print at which hours the discount was applied. How did having the clock behind an interface make this experiment possible?

All the types we have written so far were classes, and all of them were reference types. C# also has types that behave like values and are copied when assigned. In the next post we move to struct: how it differs from a class, the traps of copy semantics, readonly struct, and when you should make a type a struct.