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_");
}
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.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.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.