Skip to main content
C# · lesson 13 of 26
C# intermediate Lesson 13 of 26

Interfaces in C#

C# interfaces by example: implementing contracts, segregation, explicit implementation, default interface methods, and swapping implementations for tests.

By SkillByExample · Updated

Defining and Implementing Interfaces

An interface is a contract: a list of members an implementing type must provide. Code written against the interface works with every implementation.

IShape[] shapes = { new Circle(5), new Rectangle(4, 6) };
foreach (IShape shape in shapes)
    Console.WriteLine($"{shape.Name}: area = {shape.Area():F2}");

Console.WriteLine(shapes[0] is IShape);
Console.WriteLine(typeof(Circle).GetInterfaces()[0].Name);

public interface IShape
{
    double Area();
    string Name { get; }
}

public class Circle(double radius) : IShape
{
    public double Area() => Math.PI * radius * radius;
    public string Name => "Circle";
}

public class Rectangle(double w, double h) : IShape
{
    public double Area() => w * h;
    public string Name => "Rectangle";
}
$ dotnet run
Circle: area = 78.54
Rectangle: area = 24.00
True
IShape

Circle and Rectangle share no base class, only a promise. The loop never checks which type it holds; it calls the interface members and each object answers for itself. Leave out Name from either class and the build fails with CS0535.

Small Interfaces, Composed

Split a fat interface into small ones so each class implements only what it can actually do (the Interface Segregation Principle).

var repo = new InMemoryRepo();
repo.Add(new Customer(1, "Ada"));
repo.Add(new Customer(2, "Alan"));

Report(repo);
Console.WriteLine(repo is IWriteRepository<Customer>);
Console.WriteLine(repo is ISearchable<Customer>);

static void Report(IReadRepository<Customer> reader) =>
    Console.WriteLine($"{reader.GetAll().Count()} customers, #2 = {reader.GetById(2)?.Name}");

public record Customer(int Id, string Name);

public interface IReadRepository<T>
{
    T? GetById(int id);
    IEnumerable<T> GetAll();
}

public interface IWriteRepository<T>
{
    void Add(T entity);
}

public interface ISearchable<T> : IReadRepository<T>
{
    IEnumerable<T> Search(string query);
}

public class InMemoryRepo : IReadRepository<Customer>, IWriteRepository<Customer>
{
    private readonly Dictionary<int, Customer> _store = new();
    public Customer? GetById(int id) => _store.GetValueOrDefault(id);
    public IEnumerable<Customer> GetAll() => _store.Values;
    public void Add(Customer c) => _store[c.Id] = c;
}
$ dotnet run
2 customers, #2 = Alan
True
False

Report asks only for IReadRepository<Customer>, so it cannot accidentally write. InMemoryRepo did not sign up for search, and the is check says so — nothing forced it to stub out a Search it cannot support.

Multiple Interfaces and IDisposable

A class has one base class but can implement any number of interfaces. IDisposable is the one you meet most often, because using calls it.

using (var p = new OrderProcessor())
{
    p.Log("start");
    Console.WriteLine(p.Validate(0, out var error) ? "ok" : error);
    Console.WriteLine(p.Validate(3, out _) ? "ok" : "bad");
}
Console.WriteLine("after using");

public interface ILogger { void Log(string message); }

public interface IValidator<T> { bool Validate(T value, out string? error); }

public class OrderProcessor : ILogger, IValidator<int>, IDisposable
{
    public void Log(string message) => Console.WriteLine($"log: {message}");

    public bool Validate(int lineCount, out string? error)
    {
        error = lineCount == 0 ? "Order must have at least one line" : null;
        return error is null;
    }

    public void Dispose() => Console.WriteLine("disposed");
}
$ dotnet run
log: start
Order must have at least one line
ok
disposed
after using

Dispose runs when the using block ends — before “after using” — even if an exception had been thrown inside the block. That guarantee is why resources such as file handles and connections implement IDisposable.

Explicit Interface Implementation

When two interfaces declare a member with the same name but different meanings, implement each one explicitly. An explicit member is reachable only through that interface.

var d = new Distance(10);
Console.WriteLine(d.Meters);
Console.WriteLine(((IMetric)d).Value);
Console.WriteLine(((IImperial)d).Value);

IImperial imp = d;
Console.WriteLine(imp.Value.ToString("F1"));

public interface IMetric { double Value { get; } }
public interface IImperial { double Value { get; } }

public class Distance(double meters) : IMetric, IImperial
{
    double IMetric.Value => meters;
    double IImperial.Value => meters * 3.28084;

    public double Meters => meters;
}
$ dotnet run
10
10
32.8084
32.8

The same object reports 10 or 32.8084 depending on which interface you look at it through. d.Value does not exist on Distance at all — the compiler would report CS1061 — so a caller must choose a unit explicitly.

Default Interface Methods (C# 8+)

An interface member can carry a default body, so adding it does not break existing implementers.

INotifier email = new EmailNotifier();
email.SendBatch(new[] { "a@x.com", "b@x.com" }, "Hello!");
Console.WriteLine(email.SenderName);

INotifier sms = new SmsNotifier();
sms.SendBatch(new[] { "555-0101", "555-0102" }, "Hi");

public interface INotifier
{
    void Send(string recipient, string message);

    void SendBatch(IEnumerable<string> recipients, string message)
    {
        foreach (var r in recipients) Send(r, message);
    }

    string SenderName => "System";
}

public class EmailNotifier : INotifier
{
    public void Send(string to, string msg) => Console.WriteLine($"Email to {to}: {msg}");
}

public class SmsNotifier : INotifier
{
    public void Send(string to, string msg) => Console.WriteLine($"SMS to {to}: {msg}");
    public void SendBatch(IEnumerable<string> to, string msg) =>
        Console.WriteLine($"SMS bulk to {string.Join(",", to)}: {msg}");
}
$ dotnet run
Email to a@x.com: Hello!
Email to b@x.com: Hello!
System
SMS bulk to 555-0101,555-0102: Hi

EmailNotifier never wrote SendBatch or SenderName and still gets them. SmsNotifier supplies its own SendBatch, which replaces the default. Note the variables are typed INotifier: a default method belongs to the interface, so new EmailNotifier().SendBatch(...) would not compile.

Interfaces for Swappable Dependencies

Depend on an interface rather than a concrete class, and a test can hand in a fake that never touches the network.

var controller = new WeatherReport(new FakeWeatherService());
Console.WriteLine(await controller.SummaryAsync("Oslo"));

var failing = new WeatherReport(new DownWeatherService());
Console.WriteLine(await failing.SummaryAsync("Oslo"));

public record Forecast(string City, double Temp, string Description);

public interface IWeatherService
{
    Task<Forecast> GetForecastAsync(string city);
}

public class FakeWeatherService : IWeatherService
{
    public Task<Forecast> GetForecastAsync(string city) =>
        Task.FromResult(new Forecast(city, 22.0, "Sunny"));
}

public class DownWeatherService : IWeatherService
{
    public Task<Forecast> GetForecastAsync(string city) =>
        throw new HttpRequestException("503 Service Unavailable");
}

public class WeatherReport(IWeatherService weather)
{
    public async Task<string> SummaryAsync(string city)
    {
        try
        {
            var f = await weather.GetForecastAsync(city);
            return $"{f.City}: {f.Temp}°C, {f.Description}";
        }
        catch (HttpRequestException ex) { return $"{city}: unavailable ({ex.Message})"; }
    }
}
$ dotnet run
Oslo: 22°C, Sunny
Oslo: unavailable (503 Service Unavailable)

WeatherReport has no idea which implementation it received. Swapping in a service that always fails lets you test the error path deterministically — something that is hard to arrange against a real API. In ASP.NET Core the same swap is one line of registration, builder.Services.AddScoped<IWeatherService, OpenWeatherService>(); see ASP.NET Core Basics.

Interface vs Abstract Class

QuestionInterfaceAbstract class
Share implementation code?Only default methods, no stateYes, including fields
How many per class?Any numberOne
Relationship”can do” (IComparable, IDisposable)“is a” (Dog is an Animal)
Constructors / protected membersStatic constructors and protected members only (C# 8+); no instance constructorsYes
Swap for a fake in testsNatural fitPossible, but awkward

A useful default: start with an interface for anything that crosses a boundary (storage, network, clock), and reach for an abstract base class only when several implementations genuinely share code. The OOP lesson shows the abstract-class side.

Practice

  1. Add a shape. Add a Square(double side) implementing IShape to the first example and print its area next to the circle’s.
Solution
IShape[] shapes = { new Circle(5), new Square(3) };
foreach (var s in shapes)
    Console.WriteLine($"{s.Name}: {s.Area():F2}");

public interface IShape { double Area(); string Name { get; } }

public class Circle(double r) : IShape
{
    public double Area() => Math.PI * r * r;
    public string Name => "Circle";
}

public class Square(double side) : IShape
{
    public double Area() => side * side;
    public string Name => "Square";
}
$ dotnet run
Circle: 78.54
Square: 9.00
  1. IComparable. Make Version(int Major, int Minor) implement IComparable<Version> so that List<Version>.Sort() orders 2.1, 1.9, 2.0 correctly.
Solution
var versions = new List<Version> { new(2, 1), new(1, 9), new(2, 0) };
versions.Sort();
Console.WriteLine(string.Join(" ", versions));

public record Version(int Major, int Minor) : IComparable<Version>
{
    public int CompareTo(Version? other) =>
        other is null ? 1 : (Major, Minor).CompareTo((other.Major, other.Minor));

    public override string ToString() => $"{Major}.{Minor}";
}
$ dotnet run
1.9 2.0 2.1

Sort() with no arguments uses IComparable<T>; tuples already compare element by element.

  1. A fake clock. Define IClock { DateTime Now { get; } }, a FixedClock, and a Greeter that says “Good morning” before noon. Test it at 09:00 and 15:00.
Solution
foreach (var hour in new[] { 9, 15 })
    Console.WriteLine(new Greeter(new FixedClock(new DateTime(2024, 1, 1, hour, 0, 0))).Greet());

public interface IClock { DateTime Now { get; } }

public class FixedClock(DateTime now) : IClock { public DateTime Now => now; }

public class Greeter(IClock clock)
{
    public string Greet() => clock.Now.Hour < 12 ? "Good morning" : "Good afternoon";
}
$ dotnet run
Good morning
Good afternoon

Code that calls DateTime.Now directly cannot be tested at a chosen time; code that asks for an IClock can.

Frequently Asked Questions

Can a class implement multiple interfaces?
Yes, a class can implement any number of interfaces. This is how C# achieves multiple inheritance of behavior — through interfaces rather than multiple class inheritance.
What are default interface methods?
Introduced in C# 8, default interface methods let you add method implementations directly on an interface without breaking existing implementors. They are useful for evolving APIs, but use them sparingly — they can make code harder to follow.
When should I use explicit interface implementation?
Use explicit implementation when two interfaces share a method name with different intended behavior, or when you want to hide an interface method from the class's public surface (callers must cast to the interface to access it).
Why can't I call a default interface method on the class variable?
A default interface method belongs to the interface, not to the implementing class, so it is only reachable through a variable typed as the interface. new EmailNotifier().SendBatch(...) fails with CS1061; INotifier n = new EmailNotifier(); n.SendBatch(...) works. The class can still declare its own SendBatch to override the default.
Should I use an interface or an abstract class?
Use an interface for a capability that unrelated types share, or for anything you want to swap in tests or through dependency injection. Use an abstract class when related types share real implementation code or need protected state. A class can implement many interfaces but inherit only one base class.