C# Abstract Classes: Share the Common Work, Require the Missing Pieces
An abstract class can provide the work every subtype shares while requiring each concrete type to finish a missing step. Build a small report system and follow every call from creation to the final line.
What you will understand
- Explain abstract and concrete classes in plain language.
- Read an abstract method declaration with no body.
- Distinguish required implementation from an optional virtual default.
- Share validated instance state and construction in an abstract base.
- Trace a normal method calling a protected abstract hook.
- Complete inherited abstract members with override.
- Recognize abstract properties and intentionally incomplete intermediate classes.
- Test shared rules, specialized results and instance independence.
Think of an abstract class as a shared plan with an unfinished step
A class can know some of the work without knowing all of it. Every practice report needs a learner name and the same heading. But a session report describes minutes, while an exercise report describes a completed count. There is no single honest detail line for every possible report.
An abstract class lets us write the shared work and explicitly leave one operation for a concrete subtype to finish. Abstract means the type is not intended to be created directly. Concrete means a class is complete enough to create an instance with new. These words describe implementation completeness, not whether the code is complicated.
Our PracticeReport stores Learner, validates the name and implements Render. Its protected BuildDetails operation has no body. SessionReport supplies that body. Read the first program in this order: create the concrete report, store its reference in a PracticeReport variable, call Render, then follow the call into the detail override.
The result combines shared and specialized work: Learner: Mina comes from the base, while Practice: 25 min comes from the subtype. One object provides both parts. You are not creating a separate invisible PracticeReport object alongside the SessionReport.
Program.cs · Create the complete report through the shared type
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Learner: Mina | Practice: 25 min
PracticeReport is an abstract base type, while SessionReport is a concrete class that can be created. The variable uses the shared type. Render provides the common heading and calls BuildDetails; runtime dispatch chooses the SessionReport override for the missing detail step.
Read each keyword and the semicolon deliberately
abstract class PracticeReport marks the type as an abstract base. protected abstract string BuildDetails(); declares a required operation: protected chooses its audience, string describes its result, and the final semicolon means there is no implementation body here.
An abstract method in a class is already a virtual operation. Do not add virtual to its abstract declaration. The concrete subtype writes protected override string BuildDetails() and supplies braces containing the actual statements. The same signature connects the declaration to that required implementation.
Unlike an empty body, an absent body cannot run. A body returning an empty string would be real behavior, even if it were useless. The abstract declaration instead says that this layer does not provide the behavior. C# refuses both direct creation of the incomplete type and an abstract method declaration that also has a body.
Run the intentional failures separately and read their diagnostics. They are teaching examples, not problems to fix by removing every abstract keyword. Decide whether the model needs a complete default behavior or requires every concrete kind to define its own answer.
Program.cs · The incomplete base cannot be created directly
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: an instance of the abstract PracticeReport type cannot be created.The base has no body for BuildDetails, so there is no complete generic report to instantiate. Create a concrete subtype rather than removing abstract just to make new compile. The type remains useful for variables, parameters and arrays.
Program.cs · An abstract method declaration has no implementation body
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: an abstract member cannot declare a body.End an abstract declaration with a semicolon. If a useful default body exists and overriding should be optional, a virtual method may fit better. abstract requires an implementation; virtual supplies one that may be replaced.
A concrete subtype must finish every remaining abstract member
Deriving from PracticeReport does not by itself make SessionReport complete. A non-abstract subtype must implement any inherited abstract members that still lack a concrete implementation. A matching constructor initializes state but does not substitute for the missing detail method.
The missing-implementation example fails at the class definition. The compiler can reject the incomplete class even when Main would not call BuildDetails directly. This is stronger than discovering a missing implementation during a particular run: the declaration does not fulfill its inherited contract.
An intermediate subtype can stay abstract while adding shared state or helper code. TimedReport stores validated minutes and leaves BuildDetails unfinished. SessionReport then completes that operation. Use such a layer only when it serves actual shared responsibilities; a small design usually needs fewer levels, not a hierarchy for every noun.
Program.cs · A concrete class must complete every inherited abstract member
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: SessionReport does not implement the inherited abstract BuildDetails member.A non-abstract derived class must provide the remaining implementation. Simply declaring the constructor is not enough. You could leave an intermediate class abstract, but a class intended for new must become complete.
Program.cs · An intermediate class can remain abstract
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Learner: Mina | Practice: 25 min
TimedReport adds shared time state but does not implement BuildDetails, so it stays abstract. SessionReport completes the hook and can be instantiated. This demonstrates deferred implementation, not a recommendation to add an extra hierarchy level to every report.
Write the common workflow once and let a small hook vary
Render has a normal body. It builds the heading and invokes BuildDetails to obtain the variable part. Although Render itself is non-virtual in this model, its call to the abstract operation still dispatches to the concrete override. A non-virtual wrapper does not disable dispatch inside its body.
This pattern is sometimes called a template method: the base owns the overall steps and the subtype supplies one step. You do not need to memorize the pattern name. First ask what is identical for every report, what must vary, and who should be allowed to call each operation.
BuildDetails is protected because it is an implementation hook, not the public report interface. Main uses Render rather than assembling headings and detail fragments itself. A loop over PracticeReport references therefore works for both concrete reports without checking which subtype each element contains.
Keep the hook small and its result meaningful. Returning a placeholder string technically completes the method but does not fulfill the reporting requirement. The guided repair later makes this distinction visible: compile-time completeness and useful behavior are both necessary.
Program.cs · One rendering loop uses different completed reports
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Learner: Mina | Practice: 25 min
Learner: Sami | Exercises: 4
The loop knows only the shared report contract. Render is implemented once in the base, but its internal call to the protected abstract hook reaches the appropriate override. The loop needs no type switch, and callers cannot directly invoke the protected hook.
Choose abstract, virtual or ordinary members by the rule you need
Use abstract when this base layer has no meaningful default implementation and every concrete kind must supply one. Use virtual when the base has a real default that derived classes may replace. Use an ordinary non-virtual method when callers should use this implementation without that overriding extension point.
An abstract class does not have to declare an abstract member. It can be marked abstract simply to prevent direct creation while sharing implemented behavior. Conversely, a class containing an abstract instance member must itself be abstract. These rules explain the language; your design still needs a useful reason for the boundary.
Properties can also be abstract. public abstract string Label { get; } promises a readable label, and a concrete override supplies its getter. This does not automatically create backing storage in the base. The isolated property example lets you focus on that contract without expanding our report model.
An abstract method has no base body to invoke. base.BuildDetails() therefore fails in the example. In contrast, a virtual default can be called through base when extending that behavior. An abstract class also cannot be sealed: sealed prevents derivation while abstract requires a concrete derived type for instances.
Program.cs · A property can also require an implementation
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Practice session
The abstract property specifies a readable string without providing a getter body. SessionKind overrides the getter. This small example isolates property syntax; you do not need an abstract property when a stored base property already expresses the model clearly.
Program.cs · There is no abstract base body to call
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: the abstract base BuildDetails member cannot be called directly.Unlike a virtual method with a default implementation, this abstract declaration has no body. The override must supply the result itself. Calling base.Render() would be a different question because Render is implemented, but calling it from BuildDetails would create recursive rendering in this model.
Test the shared rule and each specialized answer separately
For the base rule, try a valid name with outside spaces and a blank name that must be rejected. For the concrete reports, test zero, an ordinary positive number and a negative number that must fail. Zero minutes or exercises is valid in this exercise; do not invent activity that did not occur.
For rendering, check that the learner heading appears once and that the correct count or duration appears in the detail. Change Mina, Sami and the numeric values to ensure the implementation reads current instance data rather than returning the reference example as a hard-coded sentence.
For dispatch, render both subtypes through PracticeReport variables or an array. Reverse the order and try an empty array: output should follow the inputs without subtype conditions. Two separate instances must keep their own data. Repeated Render calls should return the same text without changing progress.
The final lab preserves one shared renderer and completes only the missing exercise hook. It produces local report strings; it does not query a learner account, persist progress, export a file or perform a network operation. Interfaces, the next lesson, offer another kind of contract when shared instance state and one base implementation are not the right fit.
Guided repair: replace a compiling placeholder with real behavior
The exercise report compiles but prints Details pending instead of the completed exercise count. Replace the placeholder with the promised result while keeping the shared heading in the base renderer.
Program.cs · A placeholder body compiles but loses the required meaning
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Learner: Sami | Details pending
The compiler sees a completed override returning string, so it accepts the declaration. It cannot infer that the exercise count must appear in the report. Replacing missing behavior with a placeholder satisfies syntax while leaving the requirement unfinished.
Repair checkpoints
- Find ExerciseReport.BuildDetails rather than changing Main.
- Read the private completed field initialized by that report’s constructor.
- Return Exercises: followed by the current completed value.
- Leave PracticeReport.Render responsible for the learner heading.
- Verify Sami with four exercises prints Learner: Sami | Exercises: 4.
Reveal the repair and explanation
Program.cs · Implement the promised detail instead of a placeholder
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Learner: Sami | Exercises: 4
The override now uses the stored completed count. The base still supplies the learner heading and shared formatting. This fixes behavior without copying Render into the derived class or adding subtype conditions to Main.
Independent lab: complete a shared report pipeline
Finish a two-kind practice reporting pipeline. Keep the common workflow and validation in the abstract base, and complete the exercise-specific operation in its concrete report.
Acceptance checklist
- Keep PracticeReport abstract and do not instantiate it directly.
- Preserve its validated Learner property and protected constructor.
- Keep Render implemented once and BuildDetails protected abstract.
- Preserve SessionReport’s minutes validation and detail override.
- Complete ExerciseReport.BuildDetails using its stored completed count.
- Keep the loop over PracticeReport[] unchanged with no subtype switches.
- Return Learner: Mina | Practice: 25 min and Learner: Sami | Exercises: 4 for the supplied inputs.
Program.cs · Shared report pipeline starter
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Learner: Mina | Practice: 25 min
Learner: Sami | TODO: exercise details
The base workflow, constructors and session report already work. Finish the exercise-specific detail using its parameter-backed field. The starter deliberately compiles so the missing requirement is visible in the report rather than hidden behind a compiler error.
Show hints
- The constructor already stores the exercise input in completed.
- The detail hook should return only its specialized text, not repeat the learner heading.
- The loop calls Render, which reaches the right hook automatically.
Reveal the reference solution
Program.cs · Shared report pipeline reference solution
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Learner: Mina | Practice: 25 min
Learner: Sami | Exercises: 4
Both concrete types provide the required detail hook, and one base Render method creates the final line. The name is trimmed once by the base constructor. Numbers are validated by the type that owns them. These reports use fixed in-memory data and do not read actual learner progress or save results.
Changed-input checks
- Zero minutes → Practice: 0 min; zero exercises → Exercises: 0.
- Changed names and counts appear in the appropriate reports.
- Blank learner names are rejected for learner.
- Negative session minutes are rejected for minutes.
- Negative exercise counts are rejected for completed.
- Reversing the array reverses output; an empty array prints no lines.
- Repeated rendering does not change stored instance values.
Recap: what is shared, and what must be supplied?
Separate the implemented base workflow from the required concrete steps, then check how constructors establish their state.
abstract class- A base type that cannot be instantiated directly.
concrete class- A non-abstract class that completes the remaining required operations.
abstract method;- Required behavior with no body at this layer.
override- Provide the inherited abstract operation’s implementation.
protected constructor- Initialize base instance state through derived construction.
implemented Render- Share the common workflow while calling a subtype hook.
abstract property- Require an accessor implementation without a base accessor body.
virtual versus abstract- An optional replacement of a default versus a required implementation.
Frequently asked questions
Why can I declare a PracticeReport variable but not new PracticeReport?
The variable can reference a concrete subtype that fulfills the contract. Direct creation of the abstract type is prohibited because it is not intended as a complete instance.
Can an abstract class have a constructor?
Yes. Its constructor initializes the base part of a concrete derived instance. It is not a separate object and does not make the abstract class directly instantiable.
Does abstract mean every method lacks a body?
No. An abstract class can mix implemented members with abstract ones, or even contain only implemented members while preventing direct creation.
What is different from virtual?
A virtual method provides a default implementation that may be overridden. An abstract method has no body and requires a concrete implementation before a class can be instantiated.
Can I call base.BuildDetails when it is abstract?
No. There is no base implementation to execute. Supply the body in an override instead.
Can an abstract subtype leave another method unfinished?
Yes, if that subtype remains abstract. A later non-abstract class must complete all inherited abstract members still lacking an implementation.
Why not use an interface immediately?
An abstract base fits this example because reports share validated instance state and one workflow. Interfaces are useful for capabilities across types and are covered next; they are not simply a replacement keyword for abstract classes.
Can you explain why the class is abstract?
Explain abstract versus concrete, constructor sharing and required behavior in plain language. Read the explanation after checking your answers.
Next step and references
An abstract class can share validated state and implemented behavior while requiring a concrete subtype to complete missing operations. Trace the normal workflow into its abstract hook, and verify useful behavior as well as compiler completeness.
Next is C# Interfaces and Contracts. You will describe a capability without requiring one shared base class and keep consumers independent of concrete implementations.
Official references
Original SovranCode explanations, examples and exercises. Technical details were checked against these references.
- Microsoft Learn: abstract keyword and mixed implementations
- Microsoft Learn: Abstract classes and members
Last updated: .