Learn Minimal APIs from start to finish. Perfect for beginners.

Interfaces in C# explained

David Grace David Grace

An interface is a contract. It says what a type can do, without saying how it does it, and it is one of the most important building blocks in C#.

What is an interface

An interface has no implementation. It can consist of properties and methods, and these are the members that any implementing class must provide. With a method, you set the name, the parameters and the return type, but no implementation.

// IDiscountable.cs
public interface IDiscountable
{
	string Name { get; }
	bool IsOnSale { get; }

	decimal ApplyDiscount(decimal price);
}

ApplyDiscount has no body here. It is just a signature. Notice that none of the members have an access modifier. By default, if there isn't one in an interface, it's public.

Implementing an interface

To use IDiscountable, we implement it against a class. Every member within the interface must then be included in that class.

// Product.cs
public class Product : IDiscountable
{
	public string Name { get; set; } = string.Empty;
	public bool IsOnSale { get; set; }

	public decimal ApplyDiscount(decimal price)
	{
		if (!IsOnSale)
		{
		  return price;
		}
		return price * 0.9m;
	}
}

The colon means Product implements IDiscountable. It must provide a property matching Name, IsOnSale, and a method matching ApplyDiscount, or the code will not compile.

Unlike an interface, a class does need a public access modifier against each member.

Polymorphism with interfaces

The good thing about interfaces is that we can implement them against a different class too. Here, Category implements IDiscountable as well, but calculates the discount at a different rate.

// Category.cs
public class Category : IDiscountable
{
	public string Name { get; set; } = string.Empty;
	public bool IsOnSale { get; set; }

	public decimal ApplyDiscount(decimal price)
	{
		if (!IsOnSale)
		{
		  return price;
		}

		return price * 0.85m;
	}
}

Because both classes implement IDiscountable, we can add instances of both into the same collection type, then loop through it and work out the discounted price for each one.

var discountableItems = new List<IDiscountable>
{
	new Product { Name = "Wireless mouse", IsOnSale = true },
	new Category { Name = "Clearance", IsOnSale = true }
};
 
foreach (var item in discountableItems)
{
	var discountedPrice = item.ApplyDiscount(25m);
	Console.WriteLine($"{item.Name}: Discounted price = {discountedPrice}");
}

Each item in the list can have a different concrete type, Product or Category, but they are all treated as IDiscountable. Each one runs its own ApplyDiscount logic. This is polymorphism.

Multiple interfaces

A class can implement as many interfaces as it needs, separated by commas. Unlike base classes, which are limited to a single inheritance, there's no limit here.

// IShippable.cs
public interface IShippable
{
	string Name { get; }
	decimal Weight { get; }

	decimal CalculateShippingCost();
}
// Product.cs
public class Product : IDiscountable, IShippable
{
	public string Name { get; set; }
	public decimal Weight { get; set; }
	public bool IsOnSale { get; set; }

	public decimal ApplyDiscount(decimal price)
	{
		return price * 0.9m;
	}

	public decimal CalculateShippingCost()
	{
		return Weight * 0.5m;
	}
}

This is one of the reasons interfaces are so useful. If we want to work out the shipping cost for each product, we can create a new collection type of IShippable, add our products, then loop through and call CalculateShippingCost for each one. Any other class that implements IShippable can be added to that same list too.

var discountableItems = new List<IShippable>
{
	new Product {Name = "Wireless mouse", Weight = 0.3m },
	new Product {Name = "Keyboard", Weight = 0.2m },
};
 
foreach (var item in discountableItems)
{
	Console.WriteLine($"{item.Name}: Shipping Price = " +
		$"{item.CalculateShippingCost()}");
}

Default interface methods

So far, none of the methods shown have had a body, but it is possible to add a default implementation.

// IShippable.cs
public interface IShippable
{
	string Name { get; }

	decimal Weight { get; }

	decimal CalculateShippingCost()
	{
		// Default interface method
		return Weight * 0.5m;
	}
}

If a class doesn't implement CalculateShippingCost, it will use this default implementation instead, as is the case with DigitalProduct below.

// DigitalProduct.cs
public class DigitalProduct : IShippable
{
	public string Name => "Digital product";

	public decimal Weight => 0;
}

DigitalProduct implements IShippable, but there's no CalculateShippingCost method, so it takes the default implementation. A class can still override the default by providing its own implementation, as Product does above.

Access modifiers

Any member that doesn't include an access modifier is automatically public. There's also internal, which is available to anything in the same assembly, and protected, which is available within the interface and any implementing classes.

You can also include private, but this is only available within the interface itself. It's possible to combine protected and internal together too.

// IShippable.cs
public interface IShippable
{
	internal string Name { get; }

	protected decimal Weight { get; }

	decimal CalculateShippingCost()
	{
		return Weight * CalculateBaseRate();
	}

	private decimal CalculateBaseRate()
	{
		return 0.5m;
	}
}

private members must have a default implementation. They cannot be part of the public contract. CalculateBaseRate is only callable from other members inside IShippable, not from implementing classes or code outside it. You still cannot use private on a member with no body, since it only makes sense alongside a default implementation.

Static abstract members

You can also include static members in an interface.

// IDiscountRate.cs
public interface IDiscountRate
{
	static decimal Rate { get => 0.2m; }
}

Rate belongs to the type itself, not an instance, so you call it as IDiscountRate.Rate rather than on an object.

var discountRate = IDiscountRate.Rate;
 
Console.WriteLine($"Discount rate = {discountRate}");

You can also mark a static member as abstract. If you do that, there's no implementation for it, and any class implementing the interface needs to provide the value itself.

// IDiscountRate.cs
public interface IDiscountRate
{
	static abstract decimal Rate { get; }
}
// SummerSale.cs
public class SummerSale : IDiscountRate
{
	public static decimal Rate => 0.2m;
}

Explicit interface implementation

If you have multiple interfaces with the same method, you can use explicit interface implementation.

// IOrder.cs
public interface IOrder
{
	decimal GetTotal();
}
// IInvoice.cs
public interface IInvoice
{
	decimal GetTotal();
}

Order implements both interfaces, and we can provide a separate implementation of GetTotal for each one.

// Order.cs
public class Order : IOrder, IInvoice
{
	decimal IOrder.GetTotal()
	{
		return 100m;
	}

	decimal IInvoice.GetTotal()
	{
		return 120m;
	}
}

To call each version, create an instance of Order, then cast it to IOrder and to IInvoice.

var order = new Order();
IOrder orderView = order;
IInvoice invoiceView = order;
 
Console.WriteLine($"Order total = {orderView.GetTotal()}");
Console.WriteLine($"Invoice total = {invoiceView.GetTotal()}");

This outputs a different amount depending on which interface we call GetTotal through. The order total is 100, and the invoice total is 120. Explicit implementation means the member is no longer accessible directly through an Order instance, so you can only call it through the interface type. It's useful when two interfaces share a member name but mean different things.

Watch the video

Watch the video where we walk through what interfaces are, polymorphism, implementing multiple interfaces, default interface methods, access modifiers, static abstract members and explicit interface implementation.

Depedency injection

Interfaces are also central to dependency injection. Rather than a class depending directly on a concrete type, it depends on an interface, and the concrete implementation is registered against it in Program.cs. We cover this in more detail in our article on injecting services in ASP.NET Core.