Interfaces in C#
Interface definition, explicit implementation, default interface methods, and real-world usage in C#.
Defining and Implementing Interfaces
An interface defines a contract — a set of members that implementing types must provide. It contains no state and no implementation (unless using default methods, covered below). Interfaces are the primary tool for polymorphism and loose coupling in C#: you program to the interface, not the concrete type, which means you can swap implementations without changing callers.
public interface IShape
{
double Area();
double Perimeter();
string Name { get; }
}
public class Circle : IShape
{
public double Radius { get; }
public Circle(double radius) => Radius = radius;
// All three interface members must be implemented
public double Area() => Math.PI * Radius * Radius;
public double Perimeter() => 2 * Math.PI * Radius;
public string Name => "Circle";
}
public class Rectangle : IShape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double w, double h) => (Width, Height) = (w, h);
public double Area() => Width * Height;
public double Perimeter() => 2 * (Width + Height);
public string Name => "Rectangle";
}
// Polymorphism through interface — code that works with IShape works with any shape
IShape[] shapes = { new Circle(5), new Rectangle(4, 6) };
foreach (IShape shape in shapes)
Console.WriteLine($"{shape.Name}: area = {shape.Area():F2}");
Interface Segregation
Large interfaces create maintenance problems: every implementor must provide all members, even ones irrelevant to their purpose. The Interface Segregation Principle says that interfaces should be small and focused. Splitting a fat interface into smaller ones means each class only implements what it actually does, making the code easier to test and extend.
// Bad — one fat interface forces every repository to implement search and bulk ops
public interface IRepository
{
IEnumerable<T> GetAll<T>();
T GetById<T>(int id);
void Add<T>(T entity);
void Update<T>(T entity);
void Delete<T>(int id);
IEnumerable<T> Search<T>(string query); // not all repos need search
void BulkInsert<T>(IEnumerable<T> entities); // not all repos need bulk
}
// Good — segregated interfaces; each class implements only what it provides
public interface IReadRepository<T>
{
T? GetById(int id);
IEnumerable<T> GetAll();
}
public interface IWriteRepository<T>
{
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
// Compose interfaces for types that need both capabilities
public interface ISearchableRepository<T> : IReadRepository<T>
{
IEnumerable<T> Search(string query);
}
// A class only implements the contracts it actually fulfills
public class CustomerRepository : IReadRepository<Customer>, IWriteRepository<Customer>
{
public Customer? GetById(int id) => /* ... */ null;
public IEnumerable<Customer> GetAll() => /* ... */ Enumerable.Empty<Customer>();
public void Add(Customer c) { /* ... */ }
public void Update(Customer c) { /* ... */ }
public void Delete(int id) { /* ... */ }
}
Multiple Interface Implementation
C# classes cannot inherit from more than one class, but they can implement any number of interfaces. This is how unrelated capabilities — logging, validation, disposal — are mixed into a single type without forcing an arbitrary inheritance hierarchy.
public interface ILogger
{
void Log(string message);
}
public interface IValidator<T>
{
bool Validate(T value, out string? errorMessage);
}
// OrderProcessor implements three separate contracts
public class OrderProcessor : ILogger, IDisposable, IValidator<Order>
{
private StreamWriter? _logWriter;
public OrderProcessor(string logPath)
{
_logWriter = new StreamWriter(logPath, append: true);
}
public void Log(string message)
{
_logWriter?.WriteLine($"[{DateTime.UtcNow:O}] {message}");
}
public bool Validate(Order order, out string? errorMessage)
{
if (order.Lines.Count == 0)
{
errorMessage = "Order must have at least one line";
return false;
}
errorMessage = null;
return true;
}
// IDisposable — release the file handle when done
public void Dispose()
{
_logWriter?.Dispose();
_logWriter = null;
}
}
Explicit Interface Implementation
When two interfaces define a member with the same name but different meanings, implicit implementation cannot satisfy both. Explicit implementation binds a method to a specific interface, making it accessible only when the object is accessed through that interface. This also hides the method from the class’s direct public surface, which is useful when you want a member to be an implementation detail.
public interface IMetric
{
double Value { get; } // value in meters
}
public interface IImperial
{
double Value { get; } // value in feet — same name, different unit
}
public class Distance : IMetric, IImperial
{
private readonly double _meters;
public Distance(double meters) => _meters = meters;
// Explicit implementation — only reachable by casting to the interface
double IMetric.Value => _meters;
double IImperial.Value => _meters * 3.28084; // convert to feet
// Non-explicit property accessible directly on the class
public double Meters => _meters;
}
var d = new Distance(10);
Console.WriteLine(d.Meters); // 10
Console.WriteLine(((IMetric)d).Value); // 10 — meters
Console.WriteLine(((IImperial)d).Value); // 32.8084 — feet
// d.Value ← compile error — ambiguous, must go through an interface
Default Interface Methods (C# 8+)
Default interface methods let you add new members to an existing interface without forcing every implementor to update. This is most valuable when you own an interface used by code you cannot touch — you can extend it without a breaking change. Use them sparingly though; overuse makes it hard to understand what a type’s behavior actually is.
public interface INotifier
{
// Required — all implementors must provide this
void Send(string recipient, string message);
// Default implementation — existing implementors inherit this for free
void SendBatch(IEnumerable<string> recipients, string message)
{
foreach (var r in recipients)
Send(r, message); // delegates to the required method
}
// Default property
string SenderName => "System";
}
// Existing implementor — written before SendBatch was added; still compiles
public class EmailNotifier : INotifier
{
public void Send(string recipient, string message)
{
Console.WriteLine($"Email to {recipient}: {message}");
}
// SendBatch is automatically inherited from the interface default
}
INotifier notifier = new EmailNotifier();
notifier.SendBatch(new[] { "[email protected]", "[email protected]" }, "Hello!");
Interfaces in Dependency Injection
Interfaces are the foundation of testable, maintainable .NET applications. By depending on interfaces instead of concrete classes, you can swap implementations — for testing, different environments, or A/B experiments — without touching the code that uses them. This is the Dependency Inversion Principle in practice.
// Define the contract
public interface IWeatherService
{
Task<WeatherForecast> GetForecastAsync(string city);
}
// Production implementation — hits a real API
public class OpenWeatherService : IWeatherService
{
private readonly HttpClient _http;
public OpenWeatherService(HttpClient http) => _http = http;
public async Task<WeatherForecast> GetForecastAsync(string city)
{
var json = await _http.GetStringAsync($"/forecast?city={city}");
return JsonSerializer.Deserialize<WeatherForecast>(json)!;
}
}
// Fake for unit tests — no network required
public class FakeWeatherService : IWeatherService
{
public Task<WeatherForecast> GetForecastAsync(string city) =>
Task.FromResult(new WeatherForecast(city, 22.0, "Sunny"));
}
// Consumer depends only on IWeatherService — works with both implementations
public class WeatherController
{
private readonly IWeatherService _weather;
// The implementation is injected — this class never knows which one it gets
public WeatherController(IWeatherService weather) => _weather = weather;
public async Task<string> GetSummary(string city)
{
var forecast = await _weather.GetForecastAsync(city);
return $"{forecast.City}: {forecast.Temp}°C, {forecast.Description}";
}
}
// Register in DI container (ASP.NET Core) — swap the line below to use a different impl
// builder.Services.AddScoped<IWeatherService, OpenWeatherService>();
Interface vs Abstract Class
Choosing between an interface and an abstract class comes down to whether you need to share implementation code and whether the relationship is “is-a” or “can-do”.
// Use abstract class when:
// - Related types share real implementation code
// - There is a clear "is-a" relationship (Dog is-a Animal)
// - You need protected or private members
public abstract class BaseRepository<T>
{
protected readonly DbContext _db;
protected BaseRepository(DbContext db) => _db = db;
// Derived classes specialize this query; the fetch logic is shared
protected abstract IQueryable<T> GetQuery();
public async Task<T?> GetByIdAsync(int id)
{
return await GetQuery()
.FirstOrDefaultAsync(e => EF.Property<int>(e, "Id") == id);
}
}
// Use interface when:
// - Unrelated types share a contract (they can-do something)
// - You need multiple "inheritance" of behavior
// - You want to swap implementations (DI, testing)
public interface IExportable
{
byte[] Export(ExportFormat format);
}
// Order and Invoice are unrelated by inheritance, but both can be exported
public class Order : IExportable { public byte[] Export(ExportFormat f) => []; }
public class Invoice : IExportable { public byte[] Export(ExportFormat f) => []; }