Preventing Method Overrides with Sealed Methods in C#

Controlling the Override Chain

It's tempting to leave every overridden method open for further customization. It works—until a derived class breaks your carefully crafted implementation, violates security assumptions, or introduces subtle bugs you never anticipated. Without the sealed keyword, your override becomes just another stepping stone in an unpredictable inheritance chain.

The sealed keyword lets you override a virtual or abstract method and then close it to further overrides. Once you seal a method, derived classes inherit your implementation without the ability to change it. This gives you fine-grained control over inheritance hierarchies—you can participate in polymorphism while preventing downstream modifications to critical behavior.

You'll learn when to seal methods to protect implementations, how sealed methods interact with class hierarchies, the performance implications of sealing, and design patterns where sealed methods enforce correctness. We'll build examples showing how sealed methods prevent common inheritance problems while maintaining the flexibility your design needs.

Understanding Sealed Method Syntax

You can only seal methods that override virtual or abstract members from a base class. The sealed keyword applies to the override declaration, preventing any further derived classes from overriding that method again. Non-virtual methods don't need sealing because they can't be overridden in the first place.

The syntax combines sealed with override. This signals two things: you're providing an override for a base member, and you're closing the override chain at this level. Classes further down the hierarchy will inherit your implementation and cannot replace it.

SealedMethodBasics.cs
public abstract class Shape
{
    // Virtual method - can be overridden
    public virtual double GetArea()
    {
        return 0;
    }

    // Abstract method - must be overridden
    public abstract double GetPerimeter();
}

public class Circle : Shape
{
    public double Radius { get; set; }

    // Override and seal - prevents further overrides
    public sealed override double GetArea()
    {
        return Math.PI * Radius * Radius;
    }

    public override double GetPerimeter()
    {
        return 2 * Math.PI * Radius;
    }
}

// This will NOT compile
/*
public class ColoredCircle : Circle
{
    // ERROR: Cannot override sealed member
    public override double GetArea()
    {
        return base.GetArea();
    }

    // This is allowed - GetPerimeter isn't sealed
    public override double GetPerimeter()
    {
        return base.GetPerimeter();
    }
}
*/

public class ValidColoredCircle : Circle
{
    public string Color { get; set; } = "Red";

    // Can override non-sealed methods
    public override double GetPerimeter()
    {
        Console.WriteLine($"Calculating perimeter for {Color} circle");
        return base.GetPerimeter();
    }

    // Inherits sealed GetArea from Circle - cannot override
}

Circle seals GetArea but leaves GetPerimeter virtual. ValidColoredCircle can override GetPerimeter because Circle didn't seal it, but attempting to override GetArea causes a compiler error. This selective sealing lets you close specific members while keeping others open for customization.

Protecting Critical Implementations

Seal methods when your override implements critical business logic, security checks, or complex algorithms that derived classes shouldn't alter. Once you've perfected an implementation, sealing it prevents accidental or malicious corruption from downstream classes that don't understand the full context.

Security-sensitive operations particularly benefit from sealing. If your override performs authentication, authorization, or data sanitization, you don't want derived classes bypassing these protections. Sealing enforces that your security implementation remains in place throughout the hierarchy.

SecurePayment.cs
public abstract class PaymentProcessor
{
    public abstract bool ValidateTransaction(decimal amount);
    public abstract void ProcessPayment(decimal amount);
}

public class SecurePaymentProcessor : PaymentProcessor
{
    // Seal security validation - too critical to allow overrides
    public sealed override bool ValidateTransaction(decimal amount)
    {
        // Critical security checks
        if (amount <= 0)
        {
            Console.WriteLine("Rejected: Invalid amount");
            return false;
        }

        if (amount > 10000)
        {
            Console.WriteLine("Requires additional verification");
            return false;
        }

        // Check for fraud patterns
        if (IsFraudulent(amount))
        {
            Console.WriteLine("Fraud detected");
            return false;
        }

        return true;
    }

    public override void ProcessPayment(decimal amount)
    {
        if (!ValidateTransaction(amount))
        {
            throw new InvalidOperationException("Transaction validation failed");
        }

        // Process the payment
        Console.WriteLine($"Processing payment: ${amount}");
    }

    private bool IsFraudulent(decimal amount)
    {
        // Fraud detection logic
        return false;
    }
}

public class CustomPaymentProcessor : SecurePaymentProcessor
{
    // Cannot override ValidateTransaction - it's sealed
    // This is good - we can't accidentally bypass security

    // Can override ProcessPayment for custom payment handling
    public override void ProcessPayment(decimal amount)
    {
        Console.WriteLine("Custom pre-processing");
        base.ProcessPayment(amount);
        Console.WriteLine("Custom post-processing");
    }
}

By sealing ValidateTransaction, SecurePaymentProcessor ensures all derived classes use the same security checks. CustomPaymentProcessor can customize payment processing but must go through the sealed validation. This creates a security guarantee that no subclass can weaken.

Sealed Methods in Template Method Pattern

The Template Method pattern uses sealed overrides to fix certain steps of an algorithm while allowing customization of others. The base class defines the algorithm structure, intermediate classes seal critical steps, and leaf classes customize remaining extension points. This creates controlled flexibility.

In this pattern, you typically have a base abstract class with a template method, intermediate classes that implement and seal some steps, and final concrete classes that customize the remaining variable parts. This layered approach lets you enforce progressively stronger constraints as you move down the hierarchy.

TemplateMethodPattern.cs
public abstract class DataImporter
{
    // Template method - defines the algorithm
    public void ImportData(string source)
    {
        Console.WriteLine("=== Starting import ===");
        ValidateSource(source);
        var data = ReadData(source);
        var transformed = TransformData(data);
        SaveData(transformed);
        Console.WriteLine("=== Import complete ===\n");
    }

    protected abstract void ValidateSource(string source);
    protected abstract string ReadData(string source);
    protected abstract string TransformData(string data);
    protected abstract void SaveData(string data);
}

public class SecureDataImporter : DataImporter
{
    // Seal validation - security requirement
    protected sealed override void ValidateSource(string source)
    {
        if (string.IsNullOrEmpty(source))
            throw new ArgumentException("Source cannot be empty");

        if (!source.StartsWith("https://"))
            throw new SecurityException("Only HTTPS sources allowed");

        Console.WriteLine($"Validated source: {source}");
    }

    // Seal data saving - ensures audit trail
    protected sealed override void SaveData(string data)
    {
        Console.WriteLine($"Saving data: {data}");
        LogAuditEntry($"Data saved: {data.Length} characters");
    }

    protected virtual void LogAuditEntry(string message)
    {
        Console.WriteLine($"[AUDIT] {DateTime.Now}: {message}");
    }

    // Leave these virtual for derived classes
    protected override string ReadData(string source)
    {
        return $"Data from {source}";
    }

    protected override string TransformData(string data)
    {
        return data.ToUpper();
    }
}

public class CustomImporter : SecureDataImporter
{
    // Cannot override ValidateSource or SaveData - they're sealed
    // This guarantees security and auditing

    // Can customize reading
    protected override string ReadData(string source)
    {
        Console.WriteLine("Custom read logic");
        return base.ReadData(source);
    }

    // Can customize transformation
    protected override string TransformData(string data)
    {
        Console.WriteLine("Custom transformation");
        return data.ToLower(); // Different from base
    }

    // Can customize audit logging
    protected override void LogAuditEntry(string message)
    {
        Console.WriteLine($"[CUSTOM AUDIT] {message}");
    }
}

SecureDataImporter seals ValidateSource and SaveData to enforce security and auditing requirements. CustomImporter can customize reading, transformation, and logging, but it must use the sealed validation and saving implementations. This creates a hierarchy where critical behaviors are fixed and optional behaviors remain flexible.

Performance Implications

Sealed methods can enable JIT compiler optimizations. When the JIT knows a method won't be overridden further, it can sometimes devirtualize the call—replacing virtual method dispatch with direct calls. This eliminates vtable lookups and can enable inlining, especially when the concrete type is known at compile time.

The performance gain varies significantly. In tight loops where virtual calls add measurable overhead, sealing can help. In typical business logic with infrequent calls, the difference is negligible. Always benchmark your specific scenario—sealing for perceived performance without measurement often optimizes the wrong thing.

PerformanceExample.cs
public abstract class Calculator
{
    public abstract int Add(int a, int b);
}

public class OptimizedCalculator : Calculator
{
    // Sealed - JIT can potentially devirtualize
    public sealed override int Add(int a, int b)
    {
        return a + b;
    }
}

public class RegularCalculator : Calculator
{
    // Not sealed - virtual dispatch required
    public override int Add(int a, int b)
    {
        return a + b;
    }
}

// Usage in hot path
public class NumberProcessor
{
    public long SumWithSealed(OptimizedCalculator calc)
    {
        long sum = 0;
        for (int i = 0; i < 1_000_000; i++)
        {
            // Compiler knows calc type, sealed Add might be inlined
            sum += calc.Add(i, i + 1);
        }
        return sum;
    }

    public long SumWithVirtual(Calculator calc)
    {
        long sum = 0;
        for (int i = 0; i < 1_000_000; i++)
        {
            // Virtual dispatch required - method could be overridden
            sum += calc.Add(i, i + 1);
        }
        return sum;
    }
}

The actual performance difference depends on JIT decisions, CPU architecture, and call patterns. Sealing entire classes often provides more consistent gains than sealing individual methods. Use profiling tools like BenchmarkDotNet to measure whether sealing helps in your specific hot paths.

Try It Yourself

Build an example that demonstrates sealed method behavior and shows how it controls inheritance. You'll see exactly where sealing prevents overrides and where it allows them.

Program.cs
abstract class Vehicle
{
    public abstract void Start();
    public abstract void Stop();
}

class Car : Vehicle
{
    public sealed override void Start()
    {
        Console.WriteLine("Car: Ignition started");
    }

    public override void Stop()
    {
        Console.WriteLine("Car: Engine stopped");
    }
}

class SportsCar : Car
{
    // Cannot override Start - it's sealed in Car

    public override void Stop()
    {
        Console.WriteLine("SportsCar: Applying sport brakes");
        base.Stop();
    }
}

Console.WriteLine("Creating and using vehicles:\n");

Vehicle car = new Car();
car.Start();
car.Stop();

Console.WriteLine();

Vehicle sportsCar = new SportsCar();
sportsCar.Start(); // Uses sealed implementation from Car
sportsCar.Stop();  // Uses override from SportsCar
SealedDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Steps:

  1. Create project: dotnet new console -n SealedDemo
  2. Change directory: cd SealedDemo
  3. Replace Program.cs with the code above
  4. Run: dotnet run

What you'll see:

Creating and using vehicles:

Car: Ignition started
Car: Engine stopped

Car: Ignition started
SportsCar: Applying sport brakes
Car: Engine stopped

Notice that SportsCar uses the Start implementation from Car because Car sealed it. SportsCar can't provide its own Start override. However, Stop remains virtual, so SportsCar successfully customizes it while calling the base implementation.

When Not to Seal Methods

Avoid sealing methods prematurely. If your class is part of a public API or framework, sealing limits extensibility that consumers might legitimately need. Seal only when you have a concrete reason—security requirements, correctness guarantees, or confirmed performance needs. Speculative sealing creates unnecessary constraints.

Don't seal methods in abstract classes meant for extensive customization. Abstract classes typically exist to be extended, and sealing methods contradicts that purpose. If you find yourself sealing most methods in an abstract class, you might need a concrete class instead, or your abstraction might be at the wrong level.

Consider composition over inheritance if you're sealing to prevent misuse. If you seal methods because you don't trust derived classes to implement them correctly, your design might benefit from preferring composition. Sealed methods signal "you can derive but not change this," while composition signals "use this as-is."

Remember that sealing is an optimization and constraint, not a feature requirement. Most applications work fine without sealed methods. Use them when you need explicit control over inheritance behavior, not as a default practice. The simpler your inheritance hierarchies, the less you'll need to seal methods.

Practical Design Guidance

Seal methods that implement security-critical operations like authentication, authorization, input validation, or cryptographic operations. These implementations need to be correct and consistent across your entire hierarchy. Sealing prevents well-meaning but incorrect customizations that weaken security.

Use sealing in frameworks and libraries where you provide extensible base classes but need to enforce certain behaviors. The template method pattern works well here—seal the algorithmic structure while allowing customization of specific steps. This gives consumers flexibility within safe boundaries.

Seal performance-critical methods only after profiling confirms virtual dispatch is a bottleneck. In hot paths processing millions of operations per second, sealed methods might help. In typical CRUD operations or request handling, virtual dispatch overhead is unmeasurable noise. Don't seal for theoretical performance gains.

Document why you sealed a method. Future maintainers should understand whether the sealing is for security, correctness, performance, or API stability. Without documentation, they might remove sealing without understanding the consequences. Comments like "// Sealed: Security requirement - do not allow override" provide valuable context.

Troubleshooting

When should I use sealed on a method?

Use sealed when you've overridden a virtual or abstract method and want to prevent further overrides in derived classes. This protects critical implementations, enforces security constraints, or signals that your implementation is final and optimized.

Can I seal a method that isn't virtual?

No, sealed only applies to methods that override virtual or abstract members. Non-virtual methods can't be overridden anyway, so sealing them would be redundant. The compiler rejects sealed on non-override methods.

Does sealing a method improve performance?

Potentially, yes. Sealed methods allow the JIT to devirtualize calls when the type is known at compile time, eliminating vtable lookups. The impact varies—measure with benchmarks. Sealing entire classes often provides more consistent gains.

What's the difference between sealed methods and sealed classes?

A sealed class prevents all inheritance—no one can derive from it. A sealed method prevents only that specific method from being overridden further, while the class remains inheritable. Use sealed classes for complete inheritance prevention.

How does sealed relate to the Liskov Substitution Principle?

Sealed methods help enforce LSP by preventing derived classes from breaking base class contracts. When your override provides the correct final implementation, sealing it ensures future derived classes can't violate behavior expectations.

Can I unseal a sealed method in a library update?

Yes, removing sealed is a non-breaking change. Consumers weren't overriding the method before, so allowing overrides won't affect existing code. Adding sealed is also safe. Changing virtual to sealed is the risky direction.

Back to Articles