TL;DR - abstract
vs interface
Abstract class: Shared implementation + state management, supports constructors for initialization
Interface: Pure contracts + multiple inheritance, no constructors
Use abstract classes when you need shared logic, protected members, or default behavior that can be overridden
Use interfaces when you need flexible contracts, multiple inheritance, or mocking capabilities
1. When should you choose an abstract class over an interface in C#?
Choose abstract classes when you need shared implementation logic and state management across derived types. Interfaces work for contracts, but abstract classes handle common behavior.
Use abstract classes when:
- You need protected fields or constructor logic
- Multiple derived classes share the same implementation
- You want to provide default behavior that can be overridden
public abstract class PaymentProcessor
{
protected readonly ILogger _logger;
protected PaymentProcessor(ILogger logger) => _logger = logger;
public abstract Task<PaymentResult> ProcessAsync(decimal amount);
protected void LogTransaction(decimal amount) =>
_logger.LogInformation("Processing ${Amount}", amount);
}
public class StripeProcessor : PaymentProcessor
{
public StripeProcessor(ILogger logger) : base(logger) { }
public override async Task<PaymentResult> ProcessAsync(decimal amount)
{
LogTransaction(amount); // Shared implementation
return await CallStripeApi(amount);
}
}
Abstract classes enforce inheritance hierarchies and provide implementation reuse. Use interfaces when you need multiple inheritance or purely behavioral contracts.
2. Can an abstract class implement multiple interfaces, and why is that useful?
Yes, abstract classes can implement multiple interfaces, creating a foundation that combines contracts with shared implementation. This pattern works well for complex domain models.
public interface IValidatable
{
ValidationResult Validate();
}
public interface IAuditable
{
DateTime CreatedAt { get; set; }
string CreatedBy { get; set; }
}
public abstract class Entity : IValidatable, IAuditable
{
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string CreatedBy { get; set; } = string.Empty;
public virtual ValidationResult Validate() => ValidationResult.Success;
protected abstract void ApplyBusinessRules();
}
public class Product : Entity
{
public string Name { get; set; } = string.Empty;
protected override void ApplyBusinessRules()
{
if (string.IsNullOrEmpty(Name))
throw new InvalidOperationException("Product name required");
}
}
This approach provides interface segregation while maintaining inheritance benefits. Your abstract class becomes a contract implementer and behavior provider simultaneously.
3. What happens if two interfaces have the same method signature, how does C# handle it?
C# allows implicit implementation when method signatures match exactly. The same method satisfies both interface contracts without explicit interface implementation.
public interface IReader
{
Task<string> ReadAsync();
}
public interface IProcessor
{
Task<string> ReadAsync(); // Same signature
}
public class FileHandler : IReader, IProcessor
{
// Single method satisfies both interfaces
public async Task<string> ReadAsync()
{
return await File.ReadAllTextAsync("data.txt");
}
}
// Both calls use the same method
IReader reader = new FileHandler();
IProcessor processor = new FileHandler();
await reader.ReadAsync(); // Same implementation
await processor.ReadAsync(); // Same implementation
If you need different behavior per interface, use explicit implementation:
public class FileHandler : IReader, IProcessor
{
async Task<string> IReader.ReadAsync() => await File.ReadAllTextAsync("input.txt");
async Task<string> IProcessor.ReadAsync() => await File.ReadAllTextAsync("config.txt");
}
4. How do default interface methods compare to abstract class methods in C# 8+?
Default interface methods provide implementation in interfaces, but abstract class methods offer more flexibility with access modifiers and state management.
Feature | Default Interface Methods | Abstract Class Methods |
---|---|---|
Access Modifiers | Public only | Public, protected, private |
State Access | No instance fields | Full field access |
Constructor Logic | Not available | Full constructor support |
Override Behavior | Can be overridden | Can be overridden |
// Default interface method
public interface ICache
{
Dictionary<string, object> Items => new();
// Default implementation - public only
public T GetOrAdd<T>(string key, Func<T> factory)
{
return Items.ContainsKey(key) ? (T)Items[key] : factory();
}
}
// Abstract class method
public abstract class CacheBase
{
protected readonly ConcurrentDictionary<string, object> _items = new();
protected virtual T GetOrAdd<T>(string key, Func<T> factory)
{
return (T)_items.GetOrAdd(key, _ => factory()!);
}
}
Abstract classes provide better encapsulation and state management for complex shared behavior.
5. When designing a plugin system, is interface or abstract class a better fit?
Interfaces work better for plugin systems because they provide contract-based extensibility without forcing inheritance hierarchies. Plugins need flexibility, not rigid inheritance.
public interface IPlugin
{
string Name { get; }
Version Version { get; }
Task InitializeAsync(IServiceProvider services);
Task<PluginResult> ExecuteAsync(PluginContext context);
}
public class PluginManager
{
private readonly List<IPlugin> _plugins = new();
public async Task LoadPluginAsync<T>() where T : IPlugin, new()
{
var plugin = new T();
await plugin.InitializeAsync(_serviceProvider);
_plugins.Add(plugin);
}
public async Task<IEnumerable<PluginResult>> ExecuteAllAsync(PluginContext context)
{
var tasks = _plugins.Select(p => p.ExecuteAsync(context));
return await Task.WhenAll(tasks);
}
}
public class EmailPlugin : IPlugin
{
public string Name => "Email Notifications";
public Version Version => new(1, 0, 0);
public Task InitializeAsync(IServiceProvider services) => Task.CompletedTask;
public async Task<PluginResult> ExecuteAsync(PluginContext context) =>
await SendEmailAsync(context.Data);
}
Interfaces enable multiple plugin implementations without inheritance constraints.
6. Can you inject an abstract class using dependency injection in ASP.NET Core?
No, you cannot directly inject abstract classes. DI containers need concrete implementations. Register concrete types that inherit from your abstract class instead.
public abstract class NotificationService
{
protected readonly ILogger _logger;
protected NotificationService(ILogger logger) => _logger = logger;
public abstract Task SendAsync(string message, string recipient);
}
public class EmailNotificationService : NotificationService
{
public EmailNotificationService(ILogger<EmailNotificationService> logger) : base(logger) { }
public override async Task SendAsync(string message, string recipient)
{
_logger.LogInformation("Sending email to {Recipient}", recipient);
await SendEmailAsync(message, recipient);
}
}
// Program.cs - Register concrete implementation
builder.Services.AddScoped<EmailNotificationService>();
// Controller - Inject concrete type
public class NotificationsController : ControllerBase
{
private readonly EmailNotificationService _notificationService;
public NotificationsController(EmailNotificationService notificationService)
{
_notificationService = notificationService;
}
}
For multiple implementations, use interfaces with abstract base classes for shared logic.
7. How does versioning differ when using abstract classes vs interfaces in libraries?
Interface changes break binary compatibility, while abstract class changes can maintain compatibility through virtual methods and new overloads.
Interface versioning challenges:
// Version 1.0
public interface IUserService
{
Task<User> GetUserAsync(int id);
}
// Version 2.0 - BREAKING CHANGE
public interface IUserService
{
Task<User> GetUserAsync(int id);
Task<User> GetUserAsync(string email); // New method breaks existing implementations
}
Abstract class versioning flexibility:
// Version 1.0
public abstract class UserServiceBase
{
public abstract Task<User> GetUserAsync(int id);
}
// Version 2.0 - NON-BREAKING
public abstract class UserServiceBase
{
public abstract Task<User> GetUserAsync(int id);
// Virtual method with default implementation
public virtual async Task<User> GetUserAsync(string email)
{
var users = await GetAllUsersAsync();
return users.FirstOrDefault(u => u.Email == email);
}
protected virtual Task<User[]> GetAllUsersAsync() => Task.FromResult(Array.Empty<User>());
}
Abstract classes provide better versioning strategies for evolving APIs.
8. What are the performance implications of using interfaces vs abstract classes in hot paths?
In performance-critical code, like tight loops or real-time processing, the way methods are dispatched matters.
Interfaces in C# use interface dispatch, which involves an extra level of indirection through a virtual method table (vtable). This can prevent the JIT compiler from inlining those calls, leading to slightly higher call latency. It also makes CPU branch prediction less efficient, especially when the call target isn’t predictable.
Abstract classes, on the other hand, use virtual method dispatch, which is generally more JIT-friendly. If you mark overrides as sealed, the JIT can often inline those calls, resulting in better performance.
In most applications, this overhead is negligible. But in hot paths, like game loops, high-frequency trading systems, or large-scale data processing, the difference can add up.
Bottom line: prefer interfaces for flexibility, but favor abstract classes or structs in extreme performance scenarios.
Benchmark results (method calls per second):
Method | Mean | Notes |
---|---|---|
InterfaceCall | 589.7 ns | |
AbstractClassCall | 566.1 ns | ~4% faster |
[MemoryDiagnoser]
public class DispatchBenchmark
{
private IWorker _interfaceWorker = new InterfaceWorker();
private AbstractWorker _abstractWorker = new ConcreteWorker();
private volatile int _sink;
[Benchmark]
public void InterfaceCall()
{
int sum = 0;
for (int i = 0; i < 1000; i++)
sum += _interfaceWorker.DoWork();
_sink = sum;
}
[Benchmark]
public void AbstractClassCall()
{
int sum = 0;
for (int i = 0; i < 1000; i++)
sum += _abstractWorker.DoWork();
_sink = sum;
}
}
public interface IWorker
{
int DoWork();
}
public class InterfaceWorker : IWorker
{
public int DoWork() => 42;
}
public abstract class AbstractWorker
{
public abstract int DoWork();
}
In most apps, this difference is negligible, use interfaces for flexibility, abstract classes for shared logic. Only optimize for dispatch cost in true hot paths.
9. Why might you use an interface for mocking but an abstract class for inheritance?
Interfaces provide clean mocking surfaces without implementation baggage, while abstract classes share real business logic across derived types.
// Interface for mocking - clean contract
public interface IPaymentGateway
{
Task<PaymentResponse> ChargeAsync(decimal amount, string token);
}
// Abstract class for shared logic
public abstract class PaymentProcessorBase
{
protected readonly IPaymentGateway _gateway;
protected readonly ILogger _logger;
protected PaymentProcessorBase(IPaymentGateway gateway, ILogger logger)
{
_gateway = gateway;
_logger = logger;
}
protected async Task<bool> ValidateAndProcessAsync(PaymentRequest request)
{
if (!ValidateRequest(request)) return false;
var response = await _gateway.ChargeAsync(request.Amount, request.Token);
LogResult(response);
return response.Success;
}
protected abstract bool ValidateRequest(PaymentRequest request);
}
// Unit test - mock interface easily
var mockGateway = new Mock<IPaymentGateway>();
mockGateway.Setup(g => g.ChargeAsync(It.IsAny<decimal>(), It.IsAny<string>()))
.ReturnsAsync(new PaymentResponse { Success = true });
Interfaces enable testability, abstract classes enable code reuse.
10. How do constructors behave differently in interfaces and abstract classes?
Interfaces cannot have constructors, while abstract classes support constructors for initialization logic that derived classes must call.
// Interface - no constructor support
public interface IRepository
{
Task<T> GetByIdAsync<T>(int id);
}
// Abstract class - constructor for shared initialization
public abstract class RepositoryBase
{
protected readonly DbContext _context;
protected readonly ILogger _logger;
// Constructor ensures proper initialization
protected RepositoryBase(DbContext context, ILogger logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
protected virtual async Task<T> GetEntityAsync<T>(int id) where T : class
{
_logger.LogDebug("Fetching entity {Type} with ID {Id}", typeof(T).Name, id);
return await _context.Set<T>().FindAsync(id);
}
}
public class UserRepository : RepositoryBase
{
// Must call base constructor
public UserRepository(DbContext context, ILogger<UserRepository> logger)
: base(context, logger) { }
}
Abstract class constructors enforce proper dependency injection and initialization patterns across inheritance hierarchies.See other c-sharp posts