← Open in the full interactive course (progress tracking, search & more)

One class, several capabilities. This is where interfaces stop looking like inheritance and start looking like what they actually are.

A single class in a real system is rarely "just one thing." An Order entity might need to be auditable (someone needs to know who changed it and when), cacheable (it's read far more often than it's written), and comparable (you need to sort a list of them by priority). None of these are the same relationship, and none of them should force Order into somebody else's inheritance chain:

public sealed class Order : IAuditable, ICacheable, IComparable<Order>
{
    public Guid Id { get; }
    public DateTime CreatedAtUtc { get; }
    public DateTime? ModifiedAtUtc { get; private set; }
    public int Priority { get; }

    // Satisfies IAuditable
    public void RecordModification() => ModifiedAtUtc = DateTime.UtcNow;

    // Satisfies ICacheable
    public string CacheKey => $"order:{Id}";
    public TimeSpan CacheDuration => TimeSpan.FromMinutes(5);

    // Satisfies IComparable<Order>
    public int CompareTo(Order? other) => Priority.CompareTo(other?.Priority ?? 0);
}
// Order extends NOTHING and implements THREE interfaces — each one an
// independent, focused capability, none of which know the others exist.

This is the payoff of composing capabilities through interfaces instead of a single tangled base class: each capability is small, independently understandable, and completely decoupled from the others. Adding a fourth capability later never requires touching the first three.

In this lesson, you'll learn how to implement several interfaces on one class cleanly, how C# resolves — or refuses to silently resolve — naming conflicts between interfaces, and how to think about class design as composing capabilities rather than inheriting identity.

What Is It?

A C# class can implement any number of interfaces, separated by commas after the base class (if any). Unlike class inheritance, there's no "one slot" limit — the compiler simply requires that the class provide an implementation for every member of every interface it lists.

public class ExpressCarrierProcessor : ShipmentProcessor, IAuditable, ICacheable
//                                       ^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^  ^^^^^^^^^^
//                                       one base class      any number of interfaces

You already saw this shape informally in lesson 073's NotificationService, where each channel implemented a single interface. This lesson is about what happens once a class needs to satisfy several interfaces at once — and what to do when two of them disagree.

Why This Matters — Composing Capabilities, Not Inheriting Identity

If C# only allowed single inheritance and no interfaces, modeling "a thing that is auditable, cacheable, and comparable" would force an impossible choice: pick one base class to inherit the logic from, and hand-roll the rest. Multiple interface implementation removes that constraint entirely — a class's set of capabilities is just a list, and that list can grow to fit reality instead of being squeezed to fit a single hierarchy.

This is the direct, practical payoff of the "is-a" vs. "can-do" distinction from lesson 077: Order is not an Auditable, it's not a Cacheable — it can be audited, it can be cached. Modeling capabilities as interfaces keeps the language matching the actual relationship.

Big Picture

ONE CLASS, THREE INDEPENDENT CONTRACTS
Order
Code that only cares about auditing
Code that only cares about caching

Each caller depends on exactly the capability it needs and nothing more. This narrow, targeted dependency shape is exactly what lesson 079 will formalize as the Interface Segregation Principle — small, focused interfaces, freely combined.

Resolving Naming Conflicts Between Interfaces

Two interfaces can declare a member with the exact same name and signature. C# does not treat this as an error by default — it treats it as one shared obligation, which a single implementation can satisfy for both:

public interface IPrintable { void Render(); }
public interface IExportable { void Render(); }   // same signature, different intent

public sealed class Document : IPrintable, IExportable
{
    // ONE method satisfies BOTH interfaces — because the signatures match exactly
    public void Render() => Console.WriteLine("Rendering document...");
}

This works cleanly when both interfaces genuinely mean the same thing by Render(). The real problem shows up when they don't — when IPrintable.Render() is supposed to produce a physical print job, and IExportable.Render() is supposed to produce a byte array, but both happen to share the name Render. A single implicit method can't honor two different meanings at once. That's exactly the case explicit interface implementation (lesson 080) exists to solve — giving each interface its own separate implementation on the same class, even when the member names collide:

public sealed class Document : IPrintable, IExportable
{
    void IPrintable.Render() => SendToPrinter();          // one meaning
    byte[] IExportable.Render() => SerializeToBytes();    // a different meaning, different return type

    // Note: overload resolution alone can't disambiguate same-name, same-parameter
    // members with DIFFERENT return types either — explicit implementation is
    // the actual mechanism, covered fully in lesson 080.
}
Preview: this lesson focuses on the common, easy case — implementing several interfaces whose members don't actually conflict in meaning. Lesson 080 is the deep dive into explicit implementation for when they do.

Simple Example

public interface IDisposableResource
{
    void Release();
}

public interface IHealthCheckable
{
    bool IsHealthy();
}

public sealed class DatabaseConnection : IDisposableResource, IHealthCheckable
{
    private bool _open = true;

    public void Release()
    {
        _open = false;
        Console.WriteLine("Connection released.");
    }

    public bool IsHealthy() => _open;
}

// Callers depend on exactly the capability they need
void Cleanup(IDisposableResource resource) => resource.Release();
void Monitor(IHealthCheckable checkable) => Console.WriteLine(checkable.IsHealthy());

var db = new DatabaseConnection();
Monitor(db);    // True
Cleanup(db);    // "Connection released."
Monitor(db);    // False — same object, viewed through a different capability

Code → Meaning → Result: DatabaseConnection is one object with two independent capabilities. Cleanup and Monitor each see only the slice of it they actually need — neither function needs to know DatabaseConnection exists as a concrete type at all.

Real-World Example — A Notification Plugin Implementing Several Capabilities

Extending lesson 073's notification system: a Slack channel plugin needs to be a working INotificationChannel, but it also needs to expose configuration validation and startup health checks — capabilities the plugin host system requires from any plugin, not just notification channels.

public interface INotificationChannel
{
    string ChannelName { get; }
    Task SendAsync(string recipient, string message, CancellationToken ct = default);
}

public interface IConfigurable
{
    IReadOnlyList<string> ValidateConfiguration();
}

public interface IStartupHealthCheckable
{
    Task<bool> CheckHealthAsync(CancellationToken ct = default);
}

public sealed class SlackChannel(SlackChannelOptions options, ISlackClient client)
    : INotificationChannel, IConfigurable, IStartupHealthCheckable
{
    public string ChannelName => "Slack";

    public async Task SendAsync(string recipient, string message, CancellationToken ct = default) =>
        await client.PostMessageAsync(options.WebhookUrl, recipient, message, ct);

    public IReadOnlyList<string> ValidateConfiguration()
    {
        var errors = new List<string>();
        if (string.IsNullOrWhiteSpace(options.WebhookUrl))
            errors.Add("Slack webhook URL is required.");
        return errors;
    }

    public async Task<bool> CheckHealthAsync(CancellationToken ct = default) =>
        await client.PingAsync(options.WebhookUrl, ct);
}

// The plugin host only ever asks for the capability it needs at that moment:
foreach (var plugin in loadedPlugins.OfType<IConfigurable>())
{
    var errors = plugin.ValidateConfiguration();
    if (errors.Count > 0)
        throw new InvalidOperationException($"Plugin misconfigured: {string.Join("; ", errors)}");
}

foreach (var channel in loadedPlugins.OfType<INotificationChannel>())
    await channel.SendAsync("ops-team", "Deployment complete.");

Why this design holds up in a real plugin host:

Under the Hood

HOW THE RUNTIME TRACKS MULTIPLE INTERFACES ON ONE TYPE
1. A TYPE'S METADATA LISTS EVERY INTERFACE IT IMPLEMENTS
2. A CAST TO AN INTERFACE IS A TYPE CHECK, NOT A CONVERSION
3. is / OfType<T>() CHECKS AGAINST EACH IMPLEMENTED INTERFACE AT RUNTIME

Common Confusion

"Implementing many interfaces" vs. "a fat interface" — not the same thing

A class implementing five small, focused interfaces (IAuditable, ICacheable, IComparable<T>...) is a healthy, common design. A class implementing one interface with thirty members, most of which it doesn't really need, is the opposite problem — a single "fat" contract forced onto every implementer. Lesson 079 covers exactly this distinction and why the second shape hurts.

Same signature ≠ automatically a conflict

Two interfaces sharing a member with the exact same name, parameters, and return type are not in conflict at all — a single method on the implementing class satisfies both, as shown with Render() above. A real conflict only exists when the interfaces expect genuinely different behavior or return types under the same name — that's the narrower case explicit interface implementation exists for.

Common Mistakes

Mistake 1 — Implementing an interface member with unrelated behavior, just because the name matches

public interface IPrintable { void Render(); }      // means: send to a physical printer
public interface IExportable { void Render(); }      // means: write to disk as a file

public sealed class ReportDocument : IPrintable, IExportable
{
    //  ONE Render() now has to somehow mean BOTH "print this" and "export this" —
    // it can't honestly satisfy both intents with one implementation
    public void Render() => Console.WriteLine("Doing... something?");
}

When two interfaces share a name but mean genuinely different things, use explicit interface implementation (lesson 080) to give each its own separate, correct implementation — never force one method body to serve two different contracts by accident.

Mistake 2 — Implementing an interface a class doesn't genuinely support, just to satisfy a type check

Adding IStartupHealthCheckable to a plugin and having CheckHealthAsync always return true with no real check, purely so the plugin "shows up" somewhere it's expected — this is dishonest about the class's actual capabilities and defeats the purpose of the interface entirely.

Only implement an interface when the class genuinely provides that capability. If a plugin has no meaningful health check, it should not implement IStartupHealthCheckable at all — letting OfType<T>() correctly exclude it, as shown in the real-world example.

Mistake 3 — Piling every possible capability onto one interface instead of splitting them

Defining a single IPlugin interface with fifteen members covering configuration, health checks, notification sending, logging, and more — forcing every plugin to implement all fifteen, even the ones that make no sense for it.

Split by genuine capability, as shown in the real-world example — INotificationChannel, IConfigurable, IStartupHealthCheckable — and let each plugin implement exactly the subset it actually supports. This is the core idea lesson 079 names and formalizes.

When Should I Use It?

Rule of thumb: if you're tempted to describe a class with the word "and" — "it's auditable and cacheable and comparable" — that's usually a sign each of those belongs in its own small interface, not crammed into one.

Mental Model

A class = one identity, one (at most) base class.
Its interfaces = a list of independent capabilities it happens to also provide.

Remember:
· There's no limit on how many interfaces one class can implement.
· Matching signatures across interfaces merge into one implementation automatically — only a genuine meaning conflict needs explicit implementation (lesson 080).
· OfType<T>() and is checks against an interface are how calling code discovers which capabilities an object actually has, without knowing its concrete type.

Key Takeaway


Check Your Understanding

Let's confirm you can reason about composing capabilities through multiple interfaces.

1. How many interfaces can a single C# class implement?

Show answer

Correct: C

Why C is correct: Unlike class inheritance, which is capped at one base class, a class may implement as many interfaces as needed — each one an independent contract the class agrees to fulfill.

Why A is incorrect: This describes single-inheritance rules for base classes, which don't apply to interfaces at all.

Why B is incorrect: There's no such cap — the Order example in this lesson alone implements three.

Why D is incorrect: Interfaces with shared member signatures can be implemented together perfectly well, as the IPrintable/IExportable Render() example demonstrated — sharing a member name is not disqualifying.

Reinforcement: The lack of a limit is precisely what makes interfaces suited to composing many independent capabilities on one class.

2. IPrintable and IExportable both declare a member named Render() with an identical signature (no parameters, void return) and the same intended meaning. What must a class implementing both do?

Show answer

Correct: B

Why B is correct: When two interfaces share an identical signature and the same intended meaning, C# allows a single implicit implementation to satisfy both — as demonstrated with Document's single Render() method in the "How It Works" section.

Why A is incorrect: Explicit interface implementation becomes necessary only when the two members mean genuinely different things — when they truly agree, a single method is simpler and correct.

Why C is incorrect: This is entirely legal, common C# and compiles without issue.

Why D is incorrect: Interface member names are fixed by their declarations — you can't rename them on the implementing side; you'd instead use explicit implementation if a genuine conflict existed.

Reinforcement: A shared signature isn't automatically a conflict — the real question is whether the two interfaces mean the same thing by it.

3. In the plugin host example, why does loadedPlugins.OfType<IStartupHealthCheckable>() work correctly even though some plugins don't implement that interface at all?

Show answer

Correct: B

Why B is correct: OfType<T>() checks each object's actual runtime type against the requested interface and filters the sequence accordingly — a plugin that doesn't implement IStartupHealthCheckable is simply skipped, with no error and no special handling required from the caller.

Why A is incorrect: No exception is thrown; non-matching items are silently excluded from the result, which is exactly the desired, safe behavior here.

Why C is incorrect: There's no such compiler behavior — a class implements only the interfaces it explicitly declares, nothing is auto-generated.

Why D is incorrect: Interface-based capability discovery works regardless of the plugins' base classes, or even if they have none in common at all — that's precisely the point of using interfaces rather than a shared hierarchy for this.

Reinforcement: This dynamic, safe capability discovery is one of the most common real-world uses of multiple interface implementation.

4. A team defines one large IPlugin interface with fifteen members covering every possible plugin capability, and forces all plugins to implement all fifteen, even when most don't apply. What is the better alternative shown in this lesson?

Show answer

Correct: A

Why A is correct: This is exactly Mistake 3 and its fix — splitting a bloated, forced contract into focused, independently implementable interfaces lets each plugin honestly declare only the capabilities it actually has, discoverable safely via OfType<T>().

Why B is incorrect: This is the throwing-default anti-pattern flagged back in lesson 075 — it compiles but fails at runtime, and defeats the entire purpose of a compiler-checked contract.

Why C is incorrect: Converting to an abstract class would force every plugin into the same single-inheritance slot and still bundle unrelated capabilities together — it doesn't solve the underlying "fat contract" problem at all.

Why D is incorrect: String-based type checks throw away compile-time safety entirely and are exactly the fragile, error-prone pattern interfaces and pattern matching are meant to replace.

Reinforcement: This exact scenario — and its full formal treatment — is what lesson 079 (Interface Segregation) is built around.

You now think of class design as composing capabilities — which sets up interface segregation (079) as the natural next question: how small should each of those interfaces actually be?


dotnetmadeeasy.com — Learn C# and .NET, the right way.