Home C# / .NET C# Interfaces Explained: Contracts, Polymorphism and Real-World Patterns

C# Interfaces Explained: Contracts, Polymorphism and Real-World Patterns

In Plain English 🔥
Imagine a universal TV remote. It has buttons labelled Power, Volume Up, Volume Down — and any TV brand (Samsung, LG, Sony) must honour those buttons, but each brand handles them internally in its own way. An interface in C# is that remote control contract: it says 'you MUST support these operations' but stays completely silent on HOW. Any class that signs the contract gets to decide its own implementation. That's it — a binding promise, nothing more.
⚡ Quick Answer
Imagine a universal TV remote. It has buttons labelled Power, Volume Up, Volume Down — and any TV brand (Samsung, LG, Sony) must honour those buttons, but each brand handles them internally in its own way. An interface in C# is that remote control contract: it says 'you MUST support these operations' but stays completely silent on HOW. Any class that signs the contract gets to decide its own implementation. That's it — a binding promise, nothing more.

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.

InterfaceBasics.cs · CSHARP
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
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
    }
}
▶ Output
[Corporate Bot] says: Good day, Alice. I trust you are well.
[Buddy Bot] says: Hey Bob! What's up?
🔥
Why `IGreeter` not `Greeter`?The leading `I` prefix is a .NET convention defined in Microsoft's Framework Design Guidelines. It's not enforced by the compiler, but every serious team and linter expects it. Breaking this convention is a red flag in code reviews — it makes interfaces indistinguishable from abstract classes at a glance.

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.

RepositoryPattern.cs · CSHARP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
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();
    }
}
▶ Output
=== Product Catalogue ===
[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
⚠️
Pro Tip: Testing Without a DatabaseCreate a `FakeProductRepository : IProductRepository` that returns hardcoded test data and never touches disk or network. Your unit tests will run in milliseconds, not seconds, and they'll never fail because someone deleted a row in the test database. This is the single biggest practical win interfaces give you.

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.

ExplicitInterfaceImpl.cs · CSHARP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
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());
    }
}
▶ Output
Writing CSV data to: sales_report.csv
Writing JSON data to: sales_report.json
DataExporter supports both CSV and JSON formats.
⚠️
Watch Out: Explicit Members Are HiddenExplicit interface members have no access modifier and are invisible on the class reference. If a junior dev calls `myExporter.Export(...)` and gets a compile error, they'll be confused. Always leave a code comment explaining WHY explicit implementation is used here — it's not immediately obvious and future-you will thank present-you.

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.

DefaultMethodsAndInheritance.cs · CSHARP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
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}");
        }
    }
}
▶ Output
Note 1 exists: True
Note 99 exists: False

Remaining notes:
[2] Review pull request
[3] Ship the feature
🔥
Interview Gold: Default Methods vs Abstract ClassesInterviewers love asking 'when would you choose an interface with default methods over an abstract class?' The answer: choose interfaces when you need multiple implementation support or are evolving a public library contract. Choose abstract classes when you need shared state, a constructor, or protected members. They solve different problems and are not interchangeable.
Feature / AspectInterfaceAbstract Class
Multiple inheritanceYes — a class can implement many interfacesNo — a class can only inherit one abstract class
Instance fields / stateNot allowed (before C# 11)Allowed — can hold shared data
ConstructorsNot allowedAllowed — can enforce initialisation
Default implementationsYes, from C# 8 onwards (use sparingly)Yes — via regular method bodies
Access modifiers on membersAll public by default; no private members pre-C# 8Full range: public, protected, private
Best used forDefining capabilities a type can have (IDisposable, IComparable)Defining a base type with shared logic (Animal → Dog, Cat)
Dependency InjectionIdeal — DI containers bind interfaces to concrete typesPossible but less common
Unit testing / mockingTrivial — Moq, NSubstitute mock interfaces nativelyHarder — 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: ProductService talks only to IProductRepository, 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. IProductReader and IProductWriter are better than one giant IProductRepository that 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 write string 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 IEmailSender perfectly, then write SmtpEmailSender sender = new SmtpEmailSender() everywhere instead of IEmailSender 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.

🔥
TheCodeForge Editorial Team Verified Author

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.

← PreviousInheritance in C#Next →Polymorphism in C#
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged