iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Interfaces

C# interfaces declare contracts: method signatures, properties, indexers, events that implementers must provide. Modern C# adds default implementations, static abstract members, and explicit interface implementations — making interfaces the recommended primary tool for polymorphism over inheritance.

Definition, DI, default methods, generic

EXAMPLE
// 1) Basic interface
public interface ILogger
{
    void Log(string message);
    void LogError(string message, Exception ex);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine($"[INFO] {message}");
    public void LogError(string message, Exception ex) => Console.Error.WriteLine($"[ERR] {message}: {ex}");
}

public class FileLogger : ILogger, IDisposable
{
    private readonly StreamWriter _writer;
    public FileLogger(string path) => _writer = new StreamWriter(path, append: true);
    public void Log(string message) => _writer.WriteLine($"[INFO] {message}");
    public void LogError(string message, Exception ex) => _writer.WriteLine($"[ERR] {message}: {ex}");
    public void Dispose() => _writer.Dispose();
}

// 2) DI — register and inject by interface
builder.Services.AddSingleton<ILogger, ConsoleLogger>();

public class OrderService
{
    private readonly ILogger _log;
    public OrderService(ILogger log) => _log = log;
    public void Place(Order o) { _log.Log($"placing {o.Id}"); /* … */ }
}

// 3) Properties + indexers + events
public interface IRepository<T>
{
    T this[int id] { get; set; }                           // indexer
    int Count { get; }                                       // read-only property
    event EventHandler<T> ItemAdded;
    void Add(T item);
    void Remove(int id);
}

// 4) Default interface methods (C# 8+)
public interface ILogger2
{
    void Log(LogLevel level, string message);
    void Info(string message)  => Log(LogLevel.Info, message);      // default
    void Warn(string message)  => Log(LogLevel.Warning, message);
    void Error(string message) => Log(LogLevel.Error, message);
    void Error(string message, Exception ex) => Log(LogLevel.Error, $"{message}: {ex}");
}

// Implementers only need to provide Log; Info/Warn/Error come for free.

public class ConsoleLogger2 : ILogger2
{
    public void Log(LogLevel level, string message) => Console.WriteLine($"[{level}] {message}");
    // No Info/Warn/Error needed.
}

// 5) Static abstract members (C# 11+) — interfaces with static contracts
public interface IParseable<T> where T : IParseable<T>
{
    static abstract T Parse(string s);
    static abstract bool TryParse(string? s, out T result);
}

public readonly struct Color : IParseable<Color>
{
    public byte R, G, B;
    public static Color Parse(string s) { /* … */ return default; }
    public static bool TryParse(string? s, out Color result) { result = default; return true; }
}

T ParseList<T>(string[] inputs) where T : IParseable<T>
{
    foreach (var i in inputs)
        T.Parse(i);                                          // call static abstract via T
    return default!;
}

// 6) Explicit interface implementation — disambiguate when types collide
public interface IControl  { void Paint(); }
public interface ISurface  { void Paint(); }

public class Renderer : IControl, ISurface
{
    void IControl.Paint() => Console.WriteLine("control");
    void ISurface.Paint() => Console.WriteLine("surface");
}

// Renderer has TWO Paint methods. Only callable via the specific interface:
IControl c = new Renderer(); c.Paint();  // 'control'
ISurface s = new Renderer(); s.Paint();  // 'surface'
// renderer.Paint() — compile error; must cast to which interface

// 7) Generic interfaces with constraints
public interface IComparable<in T>          { int CompareTo(T other); }
public interface IEqualityComparer<T>       { bool Equals(T? x, T? y); int GetHashCode(T obj); }
public interface IRepository<T> where T : class, IIdentifiable
{
    Task<T?> GetById(int id);
    Task<List<T>> List();
    Task Save(T entity);
}

// 8) Covariance + contravariance — <out T>, <in T>
public interface IProducer<out T> { T Produce(); }            // covariant out
public interface IConsumer<in T>  { void Consume(T x); }      // contravariant in

IProducer<Cat> cats = new CatBreeder();
IProducer<Animal> animals = cats;                              // works: Cat -> Animal

IConsumer<Animal> animalEater = new SafariConsumer();
IConsumer<Cat> catEater = animalEater;                         // works: Animal -> Cat

// 9) Marker interfaces — empty, used for type discrimination
public interface IAuditable {}
public interface ISerializable {}
// Usage:
if (entity is IAuditable)
    audit.Track(entity);

// Use ATTRIBUTES instead unless you specifically need polymorphism over the marker.

// 10) When to use interface vs abstract class
// • interface — pure contract, multiple inheritance, default methods for shared logic
// • abstract class — shared state, constructor invariants, protected helpers
// Modern guidance: prefer interfaces + DI; reach for abstract base only for genuine 'is-a' families

// 11) Patterns enabled by interfaces
// • Strategy: different algos behind the same contract
// • Repository: data access abstracted
// • Adapter: shape an external API into your interface
// • Decorator: wrap an implementation, add cross-cutting concerns
// • Mediator: components talk via a hub interface
// • Plugin systems: discover implementations at runtime

// 12) Source generators + interfaces
// • Refit generates HTTP clients from interface declarations
// • Mediator / MediatR + IRequest<TResponse>
// • Records implementing IEquatable<T> get auto value-equality

public interface IGitHubApi
{
    [Get("/users/{user}")]
    Task<User> GetUser(string user);
}
// var github = RestService.For<IGitHubApi>("https://api.github.com");

// 13) Common bugs
// • Adding a method to an interface — breaks every implementer unless you provide a default
// • Implementing two interfaces with the same method name + different semantics — use explicit
// • Default interface method shadowed by a base class method — explicit cast disambiguates
// • static abstract members requiring C# 11+ + .NET 7+ — runtime version mismatch
// • Marker interfaces growing into 'magic' contracts — switch to attributes
// • IDisposable forgotten on resource-holding implementations — wire 'using'
// • Interface segregation violated — big interface; refactor into smaller pieces
// • Returning interface from a method whose contract is the concrete shape — frustrates callers

Why it matters

Interfaces are how modern C# expresses contracts. Use them for DI, the strategy/repository/adapter patterns, and as the polymorphism primitive of choice. Default methods let you evolve an interface without breaking implementers; static abstract members enable generic factories; explicit implementation disambiguates collisions. Keep interfaces small — segregate when they grow.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
public interface IGreet { string Hello(); }
public class User : IGreet { public string Hello() => "hi"; }
Try it Yourself »

Discussion

Loading…