Last Updated: May 17, 2026
Polymorphism is the third pillar of object-oriented programming, sitting next to inheritance and encapsulation. The word comes from Greek roots meaning "many forms," and in C# that translates to one method name, one operator, or one base reference being able to take on different concrete behaviors depending on context. This chapter introduces the idea at a conceptual level and shows why it matters in real e-commerce code.
The textbook one-liner is "one interface, many forms." A more practical phrasing: the same call site can produce different runtime behavior depending on what it is called on, or what arguments it is called with. The "interface" in this definition is the call you write (item.Process(), cart + product, Send(message)), and the "many forms" are the different method bodies that can actually run.
The key insight is that polymorphism isn't a single C# feature. It's a property of several language features working together. When you write cart.AddItem(product), the compiler and runtime decide which version of AddItem to invoke based on rules that depend on which kind of polymorphism is in play. There are two broad categories, and each has its own mechanism:
Both are useful. Both show up constantly in real C# code. And they are easy to confuse because the surface syntax looks similar.
A small contrast makes the difference concrete:
The two calls look almost identical, but the compiler picks different methods based on the number and types of arguments. That decision happens before the program ever runs. Now compare runtime polymorphism:
Every loop iteration calls item.Send(). The reference type is always Notification. But the runtime walks down to the actual object on the heap and picks the most-derived Send for each one. That decision happens at runtime, not at compile time.
These two examples cover the punchline of the entire chapter. Everything else in this section unpacks how each mechanism works in detail.
Polymorphism earns its place in OOP because it lets code handle many concrete types through a single shared abstraction. Without it, you end up with long chains of type checks that grow every time someone adds a new variant. With it, the dispatch is the language's job, and new variants slot in without touching the calling code.
Imagine an order-processing service that has to handle different kinds of payment. A version without polymorphism might look like this:
The code works, but every new payment type means another branch in the if-else chain. Worse, the same chain probably shows up in other places (refund logic, reporting, fraud checks), and they will drift apart over time. The shape of this code is brittle.
A polymorphic version replaces the type-tag check with a virtual method call:
The OrderProcessor.Process method no longer cares what kind of payment it received. It hands the work to the payment itself and lets dispatch sort it out. Adding a LoyaltyPointsPayment later means writing one new class. The processor stays untouched.
This points at a design principle that polymorphism enables: the Open-Closed Principle. A class should be open for extension (you can add new behavior by adding new types) but closed for modification (existing code doesn't have to change to accept those new types). The if-else version violates this principle. The polymorphic version honors it.
Polymorphism also pays off in collections of mixed types. A Cart can hold any subclass of CartItem and treat them uniformly:
One loop, three completely different total-calculation rules. The loop doesn't know or care which kind of item it has at any moment, and adding a fourth kind doesn't change the loop at all. This is the everyday payoff of runtime polymorphism in production C# code.
C# offers polymorphism through two distinct mechanisms. Each chapter in this section explores one of them in detail. A short tour helps orient you:
The diagram splits polymorphism into the two categories and lists the C# features that fall under each. Compile-time polymorphism in C# comes from overloading: writing multiple methods with the same name but different parameter lists, or defining operators (+, -, ==) for your own types. The compiler picks the right one at compile time based on the argument types.
Runtime polymorphism comes from inheritance plus virtual dispatch. A base class declares a method as virtual (or abstract), derived classes override it, and at runtime the call goes to the derived version regardless of which reference type was used to make the call. Covariant return types are a small refinement that lets an overriding method return a more specific type than the base.
Here is how each lesson in this section maps to the diagram:
virtual and override cooperate, how the CLR finds the right method at runtime, and the everyday patterns that rely on this dispatch.new (method hiding) breaks polymorphism in a way that surprises people, and which one to reach for in different situations.You will not need any of those details to understand the rest of this lesson. The point of the tour is that every flavor of polymorphism in C# fits somewhere in that tree.
The mechanism behind the two categories has a name. Compile-time polymorphism uses static dispatch: the compiler resolves which method body to call by looking at the source, baking the decision into the compiled IL. The IL instruction is call, and it names the exact method to invoke.
Runtime polymorphism uses dynamic dispatch: the IL records that the call must look up the right method based on the object's actual type at runtime. The IL instruction is callvirt, and the runtime consults a hidden table on the object (called a method table or vtable) to find the right slot.
Here is a small mental model:
The diagram shows the same call site (payment.Process()) routed through one of two paths depending on whether the method is virtual. If it is, the call becomes dynamic and the runtime does the picking. If it isn't, the call is static and the compiler does the picking. Method overloading falls into the static-dispatch path: the compiler picks among the overloads based on the argument types it sees, then emits the chosen method directly.
You don't need to know the IL instructions to use polymorphism. The mental shortcut that matters is: virtual methods are decided when the program runs; everything else is decided when it compiles. That single rule predicts the behavior of every polymorphism feature in C#.
Cost: Virtual calls have a small extra step compared to direct calls (the runtime looks up the method through the type's method table). In normal code the cost is irrelevant. In tight inner loops processing millions of items per second, it can show up in profiling, and most teams measure before optimizing rather than guessing.
The cost callout is here for completeness. Don't optimize away virtual dispatch on instinct. Virtual calls are how most idiomatic C# polymorphism works, and the JIT is good at inlining virtual calls in the common case where the type can be predicted.
The earlier notification example showed runtime polymorphism in the smallest possible form. Stretching it a little further makes the design payoff clearer.
Imagine a notification outbox that processes messages from a single base type. The outbox doesn't know what kinds of notification exist; it only knows they all support Send.
Every line in Outbox.Flush calls notification.Send(). The variable's static type is Notification. Three different method bodies run, one per object, because Send is virtual (specifically, abstract, which is virtual without a default implementation). That's runtime polymorphism doing the dispatching.
Three things are worth noting about this design:
Outbox class never mentions EmailNotification, SmsNotification, or PushNotification. It works against the abstraction.WebhookNotification, requires no change to Outbox. Define the new class, override Send, and queue an instance.switch on the type, no reflection, no manual dispatch table. The language does the routing.This pattern shows up in just about every well-factored C# application: dispatchers, handlers, strategies, plugins, validators. The mechanics are the same as the toy example. The base class declares the contract, the derived classes fill in the details, and the calling code works through the base reference.
A few beliefs about polymorphism come up often enough to be worth correcting up front.
"Polymorphism is the same as inheritance." Inheritance is one way to enable polymorphism, but they aren't the same. Inheritance is a structural relationship between classes. Polymorphism is a behavior at call sites. You can have inheritance without polymorphism (a derived class that doesn't override anything), and you can have polymorphism without inheritance (method overloading is polymorphism without any inheritance). Interfaces also enable polymorphism, and an interface is not inheritance in the same sense as class Derived : Base.
"Polymorphism is just overloading." Overloading is one of two kinds of polymorphism, and the more limited one. The compiler picks an overload at compile time based on argument types. That's useful, but it doesn't give you the open-closed payoff that overriding does. Conflating the two leads people to write if (item is Card) ... else if (item is Coupon) ... instead of using virtual dispatch.
"Polymorphism is a feature." It's a property of several features working together. C# doesn't have a polymorphism keyword. It has virtual, override, abstract, new, method overloading, operator overloading, interfaces, and generics, and those together produce polymorphic behavior. Asking "does C# support polymorphism?" is a little like asking "does C# support readable code?" The answer is "yes, through many mechanisms, used well."
"Polymorphism always involves dynamic dispatch." Only runtime polymorphism does. Method overloading is resolved at compile time and never touches the method table at runtime. The compiler picks the overload and emits a direct call. It is still a form of polymorphism: same name, different behavior depending on context.
"Polymorphism makes code slower." Virtual dispatch has a small cost, but the cost is rarely the bottleneck in real applications. In most cases, the design clarity polymorphism buys you is worth far more than the cycles it spends on a method table lookup. The performance discussion is real, but it belongs in profiler output, not in design discussions about whether to use virtual.
A short reference table captures these in one place:
| Belief | Reality |
|---|---|
| Polymorphism = inheritance | Inheritance is structural; polymorphism is behavioral. Related but not the same. |
| Polymorphism = overloading | Overloading is one kind; runtime polymorphism with virtual/override is the more powerful kind. |
| Polymorphism is a single C# feature | It's a property emerging from several features (virtual, override, abstract, overloading, interfaces, generics). |
| Polymorphism always uses dynamic dispatch | Compile-time polymorphism uses static dispatch. The compiler picks the overload. |
| Polymorphism hurts performance | Virtual call cost is small. Most code is bottlenecked elsewhere, and the JIT can inline many virtual calls. |
Knowing the categories is one thing; knowing when to reach for each is the next step. The next four chapters cover the mechanics in detail. As a rough orientation:
| You want to... | Mechanism | Chapter |
|---|---|---|
| Provide multiple ways to call the same method with different argument shapes | Method overloading | 2 |
Define how +, ==, or other operators work on your own type | Operator overloading | 2 |
| Let derived classes replace a base method's behavior, with dispatch by object type | virtual + override | 3 |
| Force derived classes to provide an implementation, with no default in the base | abstract method | 3 |
Understand exactly how new differs from override (and why it's usually the wrong choice) | new keyword hiding | 4 |
| Have an overriding method return a more specific type than its base | Covariant return types | 5 |
Each row corresponds to a different problem and a different language feature. Once you can name the problem you have, the mechanism falls out naturally. Most everyday code reaches for overloading and virtual/override. Operator overloading shows up around custom value-like types (money, vectors, time spans). Abstract methods show up when the base class is a template waiting for subclasses to complete it. new and covariant returns are edge cases.
A small worked example shows how overloading and overriding can coexist in the same class without any conflict. Both are polymorphism, but they answer different questions.
The first call uses runtime polymorphism: Send(string) is abstract on the base, the runtime dispatches to EmailNotification.Send. The second call uses compile-time polymorphism to pick Send(string, string), which is a non-virtual base method that internally calls back into the virtual Send(string) and so re-enters runtime dispatch. The two mechanisms compose cleanly.
A short summary of the model that will carry through the rest of the section:
new are related but distinct. They look similar in syntax and trip people up regularly. The remaining lessons in this section tease them apart.Keep the static-vs-dynamic dispatch picture in mind as you move into the _Compile-Time Polymorphism_ lesson. Overloading is the first concrete mechanism you'll see, and that lesson spends most of its time on the compiler's rules for picking an overload when more than one candidate matches. Those rules only make sense once you've internalized that the decision happens before the program runs.
virtual/override, abstract methods, interfaces).if/else chains on a type-tag field are usually a sign that runtime polymorphism would be a better fit.