C# Method Overriding

Method overriding lets a derived C# class replace behavior it inherits from a base class. It matters because code can work with a general type, such as Employee or Shape, while each specific derived type runs its own version of a method. Overriding is one of the main tools behind runtime polymorphism in object-oriented C#.

Overview: How Method Overriding Works

A method can be overridden only when the base class intentionally allows it. The base member must be marked virtual, abstract, or already be an override. The derived class then writes a method with the same signature and marks it with override. The signature includes the method name, parameter types, parameter order, and compatible return type rules. If the signature does not match, you are not overriding; you are creating another method.

Overriding is different from method overloading. Overloading means several methods have the same name but different parameter lists, and the compiler chooses one at compile time. Overriding means a derived class supplies a new implementation for the same inherited method, and the CLR chooses the implementation at runtime based on the actual object type.

For example, a variable declared as InvoiceDocument can hold a PaidInvoice object. If InvoiceDocument.PrintStatus is virtual and PaidInvoice overrides it, a call through the base variable runs the PaidInvoice version. The variable type controls what members the compiler lets you call, but the runtime object type controls which override runs.

Under the hood, the compiler records virtual methods and overrides in type metadata. At runtime, objects carry type information for their real concrete class. When a virtual call is made, the CLR uses that runtime type information to dispatch to the most specific override. This is why overriding continues to work even when objects are stored in arrays, lists, or parameters typed as the base class.

Not every method should be virtual. A virtual method is an extension point, so it becomes part of the base class’s design contract. Derived classes may depend on when it is called, what arguments it receives, and what the base implementation promises. For behavior that must never change, leave the method non-virtual. For behavior that every derived class must provide, use an abstract method in an abstract base class.

Syntax

class BaseClass
{
    public virtual string Describe()
    {
        return "base description";
    }
}

class DerivedClass : BaseClass
{
    public override string Describe()
    {
        return "derived description";
    }
}
Part Meaning
virtual Allows a base class method, property, indexer, or event to be overridden.
override Replaces an inherited virtual, abstract, or overridden member.
abstract Requires derived concrete classes to provide an override; the base member has no body.
base.Method() Calls the inherited implementation from inside the override.
sealed override Overrides a method and prevents further derived classes from overriding it again.
new Hides an inherited member. It is not polymorphic overriding.

Examples

Example 1: Basic Method Overriding

using System;

class Notification
{
    public virtual string FormatMessage(string message)
    {
        return "Notification: " + message;
    }
}

class EmailNotification : Notification
{
    public override string FormatMessage(string message)
    {
        return "Email: " + message;
    }
}

class Program
{
    static void Main()
    {
        Notification notification = new EmailNotification();
        Console.WriteLine(notification.FormatMessage("Your report is ready"));
    }
}

Output:

Email: Your report is ready

The variable is declared as Notification, but the object is an EmailNotification. Because FormatMessage is virtual and overridden, the CLR runs the derived implementation. This is the key benefit of overriding: callers can use the base type and still get specific behavior.

Example 2: Calling the Base Method From an Override

using System;

class Report
{
    public virtual string BuildTitle()
    {
        return "Monthly Report";
    }
}

class SalesReport : Report
{
    public override string BuildTitle()
    {
        return base.BuildTitle() + " - Sales";
    }
}

class Program
{
    static void Main()
    {
        Report report = new SalesReport();
        Console.WriteLine(report.BuildTitle());
    }
}

Output:

Monthly Report - Sales

An override does not have to ignore the base implementation. Here SalesReport reuses the shared title text by calling base.BuildTitle(), then adds its own suffix. This pattern is useful when the base method performs validation, logging, setup, or common formatting that the derived class should preserve.

Example 3: Realistic Pricing Rules With Polymorphism

using System;
using System.Collections.Generic;
using System.Globalization;

class Subscription
{
    public Subscription(string customer, decimal monthlyPrice)
    {
        Customer = customer;
        MonthlyPrice = monthlyPrice;
    }

    public string Customer { get; }
    protected decimal MonthlyPrice { get; }

    public virtual decimal CalculateMonthlyCharge()
    {
        return MonthlyPrice;
    }
}

class StudentSubscription : Subscription
{
    public StudentSubscription(string customer, decimal monthlyPrice)
        : base(customer, monthlyPrice)
    {
    }

    public override decimal CalculateMonthlyCharge()
    {
        return MonthlyPrice * 0.5m;
    }
}

class BusinessSubscription : Subscription
{
    private readonly int seats;

    public BusinessSubscription(string customer, decimal monthlyPrice, int seats)
        : base(customer, monthlyPrice)
    {
        this.seats = seats;
    }

    public override decimal CalculateMonthlyCharge()
    {
        return MonthlyPrice * seats;
    }
}

class Program
{
    static void Main()
    {
        List<Subscription> subscriptions = new List<Subscription>
        {
            new Subscription("Ana", 20m),
            new StudentSubscription("Ben", 20m),
            new BusinessSubscription("Contoso", 15m, 3)
        };

        foreach (Subscription subscription in subscriptions)
        {
            string charge = subscription.CalculateMonthlyCharge().ToString("0.00", CultureInfo.InvariantCulture);
            Console.WriteLine($"{subscription.Customer}: ${charge}");
        }
    }
}

Output:

Ana: $20.00
Ben: $10.00
Contoso: $45.00

The list is typed as List<Subscription>, so the loop treats every item uniformly. The base subscription charges the regular price, the student subscription applies a discount, and the business subscription multiplies by seat count. Adding a new subscription type later does not require changing the loop.

Example 4: Sealing an Override

using System;

class AuditRecord
{
    public virtual string Category()
    {
        return "General";
    }
}

class SecurityAuditRecord : AuditRecord
{
    public sealed override string Category()
    {
        return "Security";
    }
}

class LoginAuditRecord : SecurityAuditRecord
{
    public string Detail()
    {
        return Category() + " login";
    }
}

class Program
{
    static void Main()
    {
        LoginAuditRecord record = new LoginAuditRecord();
        Console.WriteLine(record.Detail());
    }
}

Output:

Security login

SecurityAuditRecord overrides Category and seals that override. LoginAuditRecord can still inherit from SecurityAuditRecord, but it cannot override Category again. Use this when a derived class needs to lock down a behavior while still allowing further inheritance for other members.

How It Works Step by Step

  1. The compiler verifies that an override method matches an inherited virtual, abstract, or override member.
  2. The compiler emits metadata connecting the derived method to the base virtual slot.
  3. When an object is created, the CLR stores runtime type information for the concrete type, such as StudentSubscription.
  4. When code calls a virtual method through a base reference, the call is dispatched through the runtime type, not merely the variable type.
  5. The most specific override runs. If no derived override exists, the nearest inherited implementation runs.
  6. If an override calls base.Method(), control temporarily jumps to the inherited implementation, then returns to the override.
  7. If an override is marked sealed, later derived classes cannot replace that member again.

Virtual dispatch is extremely useful, but it is not magic conversion. The object remains one object of its concrete type. A base variable simply views it through the base contract. That is why the compiler only allows calls to members declared on the base type, while runtime dispatch can still choose derived implementations for virtual members that are part of that contract.

Common Mistakes

Mistake 1: Changing the Signature by Accident

using System;

class Exporter
{
    public virtual void Export(string fileName)
    {
        Console.WriteLine("Base export: " + fileName);
    }
}

class CsvExporter : Exporter
{
    public void Export(object fileName)
    {
        Console.WriteLine("CSV export: " + fileName);
    }
}

class Program
{
    static void Main()
    {
        Exporter exporter = new CsvExporter();
        exporter.Export("sales.csv");
    }
}

Output:

Base export: sales.csv

CsvExporter created a new overload with object, not an override of Export(string). A base-typed variable still calls the base method. The corrected version uses the exact same parameter type and the override keyword.

using System;

class Exporter
{
    public virtual void Export(string fileName)
    {
        Console.WriteLine("Base export: " + fileName);
    }
}

class CsvExporter : Exporter
{
    public override void Export(string fileName)
    {
        Console.WriteLine("CSV export: " + fileName);
    }
}

class Program
{
    static void Main()
    {
        Exporter exporter = new CsvExporter();
        exporter.Export("sales.csv");
    }
}

Output:

CSV export: sales.csv

Mistake 2: Using new When You Need override

using System;

class Ticket
{
    public virtual string Priority()
    {
        return "Normal";
    }
}

class IncidentTicket : Ticket
{
    public new string Priority()
    {
        return "High";
    }
}

class Program
{
    static void Main()
    {
        Ticket ticket = new IncidentTicket();
        Console.WriteLine(ticket.Priority());
    }
}

Output:

Normal

The new keyword hides the inherited method. It silences the compiler warning, but it does not create polymorphic behavior. If callers use the base type, the base implementation is selected. Use override when derived objects must behave differently through base references.

Mistake 3: Trying to Override a Non-Virtual Method

class Receipt
{
    public string Print()
    {
        return "Receipt";
    }
}

class GiftReceipt : Receipt
{
    public override string Print()
    {
        return "Gift receipt";
    }
}

This code does not compile because Receipt.Print is not marked virtual, abstract, or override. The base class author must opt in to overriding. The fix is to mark the base method virtual if the behavior is meant to be replaceable.

Best Practices

  • Mark a method virtual only when derived classes are expected to customize it.
  • Use override instead of new for polymorphic replacement.
  • Keep override signatures exactly aligned with the base member.
  • Call base.Method() when the base implementation enforces required shared behavior.
  • Document the rules for virtual methods in real projects: valid inputs, side effects, and whether derived overrides should call base.
  • Avoid calling overridable methods from constructors, because derived state may not be initialized yet.
  • Use abstract when every concrete derived class must provide its own implementation.
  • Use sealed override when a behavior should be customized once and then locked down.
  • Prefer interfaces when unrelated classes share a capability but do not share state or implementation.

Practice Exercises

  1. Create a base Vehicle class with a virtual Describe method. Override it in Car and Bicycle, then store both in a Vehicle[].
  2. Create an abstract DiscountRule class with an abstract Apply method. Implement percentage and fixed-amount discounts.
  3. Write a base Logger with a virtual Format method. In a derived logger, call base.Format() and append extra context.

Summary

  • Method overriding replaces inherited virtual behavior in a derived class.
  • The base member must be virtual, abstract, or already overridden.
  • The derived method must use override and match the inherited signature.
  • Virtual calls are dispatched by the CLR using the object’s runtime type.
  • base.Method() lets an override reuse inherited behavior.
  • new hides a method; it does not override it polymorphically.
  • Use virtual methods deliberately because they become extension points in your class design.