Defining and Implementing Interfaces — Contracts Without Implementation

An interface is a pure contract — a named set of method, property, and event signatures that a class agrees to implement, with absolutely no implementation code in the interface itself. When a class implements an interface, it promises to provide a working implementation of every member declared in that interface. Code that depends on the interface does not care which concrete class satisfies the contract — it only cares that the contract is fulfilled. This decoupling is the foundation of testable, maintainable ASP.NET Core architecture.

Defining an Interface

// Interface declaration — only signatures, no bodies
// Naming convention: prefix with capital I
public interface IEmailSender
{
    // Method signatures — no implementation, no body
    Task SendAsync(string to, string subject, string body);
    Task SendBulkAsync(IEnumerable<string> recipients, string subject, string body);

    // Property signature — implementing class must provide the property
    string SenderName { get; }
    bool   IsConfigured { get; }
}

// A second interface
public interface ITemplateRenderer
{
    string Render(string templateName, object model);
    bool   TemplateExists(string templateName);
}

// ── Implementing one interface ────────────────────────────────────────────
public class SmtpEmailSender : IEmailSender
{
    private readonly SmtpSettings _settings;

    public SmtpEmailSender(SmtpSettings settings) => _settings = settings;

    public string SenderName   => _settings.DisplayName;
    public bool   IsConfigured => !string.IsNullOrEmpty(_settings.Host);

    public async Task SendAsync(string to, string subject, string body)
    {
        // Real SMTP implementation
        Console.WriteLine($"[SMTP] Sending to {to}: {subject}");
        await Task.Delay(10);
    }

    public async Task SendBulkAsync(IEnumerable<string> recipients, string subject, string body)
    {
        foreach (var recipient in recipients)
            await SendAsync(recipient, subject, body);
    }
}

// ── Implementing multiple interfaces ──────────────────────────────────────
public class NotificationService : IEmailSender, ITemplateRenderer
{
    public string SenderName   => "Notification Service";
    public bool   IsConfigured => true;

    public async Task SendAsync(string to, string subject, string body)
        => Console.WriteLine($"Notification to {to}");

    public async Task SendBulkAsync(IEnumerable<string> recipients, string subject, string body)
    {
        foreach (var r in recipients) await SendAsync(r, subject, body);
    }

    public string Render(string templateName, object model)
        => $"Rendered: {templateName}";

    public bool TemplateExists(string templateName)
        => templateName.StartsWith("email_");
}
Note: C# allows a class to implement any number of interfaces, but can only inherit from one base class. This is the core flexibility advantage of interfaces over inheritance: you can mix and match contracts freely. A UserService can implement IUserService, IAuditLogger, and ICacheable simultaneously, satisfying three different contracts. In ASP.NET Core, this is how framework features like IDisposable, IAsyncDisposable, and IHostedService are implemented alongside your own interfaces.
Tip: Follow the Interface Segregation Principle — keep interfaces small and focused on a single responsibility. An IEmailSender that only contains email-sending methods is better than a fat INotificationService that includes email, SMS, push, and template rendering all in one interface. Small interfaces are easier to implement (especially in tests with mocks), easier to swap, and communicate intent more clearly. If an interface grows beyond 5–7 members, consider splitting it.
Warning: Interfaces cannot contain instance fields or constructors — only method, property, event, and indexer signatures (plus default method implementations in C# 8+). If you need shared state or a constructor parameter, use an abstract class instead. A common mistake is trying to add a field to an interface: public string DefaultSubject = "Hello"; — this is a compile error. Interfaces express what a type can do, not what data it holds.

Interface as a Type

// A variable declared as an interface type can hold ANY implementing object
IEmailSender sender = new SmtpEmailSender(settings);
await sender.SendAsync("alice@example.com", "Welcome", "Thanks for joining!");

// The concrete type can be swapped transparently
IEmailSender testSender = new FakeEmailSender();   // test double
await testSender.SendAsync("test@example.com", "Test", "Test body");

// Checking interface implementation
var service = new NotificationService();
if (service is IEmailSender emailSender)
    await emailSender.SendAsync("bob@example.com", "Hi", "Hello from interface check");

Common Mistakes

Mistake 1 — Adding implementation to an interface (before C# 8)

❌ Wrong — interfaces cannot have method bodies (without the default keyword in C# 8+):

public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body)
    {
        Console.WriteLine("Sending...");  // compile error in pre-C# 8!
    }
}

✅ Correct — only signatures in the interface; implementation in the class.

Mistake 2 — Not implementing all interface members

❌ Wrong — compile error if any interface member is missing:

public class SmtpEmailSender : IEmailSender
{
    // Missing SendBulkAsync and IsConfigured — compile error!
    public async Task SendAsync(string to, string subject, string body) { }
    public string SenderName => "SMTP";
}

✅ Correct — implement every member declared in the interface.

🧠 Test Yourself

A variable is declared as IEmailSender sender = new SmtpEmailSender(settings);. Can you call a method that exists on SmtpEmailSender but is NOT declared in IEmailSender?