C# Interface vs Abstract Class: When to Use Each on .NET 10

Last updated: October 12, 2026 · Tested with .NET 10 (SDK 10.0.401, runtime 10.0.12), C# 14, Windows 11

Short answer: use an interface to describe what a type can do. A class can implement many interfaces, structs can implement them, and they are what dependency injection and test fakes are built on. Use an abstract class when related classes share state and code: fields, a constructor, and a fixed algorithm with steps that subclasses fill in. A class has only one base class, so an abstract base is a bigger commitment. In real code the two are often combined: callers depend on the interface, and an abstract base class implements it to share the plumbing. Default interface methods (C# 8) and static abstract members (C# 11) have changed the old textbook answers, so this post shows what each one can and can’t do. Every example was compiled and run on .NET 10, and the compiler errors are copied from the build.

Interface vs Abstract Class at a Glance (C# 14)

InterfaceAbstract class
How many per typeAny numberOne base class
Structs and recordsCan implement itStructs can’t inherit (CS0527); record classes can
Instance fields / stateNo (CS0525)Yes
ConstructorsNoYes, including primary constructors
Method bodiesDefault implementations (C# 8+)Yes
Static membersYes, including static fields and static abstract (C# 11)Yes, but no static abstract
Access modifiers on membersPublic by default; private, protected allowed (C# 8+)Any
Adding a member laterBreaks implementers (CS0535) unless it has a default bodyNon-abstract members don’t break subclasses
Typical useCapabilities, DI contracts, test fakes, generic constraintsShared base code for a family of classes

An Interface: a Contract Any Type Can Sign

public interface IPaymentProcessor
{
    bool Charge(decimal amount);
}

public class StripeProcessor : IPaymentProcessor
{
    public bool Charge(decimal amount) => amount > 0; // real code would call the Stripe API
}

public class FakeProcessor : IPaymentProcessor // for unit tests, no base class needed
{
    public List<decimal> Charges { get; } = [];
    public bool Charge(decimal amount) { Charges.Add(amount); return true; }
}
IPaymentProcessor[] processors = [new StripeProcessor(), new FakeProcessor()];
foreach (var processor in processors)
{
    Console.WriteLine($"{processor.GetType().Name}: charged = {processor.Charge(49.90m)}");
}
StripeProcessor: charged = True
FakeProcessor: charged = True

The two classes share nothing but the contract. FakeProcessor exists for tests and records every charge. It doesn’t need to inherit from anything, which is why interfaces are what you register in ASP.NET Core’s DI container (services.AddScoped<IPaymentProcessor, StripeProcessor>()) and what mocking libraries generate.

An Abstract Class: Shared State and a Fixed Algorithm

public abstract class Employee(string name)
{
    public string Name { get; } = name;   // shared state
    private static int _nextId;
    public int Id { get; } = ++_nextId;

    public abstract decimal MonthlyPay();  // each subclass must implement

    // Shared behaviour that uses the abstract member (template method)
    public string PayslipLine() => $"#{Id} {Name,-6} {GetType().Name,-16} {MonthlyPay(),10:C}";
}

public class SalariedEmployee(string name, decimal yearly) : Employee(name)
{
    public override decimal MonthlyPay() => Math.Round(yearly / 12m, 2);
}

public class Contractor(string name, int hours, decimal rate) : Employee(name)
{
    public override decimal MonthlyPay() => hours * rate;
}
Employee[] staff = [new SalariedEmployee("Ada", 96_000m), new Contractor("Linus", 120, 85m)];
foreach (var e in staff)
{
    Console.WriteLine(e.PayslipLine());
}
#1 Ada    SalariedEmployee  $8,000.00
#2 Linus  Contractor       $10,200.00

Here the base class owns the data (Name, an Id from a shared counter) and the algorithm (PayslipLine). Subclasses supply only the part that varies, MonthlyPay. An interface can’t do this: it has no constructor and no instance fields. Primary constructors (C# 12) on abstract classes keep the subclasses short.

The Compiler Errors You’ll Hit

Each snippet below was built on its own; the error is copied from dotnet build.

CS0144: Cannot create an instance of the abstract type or interface

var p = new PaymentProcessor();
var e = new Employee();
public interface PaymentProcessor { void Charge(decimal amount); }
public abstract class Employee { }
Program.cs(1,9): error CS0144: Cannot create an instance of the abstract type or interface 'PaymentProcessor'
Program.cs(2,9): error CS0144: Cannot create an instance of the abstract type or interface 'Employee'

Create a concrete class instead (new StripeProcessor()), or let the DI container create it. Note the interface here has no I prefix. The compiler doesn’t care, but the convention makes this mistake easy to spot.

CS0535: Does not implement interface member

public interface IPaymentProcessor
{
    bool Charge(decimal amount);
    void Refund(decimal amount);
}
public class StripeProcessor : IPaymentProcessor
{
    public bool Charge(decimal amount) => true;
}
Program.cs(6,32): error CS0535: 'StripeProcessor' does not implement interface member 'IPaymentProcessor.Refund(decimal)'

Every interface member without a default body must be implemented. This is also what happens to every implementer when you add a member to a published interface. Add the method, or in Visual Studio / Rider use the quick fix “Implement interface”.

CS0534: Does not implement inherited abstract member

public abstract class Employee
{
    public abstract decimal CalculatePay();
}
public class Intern : Employee { }
Program.cs(5,14): error CS0534: 'Intern' does not implement inherited abstract member 'Employee.CalculatePay()'

The same rule for abstract classes: override every abstract member, or mark the subclass abstract too.

CS1721: Cannot have multiple base classes

public abstract class Employee { }
public abstract class Auditable { }
public class Manager : Employee, Auditable { }
Program.cs(3,34): error CS1721: Class 'Manager' cannot have multiple base classes: 'Employee' and 'Auditable'

This is the practical reason to prefer interfaces for capabilities. If “auditable” were an interface, Manager could have Employee as its base class and implement IAuditable as well.

CS0525: Interfaces cannot contain instance fields

public interface IHasName
{
    string name;
}
Program.cs(3,12): error CS0525: Interfaces cannot contain instance fields

Declare a property (string Name { get; }) and let the class hold the data. Static fields are allowed in interfaces since C# 8, but they are shared by everyone, so they’re no substitute for per-object state.

CS0527: Type in interface list is not an interface (struct inheriting a class)

public struct Money : Base { }
public abstract class Base { }
Program.cs(1,23): error CS0527: Type 'Base' in interface list is not an interface

Structs can’t inherit from any class, abstract or not. They can only implement interfaces, which is one more reason contracts that structs may need to meet (IEquatable<T>, IComparable<T>, the generic math interfaces) are interfaces.

Default Interface Methods: Why Greet() Isn’t on the Class (CS1061)

Since C# 8 an interface member can have a body. It looks like an abstract class with multiple inheritance, but there is a catch:

IGreeter viaInterface = new EnglishGreeter();
EnglishGreeter viaClass = new EnglishGreeter();
Console.WriteLine(viaInterface.Greet("Ada"));
Console.WriteLine(viaClass.Greet("Ada"));
public interface IGreeter
{
    string Name { get; }
    string Greet(string who) => $"Hello {who}, I am {Name}";
}
public class EnglishGreeter : IGreeter
{
    public string Name => "English";
}
Program.cs(4,28): error CS1061: 'EnglishGreeter' does not contain a definition for 'Greet' and no accessible extension method 'Greet' accepting a first argument of type 'EnglishGreeter' could be found (are you missing a using directive or an assembly reference?)

A default implementation belongs to the interface, not to the class. EnglishGreeter doesn’t have a Greet method of its own, so you can call it only through an IGreeter reference. A class that declares its own Greet replaces the default:

public interface IGreeter
{
    string Name { get; }
    string Greet(string who) => $"Hello {who}, I am {Name}"; // default implementation (C# 8+)
}

public class EnglishGreeter : IGreeter
{
    public string Name => "English";
}

public class GermanGreeter : IGreeter
{
    public string Name => "German";
    public string Greet(string who) => $"Hallo {who}, ich bin {Name}";
}
IGreeter viaInterface = new EnglishGreeter();
Console.WriteLine(viaInterface.Greet("Ada"));

var german = new GermanGreeter();
Console.WriteLine(german.Greet("Ada"));                 // class's own method
Console.WriteLine(((IGreeter)german).Greet("Ada"));     // same method: it implements the interface member
Hello Ada, I am English
Hallo Ada, ich bin German
Hallo Ada, ich bin German

What default methods are for: adding a member to an interface that is already published without breaking every implementer (the CS0535 problem above). They are not a way to share code between classes, because they can’t touch instance state that the interface doesn’t expose as members. Since C# 8, interfaces can also have private helper methods, protected members and static fields. I compiled an ILogger interface with all three on .NET 10 and it ran fine. For shared code you’d write in a base class, an abstract class is still the clearer tool.

Static Abstract Members: Something Only Interfaces Can Do

C# 11 lets an interface require static members, operators included. That is how .NET’s generic math works, and an abstract class has no equivalent. One generic method sums int, decimal and a custom Money struct:

public readonly record struct Money(decimal Amount)
    : IAdditionOperators<Money, Money, Money>, IAdditiveIdentity<Money, Money>
{
    public static Money AdditiveIdentity => new(0m);
    public static Money operator +(Money a, Money b) => new(a.Amount + b.Amount);
    public override string ToString() => Amount.ToString("C");
}
Console.WriteLine(Total([1, 2, 3]));
Console.WriteLine(Total([1.5m, 2.25m]));
Console.WriteLine(Total([new Money(10m), new Money(2.5m)]));

static T Total<T>(IEnumerable<T> values) where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
    T sum = T.AdditiveIdentity;
    foreach (var v in values) sum += v;
    return sum;
}
6
3.75
$12.50

T.AdditiveIdentity calls a static member on the type parameter. The constraint IAdditionOperators<T, T, T> is what lets += compile for any T. Money is a struct, so it couldn’t have used an abstract base class even if one existed.

Explicit Interface Implementation: Same Member Name in Two Interfaces

public interface IExportable { string Name { get; } }
public interface IAuditable { string Name { get; } }

public class ReportFile : IExportable, IAuditable
{
    public string Name => "q3-report.pdf";
    string IAuditable.Name => "Report Q3 (audited)"; // only visible through IAuditable
}
var file = new ReportFile();
Console.WriteLine(file.Name);
Console.WriteLine(((IExportable)file).Name);
Console.WriteLine(((IAuditable)file).Name);
q3-report.pdf
q3-report.pdf
Report Q3 (audited)

The public Name serves the class and IExportable. IAuditable gets its own version, visible only through an IAuditable reference. Abstract classes have no equivalent, because a class has only one base class to clash with.

Structs Behind an Interface: the Boxing Gotcha

public interface ICounter
{
    int Value { get; }
    void Increment();
}

public struct Counter : ICounter
{
    public int Value { get; private set; }
    public void Increment() => Value++;
}
var counter = new Counter();
ICounter boxed = counter;      // boxing: copies the struct
boxed.Increment();
boxed.Increment();
Console.WriteLine($"boxed.Value = {boxed.Value}, counter.Value = {counter.Value}");

Bump(ref counter);
Console.WriteLine($"After generic call: counter.Value = {counter.Value}");

static void Bump<T>(ref T c) where T : ICounter => c.Increment(); // no boxing
boxed.Value = 2, counter.Value = 0
After generic call: counter.Value = 1

Assigning a struct to an interface variable boxes it, which makes a copy on the heap. The two Increment calls changed the copy, and counter stayed at 0. A generic method constrained to the interface (where T : ICounter) calls the struct’s method directly, with no box and no copy. That’s why high-performance .NET APIs take T with an interface constraint instead of the interface type itself.

Using Both: Interface for Callers, Abstract Base for Implementers

public interface IEntity { int Id { get; } }

public interface IRepository<T> where T : IEntity
{
    void Add(T item);
    T? GetById(int id);
    int Count { get; }
}

// Shared plumbing lives in an abstract base; callers depend only on the interface
public abstract class RepositoryBase<T> : IRepository<T> where T : IEntity
{
    public abstract void Add(T item);
    public abstract IEnumerable<T> All();
    public T? GetById(int id) => All().FirstOrDefault(x => x.Id == id);
    public int Count => All().Count();
}

public class InMemoryRepository<T> : RepositoryBase<T> where T : IEntity
{
    private readonly List<T> _items = [];
    public override void Add(T item) => _items.Add(item);
    public override IEnumerable<T> All() => _items;
}

public record Customer(int Id, string Name) : IEntity;
IRepository<Customer> repo = new InMemoryRepository<Customer>();
repo.Add(new Customer(1, "Ada"));
repo.Add(new Customer(2, "Linus"));
Console.WriteLine($"Count: {repo.Count}, #2 = {repo.GetById(2)?.Name}");
Count: 2, #2 = Linus

Callers (and the DI container) see only IRepository<T>. RepositoryBase<T> implements GetById and Count once, on top of two abstract members, and a new storage needs only Add and All. A test can still implement IRepository<T> directly without touching the base class. Keep the base class optional, so no caller depends on it.

When to Use Which

SituationUse
A service you register in DI and fake in testsInterface
A capability many unrelated types share (IDisposable, IComparable<T>)Interface
Structs need to meet the contractInterface
Generic code that needs operators or static factoriesInterface with static abstract members
Several classes share fields, constructor logic and an algorithmAbstract class
You ship a library and expect to add members laterAbstract class, or an interface with default methods
Both: a public contract plus shared plumbingInterface + optional abstract base that implements it

FAQ

Can an abstract class implement an interface without implementing its members? Yes. It can declare them abstract and leave them to subclasses, as RepositoryBase<T>.Add does above.

Do default interface methods make abstract classes obsolete? No. Interfaces still can’t have instance fields or constructors, and a default method isn’t callable on the class type (CS1061). Default methods are mainly a versioning tool.

Is calling through an interface slower? Interface calls and virtual calls both go through indirection, and the JIT can often devirtualize either one. The difference rarely matters next to I/O, so pick based on design and measure if a hot path says otherwise. The cost that does show up is boxing structs, shown above.

Related: SOLID principles in C# · Dependency injection in C# · C# primary constructors · Records vs classes vs structs

References: interface keyword (Microsoft Learn) · abstract keyword · Default interface methods tutorial · Generic math

This article is part of the C# tutorials guide (language features, patterns, performance and testing).

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *