C# Interfaces Explained: Contracts, Polymorphism and Real-World Patterns
Every non-trivial C# codebase you'll ever work on uses interfaces. They're the backbone of dependency injection frameworks like ASP.NET Core's built-in DI container, they power mocking libraries like Moq, and they're the reason you can swap a database provider without touching a single line of business logic. If you've ever seen ILogger, IEnumerable, or IDisposable in .NET code and wondered why everything starts with 'I', this article is for you.
The core problem interfaces solve is tight coupling. Without them, your OrderService class might directly instantiate a SqlDatabase object. Now your business logic is physically welded to SQL Server. Want to write a unit test? You need a real database. Want to switch to PostgreSQL? You're rewriting code in the wrong layer. Interfaces break that dependency by introducing a middleman contract — your OrderService only ever knows about IDatabase, and it genuinely doesn't care what's behind it.
By the end of this article you'll understand not just the syntax of interfaces but the design thinking behind them. You'll be able to define your own interfaces, implement them across multiple classes, use explicit interface implementation to resolve naming conflicts, and recognise the patterns — like Strategy and Repository — that interfaces make possible. You'll also know the most common mistakes developers make and exactly how to avoid them.
What an Interface Actually Is — and What It Isn't
An interface is a pure contract. It defines a set of members — methods, properties, events, or indexers — that any implementing class or struct must provide. Before C# 8, interfaces contained zero implementation. From C# 8 onwards, default interface methods exist (we'll cover that), but the mental model of 'contract first' still holds.
Here's what makes interfaces different from abstract classes: an interface carries no state. There are no instance fields. It can't hold data. It just describes capability. Think of it as a job description versus an employee. The job description says 'must be able to drive a forklift.' It doesn't drive anything itself.
A class can implement as many interfaces as it wants — that's the escape hatch C# gives you instead of multiple inheritance. A Document class might implement IPrintable, IExportable, and IVersioned simultaneously. That's three separate capability contracts, all in one class.
The naming convention — the leading I — is a .NET standard enforced by StyleCop and every serious team. It's not optional etiquette; it's how developers instantly recognise a contract type from a concrete type at a glance.
using System; // The interface — a pure contract describing a capability. // No fields, no constructor, no implementation (pre-C# 8 style). public interface IGreeter { // Any class implementing IGreeter MUST provide this method. // Access modifiers are implicitly public on interface members. string BuildGreeting(string recipientName); // Read-only property — implementors must expose a Name. string GreeterName { get; } } // FormalGreeter honours the contract in one way... public class FormalGreeter : IGreeter { public string GreeterName => "Corporate Bot"; public string BuildGreeting(string recipientName) { // Formal implementation — very stiff. return $"Good day, {recipientName}. I trust you are well."; } } // CasualGreeter honours the same contract in a completely different way. public class CasualGreeter : IGreeter { public string GreeterName => "Buddy Bot"; public string BuildGreeting(string recipientName) { // Casual implementation — same contract, totally different personality. return $"Hey {recipientName}! What's up?"; } } public class Program { // Notice: the parameter type is IGreeter, not FormalGreeter or CasualGreeter. // This method has NO idea which concrete class it receives — and it doesn't need to. static void PrintGreeting(IGreeter greeter, string name) { Console.WriteLine($"[{greeter.GreeterName}] says: {greeter.BuildGreeting(name)}"); } static void Main() { IGreeter formal = new FormalGreeter(); IGreeter casual = new CasualGreeter(); PrintGreeting(formal, "Alice"); // Passes a FormalGreeter through IGreeter PrintGreeting(casual, "Bob"); // Passes a CasualGreeter through the same slot } }
[Buddy Bot] says: Hey Bob! What's up?
Real-World Interfaces: The Repository Pattern in Plain English
The most practical place to see interfaces earn their pay is the Repository pattern. Your business logic needs to read and write data — but it shouldn't care whether that data lives in SQL Server, an in-memory list, or a JSON file on disk.
By defining an IProductRepository interface, your ProductService class can be written once and tested without a database. During tests you pass in a FakeProductRepository that returns hardcoded data instantly. In production you pass in a SqlProductRepository that hits the real database. Same ProductService, zero changes.
This is also how ASP.NET Core's dependency injection works. You register IProductRepository → SqlProductRepository in Program.cs, and the DI container injects the right implementation wherever the interface is requested. Swap the registration to a different concrete class and your entire app behaves differently without touching business logic.
This design is called the Dependency Inversion Principle — the D in SOLID. High-level modules (business logic) should not depend on low-level modules (database code). Both should depend on abstractions (interfaces). Once this clicks, you'll see why interfaces are everywhere in professional .NET codebases.
using System; using System.Collections.Generic; using System.Linq; // --- The Contract --- // ProductService will ONLY ever know about this interface. public interface IProductRepository { IEnumerable<Product> GetAllProducts(); Product? GetById(int productId); void AddProduct(Product product); } // --- The Domain Model --- public class Product { public int Id { get; set; } public string Name { get; set; } = string.Empty; public decimal Price { get; set; } } // --- Real Implementation (used in production) --- // Imagine this hits SQL Server. We keep it simple here. public class InMemoryProductRepository : IProductRepository { // Simulates a database table in memory. private readonly List<Product> _products = new() { new Product { Id = 1, Name = "Mechanical Keyboard", Price = 129.99m }, new Product { Id = 2, Name = "USB-C Hub", Price = 49.99m } }; public IEnumerable<Product> GetAllProducts() => _products; public Product? GetById(int productId) => _products.FirstOrDefault(p => p.Id == productId); public void AddProduct(Product product) { // Auto-increment ID, just like a real DB sequence would. product.Id = _products.Count + 1; _products.Add(product); } } // --- Business Logic Layer --- // ProductService has NO knowledge of how data is stored. // It only talks to IProductRepository — could be SQL, Mongo, a flat file, anything. public class ProductService { private readonly IProductRepository _repository; // The repository is INJECTED — ProductService doesn't create it. public ProductService(IProductRepository repository) { _repository = repository; } public void DisplayAllProducts() { var products = _repository.GetAllProducts(); Console.WriteLine("=== Product Catalogue ==="); foreach (var product in products) { // Business formatting logic lives here, not in the repository. Console.WriteLine($" [{product.Id}] {product.Name} — ${product.Price:F2}"); } } public void AddNewProduct(string name, decimal price) { var newProduct = new Product { Name = name, Price = price }; _repository.AddProduct(newProduct); Console.WriteLine($"Added: '{name}' with ID {newProduct.Id}"); } } public class Program { static void Main() { // Wire up the dependency: tell the app which concrete class backs the interface. // In ASP.NET Core this line lives in Program.cs / Startup.cs. IProductRepository repository = new InMemoryProductRepository(); var service = new ProductService(repository); service.DisplayAllProducts(); service.AddNewProduct("Wireless Mouse", 35.00m); service.DisplayAllProducts(); } }
[1] Mechanical Keyboard — $129.99
[2] USB-C Hub — $49.99
Added: 'Wireless Mouse' with ID 3
=== Product Catalogue ===
[1] Mechanical Keyboard — $129.99
[2] USB-C Hub — $49.99
[3] Wireless Mouse — $35.00
Explicit Interface Implementation — Solving Naming Conflicts Cleanly
Sometimes a class implements two interfaces that both define a member with the same name but different intended behaviours. This is where explicit interface implementation saves the day.
With explicit implementation you prefix the member name with the interface name (IInterfaceName.MethodName). The method becomes inaccessible through the class reference directly — you must cast to the interface to reach it. This keeps the class's public API clean while still honouring both contracts.
A practical scenario: you're building a DataExporter class that implements both ICsvExporter and IJsonExporter. Both interfaces happen to define a Export(string filePath) method. Without explicit implementation, you'd have a single Export method trying to be two things at once. With explicit implementation, each interface gets its own dedicated implementation, and callers using the interface reference get exactly the right behaviour.
This isn't a common pattern day-to-day, but when you need it you really need it — and knowing it exists separates intermediate developers from beginners. It also comes up in legacy COM interop code in .NET, where interface conflicts are common.
using System; // Two interfaces with a conflicting method signature. public interface ICsvExporter { // Export to CSV format. string Export(string filePath); } public interface IJsonExporter { // Export to JSON format — same signature, totally different intent. string Export(string filePath); } public class DataExporter : ICsvExporter, IJsonExporter { // Explicit implementation for ICsvExporter. // Note: NO access modifier — explicit implementations are always private by default. string ICsvExporter.Export(string filePath) { return $"Writing CSV data to: {filePath}.csv"; } // Explicit implementation for IJsonExporter. string IJsonExporter.Export(string filePath) { return $"Writing JSON data to: {filePath}.json"; } // You can still have a regular public method on the class itself. public string ExportSummary() { return "DataExporter supports both CSV and JSON formats."; } } public class Program { static void Main() { var exporter = new DataExporter(); // exporter.Export("report") — this would NOT compile. // Explicit implementations are only reachable via the interface reference. // Cast to the specific interface to get the right behaviour. ICsvExporter csvExporter = exporter; IJsonExporter jsonExporter = exporter; Console.WriteLine(csvExporter.Export("sales_report")); Console.WriteLine(jsonExporter.Export("sales_report")); Console.WriteLine(exporter.ExportSummary()); } }
Writing JSON data to: sales_report.json
DataExporter supports both CSV and JSON formats.
Default Interface Methods and Interface Inheritance — C# 8+ Power Features
C# 8 introduced default interface methods — a way to add new functionality to an interface without breaking every class that already implements it. This was a huge deal for library authors who couldn't previously add members to a public interface without forcing a breaking change on thousands of implementors.
A default method has a body defined right on the interface. Implementing classes inherit the default behaviour automatically but can override it if they want. It's not inheritance in the classical OOP sense — it's more like a safety net for evolving contracts over time.
Interfaces can also inherit from other interfaces, letting you build layered capability contracts. IReadableRepository might define GetById and GetAll. IWritableRepository might extend it with Add, Update, and Delete. Your full IProductRepository then inherits both — and a read-only caching layer can implement just IReadableRepository without faking out write methods.
Know the limits though: default methods are a library evolution tool, not a replacement for abstract classes. If you need shared state or a constructor, you still want an abstract class. Use default methods sparingly — overusing them muddies the 'pure contract' clarity that makes interfaces valuable in the first place.
using System; using System.Collections.Generic; // Base interface — read-only capability. public interface IReadableRepository<T> { T? GetById(int id); IEnumerable<T> GetAll(); // Default method added in C# 8 — existing implementors get this for free. // They can override it, but they don't HAVE to. bool Exists(int id) => GetById(id) != null; // Default implementation uses GetById } // Extended interface — adds write capability on top of read. public interface IWritableRepository<T> : IReadableRepository<T> { void Add(T item); void Delete(int id); } public class Note { public int Id { get; set; } public string Content { get; set; } = string.Empty; } // Implements the full read-write contract. public class NoteRepository : IWritableRepository<Note> { private readonly List<Note> _notes = new() { new Note { Id = 1, Content = "Buy groceries" }, new Note { Id = 2, Content = "Review pull request" } }; public Note? GetById(int id) => _notes.Find(n => n.Id == id); public IEnumerable<Note> GetAll() => _notes; public void Add(Note note) { note.Id = _notes.Count + 1; _notes.Add(note); } public void Delete(int id) => _notes.RemoveAll(n => n.Id == id); // We're NOT overriding Exists() — we're happy with the default implementation. } public class Program { static void Main() { IWritableRepository<Note> notes = new NoteRepository(); // Exists() comes from the default interface method — no override needed. Console.WriteLine($"Note 1 exists: {notes.Exists(1)}"); // Uses default Console.WriteLine($"Note 99 exists: {notes.Exists(99)}"); // Uses default notes.Add(new Note { Content = "Ship the feature" }); notes.Delete(1); Console.WriteLine("\nRemaining notes:"); foreach (var note in notes.GetAll()) { Console.WriteLine($" [{note.Id}] {note.Content}"); } } }
Note 99 exists: False
Remaining notes:
[2] Review pull request
[3] Ship the feature
| Feature / Aspect | Interface | Abstract Class |
|---|---|---|
| Multiple inheritance | Yes — a class can implement many interfaces | No — a class can only inherit one abstract class |
| Instance fields / state | Not allowed (before C# 11) | Allowed — can hold shared data |
| Constructors | Not allowed | Allowed — can enforce initialisation |
| Default implementations | Yes, from C# 8 onwards (use sparingly) | Yes — via regular method bodies |
| Access modifiers on members | All public by default; no private members pre-C# 8 | Full range: public, protected, private |
| Best used for | Defining capabilities a type can have (IDisposable, IComparable) | Defining a base type with shared logic (Animal → Dog, Cat) |
| Dependency Injection | Ideal — DI containers bind interfaces to concrete types | Possible but less common |
| Unit testing / mocking | Trivial — Moq, NSubstitute mock interfaces natively | Harder — requires virtual methods |
🎯 Key Takeaways
- An interface is a binding contract — it defines WHAT a type can do, never HOW. Any class that signs the contract must deliver every member, but the implementation is entirely its own business.
- The Repository pattern is the clearest real-world example of interfaces earning their value:
ProductServicetalks only toIProductRepository, making it trivially swappable between SQL, in-memory, and fake test implementations without changing a line of business logic. - Explicit interface implementation is the clean solution when two interfaces clash on a method name — the method is hidden from the class reference and only reachable through a cast to the specific interface type.
- Default interface methods (C# 8+) exist for library evolution, not everyday design. Use them to add new members to a published interface without breaking existing implementors — not as a shortcut to avoid proper abstraction thinking.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Making interfaces too large (Interface Segregation violation) — Symptoms: implementing classes full of
throw new NotImplementedException()because they only care about two of the ten interface members. Fix: split fat interfaces into focused, single-purpose ones.IProductReaderandIProductWriterare better than one giantIProductRepositorythat forces every implementor to handle both. - ✕Mistake 2: Adding access modifiers to interface members — The compiler error 'The modifier public is not valid for this item' appears when you write
public string GetName();inside an interface. Interface members are implicitly public. Just writestring GetName();— no modifier needed. The only exception is C# 8+ default methods, which can have private access modifiers. - ✕Mistake 3: Depending on concrete types instead of the interface — You define
IEmailSenderperfectly, then writeSmtpEmailSender sender = new SmtpEmailSender()everywhere instead ofIEmailSender sender = new SmtpEmailSender(). Now your code is still tightly coupled to the concrete class and you get none of the testability or swap-ability benefits. Always declare variables and parameters using the interface type, not the concrete type.
Interview Questions on This Topic
- QWhat is the difference between an interface and an abstract class in C#, and how do you decide which one to use in a real project?
- QCan you explain what explicit interface implementation is and give a scenario where you'd actually need it?
- QIf I have an `IDisposable` interface that only has one method, why not just use a base class instead? What's the real advantage of the interface here?
Frequently Asked Questions
Can a C# class implement multiple interfaces at the same time?
Yes — this is one of the primary reasons interfaces exist. C# doesn't support multiple class inheritance, but a class can implement as many interfaces as needed. For example, public class Document : IPrintable, IExportable, IVersioned is perfectly valid. Each interface simply adds a new set of required members to the class.
Can an interface have a constructor in C#?
No. Interfaces cannot define constructors because you can never instantiate an interface directly — only classes and structs can be instantiated. An interface exists purely to describe a contract; the concrete class that implements it handles its own initialisation through its own constructor.
What does it mean when people say 'program to an interface, not an implementation'?
It means your variables, method parameters, and return types should use the interface type (IProductRepository) rather than the concrete class type (SqlProductRepository). This keeps your code flexible — you can swap the concrete implementation at any time without changing the code that depends on it. It's the foundation of testable, loosely coupled architecture and directly enables dependency injection.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.