Need a private training for your team? Request a private training
Not ready to book yet? Request an offer
The S.O.L.I.D principles were introduced by Robert C. Martin (also known as Uncle Bob) in 2000. The intention behind these principles is to make software designs more understandable, easier to maintain, and easier to extend. These principles are essential for every developer to know because they assist in writing better code and in better understanding code that was written with these principles in mind.
One might not believe it, but design patterns did not originate in software architecture. Instead, patterns were first described in the 1970s by a real 'bricks and mortar' architect. A pattern is defined as a reusable solution to a common problem, without dictating the exact implementation. This characteristic makes patterns an ideal learning tool and a powerful means for communicating design ideas. Of course, there are also anti-patterns—approaches that are frequently used but are better avoided.
We will start with a simple creational pattern, Singleton, and discuss its implementation in C#
Many times you need to provide a base class for developers to inherit from. Here we discuss building a more robust version using the template method pattern.
When developing software, you often find yourself continuously creating new objects. While the fundamental act of creating new objects is not inherently problematic, it can lead to design challenges or increased complexity. Creational patterns such as Factory Method can assist you in managing object creation in a way that is appropriate for the situation.
Strategy and Chain-Of-Responsibility are two behavioral patterns that assist in defining a family of algorithms, encapsulating each one, and making them interchangeable. By applying these patterns, you can allow the client to choose the algorithm that best suits their needs at runtime.
Iterators are a fundamental part of the .NET framework. They provide a generic way of navigating through collections. By using the yield keyword, you can create an iterator that returns a sequence of values. This approach is particularly useful when you need to asynchronously iterate over a collection.
The State pattern is a behavioral pattern that eliminates sprawling
if/switch blocks by delegating state-dependent behavior to
dedicated state objects. Each state encapsulates what the object should do
in that situation, and transitions are explicit and traceable. For more
complex workflows, a formal state machine library makes the model even
clearer and easier to visualize.
The Flyweight pattern is a structural pattern that reduces memory consumption by sharing as much data as possible between a large number of fine-grained objects. It separates intrinsic state (shared, immutable) from extrinsic state (caller-supplied, contextual), so that a single flyweight instance can serve many different contexts simultaneously.
The Composite pattern lets you treat individual objects and groups of objects uniformly by placing them in a tree structure. It is ideal whenever your domain naturally forms part-whole hierarchies — file systems, UI component trees, organizational charts, or mathematical expression trees.
The Interpreter pattern defines a grammar for a language and provides an interpreter to process sentences in that language. In .NET, expression trees are the canonical example: LINQ providers translate C# expression objects into SQL, JSON queries, or other target languages at runtime.
The Builder pattern separates the construction of a complex object from its
representation, so the same construction process can produce different results.
Modern .NET embraces fluent builder APIs throughout — from
WebApplication.CreateBuilder() to test data factories — making
this a pattern every developer encounters daily.
WebApplicationBuilder, IHostBuilder, and test data buildersThe Visitor pattern lets you add new operations to an object structure without modifying its classes. It solves the "expression problem": how do you add new behaviors to a fixed set of types? Modern C# offers two complementary approaches — classic double-dispatch Visitor and pattern-matching switch expressions.
Accept and Visit work togetherswitch expressionsdynamic dispatch to avoid repetitive Accept boilerplateThe Observer pattern defines a one-to-many dependency: when one object changes state, all its dependents are notified automatically. It is the foundation of event-driven programming in .NET — and understanding it deeply helps you avoid common pitfalls like memory leaks, unexpected notification order, and threading issues.
IObservable<T> / IObserver<T> interfaces as a push-based alternative
Where Observer is one-to-many, Mediator centralizes many-to-many communication.
Instead of objects knowing about each other, they all talk to a single mediator
that coordinates interactions. This dramatically reduces coupling in complex UIs
and service layers — and is the pattern behind INotifyPropertyChanged,
MediatR, and message bus architectures.
INotifyPropertyChanged — and avoiding the stringly-typed anti-patternnameof() and source generators to make property notifications refactor-safeThe Adapter pattern converts the interface of a class into another interface that clients expect. It lets you plug in components that would otherwise be incompatible — without modifying either side. This is one of the most frequently used patterns in real codebases, wherever you integrate third-party libraries or legacy code.
The Decorator pattern attaches additional behavior to an object dynamically by wrapping it in a decorator object that shares the same interface. It is a flexible alternative to subclassing — you can stack multiple decorators and combine behaviors at runtime rather than baking them into a class hierarchy.
Stream, ASP.NET Core middleware, and DI-based decorationThe Proxy pattern provides a surrogate that controls access to another object. Unlike Decorator — which adds behavior — Proxy controls whether and when the real object is reached. The three main flavors are virtual proxy (lazy loading), protection proxy (access control), and remote proxy (network transparency).
Facade is both a GOF pattern and a Microsoft pattern. Here we will discuss the differences and similarities between the two.
The Value Object pattern represents objects that are defined by their values rather than a unique identity, such as a Money or EmailAddress type. Value objects are immutable and compared by their properties, making your code more robust, intention-revealing, and less error-prone.
The repository pattern abstracts data access logic by providing a collection-like interface for accessing domain objects, isolating the domain layer from data access concerns. It centralizes CRUD operations, making the codebase more maintainable and testable. The specification pattern allows you to encapsulate business rules or query criteria into reusable, composable objects, making your code more expressive and testable. It helps separate the "what to query" from "how to query," often used with LINQ to filter entities.
The result pattern refers to a coding convention where methods return a special type (often named Result, OperationResult, etc.) that encapsulates both success/failure status and associated data or error messages. This pattern improves error handling by avoiding exceptions for flow control and encouraging explicit success/failure checks in the calling code
Most developers are not naturally skilled as graphic designers. This recognition has led to the development of design patterns that enable developers to concentrate on coding the application's behavior while allowing graphic designers to create compelling user interfaces. The cornerstone among these patterns is known as Model-View-Whatever (MVW), with Whatever being adaptable based on the specific technology in use. Grasping the MVW pattern is crucial, as it forms the foundation for developing both Windows and web applications.
When do you most need patterns? The answer is particularly when you're developing a framework on your own. Building a framework involves integrating new features while maintaining backward compatibility, a challenge that can be significantly simplified through the proper application of patterns. Therefore, in this final part of the training, we will construct a reusable library. During this process, we will encounter several challenges and address them by applying the appropriate patterns.
So, how can you become a better developer? One of the best ways is to learn about design patterns. Design Patterns provide reusable solutions for common software design challenges. In this training, we identify software design problems and explore how to address them using the most suitable Design Pattern. We will discuss creational, behavioral and structural patterns. And of course we will explore implementation choices in .NET. All examples and labs use the latest LTS version of .NET and Visual Studio. Labs also work with any recent .NET and VS version.
This course is intended for experienced programmers who are familiar with C# and have a working experience of .NET.