Skip to main content
10 Essential Patterns For c# and .NET Programmers With Examples. — Anselm Fowel
Technology

10 Essential Patterns For c# and .NET Programmers With Examples.

Updated
39 min read
389 views
Share:

10 Essential Patterns For c# and .NET Programmers With Examples.

C# is a popular programming language that is widely used for building applications on the .NET platform. As a C# and .NET programmer, it’s…

10 Essential Patterns For c# and .NET Programmers With Examples.

C# is a popular programming language that is widely used for building applications on the .NET platform. As a C# and .NET programmer, it’s essential to have a solid understanding of design patterns. Design patterns are reusable solutions to commonly occurring problems in software design. They help in creating code that is maintainable, scalable, and efficient. In this article, we will discuss ten essential patterns for C# and .NET programmers.

  1. Singleton Pattern

The Singleton pattern is one of the most popular patterns in C#. It is used when we need to ensure that only one instance of a class exists in the system. This pattern is useful when creating objects that maintain a single state throughout the system. A good example of this is the logger object that should only be created once, and all instances should share the same state.

Here’s an example of the Singleton pattern in C#

public class Logger
{
private static Logger instance;

// private constructor to prevent object creation from outside the class
private Logger() { }

public static Logger Instance
{
get
{
if (instance == null)
{
instance = new Logger();
}
return instance;
}
}

public void Log(string message)
{
Console.WriteLine(message);
}
}

In the example above, the Logger class implements the Singleton pattern. The class has a private constructor to prevent object creation from outside the class. The class also has a private static instance variable that holds the single instance of the Logger class. The public static Instance property is used to access the single instance of the Logger class. The get method of the Instance property checks if the instance variable is null and creates a new instance if it is null. Finally, the Log method is used to log messages to the console.

By implementing the Singleton pattern, we ensure that only one instance of the Logger class exists in the system, and all instances share the same state. This is useful when we want to log messages throughout the system and maintain a single log file.

Here’s an example of the Singleton pattern implemented on a User model in C#:

public class User
{
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }

private static User instance;

// private constructor to prevent object creation from outside the class
private User() { }

public static User Instance
{
get
{
if (instance == null)
{
instance = new User();
}
return instance;
}
}
}

In the example above, the User class implements the Singleton pattern. The class has private setters for the Name, Email, and Age properties to ensure these values can only be set once during object creation. The class also has a private static instance variable that holds the single instance of the User class. The public static Instance property is used to access the single instance of the User class. The get method of the Instance property checks if the instance variable is null and creates a new instance if it is null.

By implementing the Singleton pattern on the User model, we ensure that only one instance of the User class exists in the system, and all instances share the same state. This is useful when we want to have a single-user object throughout the system, and we want to maintain a single-user session. For example, in a web application, we can use the Singleton pattern to maintain a single user object across multiple requests.

2. Factory Pattern

The Factory pattern is another popular pattern in C#. It provides a way to create objects without exposing the creation logic to the client. The Factory pattern is useful when we need to create objects that have complex initialization logic or when we want to create objects of different types based on a specific condition.

Here’s an example of the Factory pattern implemented on a Logger class in C#:

public interface ILogger
{
void Log(string message);
}

public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}

public class FileLogger : ILogger
{
private readonly string fileName;

public FileLogger(string fileName)
{
this.fileName = fileName;
}

public void Log(string message)
{
using (var writer = new StreamWriter(fileName, true))
{
writer.WriteLine(message);
}
}
}

public static class LoggerFactory
{
public static ILogger GetLogger(string loggerType)
{
switch (loggerType)
{
case "console":
return new ConsoleLogger();
case "file":
return new FileLogger("log.txt");
default:
throw new ArgumentException("Invalid logger type.");
}
}
}

In the example above, we have defined an interface named ILogger that defines the Log method. We have also created two concrete classes that implement the ILogger interface: ConsoleLogger and FileLogger. The ConsoleLogger class logs messages to the console, while the FileLogger class logs messages to a file.

We have also created a static class named LoggerFactory that contains a GetLogger method. The GetLogger method takes a string argument that specifies the type of logger to create. The method then uses a switch statement to create and return an instance of the appropriate logger type.

By using the Factory pattern, we can decouple the client code from the implementation details of the logger classes. The client code only needs to know the type of logger it wants to use, and the Factory takes care of creating the appropriate logger instance.

Here’s an example of the Factory pattern implemented on a User class in C#:

public interface IUser
{
string Name { get; }
string Email { get; }
int Age { get; }
}

public class RegularUser : IUser
{
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
}

public class PremiumUser : IUser
{
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
public decimal Discount { get; set; }
}

public static class UserFactory
{
public static IUser CreateUser(bool isPremium)
{
if (isPremium)
{
return new PremiumUser { Discount = 0.1m };
}
else
{
return new RegularUser();
}
}
}

In the example above, we have defined an interface named IUser that defines the Name, Email, and Age properties. We have also created two concrete classes that implement the IUser interface: RegularUser and PremiumUser. The PremiumUser class has an additional Discount property.

We have also created a static class named UserFactory that contains a CreateUser method. The CreateUser method takes a boolean argument that specifies whether to create a RegularUser or a PremiumUser. The method then uses an if statement to create and return an instance of the appropriate user type.

By using the Factory pattern, we can encapsulate the user creation logic in a separate class and decouple the client code from the implementation details of the user classes. The client code only needs to know whether to create a RegularUser or a PremiumUser, and the Factory takes care of creating the appropriate user instance.

Here’s another example of the Factory pattern implemented on a Payment Gateway class in C#:

public interface IPaymentGateway
{
bool ProcessPayment(string cardNumber, decimal amount);
}

public class PayPalGateway : IPaymentGateway
{
public bool ProcessPayment(string cardNumber, decimal amount)
{
// Process payment via PayPal API
return true;
}
}

public class StripeGateway : IPaymentGateway
{
public bool ProcessPayment(string cardNumber, decimal amount)
{
// Process payment via Stripe API
return true;
}
}

public static class PaymentGatewayFactory
{
public static IPaymentGateway GetGateway(string gatewayType)
{
switch (gatewayType)
{
case "paypal":
return new PayPalGateway();
case "stripe":
return new StripeGateway();
default:
throw new ArgumentException("Invalid gateway type.");
}
}
}

In the example above, we have defined an interface named IPaymentGateway that defines the ProcessPayment method. We have also created two concrete classes that implement the IPaymentGateway interface: PayPalGateway and StripeGateway. The ProcessPayment method in each of these classes implements the payment processing logic using the respective payment gateway APIs.

We have also created a static class named PaymentGatewayFactory that contains a GetGateway method. The GetGateway method takes a string argument that specifies the type of payment gateway to use. The method then uses a switch statement to create and return an instance of the appropriate gateway type.

By using the Factory pattern, we can decouple the client code from the implementation details of the payment gateway classes. The client code only needs to know the type of payment gateway to use, and the Factory takes care of creating the appropriate gateway instance.

In all of the above examples, the Factory pattern helps to create objects without exposing the creation logic to the client code. This allows for greater flexibility, as the implementation details can be changed without affecting the client code. It also helps to reduce coupling between objects, making the code more maintainable and extensible.

3. Observer Pattern

The Observer pattern is used when we need to notify a group of objects about the state changes of another object. This pattern is useful when we want to create loosely coupled systems where objects are not directly dependent on each other.

Here’s an example of the Observer pattern implemented on a WeatherStation class in C#:

using System.Collections.Generic;

public interface IObserver
{
void Update(float temperature, float humidity, float pressure);
}

public interface ISubject
{
void RegisterObserver(IObserver observer);
void RemoveObserver(IObserver observer);
void NotifyObservers();
}

public class WeatherStation : ISubject
{
private float temperature;
private float humidity;
private float pressure;
private List<IObserver> observers;

public WeatherStation()
{
observers = new List<IObserver>();
}

public void RegisterObserver(IObserver observer)
{
observers.Add(observer);
}

public void RemoveObserver(IObserver observer)
{
observers.Remove(observer);
}

public void NotifyObservers()
{
foreach (IObserver observer in observers)
{
observer.Update(temperature, humidity, pressure);
}
}

public void SetMeasurements(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
NotifyObservers();
}
}

public class CurrentConditionsDisplay : IObserver
{
private float temperature;
private float humidity;

public void Update(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
Display();
}

public void Display()
{
Console.WriteLine("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
}
}

In the example above, we have defined an interface named IObserver that defines the Update method. We have also defined an interface named ISubject that defines the RegisterObserver, RemoveObserver, and NotifyObservers methods.

We have created a WeatherStation class that implements the ISubject interface. The WeatherStation class maintains the current temperature, humidity, and pressure readings, and provides methods to register and remove observers and notify observers of changes.

We have also created a CurrentConditionsDisplay class that implements the IObserver interface. The CurrentConditionsDisplay class maintains its own copies of the temperature and humidity readings and provides a Display method that is called whenever the Update method is called by the WeatherStation.

By using the Observer pattern, we can decouple the WeatherStation object from the CurrentConditionsDisplay object. The WeatherStation object only needs to know how to register, remove, and notify observers, and the CurrentConditionsDisplay object only needs to know how to update and display its own readings.

In this way, the Observer pattern promotes loose coupling between objects, making the code more maintainable and extensible.

Another example of the Observer pattern can be implemented on a StockMarket class in C#:

using System.Collections.Generic;

public interface IObserver
{
void Update(string stockSymbol, decimal price);
}

public interface ISubject
{
void RegisterObserver(IObserver observer);
void RemoveObserver(IObserver observer);
void NotifyObservers(string stockSymbol, decimal price);
}

public class StockMarket : ISubject
{
private Dictionary<string, decimal> stockPrices;
private List<IObserver> observers;

public StockMarket()
{
stockPrices = new Dictionary<string, decimal>();
observers = new List<IObserver>();
}

public void RegisterObserver(IObserver observer)
{
observers.Add(observer);
}

public void RemoveObserver(IObserver observer)
{
observers.Remove(observer);
}

public void NotifyObservers(string stockSymbol, decimal price)
{
foreach (IObserver observer in observers)
{
observer.Update(stockSymbol, price);
}
}

public void UpdateStockPrice(string stockSymbol, decimal price)
{
stockPrices[stockSymbol] = price;
NotifyObservers(stockSymbol, price);
}
}

public class StockTicker : IObserver
{
private string stockSymbol;
private decimal price;

public StockTicker(string stockSymbol)
{
this.stockSymbol = stockSymbol;
}

public void Update(string stockSymbol, decimal price)
{
if (this.stockSymbol == stockSymbol)
{
this.price = price;
Display();
}
}

public void Display()
{
Console.WriteLine("Stock " + stockSymbol + " price: " + price);
}
}

In the example above, we have defined an interface named IObserver that defines the Update method. We have also defined an interface named ISubject that defines the RegisterObserver, RemoveObserver, and NotifyObservers methods.

We have created a StockMarket class that implements the ISubject interface. The StockMarket class maintains a dictionary of current stock prices and provides methods to register and remove observers and notify observers of changes.

We have also created a StockTicker class that implements the IObserver interface. The StockTicker class maintains its own copy of the stock symbol and price and provides a Display method that is called whenever the Update method is called by the StockMarket.

By using the Observer pattern, we can decouple the StockMarket object from the StockTicker object. The StockMarket object only needs to know how to register, remove, and notify observers, and the StockTicker object only needs to know how to update and display its own stock symbol and price.

In this way, the Observer pattern promotes loose coupling between objects, making the code more maintainable and extensible.

4. Decorator Pattern

The Decorator pattern is used when we want to add functionality to an object at runtime without affecting other objects of the same class. This pattern is useful when we have a class that has some fixed functionality, but we want to add more functionality to it without modifying its source code.

Here’s an example of the Decorator pattern in C#:

using System;

// The component interface defines operations that can be altered by decorators.
public interface IComponent
{
void Operation();
}

// The concrete component provides the default implementation of the operations.
public class ConcreteComponent : IComponent
{
public void Operation()
{
Console.WriteLine("ConcreteComponent operation");
}
}

// The base decorator class follows the same interface as the component.
public abstract class BaseDecorator : IComponent
{
private IComponent component;

public BaseDecorator(IComponent component)
{
this.component = component;
}

public virtual void Operation()
{
component.Operation();
}
}

// The concrete decorators modify the behavior of the component in different ways.
public class ConcreteDecoratorA : BaseDecorator
{
public ConcreteDecoratorA(IComponent component) : base(component)
{
}

public override void Operation()
{
base.Operation();
Console.WriteLine("ConcreteDecoratorA operation");
}
}

public class ConcreteDecoratorB : BaseDecorator
{
public ConcreteDecoratorB(IComponent component) : base(component)
{
}

public override void Operation()
{
base.Operation();
Console.WriteLine("ConcreteDecoratorB operation");
}
}

// Client code can use any combination of components and decorators.
public class Client
{
public void Main()
{
// Create a concrete component and add decorators to it.
ConcreteComponent component = new ConcreteComponent();
ConcreteDecoratorA decoratorA = new ConcreteDecoratorA(component);
ConcreteDecoratorB decoratorB = new ConcreteDecoratorB(decoratorA);

// Execute the operation with all the decorators applied.
decoratorB.Operation();
}
}

In this example, we have defined an interface named IComponent that defines the Operation method. We have also created a ConcreteComponent class that implements the IComponent interface and provides a default implementation of the Operation method.

We have then created a BaseDecorator abstract class that implements the IComponent interface and maintains a reference to the wrapped component. We have also created two concrete decorators, ConcreteDecoratorA and ConcreteDecoratorB, that inherit from BaseDecorator and modify the behavior of the component by adding their own operations before or after the component’s default operation.

Finally, we have created a Client class that creates a ConcreteComponent object and decorates it with ConcreteDecoratorA and ConcreteDecoratorB. The client then executes the Operation method, which will call the default operation of the ConcreteComponent object, followed by the operations added by ConcreteDecoratorA and ConcreteDecoratorB.

By using the Decorator pattern, we can add new behavior to objects at runtime without modifying their underlying classes. This allows us to extend the functionality of objects in a flexible and reusable way, without introducing complex inheritance hierarchies or breaking existing code.

Another example of the Decorator pattern in C#:

using System;

// The component interface defines the basic operations of a document.
public interface IDocument
{
void Open();
void Close();
}

// The concrete component represents a plain text document.
public class TextDocument : IDocument
{
private string content;

public TextDocument(string content)
{
this.content = content;
}

public void Open()
{
Console.WriteLine("Opening text document...");
}

public void Close()
{
Console.WriteLine("Closing text document...");
}

public string Content
{
get { return content; }
set { content = value; }
}
}

// The base decorator class adds formatting to a document.
public abstract class DocumentDecorator : IDocument
{
protected IDocument document;

public DocumentDecorator(IDocument document)
{
this.document = document;
}

public virtual void Open()
{
document.Open();
}

public virtual void Close()
{
document.Close();
}
}

// The concrete decorators add different types of formatting to the document.
public class BoldDocumentDecorator : DocumentDecorator
{
public BoldDocumentDecorator(IDocument document) : base(document)
{
}

public override void Open()
{
base.Open();
Console.WriteLine("Adding bold formatting to the document...");
}

public override void Close()
{
base.Close();
Console.WriteLine("Removing bold formatting from the document...");
}
}

public class ItalicDocumentDecorator : DocumentDecorator
{
public ItalicDocumentDecorator(IDocument document) : base(document)
{
}

public override void Open()
{
base.Open();
Console.WriteLine("Adding italic formatting to the document...");
}

public override void Close()
{
base.Close();
Console.WriteLine("Removing italic formatting from the document...");
}
}

// Client code can use any combination of components and decorators.
public class Client
{
public void Main()
{
// Create a text document and add decorators to it.
TextDocument document = new TextDocument("Hello, world!");
BoldDocumentDecorator boldDecorator = new BoldDocumentDecorator(document);
ItalicDocumentDecorator italicDecorator = new ItalicDocumentDecorator(boldDecorator);

// Open and close the decorated document.
italicDecorator.Open();
Console.WriteLine(italicDecorator.Document.Content);
italicDecorator.Close();
}
}

In this example, we have defined an interface named IDocument that defines the Open and Close methods. We have also created a TextDocument class that implements the IDocument interface and represents a plain text document.

We have then created a base DocumentDecorator abstract class that implements the IDocument interface and maintains a reference to the wrapped document. We have also created two concrete decorators, BoldDocumentDecorator and ItalicDocumentDecorator, that inherit from DocumentDecorator and add bold and italic formatting to the document, respectively.

Finally, we have created a Client class that creates a TextDocument object and decorates it with BoldDocumentDecorator and ItalicDocumentDecorator. The client then opens and closes the decorated document, which will call the Open and Close methods of the decorators and the TextDocument object.

By using the Decorator pattern, we can add new functionality to objects at runtime without modifying their underlying classes. In this example, we have added formatting to a document without changing the TextDocument class, making the code more flexible and easier to maintain.

5. Command Pattern

The Command pattern is used when we want to encapsulate a request as an object and pass it to other objects to execute it. This pattern is useful when we want to implement undo-redo functionality or when we want to execute a set of commands in a specific order.

Here’s an example of the Command pattern in C#:

using System;

// The Receiver class defines the object that will receive the commands.
public class Light
{
public void TurnOn()
{
Console.WriteLine("Light turned on.");
}

public void TurnOff()
{
Console.WriteLine("Light turned off.");
}
}

// The Command interface declares the execution method for the commands.
public interface ICommand
{
void Execute();
}

// Concrete commands implement the Execute method and call the appropriate method on the receiver.
public class TurnOnLightCommand : ICommand
{
private Light light;

public TurnOnLightCommand(Light light)
{
this.light = light;
}

public void Execute()
{
light.TurnOn();
}
}

public class TurnOffLightCommand : ICommand
{
private Light light;

public TurnOffLightCommand(Light light)
{
this.light = light;
}

public void Execute()
{
light.TurnOff();
}
}

// The Invoker class holds the commands and calls their Execute methods.
public class Switch
{
private ICommand turnOnCommand;
private ICommand turnOffCommand;

public Switch(ICommand turnOnCommand, ICommand turnOffCommand)
{
this.turnOnCommand = turnOnCommand;
this.turnOffCommand = turnOffCommand;
}

public void TurnOn()
{
turnOnCommand.Execute();
}

public void TurnOff()
{
turnOffCommand.Execute();
}
}

// The Client class creates the commands and the invoker and executes them.
public class Client
{
public void Main()
{
Light light = new Light();
ICommand turnOnCommand = new TurnOnLightCommand(light);
ICommand turnOffCommand = new TurnOffLightCommand(light);
Switch lightSwitch = new Switch(turnOnCommand, turnOffCommand);

lightSwitch.TurnOn();
lightSwitch.TurnOff();
}
}

In this example, we have defined a Receiver class named Light that defines the object that will receive the commands. We have also defined a Command interface that declares the execution method for the commands, and two concrete commands, TurnOnLightCommand and TurnOffLightCommand, that implement the ICommand interface and call the appropriate method on the Light object.

We have then created an Invoker class named Switch that holds the commands and calls their Execute methods. The Switch class has two methods, TurnOn and TurnOff, that execute the corresponding commands.

Finally, we have created a Client class that creates the commands and the invoker and executes them. The client creates a Light object, creates the TurnOnLightCommand and TurnOffLightCommand objects with the Light object as their parameter, and creates a Switch object with the commands as its parameters. The client then executes the commands using the TurnOn and TurnOff methods of the Switch object.

By using the Command pattern, we can decouple the object that sends a request from the object that receives and executes it, which makes the code more flexible and easier to maintain. In this example, the client does not need to know anything about the Light object, it only needs to know about the commands and the invoker.

Here’s another example of the Command pattern in C#:

using System;

// The Receiver class defines the object that will receive the commands.
public class Car
{
private int speed = 0;

public void IncreaseSpeed(int amount)
{
speed += amount;
Console.WriteLine($"Speed increased to {speed} km/h.");
}

public void DecreaseSpeed(int amount)
{
speed -= amount;
Console.WriteLine($"Speed decreased to {speed} km/h.");
}
}

// The Command interface declares the execution method for the commands.
public interface ICommand
{
void Execute();
}

// Concrete commands implement the Execute method and call the appropriate method on the receiver.
public class IncreaseSpeedCommand : ICommand
{
private Car car;
private int amount;

public IncreaseSpeedCommand(Car car, int amount)
{
this.car = car;
this.amount = amount;
}

public void Execute()
{
car.IncreaseSpeed(amount);
}
}

public class DecreaseSpeedCommand : ICommand
{
private Car car;
private int amount;

public DecreaseSpeedCommand(Car car, int amount)
{
this.car = car;
this.amount = amount;
}

public void Execute()
{
car.DecreaseSpeed(amount);
}
}

// The Invoker class holds the commands and calls their Execute methods.
public class Driver
{
private ICommand increaseSpeedCommand;
private ICommand decreaseSpeedCommand;

public Driver(ICommand increaseSpeedCommand, ICommand decreaseSpeedCommand)
{
this.increaseSpeedCommand = increaseSpeedCommand;
this.decreaseSpeedCommand = decreaseSpeedCommand;
}

public void Accelerate()
{
increaseSpeedCommand.Execute();
}

public void Brake()
{
decreaseSpeedCommand.Execute();
}
}

// The Client class creates the commands and the invoker and executes them.
public class Client
{
public void Main()
{
Car car = new Car();
ICommand increaseSpeedCommand = new IncreaseSpeedCommand(car, 10);
ICommand decreaseSpeedCommand = new DecreaseSpeedCommand(car, 10);
Driver driver = new Driver(increaseSpeedCommand, decreaseSpeedCommand);

driver.Accelerate();
driver.Brake();
}
}

In this example, we have defined a Receiver class named Car that defines the object that will receive the commands. We have also defined a Command interface that declares the execution method for the commands, and two concrete commands, IncreaseSpeedCommand and DecreaseSpeedCommand, that implement the ICommand interface and call the appropriate method on the Car object.

We have then created an Invoker class named Driver that holds the commands and calls their Execute methods. The Driver class has two methods, Accelerate and Brake, that execute the corresponding commands.

Finally, we have created a Client class that creates the commands and the invoker and executes them. The client creates a Car object, creates the IncreaseSpeedCommand and DecreaseSpeedCommand objects with the Car object as their parameter, and creates a Driver object with the commands as its parameters. The client then executes the commands using the Accelerate and Brake methods of the Driver object.

Enjoying this article?

Get more like it in your inbox — practical engineering leadership, fintech, and AI. No spam, unsubscribe anytime.

By using the Command pattern, we can decouple the object that sends a request from the object that receives and executes it, which makes the code more flexible and easier to maintain. In this example, the client does not need to know anything about the Car object, it only needs to know about the commands and the invoker.

6. Adapter Pattern

The Adapter pattern is used when we want to convert the interface of one class into the interface expected by another class. This pattern is useful when we have two classes that cannot communicate directly due to incompatible interfaces.

Here’s an example of the Adapter pattern in C#:

using System;

// The Target interface defines the operations that the client can perform.
public interface ITarget
{
void Request();
}

// The Adaptee is the existing interface that needs to be adapted to the Target interface.
public class Adaptee
{
public void SpecificRequest()
{
Console.WriteLine("Adaptee: SpecificRequest called.");
}
}

// The Adapter implements the Target interface and adapts the Adaptee to it.
public class Adapter : ITarget
{
private readonly Adaptee _adaptee;

public Adapter(Adaptee adaptee)
{
_adaptee = adaptee;
}

public void Request()
{
_adaptee.SpecificRequest();
}
}

// The Client interacts with the Target interface.
public class Client
{
public void Main()
{
Adaptee adaptee = new Adaptee();
ITarget target = new Adapter(adaptee);

target.Request();
}
}

In this example, we have defined an Adaptee class that has a method called SpecificRequest. We want to adapt this interface to a Target interface that has a method called Request. To do this, we have defined an Adapter class that implements the Target interface and adapts the Adaptee to it.

The Adapter constructor takes an Adaptee object as a parameter, and the Request method of the Adapter class calls the SpecificRequest method of the Adaptee object. This way, we can use the Adaptee object through the Adapter object as if it were a Target object.

We have also defined a Client class that creates an Adaptee object, creates an Adapter object with the Adaptee object as its parameter, and uses the Target interface to call the Request method of the Adapter object.

By using the Adapter pattern, we can adapt an existing interface to a new interface without changing the existing interface or the code that uses it. This allows us to reuse existing code and makes the code more flexible and easier to maintain.

Here’s another example of the Adapter pattern in C#:

using System;

// The Target interface defines the operations that the client can perform.
public interface ITarget
{
void Send(string message);
}

// The Adaptee is the existing interface that needs to be adapted to the Target interface.
public class LegacyMessenger
{
public void SendMessage(string message)
{
Console.WriteLine($"LegacyMessenger: Sending message '{message}'");
}
}

// The Adapter implements the Target interface and adapts the Adaptee to it.
public class MessengerAdapter : ITarget
{
private readonly LegacyMessenger _legacyMessenger;

public MessengerAdapter(LegacyMessenger legacyMessenger)
{
_legacyMessenger = legacyMessenger;
}

public void Send(string message)
{
_legacyMessenger.SendMessage(message);
}
}

// The Client interacts with the Target interface.
public class Client
{
public void Main()
{
LegacyMessenger legacyMessenger = new LegacyMessenger();
ITarget target = new MessengerAdapter(legacyMessenger);

target.Send("Hello, world!");
}
}

In this example, we have a LegacyMessenger class that has a method called SendMessage, and we want to adapt this interface to a Target interface that has a method called Send. We have defined a MessengerAdapter class that implements the Target interface and adapts the LegacyMessenger to it.

The MessengerAdapter constructor takes a LegacyMessenger object as a parameter, and the Send method of the MessengerAdapter class calls the SendMessage method of the LegacyMessenger object. This way, we can use the LegacyMessenger object through the MessengerAdapter object as if it were a Target object.

We have also defined a Client class that creates a LegacyMessenger object, creates a MessengerAdapter object with the LegacyMessenger object as its parameter, and uses the Target interface to call the Send method of the MessengerAdapter object.

By using the Adapter pattern, we can adapt an existing interface to a new interface without changing the existing interface or the code that uses it. This allows us to reuse existing code and makes the code more flexible and easier to maintain.

7. Strategy Pattern

The Strategy pattern is used when we want to encapsulate different algorithms and allow the client to choose which algorithm to use at runtime. This pattern is useful when we have multiple algorithms that can be used to solve a problem, and we want to select the appropriate algorithm based on a specific condition.

Here’s an example of the Strategy pattern in C#:

using System;

// The Strategy interface defines the operations that each strategy must implement.
public interface IStrategy
{
void Execute(int[] data);
}

// The ConcreteStrategyA class implements a strategy that sorts an array in ascending order.
public class ConcreteStrategyA : IStrategy
{
public void Execute(int[] data)
{
Array.Sort(data);
Console.WriteLine("Sorted data in ascending order:");
foreach (int item in data)
{
Console.Write($"{item} ");
}
Console.WriteLine();
}
}

// The ConcreteStrategyB class implements a strategy that sorts an array in descending order.
public class ConcreteStrategyB : IStrategy
{
public void Execute(int[] data)
{
Array.Sort(data);
Array.Reverse(data);
Console.WriteLine("Sorted data in descending order:");
foreach (int item in data)
{
Console.Write($"{item} ");
}
Console.WriteLine();
}
}

// The Context class maintains a reference to a Strategy object and provides an interface to interact with it.
public class Context
{
private readonly IStrategy _strategy;

public Context(IStrategy strategy)
{
_strategy = strategy;
}

public void SetStrategy(IStrategy strategy)
{
_strategy = strategy;
}

public void SortData(int[] data)
{
_strategy.Execute(data);
}
}

// The Client interacts with the Context class to use a specific strategy.
public class Client
{
public void Main()
{
int[] data = { 4, 2, 1, 5, 3 };

Context context = new Context(new ConcreteStrategyA());
context.SortData(data);

context.SetStrategy(new ConcreteStrategyB());
context.SortData(data);
}
}

In this example, we have defined two strategies: ConcreteStrategyA and ConcreteStrategyB. ConcreteStrategyA sorts an array in ascending order, and ConcreteStrategyB sorts an array in descending order. Both strategies implement the IStrategy interface.

We have also defined a Context class that maintains a reference to a Strategy object and provides an interface to interact with it. The Context class has a method called SortData that calls the Execute method of the current strategy object.

We have defined a Client class that creates an array of integers, creates a Context object with a ConcreteStrategyA object as its parameter, and calls the SortData method of the Context object. We then set the strategy of the Context object to a ConcreteStrategyB object and call the SortData method again.

By using the Strategy pattern, we can encapsulate different algorithms and make them interchangeable without changing the code that uses them. This allows us to reuse existing code and makes the code more flexible and easier to maintain.

Here’s another example of the Strategy pattern in C#:

using System;

// The Strategy interface defines the operations that each strategy must implement.
public interface IShippingStrategy
{
decimal CalculateShippingCost(decimal weight, decimal distance);
}

// The ConcreteStrategyA class implements a strategy for ground shipping.
public class GroundShippingStrategy : IShippingStrategy
{
public decimal CalculateShippingCost(decimal weight, decimal distance)
{
decimal cost = 0;

// Calculate shipping cost based on weight and distance for ground shipping
// ...

return cost;
}
}

// The ConcreteStrategyB class implements a strategy for air shipping.
public class AirShippingStrategy : IShippingStrategy
{
public decimal CalculateShippingCost(decimal weight, decimal distance)
{
decimal cost = 0;

// Calculate shipping cost based on weight and distance for air shipping
// ...

return cost;
}
}

// The Context class maintains a reference to a ShippingStrategy object and provides an interface to interact with it.
public class ShippingContext
{
private readonly IShippingStrategy _shippingStrategy;

public ShippingContext(IShippingStrategy shippingStrategy)
{
_shippingStrategy = shippingStrategy;
}

public void SetShippingStrategy(IShippingStrategy shippingStrategy)
{
_shippingStrategy = shippingStrategy;
}

public decimal CalculateShippingCost(decimal weight, decimal distance)
{
return _shippingStrategy.CalculateShippingCost(weight, distance);
}
}

// The Client interacts with the ShippingContext class to calculate the shipping cost using a specific strategy.
public class Client
{
public void Main()
{
decimal weight = 10;
decimal distance = 100;

ShippingContext context = new ShippingContext(new GroundShippingStrategy());
decimal groundShippingCost = context.CalculateShippingCost(weight, distance);

context.SetShippingStrategy(new AirShippingStrategy());
decimal airShippingCost = context.CalculateShippingCost(weight, distance);

Console.WriteLine($"Ground shipping cost: {groundShippingCost}");
Console.WriteLine($"Air shipping cost: {airShippingCost}");
}
}

In this example, we have defined two strategies: GroundShippingStrategy and AirShippingStrategy. GroundShippingStrategy calculates the shipping cost for ground shipping based on weight and distance, and AirShippingStrategy calculates the shipping cost for air shipping based on weight and distance. Both strategies implement the IShippingStrategy interface.

We have also defined a ShippingContext class that maintains a reference to a ShippingStrategy object and provides an interface to interact with it. The ShippingContext class has a method called CalculateShippingCost that calls the CalculateShippingCost method of the current strategy object.

We have defined a Client class that creates two decimal variables, creates a ShippingContext object with a GroundShippingStrategy object as its parameter, and calls the CalculateShippingCost method of the ShippingContext object with the two decimal variables as parameters. We then set the strategy of the ShippingContext object to an AirShippingStrategy object and call the CalculateShippingCost method again with the same decimal variables.

By using the Strategy pattern, we can encapsulate different algorithms for calculating the shipping cost and make them interchangeable without changing the code that uses them. This allows us to reuse existing code and makes the code more flexible and easier to maintain.

8. Template Method Pattern

The Template Method pattern is used when we want to define the outline of an algorithm in a base class and allow the derived classes to implement the details. This pattern is useful when we have a set of steps that need to be followed in a specific order, but some of the steps can be implemented in different ways.

Here’s an example of the Template Method pattern in C#:

using System;

// The AbstractClass defines the template method that contains a series of method calls that define the algorithm.
public abstract class AbstractClass
{
public void TemplateMethod()
{
Operation1();
Operation2();
Operation3();
}

// The primitive operations are abstract methods that must be implemented by the subclasses.
protected abstract void Operation1();
protected abstract void Operation2();

// The hook operation is an optional method that can be overridden by the subclasses.
protected virtual void Operation3()
{
Console.WriteLine("AbstractClass.Operation3() called");
}
}

// The ConcreteClassA implements the primitive operations to carry out the algorithm defined in the AbstractClass.
public class ConcreteClassA : AbstractClass
{
protected override void Operation1()
{
Console.WriteLine("ConcreteClassA.Operation1() called");
}

protected override void Operation2()
{
Console.WriteLine("ConcreteClassA.Operation2() called");
}
}

// The ConcreteClassB implements the primitive operations to carry out a different algorithm defined in the AbstractClass.
public class ConcreteClassB : AbstractClass
{
protected override void Operation1()
{
Console.WriteLine("ConcreteClassB.Operation1() called");
}

protected override void Operation2()
{
Console.WriteLine("ConcreteClassB.Operation2() called");
}

protected override void Operation3()
{
Console.WriteLine("ConcreteClassB.Operation3() called");
}
}

// The Client uses the TemplateMethod to carry out the algorithm defined in the AbstractClass using a ConcreteClass object.
public class Client
{
public void Main()
{
AbstractClass abstractClass = new ConcreteClassA();
abstractClass.TemplateMethod();

Console.WriteLine();

abstractClass = new ConcreteClassB();
abstractClass.TemplateMethod();
}
}

In this example, we have defined an abstract class that defines a template method called TemplateMethod. TemplateMethod contains a series of method calls that define the algorithm. We have also defined two primitive operations, Operation1 and Operation2, that must be implemented by the subclasses, and a hook operation, Operation3, that is an optional method that can be overridden by the subclasses.

We have defined two concrete classes, ConcreteClassA and ConcreteClassB, that implement the primitive operations to carry out different algorithms defined in the AbstractClass.

We have also defined a Client class that creates an AbstractClass object using a ConcreteClassA object and calls the TemplateMethod to carry out the algorithm defined in the AbstractClass. We then create an AbstractClass object using a ConcreteClassB object and call the TemplateMethod again to carry out a different algorithm defined in the AbstractClass.

By using the Template Method pattern, we can define the skeleton of an algorithm in a base class and let the subclasses implement the details of the algorithm by implementing the primitive operations. This allows us to reuse existing code and makes the code more flexible and easier to maintain.

Another example of the Template Method pattern applied to a user model in C#:

using System;

// The AbstractUser class defines a template method called AuthenticateUser that contains a series of method calls that define the authentication process.
public abstract class AbstractUser
{
public void AuthenticateUser(string username, string password)
{
if (IsValidUsername(username))
{
if (IsValidPassword(password))
{
Console.WriteLine($"User {username} authenticated successfully.");
}
else
{
Console.WriteLine($"Invalid password for user {username}.");
}
}
else
{
Console.WriteLine($"Invalid username: {username}.");
}
}

// The IsValidUsername and IsValidPassword methods are abstract methods that must be implemented by the subclasses.
protected abstract bool IsValidUsername(string username);
protected abstract bool IsValidPassword(string password);
}

// The BasicUser class implements the IsValidUsername and IsValidPassword methods to define the authentication process for basic users.
public class BasicUser : AbstractUser
{
protected override bool IsValidUsername(string username)
{
// Basic users have usernames that are between 5 and 10 characters long.
return username.Length >= 5 && username.Length <= 10;
}

protected override bool IsValidPassword(string password)
{
// Basic users have passwords that are at least 8 characters long.
return password.Length >= 8;
}
}

// The AdminUser class implements the IsValidUsername and IsValidPassword methods to define the authentication process for admin users.
public class AdminUser : AbstractUser
{
protected override bool IsValidUsername(string username)
{
// Admin users have usernames that are between 8 and 15 characters long and start with "admin_".
return username.StartsWith("admin_") && username.Length >= 8 && username.Length <= 15;
}

protected override bool IsValidPassword(string password)
{
// Admin users have passwords that are at least 10 characters long and contain at least one digit and one uppercase letter.
bool hasDigit = false;
bool hasUppercase = false;

foreach (char c in password)
{
if (Char.IsDigit(c))
{
hasDigit = true;
}
else if (Char.IsUpper(c))
{
hasUppercase = true;
}

if (hasDigit && hasUppercase)
{
return true;
}
}

return false;
}
}

// The Client uses the AuthenticateUser method to authenticate a user.
public class Client
{
public void Main()
{
AbstractUser basicUser = new BasicUser();
basicUser.AuthenticateUser("johndoe", "password123");

Console.WriteLine();

AbstractUser adminUser = new AdminUser();
adminUser.AuthenticateUser("admin_jane", "Password123");
}
}

In this example, we have defined an AbstractUser class that defines a template method called AuthenticateUser that contains a series of method calls that define the authentication process. We have also defined two abstract methods, IsValidUsername and IsValidPassword, that must be implemented by the subclasses to define the validation rules for usernames and passwords.

We have defined two concrete classes, BasicUser and AdminUser, that implement the IsValidUsername and IsValidPassword methods to define the authentication process for basic users and admin users, respectively.

We have also defined a Client class that creates a BasicUser object and an AdminUser object and calls the AuthenticateUser method to authenticate the users using their usernames and passwords. The authentication process is defined by the template method in the AbstractUser class, which calls the IsValidUsername and IsValidPassword methods in the concrete classes to validate the input.

By using the Template Method pattern, we can encapsulate the common authentication process in the AbstractUser class and allow the concrete subclasses to define their own validation rules for usernames and passwords.

In this example, the BasicUser class defines validation rules that require a username to be between 5 and 10 characters long and a password to be at least 8 characters long. The AdminUser class defines validation rules that require a username to start with “admin_”, be between 8 and 15 characters long, and a password to be at least 10 characters long and contain at least one digit and one uppercase letter.

This approach allows us to reuse the common authentication process across multiple user types while still allowing each user type to have its own specific validation rules. It also makes it easy to add new user types in the future by simply creating a new concrete subclass that implements the IsValidUsername and IsValidPassword methods.

Overall, the Template Method pattern is a useful pattern for situations where we want to define a common algorithm or process that can be customized by subclasses while still maintaining the same basic structure.

9. State Pattern

The State pattern is used when we want to change the behavior of an object based on its state. This pattern is useful when we have an object that can be in multiple states, and we want to define the behavior of the object based on its state.

The State pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes. It does this by encapsulating each possible state of an object as a separate class that implements a common interface. When the object’s state changes, it switches to a new state class, which then defines its behavior.

One example of the State pattern in C# is a traffic light controller. In this example, we’ll create a TrafficLight class that can be in one of three states: Red, Yellow, or Green. Each state will be represented by a separate class that implements the ITrafficLightState interface.

Here’s how the TrafficLight class might look:

public class TrafficLight
{
private ITrafficLightState currentState;

public TrafficLight()
{
currentState = new RedState();
}

public void ChangeState(ITrafficLightState newState)
{
currentState = newState;
}

public void Request()
{
currentState.HandleRequest(this);
}
}

The TrafficLight class has a currentState field that holds the current state of the traffic light. It has a constructor that initializes the state to Red, and two methods: ChangeState, which allows the state to be changed, and Request, which delegates the request to the current state.

Next, we’ll create the ITrafficLightState interface, which defines the methods that each state class must implement:

public interface ITrafficLightState
{
void HandleRequest(TrafficLight light);
}

Each state class will implement this interface and define its own implementation of the HandleRequest method.

Here’s an example of the RedState class:

public class RedState : ITrafficLightState
{
public void HandleRequest(TrafficLight light)
{
Console.WriteLine("Traffic light is red");
light.ChangeState(new GreenState());
}
}

The RedState class implements the HandleRequest method by printing out that the traffic light is red and then changing the state to GreenState.

Here’s an example of the GreenState class:

public class GreenState : ITrafficLightState
{
public void HandleRequest(TrafficLight light)
{
Console.WriteLine("Traffic light is green");
light.ChangeState(new YellowState());
}
}

The GreenState class implements the HandleRequest method by printing out that the traffic light is green and then changing the state to YellowState.

Finally, here’s an example of the YellowState class:

public class YellowState : ITrafficLightState
{
public void HandleRequest(TrafficLight light)
{
Console.WriteLine("Traffic light is yellow");
light.ChangeState(new RedState());
}
}

The YellowState class implements the HandleRequest method by printing out that the traffic light is yellow and then changing the state to RedState.

With this implementation, we can create a TrafficLight object and change its state by calling its ChangeState method. When we call the Request method, it will delegate the request to the current state, which will handle it based on its specific behavior. This allows us to easily implement a traffic light controller that changes its behavior based on its internal state.

Another example of the State pattern in C# is a video player that can be in one of three states: Playing, Paused, or Stopped. Each state will be represented by a separate class that implements the IVideoPlayerState interface.

Here’s how the VideoPlayer class might look:

public class VideoPlayer
{
private IVideoPlayerState currentState;
private string currentVideo;

public VideoPlayer()
{
currentState = new StoppedState();
}

public void ChangeState(IVideoPlayerState newState)
{
currentState = newState;
}

public void Play(string video)
{
currentVideo = video;
currentState.Play(this);
}

public void Pause()
{
currentState.Pause(this);
}

public void Stop()
{
currentState.Stop(this);
}

public string GetCurrentVideo()
{
return currentVideo;
}
}

The VideoPlayer class has a currentState field that holds the current state of the video player. It has a constructor that initializes the state to Stopped, and four methods: ChangeState, which allows the state to be changed, Play, which starts playing a video, Pause, which pauses the currently playing video, Stop, which stops the currently playing video, and GetCurrentVideo, which returns the name of the currently playing video.

Next, we’ll create the IVideoPlayerState interface, which defines the methods that each state class must implement:

public interface IVideoPlayerState
{
void Play(VideoPlayer player);
void Pause(VideoPlayer player);
void Stop(VideoPlayer player);
}
//Each state class will implement this interface and define its own implementation of the Play, Pause, and Stop methods.

//Here's an example of the PlayingState class:
public class PlayingState : IVideoPlayerState
{
public void Play(VideoPlayer player)
{
Console.WriteLine("Video is already playing");
}

public void Pause(VideoPlayer player)
{
Console.WriteLine("Pausing video");
player.ChangeState(new PausedState());
}

public void Stop(VideoPlayer player)
{
Console.WriteLine("Stopping video");
player.ChangeState(new StoppedState());
}
}

The PlayingState class implements the Play, Pause, and Stop methods. When the Play method is called, it prints out that the video is already playing. When the Pause method is called, it prints out that it is pausing the video and changes the state to PausedState. When the Stop method is called, it prints out that it is stopping the video and changes the state to StoppedState.

Here’s an example of the PausedState class:

public class PausedState : IVideoPlayerState
{
public void Play(VideoPlayer player)
{
Console.WriteLine("Resuming video");
player.ChangeState(new PlayingState());
}

public void Pause(VideoPlayer player)
{
Console.WriteLine("Video is already paused");
}

public void Stop(VideoPlayer player)
{
Console.WriteLine("Stopping video");
player.ChangeState(new StoppedState());
}
}

10. Facade Pattern

The Facade pattern is used when we want to provide a simplified interface to a complex system. This pattern is useful when we have a complex system that consists of multiple subsystems, and we want to provide a simple interface to the client.

The Facade pattern is a structural design pattern that provides a simple interface to a complex system of classes, making it easier to use. It is often used to simplify the use of a library or framework by providing a high-level interface that abstracts away the details of the underlying implementation.

Let’s take an example of a car engine. A car engine consists of many complex parts such as the piston, crankshaft, cylinder, fuel injection system, and so on. To start the engine, you need to follow a complex set of steps such as turning the key in the ignition, pressing the accelerator pedal, and so on. The Facade pattern can be used to simplify the process of starting the engine by providing a simple interface that abstracts away the details of the underlying implementation.

In this example, we will create a CarEngineFacade class that provides a simple interface for starting and stopping the engine. The CarEngineFacade class will internally use the complex system of classes to start and stop the engine.

// Complex system of classes
class Piston
{
public void Move() { /* Move piston */ }
}

class Crankshaft
{
public void Rotate() { /* Rotate crankshaft */ }
}

class FuelInjectionSystem
{
public void InjectFuel() { /* Inject fuel */ }
}

// Facade class
class CarEngineFacade
{
private readonly Piston piston;
private readonly Crankshaft crankshaft;
private readonly FuelInjectionSystem fuelInjectionSystem;

public CarEngineFacade()
{
piston = new Piston();
crankshaft = new Crankshaft();
fuelInjectionSystem = new FuelInjectionSystem();
}

public void Start()
{
piston.Move();
crankshaft.Rotate();
fuelInjectionSystem.InjectFuel();
Console.WriteLine("Engine started");
}

public void Stop()
{
Console.WriteLine("Stopping engine");
}
}

In the above code, we have created a CarEngineFacade class that provides a Start method to start the engine and a Stop method to stop the engine. The CarEngineFacade class internally uses the complex system of classes such as Piston, Crankshaft, and FuelInjectionSystem to start and stop the engine.

To start the engine using the CarEngineFacade class, we can do:

CarEngineFacade engine = new CarEngineFacade();
engine.Start();

This will print out “Engine started” and internally start the engine by moving the piston, rotating the crankshaft, and injecting fuel using the FuelInjectionSystem.

To stop the engine, we can do:

engine.Stop();

This will print out “Stopping engine”.

Overall, the Facade pattern is useful when you want to simplify the use of a complex system of classes by providing a simple interface that abstracts away the details of the underlying implementation. By encapsulating the complexity of the system behind a simple facade, the Facade pattern can help make your code more modular and easier to maintain.

In conclusion, these ten essential patterns are crucial for C# and .NET programmers to master. Design patterns help in creating code that is maintainable, scalable, and efficient. By understanding and implementing these patterns, you can create high-quality software that meets the requirements of your clients and end-users.

Enjoyed this article? Share it with others!

Share:

Get new posts in your inbox

Occasional, practical notes on engineering leadership, fintech, and building with AI. No spam, unsubscribe anytime.

Comments (10)

Leave a Comment

Comments are moderated and will appear after review.

Uchenna Anozie

May 18, 2023

Small addition: the same techniques work about half as well when the team is fully remote across three timezones. The written-first shift really is the unlock.

Obinna Okoro

May 4, 2023

Junior eng, 9 months into my first fintech role, so a lot of this is above me, but the first bit made a concept click that I had been nodding along to in code review for months. Thanks for writing at a level that doesn't gatekeep newer engineers out.

Emeka Ibe

April 29, 2023

Wish I had read this three years ago.

Ibrahim Sani

April 29, 2023

Small addition: the same techniques work about half as well when the team is fully remote across three timezones. The written-first shift really is the unlock.

Funke Ogunbanjo

April 18, 2023

Broadly agree, but this framing assumes the org rewards this kind of leadership rather than punishing it. In a first-time manager situation the sequencing has to change.

Bola Olowu

April 17, 2023

Does the approach here still hold on a 4-engineer team? We're at the smaller end of that and some of these patterns feel like they need a dedicated platform team to run properly.

Sarah Hernandez

April 16, 2023

Does the approach here still hold on a 3-engineer team? We're at the awkward middle and some of these patterns feel like they need a dedicated SRE to run properly.

Ryan Lewis

April 16, 2023

The "10 Essential Patterns For c# and .NET Programmers With Examples." section is doing a lot of work.

Arthur Halliwell

April 13, 2023

Independent consultant, 6 due-diligence engagements a year across UK/EU fintechs. Reading this on the walk in — the middle bit lands, because I spent this week trying not to fumble monolith-to-services trap on my clients. Honestly the framing would have saved me at least one bad 1:1.

Ethan Clark

April 12, 2023

Question on the second half — how do you actually apply this with someone you personally hired? Struggling with that specific case on my service right now.

About the author

Anselm Fowel

Anselm Fowel

Chief Technology Officer & fintech architect. 16+ years leading engineering across AlliancePay, Mondu, Transalliance, Global Accelerex, and Fidelity Bank — writing here about engineering leadership, fintech architecture, and AI in production.

Read next