C# Interfaces

An interface in C# defines a contract: a set of members that a type promises to provide. Interfaces matter because they let code depend on capabilities instead of specific classes. That makes programs easier to test, extend, and combine when different types need to be used through the same API.

Overview: How Interfaces Work

An interface describes what a type can do, not what concrete object it must be. For example, IPrintable might require a Print method, while ISavable might require a Save method. A class, struct, or record can implement an interface by declaring the interface name after a colon and providing all required members.

Interfaces are central to polymorphism. A variable declared as an interface type can refer to any object that implements that interface. If Receipt and Invoice both implement IPrintable, a method can accept IPrintable and call Print without knowing whether the runtime object is a receipt, invoice, report, or something added later.

Internally, the compiled interface becomes metadata that lists required members. When a concrete type implements the interface, the compiler verifies that matching public members exist, or that explicit interface implementations exist. At runtime, an interface variable stores a reference to the same concrete object. The object is not copied or converted. The CLR uses an interface dispatch mechanism to route calls such as item.Print() to the implementation supplied by the object’s actual type.

A class can inherit from only one class, but it can implement many interfaces. This is one of the biggest reasons interfaces are so useful in C#. A FileLogger could implement ILogger, IDisposable, and IAsyncDisposable while still inheriting from another base class. Interfaces model capabilities across unrelated types.

Modern C# interfaces can contain more than methods. They can require properties, indexers, events, and static abstract members. They may also contain default interface methods with a body, although most beginner and application-level interfaces still use abstract member declarations only. Interface members are public by default when they are part of the contract, and implementing members on a class are usually public unless you use explicit interface implementation.

Use interfaces when callers need a stable contract and concrete classes should remain replaceable. For example, business code should often depend on IEmailSender instead of SmtpEmailSender. Tests can then provide a fake sender, and production can use the real sender, while the calling code stays the same.

Syntax

interface IInterfaceName
{
    string Name { get; }
    void DoWork();
}

class ConcreteType : IInterfaceName
{
    public string Name { get; }

    public ConcreteType(string name)
    {
        Name = name;
    }

    public void DoWork()
    {
        Console.WriteLine(Name + " is working");
    }
}
Part Meaning
interface Declares a contract that implementing types must satisfy.
IInterfaceName Common C# naming convention: interface names often start with I.
Member without body Declares a required method, property, event, or indexer.
class ConcreteType : IInterfaceName Declares that the class implements the interface.
public implementation Provides the member so callers using the interface can access it.
Interface variable A variable such as IInterfaceName item can hold any implementing object.

Examples

Example 1: A Simple Interface Contract

using System;

interface IPrintable
{
    string Title { get; }
    void Print();
}

class Invoice : IPrintable
{
    public Invoice(string title, decimal total)
    {
        Title = title;
        Total = total;
    }

    public string Title { get; }
    public decimal Total { get; }

    public void Print()
    {
        Console.WriteLine($"{Title}: ${Total:0.00}");
    }
}

class Program
{
    static void Main()
    {
        IPrintable document = new Invoice("Invoice 1042", 189.5m);
        Console.WriteLine(document.Title);
        document.Print();
    }
}

Output:

Invoice 1042
Invoice 1042: $189.50

IPrintable requires a read-only Title property and a Print method. Invoice satisfies that contract with public members. The variable is typed as IPrintable, so the calling code sees only the interface contract, even though the runtime object is an Invoice.

Example 2: Different Classes Through One Interface

using System;
using System.Collections.Generic;

interface INotificationSender
{
    void Send(string recipient, string message);
}

class EmailSender : INotificationSender
{
    public void Send(string recipient, string message)
    {
        Console.WriteLine($"Email to {recipient}: {message}");
    }
}

class SmsSender : INotificationSender
{
    public void Send(string recipient, string message)
    {
        string shortMessage = message.Length <= 10 ? message : message.Substring(0, 10);
        Console.WriteLine($"SMS to {recipient}: {shortMessage}");
    }
}

class Program
{
    static void Main()
    {
        List<INotificationSender> senders = new List<INotificationSender>
        {
            new EmailSender(),
            new SmsSender()
        };

        foreach (INotificationSender sender in senders)
        {
            sender.Send("Mina", "Order shipped");
        }
    }
}

Output:

Email to Mina: Order shipped
SMS to Mina: Order ship

EmailSender and SmsSender do not need a shared base class. They simply share a capability: sending a notification. The loop depends on INotificationSender, so a later PushSender class could be added without changing the loop.

Example 3: Implementing Multiple Interfaces

using System;

interface INamed
{
    string Name { get; }
}

interface IDiscountable
{
    decimal ApplyDiscount(decimal percent);
}

class Product : INamed, IDiscountable
{
    public Product(string name, decimal price)
    {
        Name = name;
        Price = price;
    }

    public string Name { get; }
    public decimal Price { get; }

    public decimal ApplyDiscount(decimal percent)
    {
        return Price * (1m - percent);
    }
}

class Program
{
    static void Main()
    {
        Product product = new Product("Keyboard", 80m);
        INamed named = product;
        IDiscountable discountable = product;

        Console.WriteLine(named.Name);
        Console.WriteLine($"Sale price: ${discountable.ApplyDiscount(0.25m):0.00}");
    }
}

Output:

Keyboard
Sale price: $60.00

Product implements two separate contracts. The same object can be viewed through INamed when only the name matters, or through IDiscountable when discount behavior matters. This keeps APIs narrow and focused.

Example 4: Explicit Interface Implementation

using System;

interface IResettable
{
    void Reset();
}

class Timer : IResettable
{
    public int Seconds { get; private set; }

    public Timer(int seconds)
    {
        Seconds = seconds;
    }

    void IResettable.Reset()
    {
        Seconds = 0;
        Console.WriteLine("Timer reset");
    }
}

class Program
{
    static void Main()
    {
        Timer timer = new Timer(30);
        Console.WriteLine(timer.Seconds);

        IResettable resettable = timer;
        resettable.Reset();

        Console.WriteLine(timer.Seconds);
    }
}

Output:

30
Timer reset
0

An explicit implementation uses the form void IResettable.Reset(). That member is callable only through an IResettable reference, not directly through a Timer variable. This is useful when an interface member is advanced, rarely used, or would clutter the class’s main public API.

How It Works Step by Step

  1. The compiler reads the interface and records its required members in metadata.
  2. When a type says it implements the interface, the compiler checks that every required member is provided with a compatible signature.
  3. For normal implementation, the implementing member must be public because it becomes part of the public contract.
  4. For explicit implementation, the member is tied to the interface name and is accessed only through that interface type.
  5. When an object is assigned to an interface variable, the reference still points to the same concrete object in memory.
  6. At compile time, the interface variable exposes only the interface members.
  7. At runtime, the CLR dispatches each interface call to the concrete implementation mapped for the object’s runtime type.

This separation between compile-time contract and runtime object is what makes interface-based designs flexible. Code can ask for the smallest capability it needs, and many unrelated types can satisfy that capability independently.

Common Mistakes

Mistake 1: Forgetting That Interface Implementations Must Be Public

interface IWorker
{
    void Work();
}

class Robot : IWorker
{
    void Work()
    {
        Console.WriteLine("Working");
    }
}

This does not compile as a normal interface implementation because Work is private by default inside a class. A public interface contract needs a public member unless you intentionally use explicit interface implementation.

using System;

interface IWorker
{
    void Work();
}

class Robot : IWorker
{
    public void Work()
    {
        Console.WriteLine("Working");
    }
}

class Program
{
    static void Main()
    {
        IWorker worker = new Robot();
        worker.Work();
    }
}

Output:

Working

Mistake 2: Expecting Interface Variables to Expose Class-Specific Members

INotificationSender sender = new EmailSender();
sender.ConfigureSmtp("smtp.example.com");

This does not compile unless ConfigureSmtp is part of INotificationSender. The variable type controls what members are visible. Usually, configuration should happen before the object is passed around as the interface.

using System;

interface INotificationSender
{
    void Send(string recipient, string message);
}

class EmailSender : INotificationSender
{
    private string server = "localhost";

    public void ConfigureSmtp(string serverName)
    {
        server = serverName;
    }

    public void Send(string recipient, string message)
    {
        Console.WriteLine($"Using {server}");
        Console.WriteLine($"Email to {recipient}: {message}");
    }
}

class Program
{
    static void Main()
    {
        EmailSender email = new EmailSender();
        email.ConfigureSmtp("smtp.example.com");

        INotificationSender sender = email;
        sender.Send("Omar", "Welcome");
    }
}

Output:

Using smtp.example.com
Email to Omar: Welcome

Mistake 3: Making Interfaces Too Large

A large interface such as IUserService with login, email, reporting, billing, and deletion methods forces every implementation to care about too many responsibilities. Prefer smaller interfaces such as IAuthenticator, IEmailSender, and IUserReporter when callers do not need all behaviors at once.

Best Practices

  • Define interfaces around behavior that callers actually need, not around every method a class happens to have.
  • Keep interfaces small and cohesive; one focused capability is easier to implement and test.
  • Use interfaces to depend on abstractions at boundaries such as repositories, clocks, email senders, payment gateways, and logging.
  • Name interfaces clearly. The I prefix is common in C#, but the rest of the name should describe the capability, such as IValidator or IReportExporter.
  • Prefer public implementation for ordinary members. Use explicit interface implementation when a member should be available only through the interface or when two interfaces have conflicting member names.
  • Do not add members casually to a widely used interface; every implementer may need to change.
  • Use an abstract class instead of an interface when you need shared fields, constructor logic, or substantial reusable implementation.
  • Program against the narrowest useful interface in method parameters and fields.

Practice Exercises

  1. Create an IPayable interface with GetPaymentAmount. Implement it in Invoice and HourlyEmployee, then print both through an IPayable[].
  2. Create an IFormatter interface with Format. Implement uppercase and bracket formatters, then write a method that accepts IFormatter.
  3. Create two interfaces, INamed and IArchivable. Implement both in a Document class and show the same object assigned to both interface types.

Summary

  • An interface defines a contract that implementing types promise to satisfy.
  • Interfaces enable polymorphism without forcing classes into the same inheritance hierarchy.
  • A type can implement multiple interfaces, but a class can inherit from only one class.
  • Interface variables expose only the members declared by the interface, even when the object has more members.
  • Normal interface implementations are public; explicit implementations are accessed through the interface type.
  • The CLR dispatches interface calls to the concrete implementation on the runtime object.
  • Good interfaces are small, stable, behavior-focused contracts.