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.
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.
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.
IAuditable, ICacheable, IComparable<Order> — simultaneously, independentlyIAuditable parameter — has zero visibility into caching or comparisonICacheable parameter — has zero visibility into auditing or comparisonEach 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.
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.
}
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.
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:
SlackChannel doesn't need to inherit from some generic PluginBase just to gain configuration validation — it implements IConfigurable directly, alongside its other capabilities.IStartupHealthCheckable — loadedPlugins.OfType<IStartupHealthCheckable>() naturally filters it out, with no special-casing required anywhere.foreach loops) never needs to know the concrete plugin types at all — it discovers capabilities purely through interface checks, using C#'s OfType<T>() as a natural capability filter.Order reference to IAuditable doesn't create a new object or copy anything — it's the same object in memory, viewed through a narrower, compile-time-checked lensloadedPlugins.OfType<IConfigurable>() checks, per object, whether that object's runtime type implements IConfigurable — this is why a plugin that doesn't implement it is cleanly filtered out rather than causing an errorA 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.
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.
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.
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.
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.
OfType<T>() and is checks against an interface are how calling code discovers which capabilities an object actually has, without knowing its concrete type.
is checks and OfType<T>() let calling code discover an object's capabilities dynamically, without knowing its concrete type — the backbone of plugin-style architectures.Let's confirm you can reason about composing capabilities through multiple interfaces.
1. How many interfaces can a single C# class implement?
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?
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?
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?
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.