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:

UML class diagram showing the INotificationService interface and its implementations (EmailNotificationService, SmsNotificationService), and how OrderService depends on INotificationService.

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#?

An interface in C# is a contract that defines what methods, properties, and events a class must implement, without specifying how they should work. It’s declared using the 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?

Use an interface when you need to define capabilities for unrelated classes, require multiple contracts on a single class (using 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#?

Yes, a class in C# can implement multiple interfaces using the syntax 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#?

Starting with C# 8.0, interfaces can have default method implementations, using the syntax 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#?

Interfaces make unit testing straightforward by allowing you to create mock implementations using frameworks like 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?

Interface segregation is the principle that classes shouldn’t be forced to implement interfaces they don’t use. Instead of one large interface with many methods, create smaller, focused interfaces like 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#?

Interfaces support loose coupling by allowing code to depend on abstractions (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#?

Interfaces are used with dependency injection by defining constructor parameters as interface types (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#?

Common interface mistakes include over-abstraction (creating interfaces for everything), leaky abstractions (letting implementation details like 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#?

Don’t use interfaces when you’ll only ever have one implementation with no testing requirements, as this adds unnecessary abstraction. Avoid interfaces for internal implementation details that won’t need alternative implementations. Also consider whether an abstract class might be more appropriate if you need to share code rather than just define a contract.
See other c-sharp posts