Polymorphism
Polymorphism lets code treat different types uniformly through a common base or interface — runtime dispatch calls the correct override.
Introduction
Polymorphism lets code treat different types uniformly through a common base or interface — runtime dispatch calls the correct override. Payment processors, notification channels, and repository implementations all plug in via polymorphism wired by DI.
Compile-time polymorphism includes method overloading and generics; runtime polymorphism uses virtual/override and interface calls. Interviewers ask to draw vtable dispatch and explain when to use each kind.
This lesson connects inheritance to practical Strategy pattern implementations.
The story
An online store checkout page lets the customer pay with Stripe or PayPal. The checkout service asks for a payment processor by name and receives the correct implementation at runtime — both honor the same IPaymentProcessor contract even though their HTTP calls differ completely under the hood.
Polymorphism plus dependency injection is how production ASP.NET Core apps swap providers without rewriting business logic.
Understanding the topic
Key concepts
- Runtime polymorphism via virtual methods and interfaces.
- Base reference can point to any derived instance.
- Interface reference decouples consumer from implementation.
- Compile-time: overloads and generics.
- Pattern matching complements polymorphism for closed hierarchies.
- DI registers interface → concrete mapping at startup.
classDiagramclass IPaymentProcessor {<<interface>>+Process(amount)}class CreditCardProcessorclass PayPalProcessorIPaymentProcessor <|.. CreditCardProcessorIPaymentProcessor <|.. PayPalProcessor
Step-by-step explanation
- Virtual call resolved at runtime via method table.
- Interface call through stub implementing type.
- Cast or pattern match when specific type needed.
- IEnumerable
polymorphism over List, Array, etc. - Generic constraints where T : IComparable enable algorithms.
- Records/sealed hierarchies use switch instead of virtual for closed sets.
Practical code example
Strategy pattern — polymorphic payment processors resolved by DI:
namespace TechLearningPro.Polymorphism;public interface IPaymentProcessor{string ProviderName { get; }Task<PaymentResult> ChargeAsync(decimal amount, CancellationToken ct);}public sealed record PaymentResult(bool Success, string TransactionId);public sealed class StripeProcessor(HttpClient http) : IPaymentProcessor{public string ProviderName => "Stripe";public async Task<PaymentResult> ChargeAsync(decimal amount, CancellationToken ct){await Task.Delay(10, ct); // simulate APIreturn new PaymentResult(true, Guid.NewGuid().ToString("N"));}}public sealed class CheckoutService(IEnumerable<IPaymentProcessor> processors){public IPaymentProcessor GetProcessor(string name) =>processors.First(p => p.ProviderName.Equals(name, StringComparison.OrdinalIgnoreCase));}
Line-by-line code explanation
interface IPaymentProcessordefines the contract every payment provider must implement.Task<PaymentResult> ChargeAsync(...)is the async method all processors expose.StripeProcessor : IPaymentProcessoris one concrete implementation registered in DI.ProviderName => "Stripe"identifies this processor for lookup by name.await Task.Delay(10, ct)simulates network latency in the sample — real code calls Stripe's API.return new PaymentResult(true, Guid.NewGuid().ToString("N"))returns a successful charge result.CheckoutService(IEnumerable<IPaymentProcessor> processors)receives all registered processors via DI.GetProcessor(string name)selects the right implementation at runtime.processors.First(p => p.ProviderName.Equals(name, StringComparison.OrdinalIgnoreCase))matches case-insensitively.
Key takeaway: IEnumerable
Real-world use
Where you'll use this in production
- Swappable payment, shipping, tax providers.
- Test doubles mocking IRepository in unit tests.
- Cross-platform rendering backends.
- Plugin architectures loading IPlugin implementations.
Best practices
- Program to interfaces not implementations.
- Register all strategies in DI with IEnumerable
. - Keep virtual methods cohesive — small surface.
- Use sealed when no further override intended.
- Prefer composition root configuration over new in business code.
Common mistakes
- switch on type instead of virtual/interface — breaks Open/Closed.
- Cast without is check — InvalidCastException.
- God interface with 20 methods — split interfaces.
- Forgetting to register implementation in DI.
Advanced interview questions
Q1BeginnerRuntime vs compile-time polymorphism?
Q2BeginnerHow does DI enable polymorphism?
Q3IntermediateStrategy pattern in C#?
Q4IntermediateWhen switch on type acceptable?
Q5AdvancedDesign notification system supporting Email, SMS, Push with zero change to caller when adding Slack.
Summary
Polymorphism enables uniform treatment of related types. Virtual methods and interfaces drive runtime dispatch. DI wires interface to implementation in ASP.NET Core. Strategy pattern is polymorphism in enterprise apps. Next: abstraction with abstract classes.