C# Programming Tutorial 0/45 lessons ~6 min read Lesson 18

    Polymorphism

    Polymorphism lets code treat different types uniformly through a common base or interface — runtime dispatch calls the correct override.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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.
    text
    classDiagram
    class IPaymentProcessor {
    <<interface>>
    +Process(amount)
    }
    class CreditCardProcessor
    class PayPalProcessor
    IPaymentProcessor <|.. CreditCardProcessor
    IPaymentProcessor <|.. PayPalProcessor

    Step-by-step explanation

    1. Virtual call resolved at runtime via method table.
    2. Interface call through stub implementing type.
    3. Cast or pattern match when specific type needed.
    4. IEnumerable polymorphism over List, Array, etc.
    5. Generic constraints where T : IComparable enable algorithms.
    6. Records/sealed hierarchies use switch instead of virtual for closed sets.

    Practical code example

    Strategy pattern — polymorphic payment processors resolved by DI:

    csharp
    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 API
    return 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 IPaymentProcessor defines the contract every payment provider must implement.
    • Task<PaymentResult> ChargeAsync(...) is the async method all processors expose.
    • StripeProcessor : IPaymentProcessor is 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 injected with all registered implementations — classic polymorphism + DI interview pattern.

    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?
    Runtime: virtual/interface dispatch; compile-time: overloads, generics.
    Q2BeginnerHow does DI enable polymorphism?
    Container resolves interface to concrete registered type at runtime.
    Q3IntermediateStrategy pattern in C#?
    Interface + multiple implementations selected at runtime.
    Q4IntermediateWhen switch on type acceptable?
    Closed sealed hierarchies, external protocol parsing — not extensible plugin sets.
    Q5AdvancedDesign notification system supporting Email, SMS, Push with zero change to caller when adding Slack.
    INotifier interface; register each in DI; caller depends on IEnumerable or factory; Open/Closed satisfied.

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.