TL;DR:
Interface in C# - TL;DR
What Is It
- Definition: Pure contract defining what methods/properties a class must have
- Purpose: Define capabilities without implementation details
- Keywords:
interface
,class : IInterface
, implement multiple
Key Features
- No Implementation: Methods are abstract by default (C# 8+ allows default implementations)
- No State: Cannot have instance fields or constructors
- Access: All members implicitly public
- Inheritance: Classes can implement multiple interfaces
Interface Types
- Method Signatures: Define what methods must exist
- Properties: Define required properties (get/set)
- Events: Define event contracts
- Default Methods: C# 8+ allows method implementations in interfaces
When to Use
- Multiple contracts needed on same class
- Dependency injection and testing (easy mocking)
- Plugin/modular architecture
- Define capabilities across unrelated classes
- “Can-do” relationships (ISerializable, IComparable)
Benefits
- Flexibility: Multiple implementation support
- Testability: Easy to mock for unit tests
- Loose Coupling: Depend on contracts, not concrete classes
- Polymorphism: Treat different classes uniformly
Common Patterns
- Repository pattern (
IRepository<T>
) - Service contracts (
IEmailService
,IPaymentService
) - Strategy pattern (different algorithms, same interface)
- Dependency injection containers
Limitations
- No shared implementation (mostly)
- All members must be public
- Cannot contain instance state
You’re building an e-commerce platform and need to send notifications, order confirmations, shipping updates, promotional emails. Right now you’re using an email service, but next month the marketing team wants SMS notifications too. Later, they might want push notifications or Slack alerts.
If you hard-code your notification logic to a specific email service, you’ll need to rewrite chunks of your application every time requirements change. But what if you could write your business logic once and swap out the notification method without touching existing code?
This is where interfaces shine. They create contracts that let you build flexible, testable systems where components can be swapped, extended, or mocked without breaking existing functionality.
What Is an Interface in C#?
An interface in C# is a contract that defines what methods, properties, and events a class must implement, but not how they should work. Think of it as a job description that lists required skills without dictating how those skills are used.
The power of interfaces lies in their ability to separate what something does from how it does it. Your code can depend on the “what” (the interface) while remaining completely ignorant of the “how” (the specific implementation).
public interface INotificationService
{
Task SendAsync(string recipient, string subject, string message);
Task<bool> IsValidRecipientAsync(string recipient);
NotificationStatus GetDeliveryStatus(string messageId);
}
This interface defines a contract for sending notifications. Any class that implements INotificationService
must provide these three methods, but each implementation can handle them completely differently.
Interface Implementation: Creating Flexible Services
Let’s see how multiple classes can implement the same interface to provide different notification strategies:
public class EmailNotificationService : INotificationService
{
private readonly SmtpClient _smtpClient;
private readonly ILogger<EmailNotificationService> _logger;
public EmailNotificationService(SmtpClient smtpClient, ILogger<EmailNotificationService> logger)
{
_smtpClient = smtpClient;
_logger = logger;
}
public async Task SendAsync(string recipient, string subject, string message)
{
try
{
var mailMessage = new MailMessage("noreply@shop.com", recipient, subject, message)
{
IsBodyHtml = true
};
await _smtpClient.SendMailAsync(mailMessage);
_logger.LogInformation("Email sent successfully to {Recipient}", recipient);
}
catch (SmtpException ex)
{
_logger.LogError(ex, "Failed to send email to {Recipient}", recipient);
throw;
}
}
public async Task<bool> IsValidRecipientAsync(string recipient)
{
// Email-specific validation
return !string.IsNullOrEmpty(recipient) &&
recipient.Contains("@") &&
await CheckEmailDeliverabilityAsync(recipient);
}
public NotificationStatus GetDeliveryStatus(string messageId)
{
// Check email delivery status via SMTP logs or third-party service
return NotificationStatus.Delivered;
}
private async Task<bool> CheckEmailDeliverabilityAsync(string email)
{
// Implementation would check MX records, blacklists, etc.
await Task.Delay(100); // Simulate async call
return true;
}
}
public class SmsNotificationService : INotificationService
{
private readonly HttpClient _httpClient;
private readonly SmsConfig _config;
private readonly ILogger<SmsNotificationService> _logger;
public SmsNotificationService(HttpClient httpClient, SmsConfig config, ILogger<SmsNotificationService> logger)
{
_httpClient = httpClient;
_config = config;
_logger = logger;
}
public async Task SendAsync(string recipient, string subject, string message)
{
var smsPayload = new
{
to = recipient,
message = $"{subject}: {message}",
apiKey = _config.ApiKey
};
var response = await _httpClient.PostAsJsonAsync(_config.ApiEndpoint, smsPayload);
if (response.IsSuccessStatusCode)
{
_logger.LogInformation("SMS sent successfully to {Recipient}", recipient);
}
else
{
_logger.LogError("Failed to send SMS to {Recipient}. Status: {StatusCode}",
recipient, response.StatusCode);
throw new NotificationException($"SMS delivery failed: {response.StatusCode}");
}
}
public async Task<bool> IsValidRecipientAsync(string recipient)
{
// Phone number validation
return !string.IsNullOrEmpty(recipient) &&
recipient.All(c => char.IsDigit(c) || c == '+' || c == '-') &&
recipient.Length >= 10;
}
public NotificationStatus GetDeliveryStatus(string messageId)
{
// Query SMS provider's delivery status API
return NotificationStatus.Pending;
}
}
Dependency Injection and Loose Coupling
Here’s where interfaces really prove their worth. Your business logic can depend on the interface, not specific implementations:
public class OrderService
{
private readonly INotificationService _notificationService;
private readonly IOrderRepository _orderRepository;
private readonly ILogger<OrderService> _logger;
// Constructor injection - we don't care which notification service we get
public OrderService(
INotificationService notificationService,
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
_notificationService = notificationService;
_orderRepository = orderRepository;
_logger = logger;
}
public async Task<OrderResult> ProcessOrderAsync(CreateOrderRequest request)
{
try
{
// Validate the order
var validationResult = ValidateOrder(request);
if (!validationResult.IsValid)
{
return OrderResult.Failed(validationResult.Errors);
}
// Create and save the order
var order = new Order
{
CustomerId = request.CustomerId,
Items = request.Items,
TotalAmount = request.Items.Sum(i => i.Price * i.Quantity),
Status = OrderStatus.Confirmed,
CreatedAt = DateTime.UtcNow
};
await _orderRepository.SaveAsync(order);
// Send confirmation - works with any notification service
await _notificationService.SendAsync(
request.CustomerEmail,
"Order Confirmation",
$"Your order #{order.Id} has been confirmed. Total: ${order.TotalAmount:F2}"
);
_logger.LogInformation("Order {OrderId} processed successfully for customer {CustomerId}",
order.Id, request.CustomerId);
return OrderResult.Success(order);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order for customer {CustomerId}", request.CustomerId);
return OrderResult.Failed($"Order processing failed: {ex.Message}");
}
}
private ValidationResult ValidateOrder(CreateOrderRequest request)
{
var result = new ValidationResult();
if (request.Items == null || !request.Items.Any())
result.AddError("Order must contain at least one item");
if (string.IsNullOrEmpty(request.CustomerEmail))
result.AddError("Customer email is required");
return result;
}
}
Dependency Injection Configuration
In your startup configuration, you can easily switch between implementations:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Development: Use email notifications
if (Environment.IsDevelopment())
{
services.AddTransient<INotificationService, EmailNotificationService>();
}
// Production: Use SMS for critical notifications
else
{
services.AddTransient<INotificationService, SmsNotificationService>();
}
// Or use a factory pattern for multiple notification types
services.AddTransient<INotificationServiceFactory, NotificationServiceFactory>();
services.AddTransient<IOrderRepository, SqlOrderRepository>();
services.AddTransient<OrderService>();
}
}
To learn more about dependency injection in C#, check out our DIP vs DI vs IoC: Understanding Key Software Design Concepts post.
Interface Contracts Visualization
Here’s how the interface contracts and implementations relate:

OrderService
depends on the INotificationService
interface, which is implemented by both EmailNotificationService
and SmsNotificationService
for flexible notification handling.
Testing with Interfaces
Interfaces make unit testing straightforward by allowing you to create mock implementations:
[Test]
public async Task ProcessOrderAsync_ValidOrder_SendsNotification()
{
// Arrange
var mockNotificationService = new Mock<INotificationService>();
var mockOrderRepository = new Mock<IOrderRepository>();
var mockLogger = new Mock<ILogger<OrderService>>();
mockNotificationService
.Setup(x => x.SendAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(Task.CompletedTask);
mockOrderRepository
.Setup(x => x.SaveAsync(It.IsAny<Order>()))
.Returns(Task.CompletedTask);
var orderService = new OrderService(
mockNotificationService.Object,
mockOrderRepository.Object,
mockLogger.Object);
var request = new CreateOrderRequest
{
CustomerId = 123,
CustomerEmail = "customer@example.com",
Items = new List<OrderItem> { new OrderItem { Price = 10.00m, Quantity = 2 } }
};
// Act
var result = await orderService.ProcessOrderAsync(request);
// Assert
Assert.IsTrue(result.IsSuccess);
mockNotificationService.Verify(
x => x.SendAsync("customer@example.com", "Order Confirmation", It.IsAny<string>()),
Times.Once);
}
Interface Segregation: Keep It Focused
Don’t create monolithic interfaces that try to do everything. Instead, create focused interfaces that serve specific purposes:
// Bad - too many responsibilities
public interface IMegaService
{
Task SendEmailAsync(string to, string subject, string body);
Task SendSmsAsync(string phone, string message);
Task LogEventAsync(string eventName, object data);
Task SaveToFileAsync(string path, byte[] data);
Task<User> GetUserAsync(int id);
}
// Better - focused interfaces
public interface INotificationService
{
Task SendAsync(string recipient, string subject, string message);
}
public interface IEventLogger
{
Task LogEventAsync(string eventName, object data);
}
public interface IFileStorage
{
Task SaveAsync(string path, byte[] data);
Task<byte[]> ReadAsync(string path);
}
This approach makes your code more flexible, testable, and easier to understand. Classes can implement multiple focused interfaces rather than one bloated interface.
C# 8+ Default Interface Implementations
Modern C# allows you to provide default implementations in interfaces, which can be useful for adding new methods without breaking existing implementations:
public interface IAdvancedNotificationService : INotificationService
{
// New method with default implementation
Task<NotificationMetrics> GetMetricsAsync(DateTime startDate, DateTime endDate)
{
// Default implementation
return Task.FromResult(new NotificationMetrics
{
TotalSent = 0,
SuccessRate = 1.0,
Period = $"{startDate:yyyy-MM-dd} to {endDate:yyyy-MM-dd}"
});
}
}
Use default implementations sparingly, they’re best for evolutionary changes to existing interfaces rather than core functionality.
Common Mistakes to Avoid
Over-abstraction: Not every class needs an interface. If you’re only ever going to have one implementation and no testing requirements, you might be over-engineering.
Deep interface hierarchies: Avoid creating complex inheritance chains with interfaces. Keep them flat and focused.
Leaky abstractions: Don’t let implementation details sneak into your interface. If your interface mentions “SQL” or “HTTP”, it’s probably too specific.
Interface explosion: Don’t create an interface for every single class. Use interfaces where they add real value, flexibility, testability, or decoupling.
Interfaces are fundamental tools for building maintainable, testable C# applications. They help you write code that’s loosely coupled, easily extensible, and simple to test. The key is using them thoughtfully, create contracts that make sense for your domain and provide real value to your architecture.
When you find yourself wanting to swap implementations, mock dependencies for testing, or build plugin-style architectures, interfaces are your best friend. They’re the foundation of modern dependency injection patterns and essential for creating professional-grade C# applications.
Frequently Asked Questions
What is an interface in C#?
interface
keyword, as in public interface INotificationService
, and can contain method signatures, properties, events, and (since C# 8.0) default implementations. Interfaces enable multiple inheritance and decoupling in your code.When should I use an interface instead of an abstract class?
class MyClass : IInterface1, IInterface2
), or want to enable dependency injection and testing. Interfaces work best for defining capabilities that cross-cut class hierarchies, while abstract classes are better when you need shared implementation code or protected members.Can a class implement multiple interfaces in C#?
public class MyClass : IInterface1, IInterface2, IInterface3
. This allows a single class to fulfill multiple contracts simultaneously, enabling flexible and modular design. Unlike class inheritance, which is limited to a single base class, you can implement as many interfaces as needed.Can interfaces have default implementations in C#?
void Method() { /* implementation */ }
directly in the interface. This feature should be used sparingly and mainly for evolutionary changes to existing interfaces, not as a substitute for proper inheritance. Default implementations help add new methods to interfaces without breaking existing code.How do interfaces help with unit testing in C#?
Moq
or NSubstitute
. You can inject mock implementations of interfaces (e.g., Mock<INotificationService>
) to isolate the code under test and verify interactions without relying on real implementations. This enables testing components in isolation with predictable dependencies.What is interface segregation and why is it important?
INotificationService
, IEventLogger
, and IFileStorage
. This approach makes your code more maintainable, flexible, and allows classes to implement only what they need without unnecessary dependencies.How do interfaces support loose coupling in C#?
INotificationService
) rather than concrete implementations (EmailNotificationService
). This enables you to change implementations without modifying dependent code, as shown in the OrderService
example that works with any notification service. Loose coupling makes your code more maintainable, testable, and adaptable to changing requirements.How are interfaces used with dependency injection in C#?
INotificationService
) instead of concrete classes. The DI container then provides the appropriate implementation at runtime using configuration like services.AddTransient<INotificationService, EmailNotificationService>()
. This approach centralizes implementation selection, simplifies testing, and makes your code more modular.What are common mistakes to avoid when using interfaces in C#?
SQL
or HTTP
into interfaces), deep inheritance hierarchies, and interface explosion (too many tiny interfaces). Focus on creating interfaces that add real value by enabling multiple implementations, testing, or decoupling, and avoid creating an interface for every class.When should I not use an interface in C#?
abstract class
might be more appropriate if you need to share code rather than just define a contract.See other c-sharp posts
- C# Abstract Classes Explained: Practical Examples, Patterns, and Best Practices
- Abstract Class vs Interface in C#: with Real-World Examples, and When to Use Them
- C# Access Modifiers Explained: Complete Guide with Examples & Best Practices
- C# 14’s Alias Any Type: A Game-Changer for Code Readability?
- Array vs ArrayList in C#: Key Differences, Performance, and When to Use Each[+Examples]
- 5 Essential Benefits of Immutability in C# Programming
- Constructor Chaining in C#: Techniques and Best Practices
- C# Default Interface Methods vs Abstract Methods: Differences, Use Cases, and Best Practices
- Understanding Delegates vs Events in C#: When and How to Use Each
- Dictionary vs Hashtable in C#: Performance, Type Safety & When to Use Each
- Why Exposing Behavior Is Better Than Exposing Data in C#: Best Practices Explained
- C# Extension Methods: Add Functionality Without Inheritance or Wrappers
- High-Volume File Processing in C#: Efficient Patterns for Handling Thousands of Files
- Immutability vs Mutability in C#: Understanding the Differences
- C# Abstract Class vs Interface: 10 Real-World Questions You Should Ask
- Lambda Expressions in C#: How and When to Use Them [Practical Examples]
- Method Overloading vs Overriding in C#: Key Differences, and Examples
- C# Nullable Reference Types: How, When, and Why to Use or Disable Them
- C# 14’s params for Collections: Say Goodbye to Arrays!
- Primary Constructors in C# 12: Simplified Class Design for Classes, Structs, and Records
- Handling Cancellation in ASP.NET Core: From Browser to Database
- What Are the Risks of Exposing Public Fields or Collections in C#?
- Static Classes vs Singleton Pattern in C#: Pros, Cons, and Real-World Examples
- Task vs ValueTask in C#: Making the Right Choice for Performance
- Tuples vs Custom Types in C#: Clean Code or Lazy Hack?
- Abstract Classes in C#: When and How to Use Them Effectively [+Examples]
- C# Data Annotations: Complete Guide with Examples, Validation, and Best Practices
- C# Generics: A Complete Guide to Type-Safe, Reusable Code [With Examples]
- What is Boxing and Unboxing in C#?
- Understanding Deadlocks in C#: Causes, Examples, and Prevention
- Thread Safety in C#: Mastering Concurrency Without Race Conditions[With Examples]
- When to Use Static Classes in C#: Best Practices and Use Cases
- Why Private Fields Matter in C#: Protect Your Object’s Internal State