C# & ASP.NET Topic
C# Interfaces & Abstract Classes
Interfaces - Pure Contracts
Basic Interface Syntax
csharp
// Simple interface
public interface ILogger
{
void Log(string message);
void LogError(string error);
}
// Implementing an interface
public class FileLogger : ILogger
{
public void Log(string message)
{
File.AppendAllText("log.txt", $"{DateTime.Now}: {message}\n");
}
public void LogError(string error)
{
File.AppendAllText("log.txt", $"{DateTime.Now}: ERROR - {error}\n");
}
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"LOG: {message}");
}
public void LogError(string error)
{
Console.WriteLine($"ERROR: {error}");
}
}
Interface Members
csharp
public interface IRepository<T>
{
// Properties
int Count { get; }
// Methods
void Add(T item);
void Remove(T item);
T GetById(int id);
IEnumerable<T> GetAll();
// Events
event Action<T> ItemAdded;
// Indexers
T this[int index] { get; }
}
// Implementing interface with all members
public class ProductRepository : IRepository<Product>
{
private List<Product> _products = new List<Product>();
public int Count => _products.Count;
public event Action<Product> ItemAdded;
public Product this[int index] => _products[index];
public void Add(Product item)
{
_products.Add(item);
ItemAdded?.Invoke(item);
}
public void Remove(Product item)
{
_products.Remove(item);
}
public Product GetById(int id)
{
return _products.FirstOrDefault(p => p.Id == id);
}
public IEnumerable<Product> GetAll()
{
return _products;
}
}
Multiple Interface Implementation
csharp
public interface ISerializable
{
string Serialize();
void Deserialize(string data);
}
public interface ICloneable<T>
{
T Clone();
}
public class Product : ISerializable, ICloneable<Product>
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Serialize()
{
return $"{Id},{Name},{Price}";
}
public void Deserialize(string data)
{
var parts = data.Split(',');
Id = int.Parse(parts[0]);
Name = parts[1];
Price = decimal.Parse(parts[2]);
}
public Product Clone()
{
return new Product { Id = this.Id, Name = this.Name, Price = this.Price };
}
}
Abstract Classes - Partial Implementation
Basic Abstract Class
csharp
// Abstract class with both abstract and concrete members
public abstract class Shape
{
// Abstract method - must be implemented by derived classes
public abstract double CalculateArea();
// Abstract property
public abstract string Name { get; }
// Concrete method - inherited as-is
public void Display()
{
Console.WriteLine($"Shape: {Name}, Area: {CalculateArea()}");
}
// Virtual method - can be overridden
public virtual string GetDescription()
{
return "This is a shape";
}
}
// Concrete implementations
public class Circle : Shape
{
public double Radius { get; set; }
public override string Name => "Circle";
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
public override string GetDescription()
{
return $"This is a circle with radius {Radius}";
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override string Name => "Rectangle";
public override double CalculateArea()
{
return Width * Height;
}
}
Abstract Classes with Constructors
csharp
public abstract class BankAccount
{
public string AccountNumber { get; }
public string Owner { get; }
public decimal Balance { get; protected set; }
// Abstract class can have constructors
protected BankAccount(string accountNumber, string owner, decimal initialBalance)
{
AccountNumber = accountNumber;
Owner = owner;
Balance = initialBalance;
}
// Abstract methods
public abstract void Deposit(decimal amount);
public abstract bool Withdraw(decimal amount);
public abstract decimal CalculateInterest();
// Concrete method
public string GetAccountInfo()
{
return $"Account: {AccountNumber}, Owner: {Owner}, Balance: {Balance:C}";
}
}
public class SavingsAccount : BankAccount
{
public decimal InterestRate { get; }
public SavingsAccount(string accountNumber, string owner, decimal initialBalance, decimal interestRate)
: base(accountNumber, owner, initialBalance)
{
InterestRate = interestRate;
}
public override void Deposit(decimal amount)
{
if (amount > 0)
{
Balance += amount;
}
}
public override bool Withdraw(decimal amount)
{
if (amount > 0 && Balance >= amount)
{
Balance -= amount;
return true;
}
return false;
}
public override decimal CalculateInterest()
{
return Balance * InterestRate;
}
}
public class CheckingAccount : BankAccount
{
public decimal OverdraftLimit { get; }
public CheckingAccount(string accountNumber, string owner, decimal initialBalance, decimal overdraftLimit)
: base(accountNumber, owner, initialBalance)
{
OverdraftLimit = overdraftLimit;
}
public override void Deposit(decimal amount)
{
if (amount > 0)
{
Balance += amount;
}
}
public override bool Withdraw(decimal amount)
{
if (amount > 0 && (Balance + OverdraftLimit) >= amount)
{
Balance -= amount;
return true;
}
return false;
}
public override decimal CalculateInterest()
{
return 0; // Checking accounts typically don't earn interest
}
}
Contract-Based Programming
Programming to Interfaces, Not Implementations
csharp
// Without interface - tightly coupled
public class OrderProcessor
{
private FileLogger _logger = new FileLogger(); // Tight coupling
public void Process(Order order)
{
_logger.Log($"Processing order {order.Id}");
// Process order...
}
}
// With interface - loosely coupled
public class OrderProcessor
{
private readonly ILogger _logger;
// Dependency injection through constructor
public OrderProcessor(ILogger logger)
{
_logger = logger;
}
public void Process(Order order)
{
_logger.Log($"Processing order {order.Id}");
// Process order...
}
}
// Usage - we can easily swap implementations
ILogger logger = new FileLogger();
// ILogger logger = new ConsoleLogger();
// ILogger logger = new DatabaseLogger();
var processor = new OrderProcessor(logger);
processor.Process(order);
Interface Segregation Principle
csharp
// BAD: One large interface
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
// Good: Segregated interfaces
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
// Classes implement only what they need
public class Human : IWorkable, IEatable, ISleepable
{
public void Work() => Console.WriteLine("Working...");
public void Eat() => Console.WriteLine("Eating...");
public void Sleep() => Console.WriteLine("Sleeping...");
}
public class Robot : IWorkable
{
public void Work() => Console.WriteLine("Working 24/7...");
// Doesn't need Eat() or Sleep()
}
Dependency Inversion Principle
The Principle
-
High-level modules should not depend on low-level modules. Both should depend on abstractions.
-
Abstractions should not depend on details. Details should depend on abstractions.
Without Dependency Inversion
csharp
// High-level module depends on low-level modules (violates DIP)
public class OrderService
{
private SqlDatabase _database; // Concrete dependency
private EmailNotifier _notifier; // Concrete dependency
public OrderService()
{
_database = new SqlDatabase();
_notifier = new EmailNotifier();
}
public void ProcessOrder(Order order)
{
_database.Save(order);
_notifier.SendEmail(order.CustomerEmail, "Order processed");
}
}
// Low-level modules
public class SqlDatabase
{
public void Save(Order order) { /* SQL implementation */ }
}
public class EmailNotifier
{
public void SendEmail(string to, string message) { /* Email implementation */ }
}
With Dependency Inversion
csharp
// Abstractions (interfaces)
public interface IOrderRepository
{
void Save(Order order);
}
public interface INotificationService
{
void Notify(string recipient, string message);
}
// High-level module depends on abstractions
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly INotificationService _notificationService;
// Constructor injection - dependencies are provided
public OrderService(IOrderRepository repository, INotificationService notificationService)
{
_repository = repository;
_notificationService = notificationService;
}
public void ProcessOrder(Order order)
{
_repository.Save(order);
_notificationService.Notify(order.CustomerEmail, "Order processed");
}
}
// Low-level modules depend on abstractions
public class SqlOrderRepository : IOrderRepository
{
public void Save(Order order)
{
// SQL-specific implementation
Console.WriteLine($"Saving order {order.Id} to SQL database");
}
}
public class MongoOrderRepository : IOrderRepository
{
public void Save(Order order)
{
// MongoDB-specific implementation
Console.WriteLine($"Saving order {order.Id} to MongoDB");
}
}
public class EmailNotificationService : INotificationService
{
public void Notify(string recipient, string message)
{
Console.WriteLine($"Sending email to {recipient}: {message}");
}
}
public class SmsNotificationService : INotificationService
{
public void Notify(string recipient, string message)
{
Console.WriteLine($"Sending SMS to {recipient}: {message}");
}
}
Practical Examples
Payment Processing System
csharp
// Contracts
public interface IPaymentGateway
{
PaymentResult ProcessPayment(PaymentRequest request);
bool ValidatePayment(string paymentId);
}
public interface IPaymentValidator
{
ValidationResult Validate(PaymentRequest request);
}
public interface IPaymentLogger
{
void LogPayment(PaymentRequest request, PaymentResult result);
}
// High-level module
public class PaymentProcessor
{
private readonly IPaymentGateway _paymentGateway;
private readonly IPaymentValidator _validator;
private readonly IPaymentLogger _logger;
public PaymentProcessor(
IPaymentGateway paymentGateway,
IPaymentValidator validator,
IPaymentLogger logger)
{
_paymentGateway = paymentGateway;
_validator = validator;
_logger = logger;
}
public PaymentResult Process(PaymentRequest request)
{
// Validate payment
var validationResult = _validator.Validate(request);
if (!validationResult.IsValid)
{
return PaymentResult.Failed(validationResult.Errors);
}
// Process payment
var result = _paymentGateway.ProcessPayment(request);
// Log result
_logger.LogPayment(request, result);
return result;
}
}
// Concrete implementations
public class CreditCardPaymentGateway : IPaymentGateway
{
public PaymentResult ProcessPayment(PaymentRequest request)
{
// Process credit card payment
Console.WriteLine("Processing credit card payment...");
return PaymentResult.Success(Guid.NewGuid().ToString());
}
public bool ValidatePayment(string paymentId)
{
// Validate with credit card processor
return true;
}
}
public class PayPalPaymentGateway : IPaymentGateway
{
public PaymentResult ProcessPayment(PaymentRequest request)
{
// Process PayPal payment
Console.WriteLine("Processing PayPal payment...");
return PaymentResult.Success(Guid.NewGuid().ToString());
}
public bool ValidatePayment(string paymentId)
{
// Validate with PayPal API
return true;
}
}
public class DatabasePaymentLogger : IPaymentLogger
{
public void LogPayment(PaymentRequest request, PaymentResult result)
{
// Log to database
Console.WriteLine($"Logged payment {result.PaymentId} to database");
}
}
// Usage
var creditCardGateway = new CreditCardPaymentGateway();
var validator = new BasicPaymentValidator();
var logger = new DatabasePaymentLogger();
var processor = new PaymentProcessor(creditCardGateway, validator, logger);
var result = processor.Process(paymentRequest);
Caching System with Strategy Pattern
csharp
// Cache strategy contract
public interface ICacheStrategy<T>
{
void Add(string key, T value, TimeSpan expiration);
bool TryGet(string key, out T value);
void Remove(string key);
void Clear();
}
// Concrete strategies
public class MemoryCacheStrategy<T> : ICacheStrategy<T>
{
private readonly Dictionary<string, (T Value, DateTime Expiration)> _cache = new();
public void Add(string key, T value, TimeSpan expiration)
{
_cache[key] = (value, DateTime.UtcNow + expiration);
}
public bool TryGet(string key, out T value)
{
if (_cache.TryGetValue(key, out var item) && item.Expiration > DateTime.UtcNow)
{
value = item.Value;
return true;
}
value = default(T);
return false;
}
public void Remove(string key)
{
_cache.Remove(key);
}
public void Clear()
{
_cache.Clear();
}
}
public class RedisCacheStrategy<T> : ICacheStrategy<T>
{
public void Add(string key, T value, TimeSpan expiration)
{
// Implementation using Redis
Console.WriteLine($"Storing {key} in Redis with expiration {expiration}");
}
public bool TryGet(string key, out T value)
{
// Implementation using Redis
Console.WriteLine($"Retrieving {key} from Redis");
value = default(T);
return false;
}
public void Remove(string key)
{
Console.WriteLine($"Removing {key} from Redis");
}
public void Clear()
{
Console.WriteLine("Clearing Redis cache");
}
}
// Cache service using strategy
public class CacheService<T>
{
private readonly ICacheStrategy<T> _cacheStrategy;
public CacheService(ICacheStrategy<T> cacheStrategy)
{
_cacheStrategy = cacheStrategy;
}
public T GetOrAdd(string key, Func<T> valueFactory, TimeSpan expiration)
{
if (_cacheStrategy.TryGet(key, out T value))
{
return value;
}
value = valueFactory();
_cacheStrategy.Add(key, value, expiration);
return value;
}
public async Task<T> GetOrAddAsync(string key, Func<Task<T>> valueFactory, TimeSpan expiration)
{
if (_cacheStrategy.TryGet(key, out T value))
{
return value;
}
value = await valueFactory();
_cacheStrategy.Add(key, value, expiration);
return value;
}
}
// Usage
ICacheStrategy<string> memoryStrategy = new MemoryCacheStrategy<string>();
ICacheStrategy<string> redisStrategy = new RedisCacheStrategy<string>();
var memoryCache = new CacheService<string>(memoryStrategy);
var redisCache = new CacheService<string>(redisStrategy);
// Can easily swap strategies without changing CacheService
Interface vs Abstract Class Comparison
When to Use Interfaces
csharp
// Use interfaces when:
// 1. You need to define a contract for multiple unrelated classes
public interface IExportable
{
byte[] Export();
}
public class PdfDocument : IExportable { /* ... */ }
public class ExcelReport : IExportable { /* ... */ }
public class ImageFile : IExportable { /* ... */ }
// 2. You need multiple inheritance of behavior
public class SmartDevice : IConnectable, IUpdatable, IDiagnosable
{
// Implements multiple interfaces
}
// 3. You want to define a capability or role
public interface IAuditable
{
DateTime CreatedAt { get; }
string CreatedBy { get; }
}
public interface ISoftDeletable
{
bool IsDeleted { get; }
void SoftDelete();
}
When to Use Abstract Classes
csharp
// Use abstract classes when:
// 1. You want to provide common base implementation
public abstract class EntityBase
{
public int Id { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
// Common functionality
public virtual bool IsNew() => Id == 0;
public abstract void Validate(); // Force derived classes to implement
}
public class User : EntityBase
{
public string Name { get; set; }
public override void Validate()
{
if (string.IsNullOrEmpty(Name))
throw new InvalidOperationException("Name is required");
}
}
// 2. You need to control object creation with constructors
public abstract class Vehicle
{
public string Make { get; }
public string Model { get; }
protected Vehicle(string make, string model)
{
Make = make;
Model = model;
}
public abstract void Start();
}
public class Car : Vehicle
{
public Car(string make, string model) : base(make, model) { }
public override void Start()
{
Console.WriteLine($"Starting {Make} {Model} car");
}
}
Advanced Patterns
Factory Pattern with Interfaces
csharp
public interface IShapeFactory
{
IShape CreateShape(ShapeType type);
}
public class ShapeFactory : IShapeFactory
{
public IShape CreateShape(ShapeType type)
{
return type switch
{
ShapeType.Circle => new Circle(),
ShapeType.Rectangle => new Rectangle(),
ShapeType.Triangle => new Triangle(),
_ => throw new ArgumentException($"Unknown shape type: {type}")
};
}
}
// Usage
IShapeFactory factory = new ShapeFactory();
IShape circle = factory.CreateShape(ShapeType.Circle);
Repository Pattern with Generic Interface
csharp
public interface IRepository<T> where T : class
{
T GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
void Update(T entity);
void Delete(T entity);
IEnumerable<T> Find(Expression<Func<T, bool>> predicate);
}
public abstract class RepositoryBase<T> : IRepository<T> where T : class
{
protected readonly DbContext _context;
protected RepositoryBase(DbContext context)
{
_context = context;
}
public virtual T GetById(int id)
{
return _context.Set<T>().Find(id);
}
public virtual IEnumerable<T> GetAll()
{
return _context.Set<T>().ToList();
}
public virtual void Add(T entity)
{
_context.Set<T>().Add(entity);
}
public virtual void Update(T entity)
{
_context.Set<T>().Update(entity);
}
public virtual void Delete(T entity)
{
_context.Set<T>().Remove(entity);
}
public virtual IEnumerable<T> Find(Expression<Func<T, bool>> predicate)
{
return _context.Set<T>().Where(predicate).ToList();
}
}
// Specific repository with additional operations
public interface IUserRepository : IRepository<User>
{
User GetByEmail(string email);
IEnumerable<User> GetActiveUsers();
}
public class UserRepository : RepositoryBase<User>, IUserRepository
{
public UserRepository(DbContext context) : base(context) { }
public User GetByEmail(string email)
{
return _context.Set<User>().FirstOrDefault(u => u.Email == email);
}
public IEnumerable<User> GetActiveUsers()
{
return Find(u => u.IsActive);
}
}
Best Practices
1. Interface Design
csharp
// Good interface design
public interface IOrderService
{
Order CreateOrder(CreateOrderRequest request);
Order GetOrder(int orderId);
void UpdateOrder(Order order);
}
// Avoid "God interfaces" - too many responsibilities
// public interface IOrderService // BAD
// {
// void CreateOrder();
// void ProcessPayment();
// void SendNotification();
// void GenerateReport();
// void UpdateInventory();
// }
2. Naming Conventions
csharp
// Interfaces start with 'I'
public interface IRepository { }
public interface ILogger { }
public interface INotificationService { }
// Abstract classes often end with 'Base'
public abstract class RepositoryBase { }
public abstract class ServiceBase { }
3. Dependency Injection
csharp
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Register interfaces with their implementations
services.AddScoped<ILogger, FileLogger>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IOrderService, OrderService>();
// This allows easy swapping of implementations
}
}
4. Testing with Interfaces
csharp
// Mock implementations for testing
public class MockLogger : ILogger
{
public List<string> LogMessages { get; } = new List<string>();
public void Log(string message)
{
LogMessages.Add(message);
}
}
[Test]
public void OrderProcessor_Should_Log_Message()
{
// Arrange
var mockLogger = new MockLogger();
var processor = new OrderProcessor(mockLogger);
var order = new Order();
// Act
processor.Process(order);
// Assert
Assert.That(mockLogger.LogMessages, Has.Count.EqualTo(1));
}
Key Differences Summary
| Aspect | Interface | Abstract Class |
|---|---|---|
| Inheritance | Multiple interfaces | Single abstract class |
| Implementation | No implementation | Partial implementation allowed |
| Members | Methods, properties, events, indexers | All class members + abstract members |
| Constructors | No constructors | Can have constructors |
| Access Modifiers | All members are public | Can have various access levels |
| Fields | No fields | Can have fields |
| When to Use | Defining contracts, multiple inheritance | Sharing common implementation |
This comprehensive guide shows how interfaces and abstract classes enable flexible, maintainable, and testable code through contract-based programming and dependency inversion!
Comments (0)
No comments yet. Be the first to comment!