Abstract Classes in C# Explained — When, Why and How to Use Them
- An abstract class is a partially built type — it contributes shared state, shared constructors, and concrete logic while mandating that subclasses complete the unfinished parts before the type is usable. Use it when you have a family of related types, not just a shared capability.
- The Template Method Pattern is the abstract class's most powerful use case — define the algorithm sequence once in a non-virtual template method, make every step abstract, and let subclasses provide domain-specific implementations. The sequence can never drift. The implementations can evolve freely.
- If your abstract class has no fields, no constructor logic, and every member is abstract, you have written a verbose interface that burns the single-inheritance slot — refactor it to an actual interface immediately.
- Abstract classes are partially built types — they share concrete logic while mandating that subclasses complete the unfinished parts before the type is usable
- The abstract keyword does two things at once: prevents instantiation and unlocks abstract member declarations that the compiler will enforce
- Abstract classes can hold fields, constructors, and concrete methods — interfaces cannot (default interface methods in C# 8 notwithstanding, they still cannot hold instance state)
- The Template Method Pattern is the killer use case — define the algorithm order once in the abstract base, let subclasses vary only the individual steps
- If your abstract class has zero fields, no constructor logic, and every member is abstract, you have accidentally written a verbose interface — refactor it
- Abstract classes enforce correctness at compile time — the build itself refuses to compile any half-built subclass, which means the broken code never reaches production
CS0144 compile error — cannot instantiate abstract type
grep -rn 'abstract class' src/ | grep -v '//\|partial'grep -rn ': PaymentProcessor\|: ReportGenerator' src/ --include='*.cs'CS0534 compile error — subclass does not implement abstract member
grep -n 'abstract ' src/PaymentProcessorBase.csgrep -n 'override' src/CreditCardProcessor.csRuntime NullReferenceException in a step that should have been provided by a subclass override
grep -n 'virtual' src/ReportGenerator.csgrep -n 'override' src/ExcelReport.csProduction Incident
FetchData() step left the data source as null, and the next step in the pipeline tried to call methods on it.FetchData(), FormatData(), RenderOutput(), and GenerateHeader() as virtual methods with empty bodies — they compiled fine, ran fine, and returned void while doing absolutely nothing. The developer who created ExcelReport overrode RenderOutput() and FormatData() but forgot FetchData() and GenerateHeader(). The compiler accepted this because virtual methods never require an override — they are optional by definition. At runtime, the empty FetchData() left the internal data source reference as null. The pipeline moved to the next step, which tried to call Count() on that null reference, and NullReferenceException was thrown in a location that looked completely unrelated to the missing override three levels up.FetchData(), FormatData(), RenderOutput(), and GenerateHeader() to abstract methods. Any concrete subclass that forgets to implement any of these now fails to compile — the build breaks with CS0534 naming the exact missing method, before the code ever reaches the CI runner's test phase.
2. Added a sealed template method GenerateReport() that calls the four steps in a fixed, enforced order. Sealed means no subclass can override the pipeline sequence — only the individual steps.
3. Added a CI build step using a Roslyn analyzer that fails the pipeline if any concrete class in the assembly has unresolved abstract member obligations.
4. Added unit tests that instantiate every concrete ReportGenerator subclass with mocked dependencies and call GenerateReport(), asserting that all four steps execute and produce non-null output.Production Debug GuideCommon symptoms when abstract classes are misused or absent in production C# systems. Most of these have a clear compiler error — pay attention to it rather than working around it.
BaseClass.MethodName()→The concrete subclass is missing one or more abstract member implementations. Check the base class for every member marked abstract — methods, properties, and indexers. Either implement all of them in the subclass or mark the subclass abstract itself to defer the obligation further down the chain. The compiler error names the exact missing member, so start there.Every non-trivial C# codebase eventually hits the same wall: you have a family of related types that share some behaviour, but each one also needs to do something uniquely its own. If you duplicate the shared code across each class you are one bug-fix away from a maintenance nightmare where the same logic exists in four places and three of them are out of date. If you stick everything in a plain base class, nothing stops a developer from instantiating it half-finished and shipping broken logic to production. Abstract classes exist precisely to close that gap — they are the language's built-in way of saying 'here is the shared foundation, but you must finish the job before this is usable.'
The problem they solve is a dual one. First, they prevent accidental instantiation of incomplete types — the compiler will not let you call new Shape() if Shape is abstract, full stop. Second, they let you mix concrete, reusable logic with abstract, must-override contracts inside a single type hierarchy. That combination is something a plain interface cannot offer (interfaces carry no implementation state) and a regular base class cannot safely enforce (nothing stops someone from using it directly, and virtual methods with empty bodies fail silently at runtime rather than loudly at build time).
By the end of this article you will know exactly when to reach for an abstract class instead of an interface or a plain base class, how to design one that actually improves your architecture rather than just adding complexity, and the subtle traps that bite even experienced C# developers. You will also walk away with two real-world examples — a payment processing system and a report generation pipeline — that you can adapt immediately in your own work.
What Abstract Classes Actually Are — And What They Are Not
An abstract class is a class marked with the abstract keyword. That one keyword does two things simultaneously: it prevents the class from being instantiated directly with new, and it unlocks the ability to declare abstract members — methods, properties, and indexers that have a signature but no body. Any non-abstract class that inherits from an abstract class is contractually obligated to provide that body, or the code will not compile. The contract is enforced by the C# compiler itself, not by unit tests, not by code review, and not by documentation.
The important mental model: an abstract class is a partially built thing. Think of it as a house with some rooms fully furnished (the concrete members) and others deliberately left as bare concrete walls (the abstract members) for the future owner to finish. The abstract class owns the floor plan and the structural foundation. The subclasses decide what goes on the walls.
What an abstract class is NOT: it is not an interface. An interface is a pure contract — no instance state, no constructor, no concrete implementation prior to C# 8 default interface methods (and even then, default interface methods still cannot hold instance fields). An abstract class can have fields, constructors, concrete methods, and access modifiers on every member. That is a fundamentally different tool. Choosing between them is an architectural decision based on whether shared state and partial implementation matter to your design — not a style preference.
Also worth stating clearly: abstract classes enforce single inheritance. A class can implement any number of interfaces but can only extend one abstract class. That constraint shapes how you model deep type hierarchies and is a key reason why the interface-plus-abstract-base pattern (defining the contract as an interface and providing the optional shared implementation as a separate abstract class) is so common in well-designed C# libraries.
using System; // Abstract class — cannot be instantiated with 'new PaymentProcessor()'. // Models the shared skeleton of every payment processor in the system. abstract class PaymentProcessor { // Concrete field — shared state every processor needs. // Protected so subclasses can read it; private to external callers. protected string processorName; // Constructor — abstract classes CAN and SHOULD have constructors // to initialise shared state. Subclasses invoke this via : base(...). // The constructor itself is never called directly from outside. protected PaymentProcessor(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Processor name cannot be empty.", nameof(name)); processorName = name; } // ABSTRACT method — no body. Every concrete subclass MUST implement this. // The compiler refuses to build any subclass that omits it. // There is no sensible default for 'how to process a payment' at this level. public abstract bool ProcessPayment(decimal amount); // ABSTRACT method — every processor validates differently. // Card processors check Luhn numbers; PayPal checks account status. public abstract bool ValidatePaymentDetails(); // CONCRETE method — shared logic every processor reuses as-is. // Receipt formatting is identical regardless of payment method. public void PrintReceipt(decimal amount) { Console.WriteLine($"[{processorName}] Receipt: ${amount:F2} processed successfully."); } // CONCRETE method — shared audit logging every processor must emit. protected void LogTransaction(decimal amount, bool success) { string status = success ? "SUCCESS" : "FAILED"; Console.WriteLine($"[AUDIT] {processorName} | Amount: ${amount:F2} | Status: {status}"); } } // Concrete class — MUST implement every abstract member or the build breaks. // CS0534 fires at compile time with the exact missing method name. class CreditCardProcessor : PaymentProcessor { private readonly string cardLastFour; public CreditCardProcessor(string cardLastFour) : base("Credit Card") // Calls the abstract class constructor { this.cardLastFour = cardLastFour; } public override bool ValidatePaymentDetails() { // Real implementation: Luhn algorithm check, expiry validation etc. bool isValid = !string.IsNullOrWhiteSpace(cardLastFour) && cardLastFour.Length == 4; Console.WriteLine($"Validating card ending in {cardLastFour}: {(isValid ? "Valid" : "Invalid")}"); return isValid; } public override bool ProcessPayment(decimal amount) { if (!ValidatePaymentDetails()) return false; Console.WriteLine($"Charging ${amount:F2} to card ending {cardLastFour} via Stripe gateway..."); LogTransaction(amount, true); PrintReceipt(amount); return true; } } class PayPalProcessor : PaymentProcessor { private readonly string accountEmail; public PayPalProcessor(string email) : base("PayPal") { accountEmail = email; } public override bool ValidatePaymentDetails() { bool isValid = accountEmail.Contains('@'); Console.WriteLine($"Validating PayPal account {accountEmail}: {(isValid ? "Valid" : "Invalid")}"); return isValid; } public override bool ProcessPayment(decimal amount) { if (!ValidatePaymentDetails()) return false; Console.WriteLine($"Redirecting ${amount:F2} to PayPal checkout for {accountEmail}..."); LogTransaction(amount, true); PrintReceipt(amount); return true; } } class Program { static void Main() { // PaymentProcessor p = new PaymentProcessor(); -- CS0144: cannot instantiate abstract type. // Polymorphism in action — same variable type, completely different behaviour. PaymentProcessor[] processors = { new CreditCardProcessor("4242"), new PayPalProcessor("user@example.com") }; foreach (PaymentProcessor processor in processors) { Console.WriteLine("---"); processor.ProcessPayment(49.99m); Console.WriteLine(); } } }
Validating card ending in 4242: Valid
Charging $49.99 to card ending 4242 via Stripe gateway...
[AUDIT] Credit Card | Amount: $49.99 | Status: SUCCESS
[Credit Card] Receipt: $49.99 processed successfully.
---
Validating PayPal account user@example.com: Valid
Redirecting $49.99 to PayPal checkout for user@example.com...
[AUDIT] PayPal | Amount: $49.99 | Status: SUCCESS
[PayPal] Receipt: $49.99 processed successfully.
- The foundation, plumbing, and wiring are done — those are the concrete methods and fields that every subclass inherits.
- The kitchen layout and bathroom finish are left to the buyer — those are the abstract methods each subclass must implement.
- You cannot move into a half-built house — the compiler prevents instantiation of the abstract class.
- Every buyer (subclass) must complete all unfinished rooms before moving in — or the compiler refuses to let the building certificate through.
- The architect (abstract class author) controls the floor plan and structural rules; the buyer controls the finishes. The architect does not care which tiles you choose, only that you tile the bathroom.
Abstract Properties and the Template Method Pattern — Where Abstract Classes Really Shine
Most developers stop at abstract methods after their first encounter with abstract classes. Abstract properties are equally powerful and solve a different problem: they force subclasses to expose specific data without dictating how that data is stored or computed. One subclass might return a hardcoded string. Another might read from configuration. A third might compute the value dynamically at runtime. The abstract property enforces that the data exists and is accessible — the implementation detail is entirely the subclass's concern.
Beyond individual abstract members, there is a design pattern that abstract classes are practically made for: the Template Method Pattern. The idea is elegantly simple — define the skeleton of an algorithm once in the abstract class (the sequence of steps and how they connect), then let each subclass provide its own implementation of each individual step. The abstract class owns the sequence and can never be overridden out of order. The subclasses own the specifics of each step.
This pattern eliminates an entire category of bugs that appears in teams that share algorithm logic through copy-paste. Someone copies the algorithm, reorders two steps, introduces a subtle race condition or a dependency on uninitialized state, and the bug does not surface for weeks. When the algorithm lives in exactly one place in the abstract class and subclasses only override the individual steps, the sequence is physically impossible to drift.
The report generation pipeline is the archetypal example: every report follows the same pipeline — fetch the data, validate and format it, render it to the output format, then dispatch it to the destination. Each of those four steps varies dramatically by report type, but the pipeline order must never change. Define the pipeline once in the abstract base. Let subclasses own each step.
using System; // Abstract base — owns the ALGORITHM (the fixed pipeline sequence). // Subclasses own the STEPS (the domain-specific implementations). abstract class ReportGenerator { // Abstract property — forces each subclass to declare its own report title. // No backing field here. The subclass decides: hardcoded, config-driven, or computed. public abstract string ReportTitle { get; } // Abstract property — forces each subclass to declare its destination. public abstract string OutputDestination { get; } // TEMPLATE METHOD — this is the algorithm skeleton. // It is concrete and NOT virtual — the pipeline sequence must never change. // Marking it sealed in subclasses prevents accidental override further down. public void GenerateReport() { Console.WriteLine($"=== Starting: {ReportTitle} ==="); Console.WriteLine($" Destination: {OutputDestination}"); FetchData(); // Step 1 — abstract, must be provided by subclass ValidateData(); // Step 2 — abstract, must be provided by subclass FormatData(); // Step 3 — abstract, must be provided by subclass RenderOutput(); // Step 4 — abstract, must be provided by subclass LogCompletion(); // Step 5 — concrete shared logic, runs for every report Console.WriteLine($"=== Complete: {ReportTitle} ===\n"); } // Abstract steps — each subclass provides its own domain-specific implementation. // Protected because they are implementation details, not public API surface. protected abstract void FetchData(); protected abstract void ValidateData(); protected abstract void FormatData(); protected abstract void RenderOutput(); // CONCRETE method — shared infrastructure every report type needs. // Subclasses inherit this without writing a single line. private void LogCompletion() { Console.WriteLine($" [LOG] {ReportTitle} pipeline completed at {DateTime.UtcNow:HH:mm:ss} UTC."); } } class SalesReport : ReportGenerator { // Computed property — title is derived from the report type, not stored. public override string ReportTitle => "Monthly Sales Summary"; public override string OutputDestination => "finance-team@company.com"; protected override void FetchData() => Console.WriteLine(" [Sales] Querying orders table from SQL Server (last 30 days)..."); protected override void ValidateData() => Console.WriteLine(" [Sales] Asserting no null revenue rows and positive unit counts..."); protected override void FormatData() => Console.WriteLine(" [Sales] Aggregating totals by region, product category, and rep..."); protected override void RenderOutput() => Console.WriteLine(" [Sales] Rendering to .xlsx with pivot tables and conditional formatting."); } class SecurityAuditReport : ReportGenerator { public override string ReportTitle => "Weekly Security Audit Log"; public override string OutputDestination => "compliance@company.com (encrypted)"; protected override void FetchData() => Console.WriteLine(" [Audit] Streaming from distributed CloudWatch event log..."); protected override void ValidateData() => Console.WriteLine(" [Audit] Checking for gaps in event sequence and tamper indicators..."); protected override void FormatData() => Console.WriteLine(" [Audit] Filtering to WARN/ERROR/CRITICAL severity only..."); protected override void RenderOutput() => Console.WriteLine(" [Audit] Encrypting PDF and attaching digital signature for compliance."); } class Program { static void Main() { // Different types — same pipeline entry point. // The abstract base guarantees the order. Subclasses guarantee the content. ReportGenerator[] scheduledReports = { new SalesReport(), new SecurityAuditReport() }; foreach (ReportGenerator report in scheduledReports) { report.GenerateReport(); } } }
Destination: finance-team@company.com
[Sales] Querying orders table from SQL Server (last 30 days)...
[Sales] Asserting no null revenue rows and positive unit counts...
[Sales] Aggregating totals by region, product category, and rep...
[Sales] Rendering to .xlsx with pivot tables and conditional formatting.
[LOG] Monthly Sales Summary pipeline completed at 14:22:07 UTC.
=== Complete: Monthly Sales Summary ===
=== Starting: Weekly Security Audit Log ===
Destination: compliance@company.com (encrypted)
[Audit] Streaming from distributed CloudWatch event log...
[Audit] Checking for gaps in event sequence and tamper indicators...
[Audit] Filtering to WARN/ERROR/CRITICAL severity only...
[Audit] Encrypting PDF and attaching digital signature for compliance.
[LOG] Weekly Security Audit Log pipeline completed at 14:22:07 UTC.
=== Complete: Weekly Security Audit Log ===
Abstract Class vs Interface — Choosing the Right Tool for the Right Job
This is the question every C# interview panel will ask, and the honest answer is more nuanced than most tutorials admit. The short version: use an interface when you are defining a capability that unrelated types might share. Use an abstract class when you are defining a family of related types that share both a contract and some real, concrete implementation.
Consider IDisposable. A FileStream, a SqlConnection, and a custom TempFileManager can all implement it. They are completely unrelated types that happen to share one capability — the ability to release unmanaged resources. An abstract class would be wrong here because these types share no common ancestry, no common state, and no common constructor logic. The interface is the right model.
Now consider a payment processing system. A CreditCardProcessor, a PayPalProcessor, and a CryptoProcessor are all payment processors — they share a processor name field, a constructor that validates the name, a receipt printing method, and an audit logging method. They share a family relationship. The abstract class is the right model because it carries that shared state and shared logic. The interface is still useful alongside it — defining IPaymentProcessor for the pure contract that external code depends on, while the abstract class provides the shared infrastructure that all implementations benefit from.
C# 8 default interface methods blurred this line, but they are best used for backward-compatible API evolution (adding a new method to a public interface without breaking existing implementors), not as a replacement for abstract class design. Default interface methods still cannot hold instance fields or constructors. They cannot maintain per-instance state. The moment you need state shared across methods on an object, you need an abstract class.
A practical heuristic: if you are writing the word Base in your class name — ControllerBase, DbContext, HttpMessageHandler — and that class has concrete methods with real working logic, you almost certainly want it to be abstract. The Base suffix is the convention signaling 'this is meant to be extended, not used directly.'
using System; using System.Collections.Generic; // INTERFACE — defines a capability. // Any type can be ITaxable regardless of family or hierarchy. // No shared state, no constructors, pure contract. interface ITaxable { decimal CalculateTax(decimal baseAmount); string TaxJurisdiction { get; } } // ABSTRACT CLASS — defines a family. // All products share a name, price, category, and pricing formula. // Shared logic lives here. Unique logic is abstract. abstract class Product : ITaxable { // Shared fields — every product has these. public string ProductName { get; } public decimal BasePrice { get; } public string Category { get; } // Constructor — initialises shared state for every product. // Subclasses call this via : base(...). The base validates common rules. protected Product(string name, decimal price, string category) { if (price <= 0) throw new ArgumentException("Price must be positive.", nameof(price)); ProductName = name; BasePrice = price; Category = category; } // ABSTRACT — every product type calculates its own category discount differently. // Electronics get percentage discounts; groceries get fixed promotional amounts. public abstract decimal GetDiscount(); // ABSTRACT — ITaxable contract, but the implementation varies by product type. // Some products are VAT-exempt; others attract import duties on top of VAT. public abstract decimal CalculateTax(decimal baseAmount); // ABSTRACT property from ITaxable — subclass declares its tax jurisdiction. public abstract string TaxJurisdiction { get; } // CONCRETE — the final price formula is identical for every product. // We own this method in the abstract class so it can never drift. public decimal GetFinalPrice() { decimal afterDiscount = BasePrice - GetDiscount(); decimal tax = CalculateTax(afterDiscount); return afterDiscount + tax; } public override string ToString() => $"{ProductName,-25} Base: ${BasePrice,7:F2} | " + $"Discount: -${GetDiscount(),5:F2} | " + $"Tax ({TaxJurisdiction}): ${CalculateTax(BasePrice - GetDiscount()),6:F2} | " + $"Final: ${GetFinalPrice(),7:F2}"; } class ElectronicProduct : Product { public ElectronicProduct(string name, decimal price) : base(name, price, "Electronics") { } // 10% loyalty discount on all electronics. public override decimal GetDiscount() => BasePrice * 0.10m; // 20% VAT — standard UK rate. public override decimal CalculateTax(decimal baseAmount) => baseAmount * 0.20m; public override string TaxJurisdiction => "UK VAT 20%"; } class GroceryProduct : Product { public GroceryProduct(string name, decimal price) : base(name, price, "Grocery") { } // Fixed £2 promotional discount. public override decimal GetDiscount() => 2.00m; // Most groceries are VAT-zero rated in the UK. public override decimal CalculateTax(decimal baseAmount) => 0.00m; public override string TaxJurisdiction => "UK VAT 0% (Zero-rated)"; } class Program { static void Main() { var inventory = new List<Product> { new ElectronicProduct("Wireless Headphones", 120.00m), new ElectronicProduct("USB-C Docking Station", 89.99m), new GroceryProduct("Organic Coffee Beans", 18.50m), new GroceryProduct("Sourdough Bread", 4.75m) }; Console.WriteLine("=== Product Pricing Summary ==="); foreach (Product item in inventory) Console.WriteLine(item); // The abstract class satisfies the ITaxable interface — so this works. // Any code that expects ITaxable can receive any Product subtype. Console.WriteLine("\n=== ITaxable Interface Polymorphism ==="); ITaxable taxableItem = new ElectronicProduct("Smart Watch", 249.99m); Console.WriteLine($"Smart Watch tax ({taxableItem.TaxJurisdiction}): " + $"${taxableItem.CalculateTax(249.99m):F2}"); } }
Wireless Headphones Base: $ 120.00 | Discount: -$ 12.00 | Tax (UK VAT 20%): $ 21.60 | Final: $ 129.60
USB-C Docking Station Base: $ 89.99 | Discount: -$ 9.00 | Tax (UK VAT 20%): $ 16.20 | Final: $ 97.19
Organic Coffee Beans Base: $ 18.50 | Discount: -$ 2.00 | Tax (UK VAT 0% (Zero-rated)): $ 0.00 | Final: $ 16.50
Sourdough Bread Base: $ 4.75 | Discount: -$ 2.00 | Tax (UK VAT 0% (Zero-rated)): $ 0.00 | Final: $ 2.75
=== ITaxable Interface Polymorphism ===
Smart Watch tax (UK VAT 20%): $50.00
| Feature / Concern | Abstract Class | Interface | Regular Base Class |
|---|---|---|---|
| Can be instantiated directly? | No — compiler error CS0144 if you try | No — interfaces are never instantiated directly | Yes — nothing prevents new BaseClass() unless you add a protected constructor |
| Can hold instance fields? | Yes — shared state across all subclasses | No — interfaces cannot have instance fields in any C# version | Yes — same as abstract class |
| Can have a constructor? | Yes — called by subclasses via : base(...) to initialise shared state | No — interfaces have no constructors | Yes — but nothing prevents direct instantiation unless you use protected |
| Can have concrete methods? | Yes — freely mixed with abstract members | Yes in C# 8+ via default interface methods, but no instance state access | Yes — all methods are concrete by default |
| Contract enforcement? | At compile time — CS0534 if any abstract member is missing in a concrete subclass | At compile time — CS0535 if any interface member is missing | None — missing overrides of virtual methods are silently ignored |
| Supports multiple implementation? | No — a class can extend only one abstract class | Yes — a class can implement any number of interfaces | No — single inheritance applies to all C# classes |
| Best suited for | A family of related types sharing state and a partial implementation | A capability that unrelated types might share | Sharing concrete implementation without enforcing a contract |
| Typical naming convention | AppendBase suffix: ReportGeneratorBase, PaymentProcessorBase | Prefix with I: IDisposable, IPaymentProcessor, ITaxable | No specific convention — but often signals missing abstract keyword |
🎯 Key Takeaways
- An abstract class is a partially built type — it contributes shared state, shared constructors, and concrete logic while mandating that subclasses complete the unfinished parts before the type is usable. Use it when you have a family of related types, not just a shared capability.
- The Template Method Pattern is the abstract class's most powerful use case — define the algorithm sequence once in a non-virtual template method, make every step abstract, and let subclasses provide domain-specific implementations. The sequence can never drift. The implementations can evolve freely.
- If your abstract class has no fields, no constructor logic, and every member is abstract, you have written a verbose interface that burns the single-inheritance slot — refactor it to an actual interface immediately.
- Abstract classes enforce correctness at compile time. The moment you mark a class abstract, the compiler becomes your quality gate — CS0534 catches missing implementations before any test runs, before any deployment, and certainly before any nightly batch job fails at 3 AM.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QCan an abstract class have a constructor, and if so, what is it used for since you cannot instantiate the abstract class directly?JuniorReveal
- QWhat is the difference between an abstract method and a virtual method in C#, and when would you choose one over the other?Mid-levelReveal
- QIf a class inherits from an abstract class but does not implement all the abstract methods, what happens — and is there any scenario where that is valid?Mid-levelReveal
- QYou are designing a plugin system where third-party developers write report exporters. How would you use abstract classes versus interfaces to enforce the plugin contract, and what trade-offs do you face with single inheritance?SeniorReveal
Frequently Asked Questions
Can an abstract class have concrete non-abstract methods in C#?
Yes — and this is one of the primary reasons to choose an abstract class over an interface. You can freely mix abstract methods (no body, must be overridden by every concrete subclass) with fully implemented concrete methods that all subclasses inherit automatically. Shared logic like audit logging, receipt printing, file path generation, or standard validation lives in the concrete methods. The unique domain-specific behaviour goes in the abstract methods. This combination — shared infrastructure plus enforced contract — is precisely the value that plain interfaces and plain base classes individually fail to provide.
Can an abstract class implement an interface in C#?
Absolutely, and this is a common and powerful pattern. An abstract class can implement an interface and either provide concrete implementations for some or all of the interface members, or re-declare them as abstract and pass the obligation to its concrete subclasses. This allows you to define the capability contract as an interface (for flexibility across unrelated types) while providing a convenient abstract base class that handles the shared implementation details. Consumers who need the shared infrastructure extend the abstract class. Consumers who need only the contract implement the interface directly.
What happens if I mark a class abstract but add no abstract members?
It is valid C# and compiles without error. The class simply cannot be instantiated directly — CS0144 fires if you try — but it carries no unimplemented obligations. Every member is concrete and fully inherited. This is occasionally useful when you want to prevent direct instantiation of a base class for design safety reasons (for example, to signal 'this class is always extended, never used directly') while still providing all the default logic. It is a reasonable design choice when used deliberately. If you find yourself doing it frequently, consider whether a protected constructor on a non-abstract class would communicate the same intent more clearly.
Can I use sealed with abstract class members in C#?
Yes, but only on overrides in derived classes, not on the original abstract declaration. You cannot mark an abstract method sealed — the two keywords contradict each other. However, in a derived class that overrides the abstract method, you can mark that override sealed to prevent further overrides deeper in the hierarchy. This is most useful for the template method pattern: if an intermediate abstract class provides a concrete implementation of a step from the grandparent abstract class, you can mark that override sealed to signal that this step is now fixed and no further specialisation is intended.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.