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

The precision tool for when two interfaces genuinely disagree — and for keeping a class's public face honest about what it's really for.

Lesson 078 left you with a promise it deferred: two interfaces that share a member name but mean genuinely different things can't both be satisfied by one ordinary method body. Here's that exact situation, forced to a head:

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() has to somehow mean both "print this" and "export this."
    // It physically cannot honor two different intents under one name and one body.
    public void Render() => Console.WriteLine("Doing... something?");
}

The compiler doesn't complain about this — the code above compiles just fine and silently does the wrong thing for whichever caller didn't get the behavior it expected. That's the actual danger: a naming collision like this doesn't fail loudly, it fails quietly, in whichever direction the one shared method body happened to be written.

In this lesson, you'll learn explicit interface implementation — the void IInterface.Method() syntax — why it exists to resolve exactly this kind of collision, how it lets a class hide an implementation detail from its own public surface, and the specific situations where it's the right tool versus where it's overkill.

What Is Explicit Interface Implementation?

Every interface member you've implemented so far has been implicit: you write a normal public method or property, and the compiler quietly connects it to the matching interface member. Explicit interface implementation is a different syntax — you prefix the member with the interface's name, drop the access modifier entirely, and the resulting member is only reachable through a reference typed as that interface:

public sealed class ReportDocument : IPrintable, IExportable
{
    void IPrintable.Render() => SendToPrinter();      // explicit — no "public", prefixed with IPrintable.
    void IExportable.Render() => WriteExportFile();    // explicit — a completely separate implementation

    private void SendToPrinter() => Console.WriteLine("Sending to physical printer...");
    private void WriteExportFile() => Console.WriteLine("Writing export file to disk...");
}

var doc = new ReportDocument();
// doc.Render();                 //  Does not compile — Render() is not on ReportDocument's public surface at all
((IPrintable)doc).Render();      //  "Sending to physical printer..."
((IExportable)doc).Render();     //  "Writing export file to disk..."

IPrintable printable = doc;
printable.Render();              //  Same call, no cast needed — the variable's static type is already IPrintable

Two things changed compared to lesson 078's shared-signature case. First, each interface now gets its own method body — the naming collision is resolved because the compiler knows exactly which Render() you mean from the reference type doing the calling. Second, neither Render() shows up when you call a member directly on a ReportDocument-typed variable — explicit members are invisible on the class's own public surface, reachable only through the interface.

Why Does It Exist?

Explicit interface implementation solves two distinct problems, and it's worth keeping them separate in your head because they lead to different design decisions:

Both problems come from the same root cause: an interface member is a promise to a specific caller, not necessarily a promise the implementing class wants to advertise to everyone. Explicit implementation is the mechanism that lets those two audiences — "code that holds an IPrintable" versus "code that holds a ReportDocument" — see different things.

Big Picture — Two Faces, One Object

THE SAME OBJECT, SEEN THROUGH DIFFERENT REFERENCE TYPES
ReportDocument doc = new ReportDocument();
Viewed as IPrintable → doc.Render() means "print"
Viewed as IExportable → doc.Render() means "export"
Viewed as ReportDocument → no Render() exists at all

This is not two objects and not two copies of any state — it's one object in memory, with the compiler restricting which door you're allowed to knock on depending on the static type of the reference you're holding.

The Syntax, Step by Step

WRITING AN EXPLICIT INTERFACE MEMBER
STEP 1 — Drop the access modifier
void IPrintable.Render() { ... }
// No "public" — explicit members can never carry an access modifier
STEP 2 — Prefix the member name with the interface it belongs to
void IPrintable.Render() { ... }
//   ^^^^^^^^^^^ this qualifier is what makes it "explicit" and disambiguates it
STEP 3 — The compiler wires it ONLY to that interface's contract
STEP 4 — Callers must go through the interface to reach it
((IPrintable)doc).Render();     // via cast
IPrintable p = doc; p.Render(); // via a variable already typed as the interface
A member can be implicit for one interface and explicit for another, on the same class. If only IExportable's meaning of Render() was ever going to be called directly on the class, you could make IExportable.Render() implicit (a normal public method) and IPrintable.Render() explicit — the choice is per-interface, not all-or-nothing.

Simple Example

public interface IBox { int Volume { get; } }
public interface IContainer { int Volume { get; } }   // same shape, different intended meaning

public sealed class ShippingCrate : IBox, IContainer
{
    private readonly int _lengthCm;
    private readonly int _widthCm;
    private readonly int _heightCm;
    private readonly int _maxPayloadCm3;

    public ShippingCrate(int lengthCm, int widthCm, int heightCm, int maxPayloadCm3)
    {
        _lengthCm = lengthCm;
        _widthCm = widthCm;
        _heightCm = heightCm;
        _maxPayloadCm3 = maxPayloadCm3;
    }

    // IBox.Volume = the crate's own physical size
    int IBox.Volume => _lengthCm * _widthCm * _heightCm;

    // IContainer.Volume = how much cargo it can actually hold (less than physical size, due to padding)
    int IContainer.Volume => _maxPayloadCm3;
}

var crate = new ShippingCrate(lengthCm: 50, widthCm: 40, heightCm: 30, maxPayloadCm3: 52_000);
Console.WriteLine(((IBox)crate).Volume);         // 60000 — physical dimensions
Console.WriteLine(((IContainer)crate).Volume);   // 52000 — usable payload capacity

Code → Meaning → Result: Both interfaces genuinely mean "volume," but they mean different kinds of volume for the same physical object. Explicit implementation lets ShippingCrate report an honest, different number for each meaning, instead of picking one and quietly lying to whichever caller wanted the other.

Real-World Example — A Payment Gateway Adapter with a Hidden Legacy Contract

A payment processing service exposes a clean, modern IPaymentGateway to the rest of the application. But the concrete Stripe adapter also has to satisfy a legacy ILegacyChargeable interface that an old batch-reconciliation job still depends on — a contract the team wants working, but never wants a new developer to stumble onto while exploring the class in an IDE.

public interface IPaymentGateway
{
    Task<PaymentResult> ChargeAsync(decimal amount, string currency, string customerToken, CancellationToken ct = default);
    Task<PaymentResult> RefundAsync(string transactionId, decimal amount, CancellationToken ct = default);
}

// A legacy interface the nightly reconciliation batch job still requires —
// its shape predates async/await and doesn't match the modern gateway's style at all
public interface ILegacyChargeable
{
    int Charge(string accountRef, int amountCents);   // synchronous, cents-based, no currency
}

public sealed class StripePaymentGateway(IStripeClient client) : IPaymentGateway, ILegacyChargeable
{
    // The gateway's real, everyday public API — this is what 99% of the application uses
    public async Task<PaymentResult> ChargeAsync(
        decimal amount, string currency, string customerToken, CancellationToken ct = default)
    {
        var response = await client.CreateChargeAsync(amount, currency, customerToken, ct);
        return response.Succeeded
            ? PaymentResult.Success(response.TransactionId)
            : PaymentResult.Failed(response.DeclineReason);
    }

    public async Task<PaymentResult> RefundAsync(
        string transactionId, decimal amount, CancellationToken ct = default)
    {
        var response = await client.CreateRefundAsync(transactionId, amount, ct);
        return response.Succeeded
            ? PaymentResult.Success(response.TransactionId)
            : PaymentResult.Failed(response.DeclineReason);
    }

    // Explicit — satisfies the legacy batch job's contract without ever showing up
    // on IntelliSense for "stripeGateway.", and without polluting the modern API's shape.
    // It's a thin, blocking adapter over the real async method — isolated here, not spread everywhere.
    int ILegacyChargeable.Charge(string accountRef, int amountCents)
    {
        var result = ChargeAsync(amountCents / 100m, "USD", accountRef)
            .GetAwaiter().GetResult();   // acceptable ONLY inside this narrow legacy-adapter seam
        return result.Success ? 1 : 0;
    }
}

// Everyday application code sees only the clean, modern surface:
IPaymentGateway gateway = new StripePaymentGateway(stripeClient);
await gateway.ChargeAsync(49.99m, "USD", customerToken, ct);
// gateway.Charge(...)   doesn't exist here — ILegacyChargeable is invisible through IPaymentGateway

// Only the old batch job, which still genuinely needs it, reaches the legacy member:
ILegacyChargeable legacy = new StripePaymentGateway(stripeClient);
legacy.Charge(accountRef: "acct_123", amountCents: 4999);

Why this design holds up in a real codebase:

Analogy — The Badge That Opens Different Doors

Think of the object as a building, and each interface reference as a different badge someone is holding. The IPrintable badge opens the "send to printer" door. The IExportable badge opens a completely different "write to disk" door — even though, confusingly, both doors happen to be labeled "Render." Someone standing at the building's main public entrance, holding no special badge at all, doesn't see either door — because Render was never part of the building's main lobby directory. The badge you're holding determines which door you can even find, let alone open — and that's exactly what the static type of your reference variable does with explicit interface members.

Under the Hood

HOW THE CLR RESOLVES AN EXPLICIT INTERFACE CALL
1. THIS IS A COMPILE-TIME RESOLUTION, DRIVEN BY STATIC TYPE
2. THE TYPE'S INTERFACE MAP (LESSON 077) STILL HAS AN ENTRY FOR EACH
3. THE MEMBER IS COMPILED WITH A QUALIFIED, NON-PUBLIC-SURFACE NAME
4. THERE IS NO EXTRA RUNTIME COST

Common Confusion

Explicit implementation is not a privacy modifier

It's tempting to think of explicit implementation as "a way to make a method private-ish." It isn't — the member is still fully public, callable by any code that has an interface-typed reference to the object. Nothing stops a caller from writing ((IPrintable)doc).Render(). What explicit implementation controls is discoverability through the concrete class, not actual access — it is a design signal ("this isn't part of this class's everyday identity"), not a security boundary.

"Same signature" doesn't automatically mean you need this — see lesson 078 first

Lesson 078 already showed that two interfaces sharing an identical signature and meaning merge cleanly into one implicit method — no explicit implementation required. Reach for explicit implementation only when the meanings genuinely diverge, as with IBox.Volume versus IContainer.Volume above. Using it reflexively for every shared name, even when one implementation would honestly satisfy both, adds indirection with no real benefit.

Common Mistakes

Mistake 1 — Reaching for explicit implementation as a general-purpose "hide this method" tool

Making a method explicit purely because a developer doesn't want it cluttering IntelliSense, when there's no genuine naming collision and no real "this belongs to a different audience" reason — the member just becomes harder to find for no real design benefit, and callers have to know a specific cast or interface-typed variable exists to use it at all.

Reserve explicit implementation for genuine collisions (Problem 1) or a deliberate decision that a member is not part of the class's everyday identity (Problem 2, like the legacy adapter above) — not as a substitute for good naming or for splitting an interface that's grown unfocused (lesson 079).

Mistake 2 — Forgetting that an explicit member requires the interface type to call it, and being surprised at compile time

var doc = new ReportDocument();
doc.Render();   //  CS1061 — 'ReportDocument' does not contain a definition for 'Render'
// This is not a bug — it's exactly what explicit implementation is designed to do.

If you find yourself constantly casting or declaring an interface-typed local variable just to reach a member, that's a signal worth pausing on — either the member genuinely belongs on the interface-typed API you're already using elsewhere, or you reached for explicit implementation when implicit would have served the class better.

Mistake 3 — Using explicit implementation to "fix" a design that should have used interface segregation instead

A class implements one bloated interface and uses explicit implementation to hide the members it doesn't want cluttering its public surface — treating the symptom (an inconvenient member showing up) instead of the cause (the interface was too broad for this implementer in the first place).

If the real problem is that an interface is trying to do too much for one implementer, the fix from lesson 079 — splitting it into focused interfaces — is usually the better move. Explicit implementation is for genuine name collisions and deliberate audience-hiding, not a patch for an interface that needed to be segregated.

When Should I Use It?

Good fit

Overkill

Rule of thumb: reach for explicit implementation only when you can name, specifically, either the colliding interface or the narrower audience you're hiding the member from. If you can't articulate either reason, an ordinary public implicit method is almost always the better, more discoverable choice.

Mental Model

Implicit implementation = the member is part of the class's own public identity — anyone holding the class sees it.
Explicit implementation = the member exists only for whoever's holding the right interface — the class itself doesn't advertise it.

Remember:
· No access modifier, always prefixed with the interface name — void IInterface.Method().
· Reachable only through an interface-typed reference — a cast, or a variable already declared as that interface.
· Solves genuine name collisions between interfaces, and lets a class keep an implementation detail off its own everyday public surface — nothing more, nothing less.

Key Takeaway


Check Your Understanding

You've seen explicit implementation solve a genuine collision and quietly hide a legacy detail. Let's confirm you know exactly when — and when not — to reach for it.

1. Given void IPrintable.Render() { ... } on a class ReportDocument, which of the following correctly calls it?

Show answer

Correct: B

Why B is correct: An explicit interface member is reachable only through a reference whose static type is the owning interface — casting to IPrintable (or holding an already-IPrintable-typed variable) is exactly how you reach it.

Why A is incorrect: This is precisely what explicit implementation prevents — Render() is not part of ReportDocument's own public surface, so this fails to compile.

Why C is incorrect: Interface implementations are instance members, not static ones — this isn't valid syntax for calling an implemented interface member either way.

Why D is incorrect: Explicit implementation is not a privacy mechanism — the member is fully callable by any code holding the correct interface-typed reference, as shown in B.

Reinforcement: Explicit members require you to go through the interface, not through the concrete class — that's their entire mechanism.

2. IBox and IContainer both declare a Volume property with the identical signature, but ShippingCrate needs them to return two genuinely different numbers. Why can't lesson 078's approach (one shared implicit member) work here?

Show answer

Correct: B

Why B is correct: This is the exact reasoning behind the whole lesson — when two interfaces genuinely disagree on what a shared name should return, a single implicit implementation is forced to pick one answer, silently shortchanging whichever caller wanted the other meaning. Explicit implementation gives each interface its own, independently correct body.

Why A is incorrect: C# allows this fine, as lesson 078 demonstrated with IPrintable/IExportable's matching Render() — the issue only arises when the meanings genuinely diverge, not merely when the names match.

Why C is incorrect: Properties can absolutely be implemented explicitly — int IBox.Volume => ... is valid, exactly as shown in the Simple Example.

Why D is incorrect: Merging the interfaces would just recreate the same naming collision inside one interface — it doesn't solve anything, and loses the distinct meanings entirely.

Reinforcement: A genuine meaning conflict — not merely a shared name — is what makes explicit implementation necessary.

3. In the StripePaymentGateway example, why is ILegacyChargeable.Charge implemented explicitly instead of as a normal public method?

Show answer

Correct: B

Why B is correct: This is Problem 2 from the "Why Does It Exist?" section — hiding an implementation detail from the class's own public surface. Making Charge explicit keeps developers who type stripeGateway. from ever seeing the old, blocking, cents-based method that exists purely for one legacy batch job.

Why A is incorrect: There's no such restriction — synchronous methods can be implemented implicitly just as easily; the choice here is deliberate, not forced by the method's signature.

Why C is incorrect: A class can implement any number of interfaces implicitly, as lesson 078 covered — StripePaymentGateway implements IPaymentGateway implicitly right alongside its one explicit member.

Why D is incorrect: Explicit implementation has no performance effect — the "Under the Hood" section confirmed resolution happens at compile time with no extra runtime cost either way.

Reinforcement: Explicit implementation is a discoverability and API-shape decision, made deliberately — not a technical requirement or a performance choice.

4. A developer explicitly implements a method purely because they think it looks cleaner in IntelliSense, even though only one interface is involved and there's no naming collision or legacy-contract reason at all. What's the concern with this?

Show answer

Correct: C

Why C is correct: This is exactly Mistake 1 — using explicit implementation as a general "hide this method" habit, without a genuine collision or a deliberate audience-hiding reason, trades away discoverability for no real benefit. Callers now have to know a specific cast or interface-typed reference exists just to use ordinary functionality.

Why A is incorrect: Explicit implementation is reserved for genuine collisions or deliberate hiding (Problems 1 and 2) — using it reflexively, without either reason, is the mistake this question describes.

Why B is incorrect: As covered in "Under the Hood," explicit implementation has no performance benefit at all — it's purely a compile-time visibility decision.

Why D is incorrect: Explicit implementation is legal with just one interface — the compiler doesn't require a collision to exist; it simply doesn't stop you from making a poor design choice.

Reinforcement: Reach for explicit implementation only when you can name a specific colliding interface or a specific audience you're deliberately hiding a member from.

5. Why does explicit interface implementation NOT act as a security or access-control boundary?

Show answer

Correct: A

Why A is correct: As the "Common Confusion" section explained, an explicit member is still fully public — any code can cast to the interface type (as trivially as ((IPrintable)doc).Render()) and call it. Explicit implementation changes what's discoverable through the concrete class, not what's actually reachable.

Why B is incorrect: The compiler fully enforces the syntax and dispatch rules of explicit implementation — that enforcement is real, it just isn't an access-control mechanism.

Why C is incorrect: Explicit members do not appear on the concrete class's member list at all, even in a de-emphasized form — they're absent, not merely hidden visually.

Why D is incorrect: Explicit implementation works with interfaces of any accessibility — public, internal, or otherwise; accessibility of the interface itself is an unrelated concern.

Reinforcement: For genuine access control, reach for private, internal, or a properly scoped interface — explicit implementation is about API shape and discoverability, not security.

You now have the full toolbox — composition, multiple interfaces, segregation, and explicit implementation — for shaping honest, focused contracts. One question remains: what principle ties all of it together into a design philosophy? That's the capstone, lesson 081.


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