Say you run shipping for a Columbus, Ohio, outdoor-gear store. Every order goes out by UPS ground, FedEx air or XPO freight, and each carrier prices a package its own way.
That’s the job of an abstract class in C#: a base class you can’t create with new, where shared code lives once and each subclass must override the abstract members. We’ll build that calculator on .NET 10, one piece at a time.
Here’s what we’re building. Three shipments, three carriers, one shared label format and a total:

- Shipment (abstract) holds the ID, customer, weight, the
Label()format and the total. - Ground, Air and Freight each supply
CarrierandCost(). - Only Freight changes
Surcharge, a virtual member with a $0 default.
Before You Start the Abstract Class Build
You need the .NET 10 SDK and VS Code. I run every step as a single-file app with dotnet run File.cs; if that’s new to you, start with my guide to create a console application in Visual Studio Code.
Every step reuses the same three orders and these rates:
| Tracking ID | Customer | Weight | Service | Rate |
|---|---|---|---|---|
| CMH-2041 | Tyler Brooks | 12 lbs | UPS ground | $8.50 + $0.45/lb |
| CMH-2042 | Jenna Morales | 3 lbs | FedEx air | $24.00 + $2.10/lb |
| CMH-2043 | Marcus Reed | 68 lbs | XPO freight | $95.00 + $0.80/lb + $45 liftgate |
You should also be comfortable with inheritance in C#, since an abstract class is useless until something derives from it.
Step 1: Declare an Abstract Class with an Abstract Method
Put abstract in front of class, then declare Cost() as an abstract method. It has no body, only a semicolon, because the base class can’t know any carrier’s price:
var box = new Shipment(); // try to create the base itself
abstract class Shipment
{
public abstract decimal Cost(); // no body
}
Build output:
C:\DotNetDemo\AbstractClassDemo\Step1Declare.cs(1,11): error CS0144: Cannot create an instance of the abstract type or interface 'Shipment'
The build failed. Fix the build errors and run again.

That error is the point. An abstract class is a template for other classes, so the compiler blocks new Shipment() outright (that’s abstraction in C# enforced at build time).
Checkpoint: one CS0144 error, “Cannot create an instance of the abstract type or interface ‘Shipment'”.
Step 2: Override the Abstract Method in a Derived Class
Now add Ground, derive it from Shipment and implement Cost() with the override keyword. Notice the variable is typed as the abstract base:
System.Globalization.CultureInfo.CurrentCulture = new("en-US");
Shipment parcel = new Ground(12);
Console.WriteLine($"Ground, 12 lbs: {parcel.Cost():C}");
abstract class Shipment
{
public abstract decimal Cost();
}
class Ground(decimal lbs) : Shipment
{
public override decimal Cost() => 8.50m + lbs * 0.45m;
}
Output:
Ground, 12 lbs: $13.90

You can’t create a Shipment, but you can declare one and point it at a Ground. The call goes to the override because an abstract method is implicitly virtual, the same mechanism as method overriding in C#.
Skip the override and the derived class won’t build:
Shipment parcel = new Ground();
abstract class Shipment { public abstract decimal Cost(); }
class Ground : Shipment { } // forgot the override
Build output:
C:\DotNetDemo\AbstractClassDemo\MissingOverride.cs(4,7): error CS0534: 'Ground' does not implement inherited abstract member 'Shipment.Cost()'
The build failed. Fix the build errors and run again.
CS0534 means a non-abstract class left an inherited abstract member empty. Either implement it, or mark the derived class abstract too.
Checkpoint: Ground, 12 lbs: $13.90 (8.50 + 12 x 0.45).
Step 3: Add an Abstract Property and a Shared Concrete Method
Abstract members aren’t limited to methods. Here Carrier is an abstract property, and Label() is a normal method that every carrier inherits:
System.Globalization.CultureInfo.CurrentCulture = new("en-US");
Shipment[] today = [new Ground(12), new Air(3)];
foreach (var s in today) Console.WriteLine(s.Label());
abstract class Shipment
{
public abstract string Carrier { get; }
public abstract decimal Cost();
public string Label() => $"{Carrier,-6}{Cost(),8:C}";
}
class Ground(decimal lbs) : Shipment
{
public override string Carrier => "UPS";
public override decimal Cost() => 8.50m + lbs * 0.45m;
}
class Air(decimal lbs) : Shipment
{
public override string Carrier => "FedEx";
public override decimal Cost() => 24m + lbs * 2.10m;
}
Output:
UPS $13.90
FedEx $30.30

Label() calls members it doesn’t implement, and each subclass fills them in. That mix of finished and unfinished code is what an interface in C# traditionally couldn’t give you.
Checkpoint: UPS at $13.90 and FedEx at $30.30.
Step 4: Give the Abstract Class a Protected Constructor
Every shipment needs a tracking ID and a weight, so that data belongs in the base. An abstract class can have a constructor; make it protected and call it from the subclass with base(...):
System.Globalization.CultureInfo.CurrentCulture = new("en-US");
var parcel = new Ground("CMH-2041", 12);
Console.WriteLine($"{parcel.Id}: {parcel.Cost():C}");
abstract class Shipment
{
protected Shipment(string id, decimal lbs)
{
(Id, Lbs) = (id, lbs);
Console.WriteLine($"1. Shipment constructor: {id}");
}
public string Id { get; }
protected decimal Lbs { get; }
public abstract decimal Cost();
}
class Ground : Shipment
{
public Ground(string id, decimal lbs) : base(id, lbs)
=> Console.WriteLine("2. Ground constructor");
public override decimal Cost() => 8.50m + Lbs * 0.45m;
}
Output:
1. Shipment constructor: CMH-2041
2. Ground constructor
CMH-2041: $13.90

The output proves the base constructor runs, even though you never create a Shipment directly. A public constructor compiles too, but code analysis rule CA1012 flags it, since only derived types can call it.
My constructor in C# guide covers chaining in more depth.
Checkpoint: line 1 from the Shipment constructor, line 2 from Ground, then the cost.
Step 5: Abstract vs Virtual Members (the Freight Surcharge)
Freight adds a $45 liftgate fee and the other carriers don’t. Making Surcharge abstract would force every carrier to write => 0m, so use virtual with a default instead:
System.Globalization.CultureInfo.CurrentCulture = new("en-US");
Shipment[] today = [new Ground(12), new Freight(68)];
foreach (var s in today)
Console.WriteLine($"{s.GetType().Name,-8}{s.Cost(),8:C}"
+ $" + {s.Surcharge:C} = {s.Cost() + s.Surcharge:C}");
abstract class Shipment
{
public abstract decimal Cost(); // must override
public virtual decimal Surcharge => 0m; // may override
}
class Ground(decimal lbs) : Shipment
{
public override decimal Cost() => 8.50m + lbs * 0.45m;
}
class Freight(decimal lbs) : Shipment
{
public override decimal Cost() => 95m + lbs * 0.80m;
public override decimal Surcharge => 45m; // liftgate
}
Output:
Ground $13.90 + $0.00 = $13.90
Freight $149.40 + $45.00 = $194.40

My rule: if every subclass needs its own answer, it’s abstract. If most share a sensible default, it’s virtual.
Checkpoint: Ground $13.90 + $0.00 = $13.90 and Freight $149.40 + $45.00 = $194.40.
Test the Finished Abstract Class Shipping Calculator
Here’s the full program from the screenshot at the top. The derived classes use primary constructors that pass their values to the protected base constructor, and the orders sit in a List in C# typed as the abstract base:
System.Globalization.CultureInfo.CurrentCulture = new("en-US");
List<Shipment> today = [
new Ground("CMH-2041", "Tyler Brooks", 12),
new Air("CMH-2042", "Jenna Morales", 3),
new Freight("CMH-2043", "Marcus Reed", 68)];
foreach (var s in today) Console.WriteLine(s.Label());
Console.WriteLine($"Total due: {today.Sum(s => s.Total):C}");
abstract class Shipment
{
protected Shipment(string id, string to, decimal lbs)
=> (Id, To, Lbs) = (id, to, lbs);
public string Id { get; }
public string To { get; }
protected decimal Lbs { get; }
public abstract string Carrier { get; }
public abstract decimal Cost();
public virtual decimal Surcharge => 0m;
public decimal Total => Cost() + Surcharge;
public string Label() =>
$"{Id} {To,-14}{Carrier,-6}{Total,9:C}";
}
class Ground(string id, string to, decimal lbs)
: Shipment(id, to, lbs)
{
public override string Carrier => "UPS";
public override decimal Cost() => 8.50m + Lbs * 0.45m;
}
class Air(string id, string to, decimal lbs)
: Shipment(id, to, lbs)
{
public override string Carrier => "FedEx";
public override decimal Cost() => 24m + Lbs * 2.10m;
}
class Freight(string id, string to, decimal lbs)
: Shipment(id, to, lbs)
{
public override string Carrier => "XPO";
public override decimal Cost() => 95m + Lbs * 0.80m;
public override decimal Surcharge => 45m; // liftgate
}
Output:
CMH-2041 Tyler Brooks UPS $13.90
CMH-2042 Jenna Morales FedEx $30.30
CMH-2043 Marcus Reed XPO $194.40
Total due: $238.60
Each line comes from the same Label() method, while Carrier and Total differ per class. Freight is $95.00 + 68 x $0.80 + $45 = $194.40, and the day’s total is $238.60.
When Should You Use an Abstract Class Instead of an Interface?
Use an abstract class when related types share state and code; use an interface when unrelated types only share a capability. Here’s the short version:
| Abstract class | Interface | |
|---|---|---|
| Instantiate with new | No | No |
| Fields and constructors | Yes | No instance fields or constructors |
| Members with code | Yes | Default methods since C# 8 |
| How many per class | One base class | Many |
| Access modifiers on members | Any, abstract ones not private | Public by default |
| Best for | An is-a family (Ground is a Shipment) | A can-do role (ITrackable) |
You can also combine them. An abstract class that implements an interface may leave the interface member abstract:
System.Globalization.CultureInfo.CurrentCulture = new("en-US");
ITrackable parcel = new Ground("CMH-2041", 12);
Console.WriteLine(parcel.Track());
interface ITrackable { string Track(); }
abstract class Shipment(string id) : ITrackable
{
public string Id { get; } = id;
public abstract decimal Cost();
public abstract string Track(); // interface member left abstract
}
class Ground(string id, decimal lbs) : Shipment(id)
{
public override decimal Cost() => 8.50m + lbs * 0.45m;
public override string Track() => $"{Id}: UPS ground, {Cost():C}";
}
Output:
CMH-2041: UPS ground, $13.90
Because C# allows only one base class, interfaces are how a class picks up extra roles; see multiple inheritance in C# for that pattern.
Troubleshooting Abstract Class Errors in C#
I packed the common mistakes into one file. The build stops with all of them:
Console.WriteLine("This file never builds");
abstract class Shipment
{
public abstract decimal Cost() { return 0m; }
private abstract string Carrier { get; }
public static abstract decimal BaseRate();
}
sealed abstract class Pallet { }
class Label { public abstract void Print(); }
Build output:
C:\DotNetDemo\AbstractClassDemo\Rules.cs(9,23): error CS0418: 'Pallet': an abstract type cannot be sealed or static
C:\DotNetDemo\AbstractClassDemo\Rules.cs(10,36): error CS0513: 'Label.Print()' is abstract but it is contained in non-abstract type 'Label'
C:\DotNetDemo\AbstractClassDemo\Rules.cs(5,29): error CS0500: 'Shipment.Cost()' cannot declare a body because it is marked abstract
C:\DotNetDemo\AbstractClassDemo\Rules.cs(7,36): error CS0112: A static member cannot be marked as 'abstract'
C:\DotNetDemo\AbstractClassDemo\Rules.cs(7,36): error CS0501: 'Shipment.BaseRate()' must declare a body because it is not marked abstract, extern, or partial
C:\DotNetDemo\AbstractClassDemo\Rules.cs(6,29): error CS0621: 'Shipment.Carrier': virtual or abstract members cannot be private
The build failed. Fix the build errors and run again.
| Error | What you wrote | Fix |
|---|---|---|
| CS0500 | An abstract method with a body | Remove the body, or drop abstract |
| CS0621 | A private abstract member | Make it public, protected or internal |
| CS0112 | static abstract in a class | Allowed only in interfaces |
| CS0418 | sealed abstract class | Pick one: they contradict each other |
| CS0513 | Abstract member in a non-abstract class | Mark the class abstract |
The extra CS0501 comes from the same static abstract line: once abstract is rejected, the method needs a body.
Note: static abstract members are legal in interfaces since C# 11 (generic math uses them), but never in a class.
Where to Take It Next
- Add a
Dronecarrier: one new class with two overrides, andLabel()works unchanged. - Seal a finished override with
sealed overrideso subclasses of Freight can’t change the surcharge, part of good encapsulation in C#. - Move the carriers into separate files; partial classes help when generated code joins in.
For the bigger picture of C# class design, these are worth reading next:
- The four OOP concepts in C# with examples
- How method overloading differs from overriding
- C# class naming conventions for base and derived types
- C# interview questions on abstract classes and interfaces
Frequently Asked Questions
What is an abstract class in C#?
It’s a class marked with the abstract keyword that you can’t instantiate. It exists to be a base class: it can hold fields, constructors and finished methods, plus abstract members that every non-abstract derived class must override.
Can an abstract class have a constructor in C#?
Yes. The constructor runs when a derived object is created, called through base(...). Make it protected, because only derived classes can call it.
Can an abstract class have non-abstract methods?
Yes. An abstract class can mix concrete methods, properties and fields with abstract members, and it can even have no abstract members at all, just to block direct instantiation.
What is the difference between an abstract and a virtual method?
An abstract method has no body and must be overridden by every non-abstract subclass. A virtual method has a default body that subclasses may override if they need to.
Can an abstract class be sealed or static?
No. sealed blocks inheritance and abstract requires it, so the compiler rejects the pair with CS0418. A static class can’t be abstract either.
Where can I read the official rules?
Microsoft’s abstract keyword reference lists every rule for abstract classes, methods and properties.
Bijay Kumar is a renowned software engineer, accomplished author, and distinguished Microsoft Most Valuable Professional (MVP) specializing in SharePoint. With a rich professional background spanning over 15 years, Bijay has established himself as an authority in the field of information technology. He possesses unparalleled expertise in multiple programming languages and technologies such as ASP.NET, ASP.NET MVC, C#.NET, and SharePoint, which has enabled him to develop innovative and cutting-edge solutions for clients across the globe. Read more…