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.
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.
Explicit interface implementation solves two distinct problems, and it's worth keeping them separate in your head because they lead to different design decisions:
IPrintable/IExportable case above.IComparable<T> or IDisposable — but that member isn't really part of how the class wants to present itself day-to-day. Explicit implementation lets the member exist (satisfying the contract) without cluttering the class's ordinary public API.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.
Render() at allIPrintable reference can reach this specific behaviorIExportable reference can reach this different behaviorRender is not something a ReportDocument "just does"; it's something you get by asking through a specific lensThis 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.
void IPrintable.Render() { ... }
// No "public" — explicit members can never carry an access modifier
void IPrintable.Render() { ... }
// ^^^^^^^^^^^ this qualifier is what makes it "explicit" and disambiguates it
IPrintable's obligation and nothing else — it is not a member of ReportDocument's own public API((IPrintable)doc).Render(); // via cast IPrintable p = doc; p.Render(); // via a variable already typed as the interface
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.
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.
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:
stripeGateway. in their editor sees ChargeAsync and RefundAsync — the modern, correct API — and never sees the synchronous, cents-based Charge method that exists purely for one old batch job's sake.StripePaymentGateway still genuinely satisfies ILegacyChargeable — the compiler enforces it, the batch job keeps working, and nobody had to duplicate the charging logic or maintain two divergent code paths.GetAwaiter().GetResult() call — normally a red flag in async code — is contained to exactly one narrow, clearly-labeled adapter seam instead of being a pattern someone might copy elsewhere in the codebase after seeing it "just sitting there" as a public method.ILegacyChargeable and its one explicit implementation is a clean, contained removal — it never got tangled into StripePaymentGateway's primary API surface.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.
IPrintable, IExportable, or the concrete class — and picks the matching method slot at compile time; it never guesses based on what the object "really is" at runtimeReportDocument's interface map has one slot for IPrintable.Render and a separate slot for IExportable.Render — two independent entries, even though they share a name, so there's no ambiguity for the runtime to resolve eitherReportDocument's own member list, only by looking through the specific interface's contractIt'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.
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.
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).
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.
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.
IBox.Volume vs. IContainer.Volume)obj.Member to be unambiguous and clean, reserving the interface-qualified form for the narrower audience that genuinely needs itprivate, internal, or a genuinely narrower interface is for, not thisvoid IInterface.Method().void IInterface.Method() — no access modifier, prefixed with the owning interface's name.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?
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?
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?
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?
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?
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.