Back to Blog
C#

C# Method Overriding: Syntax, Behavior, and Pitfalls

c# method overriding: Learn how C# method overriding works: virtual and override keywords, runtime dispatch, base calls, and common pitfalls.

C#PolymorphismVirtual MethodsInheritanceOverride
Diagram showing a base class method being overridden by a derived class in C#

c# method overriding requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

In C#, method overriding lets a derived class replace a base class method's implementation while preserving the same signature. The feature is built on two keywords: virtual in the base class and override in the derived class. Without both, the compiler treats the method as new and hides the base implementation instead of overriding it.

What Method Overriding Requires in C#

For a method to be overridden, the base method must be declared with virtual, abstract, or override (when the chain continues). The derived method must use override and match the base method's signature: same name, parameter types, and return type (covariant returns are not supported in C# as of the latest stable versions). The accessibility of the overriding method must be the same as the base method; you cannot change public to protected when overriding.

public class BaseRepository { public virtual void Save(string entity) { Console.WriteLine("Base save"); } } public class SqlRepository : BaseRepository { public override void Save(string entity) { Console.WriteLine("SQL save"); } }

The virtual keyword marks the method as eligible for override. The override keyword in the derived class tells the compiler that this method intentionally replaces the base implementation. If you omit override and define a method with the same signature, the compiler will warn you that you are hiding the base member and you should use new if that is the intention.

How the Runtime Selects the Override

Method overriding relies on virtual dispatch. When you call a virtual method through a base class reference, the runtime determines the actual type of the object and invokes the most derived override. This is the foundation of polymorphism in C#.

BaseRepository repo = new SqlRepository(); repo.Save("order"); // Output: SQL save

Even though repo is typed as BaseRepository, the call resolves to SqlRepository.Save because the object is an instance of SqlRepository. This behavior is consistent across inheritance chains: if SqlRepository itself is inherited and its override is overridden again, the most derived implementation wins.

The dispatch decision is made at runtime, not compile time. This gives you the ability to write code that depends on an abstraction while allowing derived classes to supply behavior. However, it also means that the base class implementation is never called unless the derived override explicitly calls it.

Calling the Base Implementation

Inside an override, you can invoke the base method using the base keyword. This is common when the derived class extends the base behavior rather than replacing it entirely.

public class AuditingRepository : BaseRepository { public override void Save(string entity) { Console.WriteLine("Audit log before save"); base.Save(entity); Console.WriteLine("Audit log after save"); } }

Calling base.Save executes the implementation defined in the immediate base class. If the base method itself is an override, the call follows the chain up one level. This pattern is useful for adding cross-cutting concerns like logging, validation, or timing without duplicating the base logic.

Keep in mind that base is only valid inside an instance method, property, or indexer. You cannot use it in a static method. Also, if the base method is abstract, there is no implementation to call; the derived class must provide the full body.

Overriding vs. Hiding with new

A common mistake is to define a method in a derived class with the same signature without using override. The compiler treats this as method hiding, and the behavior differs significantly from overriding.

public class Base { public virtual void Display() => Console.WriteLine("Base"); } public class Derived : Base { public new void Display() => Console.WriteLine("Derived"); }

When you call Display on a Derived instance through a Base reference, the base implementation runs because the method was hidden, not overridden:

Base b = new Derived(); b.Display(); // Base Derived d = new Derived(); d.Display(); // Derived

This is rarely what you want. Hiding breaks polymorphism and leads to confusing behavior. Use new only when you intentionally want to shadow the base member and you are certain that calls through base references should not be affected. In most cases, if you want polymorphic behavior, override is the correct choice.

Sealed Overrides and Preventing Further Overriding

You can stop an override from being overridden again by applying the sealed modifier to the override. This is useful when you want to lock down behavior in a specific derived class and prevent subclasses from altering it.

public class SpecialRepository : BaseRepository { public sealed override void Save(string entity) { Console.WriteLine("Special save"); } } public class MoreSpecialRepository : SpecialRepository { // Compile error: cannot override sealed method // public override void Save(string entity) { } }

Sealing an override is a design decision. It communicates that the implementation is final and should not be changed further down the hierarchy. It also gives the compiler more freedom to optimize calls in some cases, though the practical performance impact is usually negligible.

Abstract Methods and the Override Contract

When a base class declares a method as abstract, it has no body and must be overridden in any concrete derived class. This is a stronger contract than virtual because it forces the derived class to provide an implementation.

public abstract class Validator { public abstract bool Validate(string input); } public class LengthValidator : Validator { public override bool Validate(string input) => input.Length > 3; }

Abstract methods are implicitly virtual. You cannot use sealed on an abstract method because there is nothing to seal. The override in the derived class can be sealed if you want to prevent further overrides.

The key difference from virtual is that a virtual method provides a default implementation that derived classes may choose to override, while an abstract method leaves no default and forces the override. Choose abstract when the base class cannot provide a meaningful default, and virtual when a sensible default exists.

Common Pitfalls and Maintainability Concerns

Method overriding is a powerful tool, but it introduces coupling between base and derived classes. Changing a virtual method's signature in the base class breaks all overrides in derived classes. Even adding a parameter with a default value can cause ambiguity because the compiler may not match the override correctly.

Another issue is calling virtual methods from constructors. When a base class constructor calls a virtual method, the derived class's override runs before the derived constructor has executed. This can lead to unexpected behavior if the override depends on fields initialized in the derived constructor.

public class Base { public Base() => Initialize(); public virtual void Initialize() { } } public class Derived : Base { private string _name = "default"; public Derived() : base() { _name = "after base"; } public override void Initialize() => Console.WriteLine(_name); }

When new Derived() is created, the base constructor runs first, which calls Initialize. At that point, _name is still "default" because the derived field initializer runs before the derived constructor body but after the base constructor call. The output is "default", not "after base". Avoid calling virtual methods from constructors unless you fully understand the initialization order.

From a maintainability perspective, prefer shallow inheritance hierarchies and limit the number of virtual methods. Each virtual method is an extension point that can be overridden, and each override is a place where behavior can diverge. If you find yourself overriding many methods, consider whether composition or interfaces would be a better fit.

c# method overriding: Practical Usage and Code Examples | RYUSLOG DEV