Creational patterns are about how objects are born. Structural patterns are about what happens next: how objects are put together to form larger structures, without turning the code into a tangle of dependencies.

They answer three questions:

  • How do we combine objects to create new functionality?
  • How do we offer a simple interface to a complex subsystem?
  • How do we add behavior to an object without changing it?

The family at a glance

Pattern What it does Use it when
Adapter (GoF) Makes incompatible interfaces work together You must use a class whose interface doesn't match the one your code expects
Bridge (GoF) Separates an abstraction from its implementation, so both can vary The same abstraction needs several implementations, for example different cloud providers
Composite (GoF) Composes objects into trees and treats them uniformly You have part-whole hierarchies: menus, folders, UI components
Decorator (GoF) Adds behavior to an object dynamically You want logging, caching or retries without touching the class
Facade (GoF) Gives a simple interface to a complex subsystem Callers need one entry point, not ten services
Flyweight (GoF) Shares common data among many objects You have many similar objects and memory matters
Proxy (GoF) Stands in for another object to control access to it Lazy loading, caching, access control or remote calls
Module Organizes code into encapsulated, reusable units Always, in some form: assemblies, namespaces, feature folders
Mixin Combines behaviors from several sources into one class You want shared behavior without deep inheritance hierarchies
Extension Object Lets objects gain new interfaces and functionality at runtime Only some instances need an optional capability

What modern .NET changes

The principle behind all of them, composition over inheritance, is older than .NET. What changed is that the platform now applies it for you in many places.

Middleware is a decorator pipeline. In ASP.NET Core each middleware wraps the next one, adds its behavior before and after, and decides whether to call it at all:

var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.Use(async (context, next) => { Console.WriteLine("Logging: start"); await next(context); Console.WriteLine("Logging: end"); }); app.Use(async (context, next) => { Console.WriteLine("Metrics: begin"); await next(context); Console.WriteLine("Metrics: done"); }); app.MapGet("/", () => "Hello"); app.Run(); 

The same shape shows up in HttpClient, where each DelegatingHandler wraps the next one.

Decorators fit the DI container. A classic decorator is a class that implements the same interface and wraps the real one. The built-in container has no dedicated API for decorators, so you register a factory (or use a library such as Scrutor):

public interface IOrderService { Task PlaceOrderAsync(int orderId, CancellationToken cancellationToken); } public class LoggingOrderService(IOrderService inner, ILogger<LoggingOrderService> logger) : IOrderService { public async Task PlaceOrderAsync(int orderId, CancellationToken cancellationToken) { logger.LogInformation("Placing order {OrderId}", orderId); await inner.PlaceOrderAsync(orderId, cancellationToken); logger.LogInformation("Order {OrderId} placed", orderId); } } 
builder.Services.AddScoped<OrderService>(); builder.Services.AddScoped<IOrderService>(sp => new LoggingOrderService( sp.GetRequiredService<OrderService>(), sp.GetRequiredService<ILogger<LoggingOrderService>>())); 

OrderService doesn't know it's being logged, and callers only see IOrderService.

Extension methods are lightweight adapters. When a legacy or third-party class doesn't match the interface your code expects, an extension method can hide the translation:

public class LegacyCustomer { public string FirstName { get; set; } = ""; public string LastName { get; set; } = ""; public string Email { get; set; } = ""; } public interface ICustomer { string GetFullName(); string GetContactInfo(); } public static class LegacyCustomerExtensions { public static ICustomer AsCustomer(this LegacyCustomer customer) => new LegacyCustomerAdapter(customer); private sealed class LegacyCustomerAdapter(LegacyCustomer customer) : ICustomer { public string GetFullName() => $"{customer.FirstName} {customer.LastName}"; public string GetContactInfo() => customer.Email; } } 
var legacy = new LegacyCustomer { FirstName = "Mario", LastName = "Rossi", Email = "mario.rossi@example.com" }; ICustomer customer = legacy.AsCustomer(); Console.WriteLine(customer.GetFullName()); // Mario Rossi 

C# 14 takes this further with extension members, which I cover in C# 14 Extension Members.

Proxies are often generated, not written. DispatchProxy in the base library, and libraries such as Castle DynamicProxy, create proxies at runtime. Many mocking and interception libraries are built on this idea.

Performance is part of the picture. Every wrapper is one more object and one more call. That's usually irrelevant, but deep decorator chains on hot paths are worth measuring. And the Flyweight idea is alive in modern APIs: Span<T> and Memory<T> let you work on slices of shared data without copying it.

A few rules that hold for all of them

  • Wrap, don't subclass. Composition keeps classes small and lets you combine behaviors freely.
  • Use what the platform gives you first: middleware, delegating handlers, extension methods, the DI container.
  • Async wrappers must stay async-correct: await the inner call and pass the CancellationToken along.
  • Keep chains short on hot paths, and measure before and after.
  • A facade simplifies, it doesn't own everything. If it keeps growing, it's becoming a god object.

Want the full story?

This is the short version. The complete article on Level Up Coding goes through how object composition evolved with the platform: middleware and pipeline architectures, hosted services, the Bridge pattern in multi-cloud applications, proxies and memory allocation, async and concurrent structures, cloud-native and distributed scenarios, generic type-safe adapters, testing and observability, and the trends ahead (source generators, functional approaches, Domain-Driven Design, event-driven architectures).

Read it here: Structural Patterns in .NET C#: Composing objects for modern applications.

Thanks for reading.