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.
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.
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.
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.
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.
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
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Steps:
- Create project:
dotnet new console -n SealedDemo
- Change directory:
cd SealedDemo
- Replace Program.cs with the code above
- 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.