When do you need this? If you're only creating one type of object, plain Factory Method is enough. Abstract Factory is for when you need to create a whole set of related objects that must go together — like all the UI controls for Windows vs. all the controls for macOS. It's less common than Factory Method, but very powerful when you need it.
What: Sometimes you don't just need one object — you need a whole family of objects that work together. Think about building a cross-platform app: you need buttons, checkboxes, and text fields that all match the same style (Windows or Mac). Abstract Factory gives you one "factory" per family — ask the Windows factory for a button and you get a Windows button; ask the Mac factory and you get a Mac button. The key guarantee: everything from the same factory is compatibleYou'll never accidentally get a Windows button with a Mac checkbox. The factory ensures all products belong to the same family. This consistency guarantee is the main reason Abstract Factory exists — it's not just about creating objects, it's about creating objects that belong together..
When: Use it when your system needs to work with multiple sets of related objects, and mixing objects from different sets would cause problems. Database providers (SQL Server vs. PostgreSQL), UI themes (Dark vs. Light), cloud services (AWS vs. Azure) — all cases where you need a consistent family.
In C# / .NET: The framework already uses this pattern — DbProviderFactory creates database connections, commands, and parameters that all match the same database provider. EF Core's UseSqlServer() vs. UseNpgsql() is also Abstract Factory in disguise.
Quick Code:
AbstractFactory.cs
// The Abstract Factory — creates a family of related UI controls
public interface IUIFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
}
// Concrete Factory — Windows family
public sealed class WinFactory : IUIFactory
{
public IButton CreateButton() => new WinButton();
public ICheckbox CreateCheckbox() => new WinCheckbox();
}
// Concrete Factory — macOS family
public sealed class MacFactory : IUIFactory
{
public IButton CreateButton() => new MacButton();
public ICheckbox CreateCheckbox() => new MacCheckbox();
}
// Client code — works with ANY family, no concrete types visible
public class Dialog(IUIFactory factory)
{
public void Render()
{
var btn = factory.CreateButton(); // Family-consistent
var chk = factory.CreateCheckbox(); // Same family guaranteed
btn.Paint();
chk.Paint();
}
}
Section 2
Prerequisites
Before diving in:
Factory Method pattern — Abstract Factory is like Factory Method but for creating groups of objects instead of just one. Make sure you understand Factory Method first — this page builds on it.Interfaces & abstract classes — The whole pattern revolves around interfaces. Your code talks to "IButton" and "ICheckbox," never to "WinButton" or "MacCheckbox" directly.Dependency Injection — In modern .NET, you register the right factory in DI and the framework handles the rest. Knowing how services are registered and resolved will help the examples click.C# Generics — Modern factory implementations use <T> to avoid writing repetitive code. Basic familiarity with generic types is enough.
Section 3
Real-World Analogies
IKEA Furniture Collections
Walk into IKEA and pick a collection — say "Modern". You get a Modern chair, Modern table, and Modern lamp that all match in style, materials, and color palette. Pick "Victorian" instead, and every piece changes to ornate wood with brass accents. You never mix a Modern chair with a Victorian table — the collection guarantees consistency. The collection catalog is the Abstract Factory; each style line is a Concrete Factory; individual pieces are the Products.
IKEA
What it means
In code
Collection catalog
The menu of furniture families
Abstract Factory interface
"Modern" line
One specific family
Concrete Factory (e.g., ModernFactory)
Chair, Table, Lamp
The types of products in each family
Product interfaces (IButton, ICheckbox)
Modern Chair specifically
One product from one family
Concrete Product (ModernButton)
Showroom floor
The code that uses the products
Client code
Car Manufacturer Platform
BMW's "3 Series" factory line produces a 3-Series engine, 3-Series chassis, and 3-Series interior — all engineered to fit together precisely. The "7 Series" line produces a completely different set of parts. You can't bolt a 3-Series engine into a 7-Series chassis — the parts are family-specific. Each production line is a Concrete Factory creating a coordinated product family.
Real World
Code Concept
BMW production blueprint
ICarFactory
"3 Series" production line
ThreeSeriesFactory
Engine, Chassis, Interior
IEngine, IChassis, IInterior
3-Series Engine
ThreeSeriesEngine
Phone Ecosystem
Choose the Apple ecosystem and you get iPhone + AirPods + Apple Watch — all seamlessly syncing via iCloud. Choose Google and you get Pixel + Pixel Buds + Wear OS watch — syncing via Google services. Products within each ecosystem are designed to work together. Mixing AirPods with a Pixel technically works, but you lose the family-specific features (seamless pairing, ecosystem sync).
Restaurant Kitchen
An Italian kitchen produces Italian appetizer + Italian main + Italian dessert — flavors, ingredients, and presentation are consistent. A Japanese kitchen produces a completely different coordinated set. You don't order an Italian appetizer followed by a Japanese main — the Abstract Factory (restaurant concept) ensures each kitchen (Concrete Factory) delivers a complete, consistent meal experience.
Section 4
Core Pattern & UML
GoF Definition:"Provide an interface for creating families of related or dependent objects without specifying their concrete classes."
The key insight: Factory Method creates one product via one method. Abstract Factory creates an entire family of products via multiple create methods, guaranteeing all products in the family are compatible. This is a creational patternOne of the five GoF pattern categories — creational patterns abstract the instantiation process, making code independent of how objects are created. that operates on two dimensions: product type (button, checkbox) and product family (Windows, Mac).
UML Class Diagram
Participant Roles
Participant
Role
Responsibility
AbstractFactory
Interface
Declares create methods for each product in the family
ConcreteFactory
Implementation
Implements all create methods, producing products from one specific family
AbstractProduct
Interface
Declares the interface for a type of product (e.g., IButton)
ConcreteProduct
Implementation
A specific product belonging to one family (e.g., WinButton)
Client
Consumer
Uses only the abstract interfaces — never sees concrete classes
Key Difference from Factory Method
Factory Method has one create method producing one product type. Abstract Factory has multiple create methods producing a coordinated family of products. Think of it as a "factory of factory methodsA creational pattern where a single method defers object creation to subclasses — Abstract Factory combines multiple factory methods into one cohesive interface.."
Section 5
Code Implementations
ClassicAbstractFactory.cs
// ── Abstract Products ──────────────────────────────────
public interface IButton
{
string Paint();
}
public interface ICheckbox
{
string Toggle();
}
// ── Abstract Factory ───────────────────────────────────
public interface IUIFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
}
// ── Windows Family ─────────────────────────────────────
public sealed class WinButton : IButton
{
public string Paint() => "[Windows Button rendered]";
}
public sealed class WinCheckbox : ICheckbox
{
public string Toggle() => "[Windows Checkbox toggled]";
}
public sealed class WinFactory : IUIFactory
{
public IButton CreateButton() => new WinButton();
public ICheckbox CreateCheckbox() => new WinCheckbox();
}
// ── macOS Family ───────────────────────────────────────
public sealed class MacButton : IButton
{
public string Paint() => "[macOS Button rendered]";
}
public sealed class MacCheckbox : ICheckbox
{
public string Toggle() => "[macOS Checkbox toggled]";
}
public sealed class MacFactory : IUIFactory
{
public IButton CreateButton() => new MacButton();
public ICheckbox CreateCheckbox() => new MacCheckbox();
}
// ── Client ─────────────────────────────────────────────
public class Application
{
private readonly IButton _button;
private readonly ICheckbox _checkbox;
public Application(IUIFactory factory)
{
// All products come from the SAME family
_button = factory.CreateButton();
_checkbox = factory.CreateCheckbox();
}
public void Render()
{
Console.WriteLine(_button.Paint());
Console.WriteLine(_checkbox.Toggle());
}
}
// ── Usage ──────────────────────────────────────────────
var os = Environment.OSVersion.Platform;
IUIFactory factory = os == PlatformID.Win32NT
? new WinFactory()
: new MacFactory();
var app = new Application(factory);
app.Render();
DependencyInjection.cs
// ── Registration in Program.cs ─────────────────────────
var builder = WebApplication.CreateBuilder(args);
// Choose family based on config
var uiPlatform = builder.Configuration["UI:Platform"]; // "windows" or "mac"
if (uiPlatform == "mac")
{
builder.Services.AddSingleton<IUIFactory, MacFactory>();
builder.Services.AddTransient<IButton, MacButton>();
builder.Services.AddTransient<ICheckbox, MacCheckbox>();
}
else
{
builder.Services.AddSingleton<IUIFactory, WinFactory>();
builder.Services.AddTransient<IButton, WinButton>();
builder.Services.AddTransient<ICheckbox, WinCheckbox>();
}
var app = builder.Build();
// ── Consumer — no new keyword, no concrete types ───────
public class DialogService(IUIFactory factory)
{
public string RenderDialog()
{
var btn = factory.CreateButton();
var chk = factory.CreateCheckbox();
return $"{btn.Paint()} | {chk.Toggle()}";
}
}
// ── Endpoint ───────────────────────────────────────────
app.MapGet("/dialog", (DialogService svc) => svc.RenderDialog());
app.Run();
Why DI?
The composition rootThe single location in an application (typically Program.cs) where all dependencies are wired up — the only place that knows about concrete types. picks the concrete factory at startup based on configuration. Client code (DialogService) only ever sees IUIFactory — zero new calls, zero platform-specific references.
KeyedServices.cs
// ── .NET 8+ Keyed Services — register BOTH families ───
var builder = WebApplication.CreateBuilder(args);
// Register both families, keyed by platform string
builder.Services.AddKeyedSingleton<IUIFactory, WinFactory>("windows");
builder.Services.AddKeyedSingleton<IUIFactory, MacFactory>("mac");
var app = builder.Build();
// ── Resolve by key at injection point ──────────────────
public class CrossPlatformRenderer(
[FromKeyedServices("windows")] IUIFactory winFactory,
[FromKeyedServices("mac")] IUIFactory macFactory)
{
public string RenderForPlatform(string platform)
{
var factory = platform == "mac" ? macFactory : winFactory;
var btn = factory.CreateButton();
var chk = factory.CreateCheckbox();
return $"{btn.Paint()} | {chk.Toggle()}";
}
}
// ── Or resolve dynamically from IServiceProvider ───────
app.MapGet("/render/{platform}", (
string platform,
IServiceProvider sp) =>
{
var factory = sp.GetRequiredKeyedService<IUIFactory>(platform);
return factory.CreateButton().Paint();
});
app.Run();
Keyed Services Advantage
Both families coexist in the container simultaneously. No if/else at startup, no custom factory resolver — the DI container handles family selection by key. This is the cleanest modern .NET approach.
Section 6
Junior vs Senior Implementation
Problem Statement
Build a cross-platform UI rendering system that supports Windows, macOS, and Linux. Each platform has its own Button, Checkbox, and TextBox controls. New platforms (e.g., web/WASM) get added every quarter. Products from the same platform must be used together — mixing Win buttons with Mac checkboxes causes rendering crashes.
How a Junior Thinks
"I'll just add a switch statement for each control type. One class, everything in one place. Easy!"
GodFactory.cs
// ❌ God Factory — all families crammed into one class
public class UIFactory
{
private readonly string _platform;
public UIFactory(string platform) => _platform = platform;
public object CreateButton() => _platform switch // ❌ Returns object!
{
"windows" => new WinButton(),
"mac" => new MacButton(),
"linux" => new LinuxButton(),
_ => throw new NotSupportedException()
};
public object CreateCheckbox() => _platform switch // ❌ Duplicated switch
{
"windows" => new WinCheckbox(),
"mac" => new MacCheckbox(),
"linux" => new LinuxCheckbox(),
_ => throw new NotSupportedException()
};
public object CreateTextBox() => _platform switch // ❌ Third switch!
{
"windows" => new WinTextBox(),
"mac" => new MacTextBox(),
"linux" => new LinuxTextBox(),
_ => throw new NotSupportedException()
};
// ❌ Every new product = another switch method
// ❌ Every new family = modify EVERY method
// ❌ Returns object — casting required everywhere
// ❌ Nothing prevents mixing: WinButton + MacCheckbox
}
Problems
God Factory — Single Point of Failure
Every family and every product type lives in one class. Adding a new platform (Web) means modifying CreateButton(), CreateCheckbox(), AND CreateTextBox(). One typo breaks all platforms. Violates OCPOpen/Closed Principle — classes should be open for extension but closed for modification. Add new behavior via new classes, not by editing existing ones..
No Family Consistency Guarantee
Returns object — callers must cast.
Nothing prevents (WinButton)factory.CreateButton() followed by (MacCheckbox)factory.CreateCheckbox().
Mixed families cause rendering crashes that only surface at runtime.
Untestable + Tightly Coupled
Can't mock the factory — it's a concrete class with new everywhere. Can't test "does this dialog render correctly with Mac controls?" without instantiating real Mac controls. No interface = no substitution.
How a Senior Thinks
"Each platform is a family — a sealed factory class that guarantees all products work together. The DI container resolves the right factory by key. Adding a new platform = one new class + one registration line. Zero changes to existing code."
IUIFactory.cs
// Abstract products — what every platform must provide
public interface IButton { string Paint(); }
public interface ICheckbox { string Toggle(); }
public interface ITextBox { string Render(); }
// Abstract Factory — creates a family of related controls
public interface IUIFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
ITextBox CreateTextBox();
}
// Concrete products — Windows family
public sealed class WinButton : IButton
{
public string Paint() => "[Win32 Button]";
}
public sealed class WinCheckbox : ICheckbox
{
public string Toggle() => "[Win32 Checkbox]";
}
public sealed class WinTextBox : ITextBox
{
public string Render() => "[Win32 TextBox]";
}
// Concrete products — macOS family
public sealed class MacButton : IButton
{
public string Paint() => "[Cocoa NSButton]";
}
public sealed class MacCheckbox : ICheckbox
{
public string Toggle() => "[Cocoa NSCheckbox]";
}
public sealed class MacTextBox : ITextBox
{
public string Render() => "[Cocoa NSTextField]";
}
WinFactory.cs
// ✅ Sealed — no unintended inheritance
// ✅ Each method returns a product from the SAME family
public sealed class WinFactory : IUIFactory
{
public IButton CreateButton() => new WinButton();
public ICheckbox CreateCheckbox() => new WinCheckbox();
public ITextBox CreateTextBox() => new WinTextBox();
}
// ✅ Adding Linux? Create LinuxFactory — ZERO changes here
public sealed class LinuxFactory : IUIFactory
{
public IButton CreateButton() => new LinuxButton();
public ICheckbox CreateCheckbox() => new LinuxCheckbox();
public ITextBox CreateTextBox() => new LinuxTextBox();
}
MacFactory.cs
public sealed class MacFactory : IUIFactory
{
public IButton CreateButton() => new MacButton();
public ICheckbox CreateCheckbox() => new MacCheckbox();
public ITextBox CreateTextBox() => new MacTextBox();
}
// Client code — works with ANY family via abstraction
public class Dialog(IUIFactory factory)
{
public void Render()
{
var btn = factory.CreateButton(); // Family-consistent ✅
var chk = factory.CreateCheckbox(); // Same family ✅
var txt = factory.CreateTextBox(); // Guaranteed compatible ✅
Console.WriteLine($"{btn.Paint()} | {chk.Toggle()} | {txt.Render()}");
}
}
Program.cs
// Register each platform family by key (.NET 8+ keyed services)
builder.Services.AddKeyedSingleton<IUIFactory, WinFactory>("windows");
builder.Services.AddKeyedSingleton<IUIFactory, MacFactory>("mac");
builder.Services.AddKeyedSingleton<IUIFactory, LinuxFactory>("linux");
// Resolve dynamically based on runtime platform detection
app.MapGet("/render/{platform}", (string platform, IServiceProvider sp) =>
{
var factory = sp.GetRequiredKeyedService<IUIFactory>(platform);
var dialog = new Dialog(factory);
dialog.Render(); // All controls from the SAME family
return Results.Ok(new
{
Button = factory.CreateButton().Paint(),
Checkbox = factory.CreateCheckbox().Toggle(),
TextBox = factory.CreateTextBox().Render()
});
});
// Adding Web/WASM platform:
// 1. Create WasmButton, WasmCheckbox, WasmTextBox (products)
// 2. Create WasmFactory : IUIFactory (factory)
// 3. Add one line: AddKeyedSingleton<IUIFactory, WasmFactory>("wasm")
// ZERO changes to existing factories, Dialog, or endpoints
Design Decisions
Family Consistency by Construction
Each factory is a sealed classA C# class that cannot be inherited. Prevents unintended subclassing and signals that the class is complete as-is. that only creates products from ONE family. It's physically impossible to mix a WinButton with a MacCheckbox — the type system enforces it.
Open/Closed via Keyed DI
Adding a new platform = one new factory class + one AddKeyedSingleton line. Zero changes to existing factories, Dialog, or any consumer. The factory interface never changes.
Interface-Only Dependencies
Dialog depends on IUIFactory — never sees concrete types. In unit tests, inject an InMemoryFactory that returns test doubles. Fast, deterministic, no real platform controls needed.
Section 7
Evolution of Abstract Factory in .NET
Abstract Factory has evolved dramatically across 20+ years of .NET — from hand-rolled abstract classes to DI-native keyed services. Understanding this history helps you recognize legacy code and choose the right modern approach.
ADO.NETActiveX Data Objects for .NET — Microsoft's data access technology providing database-independent data access through providers. shipped with provider-specific classes — SqlConnection, OleDbConnection, etc. — but no abstract factory to create them uniformly.
Developers hand-rolled abstract factory hierarchies using abstract class since interfaces couldn't have default implementations and generics didn't exist yet.
NET1_ManualFactory.cs
// .NET 1.x — No DbProviderFactory yet! Provider-specific code everywhere
SqlConnection conn = new SqlConnection(connString); // Hardcoded to SQL Server
SqlCommand cmd = conn.CreateCommand();
SqlDataAdapter adapter = new SqlDataAdapter(cmd);
// Hand-rolled abstract factory (pre-generics)
public abstract class UIFactory
{
public abstract object CreateButton(); // Returns object — no generics yet
public abstract object CreateCheckbox();
}
public class WinFactory : UIFactory
{
public override object CreateButton() => new WinButton();
public override object CreateCheckbox() => new WinCheckbox();
}
Problems
No generics — factories returned object, forcing unsafe casts.
No DI — callers created factories with new.
Switching database provider meant rewriting every data access call.
.NET 2.0 (2005): Generics + DbProviderFactory
Two major additions: generics enabled type-safe abstract factories without casting, and DbProviderFactory was introduced as the canonical Abstract Factory in the .NET framework.
Each database vendor (SQL Server, Oracle, OleDb) shipped a concrete factory creating family-consistent Connection, Command, and DataAdapter objects — configurable via app.config.
NET2_Generics.cs
// .NET 2.0 — DbProviderFactory: THE canonical Abstract Factory
DbProviderFactory factory = DbProviderFactories.GetFactory("System.Data.SqlClient");
DbConnection conn = factory.CreateConnection(); // SqlConnection
DbCommand cmd = factory.CreateCommand(); // SqlCommand
DbDataAdapter adapter = factory.CreateDataAdapter(); // SqlDataAdapter
// .NET 2.0 — Generic Abstract Factory, type-safe, no casting
public interface IFactory<TProductA, TProductB>
where TProductA : IProductA
where TProductB : IProductB
{
TProductA CreateProductA();
TProductB CreateProductB();
}
public sealed class WinFactory
: IFactory<WinButton, WinCheckbox>
{
public WinButton CreateProductA() => new WinButton();
public WinCheckbox CreateProductB() => new WinCheckbox();
}
Improvement
Type-safe products via generics, config-driven provider selection via DbProviderFactories. But still no standard DI — factories created manually or via static helpers. Note: covarianceA feature allowing a more derived type to be used where a less derived type is expected — e.g., IFactory<WinButton> can be used as IFactory<IButton> if TProduct is marked 'out'. Added in C# 4.0 / .NET 4.0. (out keyword on generics) came later in .NET 4.0.
.NET 3.5–4.0 (2008–2010): IoC Containers
Third-party IoCInversion of Control — a principle where the framework calls your code (not the other way around). DI containers are the most common IoC mechanism. containers — Unity, Autofac, Ninject — arrived with .NET 3.5. Then .NET 4.0 added MEFManaged Extensibility Framework — .NET's built-in plugin/composition system using [Export] and [Import] attributes for dependency discovery. Shipped with .NET 4.0. (Managed Extensibility Framework) as a built-in plugin system. Abstract Factory implementations shifted from manual hierarchies to container-managed registrations.
NET35_IoC.cs
// MEFManaged Extensibility Framework — a .NET library for composing applications from discoverable parts (plugins), using attributes like [Export] and [Import]. — plugin-based factory discovery
[Export(typeof(IUIFactory))]
[ExportMetadata("Platform", "windows")]
public sealed class WinFactory : IUIFactory
{
public IButton CreateButton() => new WinButton();
public ICheckbox CreateCheckbox() => new WinCheckbox();
}
// Autofac — keyed registration (predecessor of .NET 8 keyed services)
var builder = new ContainerBuilder();
builder.RegisterType<WinFactory>().Keyed<IUIFactory>("windows");
builder.RegisterType<MacFactory>().Keyed<IUIFactory>("mac");
var container = builder.Build();
// Resolve by key
var factory = container.ResolveKeyed<IUIFactory>("windows");
Key Shift
Factories became container-managed, but each IoC library had its own API. No standard approach — lock-in to Autofac/Unity/Ninject.
.NET Core 1.0 (2016): Built-in DI
Microsoft shipped a built-in DI container with IServiceCollectionThe .NET interface for registering services and their lifetimes (Singleton, Scoped, Transient) in the built-in dependency injection container.. Manual abstract factory classes became less necessary — the container itself acts as a factory. Patterns shifted to registering Func<string, IFactory> delegates or using IServiceProvider to resolve families.
NETCore_DI.cs
// .NET Core DI — factory delegate pattern
services.AddSingleton<WinFactory>();
services.AddSingleton<MacFactory>();
// Register a resolver delegate (workaround — no keyed services yet)
services.AddSingleton<Func<string, IUIFactory>>(sp => key => key switch
{
"windows" => sp.GetRequiredService<WinFactory>(),
"mac" => sp.GetRequiredService<MacFactory>(),
_ => throw new ArgumentException($"Unknown platform: {key}")
});
// Usage — inject the delegate
public class Renderer(Func<string, IUIFactory> factoryResolver)
{
public string Render(string platform)
{
var factory = factoryResolver(platform);
return factory.CreateButton().Paint();
}
}
Limitation
The Func<string, T> workaround was clunky — switch statements leaked into DI registration. No first-class keyed service support until .NET 8.
.NET 6–7 (2021–2022): Minimal APIs
WebApplicationBuilder and minimal APIsA .NET 6+ feature for building HTTP APIs with minimal boilerplate — using lambda expressions directly in Program.cs instead of controllers. simplified registration. IHttpClientFactoryA .NET factory that creates pre-configured HttpClient instances with proper lifecycle management — prevents socket exhaustion and DNS issues. demonstrated framework-level Abstract Factory usage — each named client creates a family of HttpClient + handler pipeline + retry policies.
EF Core providers continued as the cleanest AF example.
NET6_MinimalAPI.cs
// .NET 6 — IHttpClientFactory: Abstract Factory for HTTP clients
// Each named client = family of HttpClient + handlers + policies
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<RetryHandler>(); // Family includes retry
// EF Core provider = Abstract Factory for database services
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseNpgsql(connString)); // Swaps 30+ internal service family
var app = builder.Build();
var client = app.Services.GetRequiredService<IHttpClientFactory>()
.CreateClient("github"); // Gets complete family
.NET 8–9+ (2023+): Keyed Services & AOT
Keyed services (AddKeyed*) eliminated the need for custom factory delegates. FrozenDictionary.NET 8+ read-only dictionary optimized for fast lookups after one-time construction — ideal for factory registries that don't change at runtime. optimized factory registries. Source generatorsCompile-time code generation in C# that produces source code during compilation, avoiding runtime reflection overhead. enabled AOTAhead-of-Time compilation — compiling code to native machine code before runtime, eliminating JIT overhead and reducing startup time.-friendly factories without reflection.
NET8_Keyed.cs
// .NET 8+ — first-class keyed services (no workarounds!)
builder.Services.AddKeyedSingleton<IUIFactory, WinFactory>("windows");
builder.Services.AddKeyedSingleton<IUIFactory, MacFactory>("mac");
builder.Services.AddKeyedSingleton<IUIFactory, LinuxFactory>("linux");
// Constructor injection by key — clean and declarative
public class Renderer([FromKeyedServices("mac")] IUIFactory factory)
{
public string Render() => factory.CreateButton().Paint();
}
// FrozenDictionary.NET 8+ immutable dictionary optimized for read-heavy scenarios — keys are hashed at creation time, giving faster lookups than Dictionary<K,V> with zero per-lookup allocations. — optimal for static factory registries
var factories = new Dictionary<string, IUIFactory>
{
["windows"] = new WinFactory(),
["mac"] = new MacFactory()
}.ToFrozenDictionary(); // O(1) lookup, zero allocations after init
Modern Best Practice
Keyed services are the idiomatic .NET 8+ approach. Source generatorsA Roslyn compiler feature that emits C# source code at compile time — used to replace runtime reflection with static code generation, enabling AOT compatibility and better performance. can auto-register factories at compile time for AOT scenarios.
Section 8
Abstract Factory in .NET Core
Abstract Factory is deeply embedded in the .NET framework. Here are real examples you use (or consume) daily:
1. DbProviderFactory — The Canonical Example
The textbook Abstract Factory in .NET. Each database provider (SQL Server, PostgreSQL, SQLite) ships a DbProviderFactory subclass that creates a family of compatible ADO.NET objects.
DbProviderFactory.cs
// Abstract Factory — creates a family of DB objects
DbProviderFactory factory = DbProviderFactories.GetFactory("Npgsql");
// All products from the SAME family (PostgreSQL)
DbConnection connection = factory.CreateConnection(); // NpgsqlConnection
DbCommand command = factory.CreateCommand(); // NpgsqlCommand
DbDataAdapter adapter = factory.CreateDataAdapter(); // NpgsqlDataAdapter
// Switch to SQL Server — just change the factory string
// factory = DbProviderFactories.GetFactory("Microsoft.Data.SqlClient");
// Now ALL products are SqlConnection, SqlCommand, etc.
2. CultureInfo — Locale Product Families
CultureInfo is a classic Abstract Factory: each culture creates a family of related formatting objects — NumberFormatInfo, DateTimeFormatInfo, TextInfo, CompareInfo — all guaranteed to be consistent for that locale.
Swap the culture, swap the entire family.
CultureFactory.cs
// Each CultureInfo = an Abstract Factory creating locale-consistent products
CultureInfo us = new("en-US");
NumberFormatInfo usNumbers = us.NumberFormat; // Product A: "$1,234.56"
DateTimeFormatInfo usDates = us.DateTimeFormat; // Product B: "3/5/2026"
TextInfo usText = us.TextInfo; // Product C: "HELLO"
CompareInfo usCompare = us.CompareInfo; // Product D: ordinal sort
CultureInfo de = new("de-DE"); // Swap factory → swap entire family
NumberFormatInfo deNumbers = de.NumberFormat; // Product A: "1.234,56 €"
DateTimeFormatInfo deDates = de.DateTimeFormat; // Product B: "05.03.2026"
TextInfo deText = de.TextInfo; // Product C: "HELLO" (but ß → SS)
CompareInfo deCompare = de.CompareInfo; // Product D: locale-aware sort
// All four products from one culture are guaranteed compatible
// Mixing en-US numbers with de-DE dates = data corruption
3. IHttpClientFactory — Named Client Families
IHttpClientFactoryA factory abstraction in Microsoft.Extensions.Http that manages HttpClient lifetimes and creates pre-configured client instances with named or typed configurations. is a textbook Abstract Factory: each named/typed client configuration creates a family of related objects — HttpClient + HttpMessageHandler pipeline + retry policies — all guaranteed to work together.
Swap the name, swap the entire family.
HttpClientFactory.cs
// Each named client = a FAMILY: HttpClient + handlers + policies
builder.Services.AddHttpClient("github", client =>
{
client.BaseAddress = new Uri("https://api.github.com");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp");
})
.AddHttpMessageHandler<AuthTokenHandler>() // Family member: auth handler
.AddStandardResilienceHandler(); // Family member: retry policy
builder.Services.AddHttpClient("stripe", client =>
{
client.BaseAddress = new Uri("https://api.stripe.com");
})
.AddHttpMessageHandler<StripeAuthHandler>() // Different family: different handler
.AddStandardResilienceHandler(); // Different retry config
// Consumer picks a family by name — gets all related objects
public class GitHubService(IHttpClientFactory factory)
{
private readonly HttpClient _client = factory.CreateClient("github");
// _client has GitHub base URL + auth handler + retry policy — complete family
}
4. EF Core Database Providers
Each EF Core database provider is an Abstract Factory that creates a family of compatible services: IRelationalConnection, ISqlGenerationHelper, IMigrationsSqlGenerator, IQuerySqlGeneratorFactory, and more.
Call UseSqlServer() vs UseNpgsql() and the entire family of 30+ internal services swaps as a unit.
EFCoreProviders.cs
// Each provider call registers an ENTIRE FAMILY of services
// SQL Server family
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseSqlServer(connString)); // Registers: SqlServerConnection,
// SqlServerSqlGenerator,
// SqlServerMigrationsSqlGenerator, etc.
// PostgreSQL family — swap one line, swap 30+ services
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseNpgsql(connString)); // Registers: NpgsqlConnection,
// NpgsqlSqlGenerator,
// NpgsqlMigrationsSqlGenerator, etc.
// SQLite family — same pattern, different family of products
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseSqlite(connString)); // Registers: SqliteConnection,
// SqliteSqlGenerator, etc.
// Your code never touches provider-specific types — pure Abstract Factory
5. IFileProvider — File System Families
IFileProvider creates a family of three related product types: IFileInfo (file metadata), IDirectoryContents (directory listing), and IChangeToken (change notifications).
Swap PhysicalFileProvider for EmbeddedFileProvider or ManifestEmbeddedFileProvider and the entire family changes — disk files vs embedded resources vs manifest resources.
FileProviderFactory.cs
// IFileProvider interface — 3 product types = Abstract Factory
public interface IFileProvider
{
IFileInfo GetFileInfo(string subpath); // Product A
IDirectoryContents GetDirectoryContents(string subpath); // Product B
IChangeToken Watch(string filter); // Product C
}
// Family 1: Physical disk files
IFileProvider disk = new PhysicalFileProvider("/app/wwwroot");
IFileInfo file = disk.GetFileInfo("index.html"); // PhysicalFileInfo
IDirectoryContents dir = disk.GetDirectoryContents(""); // PhysicalDirectoryContents
IChangeToken watch = disk.Watch("**/*.css"); // PollingChangeToken
// Family 2: Embedded resources (compiled into DLL)
IFileProvider embedded = new EmbeddedFileProvider(typeof(Program).Assembly);
IFileInfo file2 = embedded.GetFileInfo("templates/email.html"); // EmbeddedResourceFileInfo
// All three products are guaranteed compatible within a family
6. IServiceProviderFactory<TContainerBuilder>
IServiceProviderFactory<TContainerBuilder> is the Abstract Factory that creates the DI container itself.
The default implementation creates Microsoft's built-in container, but third-party libraries (Autofac, Lamar) provide their own factory creating a completely different family of container internals — scope management, resolution strategy, lifetime tracking — all swapped as a unit.
ServiceProviderFactory.cs
// Default family: Microsoft's built-in DI container
var builder = WebApplication.CreateBuilder(args);
// Uses DefaultServiceProviderFactory internally —
// creates: ServiceProvider + ServiceScope + ServiceScopeFactory
// Autofac family: swap one line, entire DI infrastructure changes
builder.Host.UseServiceProviderFactory(
new AutofacServiceProviderFactory());
// Now creates: AutofacServiceProvider + LifetimeScope +
// PropertyInjection + ModuleRegistration — different family!
builder.Host.ConfigureContainer<ContainerBuilder>(container =>
{
container.RegisterModule<DataAccessModule>();
container.RegisterModule<LoggingModule>();
});
// Your app code doesn't change — IServiceProvider is the abstraction
app.Services.GetRequiredService<IOrderService>(); // Works with any family
7. Encoding — Encoder + Decoder Families
System.Text.Encoding is a textbook Abstract Factory: each encoding (UTF-8, Unicode, ASCII) creates a family of Encoder + Decoder + byte[] Preamble that are guaranteed compatible.
You can also register custom encoding families via EncodingProvider.
System creates families of related objects that must work together (UI themes, DB providers, cloud SDKs)
You need to swap entire families at runtime or via configuration (switch all Windows controls to Mac controls)
Products from different families are incompatible — mixing them causes bugs (SQL Server connection with Oracle command)
You want to enforce consistency — guarantee no accidental cross-family mixing
Cross-platform apps, multi-database support, multi-cloudAn architecture that works across multiple cloud providers (AWS, Azure, GCP) — Abstract Factory abstracts each provider as a product family. abstraction, or white-labelA product built once but branded differently for multiple clients — each client's brand becomes a product family (colors, logos, components). theming
Don't Use When
You only create one type of object — use Factory Method instead
Products don't need to be related or compatible — no family concept exists
You have only one family — no variation to abstract over; just use concrete classes
DI container with keyed services handles the variation simply enough without a factory hierarchy
Object creation is simple and doesn't vary — don't add abstraction layers you don't need
Decision Flowchart
Section 10
Comparisons
Abstract Factory vs Factory Method
Abstract Factory
Creates families of related objects
Multiple create methods per factory
Ensures product compatibility
Horizontal extension (new families)
VS
Factory Method
Creates a single product type
One create method per factory
No family consistency guarantee
Vertical extension (new product variants)
Abstract Factory vs Builder
Abstract Factory
Creates multiple related objects at once
Products are simple — no construction steps
Focus: what family of objects to create
Returns ready-to-use products
VS
Builder
Constructs one complex object step-by-step
Product assembly has multiple steps
Focus: how to construct the object
Returns product only after final Build()
Abstract Factory vs Strategy
Abstract Factory
Creates objects (creational)
Swaps entire product families
Defines what gets created
Products have different interfaces
VS
StrategyA behavioral pattern that encapsulates interchangeable algorithms behind a common interface, letting the algorithm vary independently from clients that use it.
Encapsulates algorithms (behavioral)
Swaps one algorithm at a time
Defines how something is done
Strategies share the same interface
Abstract Factory vs Service Locator
Abstract Factory
Type-safe at compile time
Explicit family contracts via interfaces
Client depends on abstractions
Adding new family = new class
VS
Service LocatorAnti-pattern where a class resolves its own dependencies from a global container at runtime, hiding dependencies and making testing difficult.
String-based lookup at runtime
No compile-time guarantees
Client depends on the container
Adding new service = config change
Section 11
SOLID Mapping
Honest assessment — Abstract Factory is one of the strongest SOLID performers among GoF patterns, but ISPInterface Segregation Principle — clients should not be forced to depend on methods they don't use. Prefer small, focused interfaces. can be a concern with large families:
Principle
Rating
Explanation
SRPSingle Responsibility Principle — a class should have only one reason to change.
Strong
Each Concrete Factory has one reason to change — its product family's requirements change
OCPOpen/Closed Principle — classes should be open for extension but closed for modification. New behavior is added by writing new code, not changing existing code.
Strong
Adding a new family = adding a new ConcreteFactory class. Zero changes to existing factories or client code
LSPLiskov Substitution Principle — subtypes must be substitutable for their base types without altering program correctness.
Strong
Any ConcreteFactory can substitute for AbstractFactory — the client doesn't care which family it gets
ISP
Caution
Large families (5+ products) create fat factory interfaces. Consider splitting into smaller focused factories if a client only needs some products
DIPDependency Inversion Principle — high-level modules should depend on abstractions, not concrete implementations.
Strong
Client depends entirely on AbstractFactory + AbstractProduct interfaces — never touches concrete types
ISP Watch: The Fat Factory Problem
If your factory interface has 8+ create methods and some clients only use 2-3 of them, you're violating ISP. Split into focused factory interfaces: IButtonFactory, IFormFactory, etc. — compose them if a client needs the full family.
Section 12
Bug Case Studies
Bug 1: Mixed Product Families
The Incident
2023, .NET 7 e-commerce app. The checkout page in production showed a Windows-style button sitting right next to a macOS-style checkbox. It looked like two different apps stitched together. Nobody got an error, no exception was thrown, and all the automated tests passed. But actual customers saw a page that looked broken.
Here is what happened step by step. A developer on the team split the UI creation into two separate interfaces: IButtonFactory for making buttons, and ICheckboxFactory for making checkboxes. On the surface, this seems reasonable — each interface has a single job, right? But the problem is that buttons and checkboxes on the same page must belong to the same visual family (both Windows-style or both macOS-style). Splitting the factory by product type destroys that guarantee.
During DI registrationDependency Injection registration — telling the DI container which concrete class to use when someone asks for an interface. Done at startup in Program.cs., one developer registered IButtonFactory pointing to the Windows implementation, while a different developer (working on a different feature) registered ICheckboxFactory pointing to the macOS implementation. Neither knew about the other's registration. The container happily resolved both — no complaints.
The checkout page injected both factories through its constructor. When it called btnFactory.Create(), it got a Windows-style button. When it called chkFactory.Create(), it got a macOS-style checkbox. The code compiled, ran without errors, and even passed unit tests (which tested each factory in isolation). But on screen, the result was a mismatched Frankenstein UI.
QA only caught it three hours later because the mismatch was subtle on most screen sizes. On a specific resolution, the Windows button and macOS checkbox had noticeably different heights, causing the layout to break visually. This is exactly the kind of "silent" bug that Abstract Factory is designed to prevent — when it is used correctly.
Time to Diagnose
3 hours — visual bug, no crash, only noticed by QA on specific screen resolution.
MixedFamilies.cs
// ❌ Two different factories injected — families mixed!
public class CheckoutPage(
IButtonFactory btnFactory, // ❌ Resolves to WinButtonFactory
ICheckboxFactory chkFactory) // ❌ Resolves to MacCheckboxFactory
{
public void Render()
{
var btn = btnFactory.Create(); // WinButton
var chk = chkFactory.Create(); // MacCheckbox — WRONG FAMILY!
}
}
Walking through the buggy code: The constructor takes two separate factory interfaces — one for buttons, one for checkboxes. This seems clean (one interface per product), but it breaks the core promise of Abstract Factory. Nothing ties these two factories to the same family. The DI container resolves them independently, so one could easily point to the Windows implementation while the other points to macOS. Inside Render(), the two products come from different families, and there is no compile-time or runtime check to catch the mismatch.
FixedFamilies.cs
// ✅ Single Abstract Factory — guarantees family consistency
public class CheckoutPage(IUIFactory factory)
{
public void Render()
{
var btn = factory.CreateButton(); // ✅ Same family
var chk = factory.CreateCheckbox(); // ✅ Same family
}
}
// ✅ Registration: ONE factory, not split per product
builder.Services.AddSingleton<IUIFactory, WinFactory>();
Why the fix works: Now the page depends on a single IUIFactory. That one factory is responsible for creating all the UI elements. Since WinFactory only knows how to create Windows-style products, every call to CreateButton() and CreateCheckbox() is guaranteed to produce matching Windows controls. You physically cannot mix families — the factory simply does not have macOS products to hand you.
How to Spot This in Your Code
Look for constructors that inject multiple factory interfaces for products that are supposed to work together (e.g., IButtonFactory + ICheckboxFactory). If those products must belong to the same visual or logical family, they should come from one factory, not separate ones. Also search your DI registrations for the same product type being registered from different family assemblies.
Lesson Learned
Never split factory interfaces by product type when products must stay in the same family. Use one IUIFactory that creates all products — that is the whole point of Abstract Factory. One factory, one family, zero mismatches.
Bug 2: Captive DependencyA scoped or transient service accidentally captured by a singleton, causing it to live far longer than intended — leading to stale data, connection leaks, or thread-safety bugs. in Factory
The Incident
2022, .NET 6 multi-tenant SaaS. A singleton factory held onto a scopedA DI lifetime where one instance is created per HTTP request (or per scope). Scoped services are disposed when the scope ends.DbContextEntity Framework Core's main class for database access — manages connections, change tracking, and query execution. Should be scoped per request.. After two hours under load, the app started returning data from the wrong tenant. Customer A was seeing Customer B's invoices. The root cause: the DbContext was meant to live for one request, but the singleton factory kept it alive forever.
To understand this bug, you need to know how DI lifetimesHow long the DI container keeps an object alive. Singleton = forever, Scoped = one per request, Transient = new every time. work. In .NET, a "scoped" service (like DbContext) is supposed to be created fresh for each HTTP request and thrown away when the request ends. A "singleton" service lives for the entire lifetime of the application — it is created once and shared by every request.
The problem: the factory was registered as a singleton. It received a DbContext through its constructor. The DI container resolved that DbContext when the factory was first created (during the very first request). From that point on, the factory — and its captured DbContext — lived forever. Every subsequent request used the same DbContext, which still had the connection string and tenant context from request number one.
In development, nobody noticed because the developer was always logged in as the same tenant. In production, with hundreds of concurrent users across different tenants, the stale DbContext served data using the wrong tenant's connection. The bug was intermittent (it depended on which tenant's request happened to create the singleton first) and only appeared under concurrent load.
Think of it like this: imagine a restaurant where one waiter (the factory) picks up a notepad (the DbContext) on their first shift and never gets a new one. That notepad has "Table 5, chicken" written on it from Monday. On Tuesday, the waiter still reads from Monday's notepad and brings chicken to the wrong table. The notepad was supposed to be replaced every shift (every request), but because the waiter never leaves (singleton), they never get a fresh one.
Time to Diagnose
8 hours — intermittent, only under concurrent load, data appeared correct in dev.
CaptiveDependency.cs
// ❌ Singleton factory captures scoped DbContext
builder.Services.AddSingleton<IReportFactory, ReportFactory>();
public sealed class ReportFactory : IReportFactory
{
private readonly AppDbContext _db; // ❌ Scoped! Captured by singleton!
public ReportFactory(AppDbContext db) => _db = db;
public IReport CreateReport(string type) =>
new Report(_db.Reports.Where(r => r.Type == type));
// ❌ _db is the SAME instance for ALL requests
// ❌ Connection string from first request's tenant
}
Walking through the buggy code: The factory is registered as a singleton, so the DI container creates it once and keeps it forever. When the container builds ReportFactory, it injects an AppDbContext — which is scoped (meant for one request). The factory stores it in _db. From now on, every call to CreateReport() from any request uses the same _db. That DbContext still points to the first tenant's database connection. It never gets refreshed because the singleton factory never gets re-created.
FixedCaptive.cs
// ✅ Factory takes IServiceScopeFactory — creates fresh scope per call
builder.Services.AddSingleton<IReportFactory, ReportFactory>();
public sealed class ReportFactory(IServiceScopeFactory scopeFactory)
: IReportFactory
{
public IReport CreateReport(string type)
{
// ✅ New scope = new DbContext = correct tenant
var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return new Report(db.Reports.Where(r => r.Type == type), scope);
}
}
// ✅ Report disposes scope when done
public sealed class Report(IQueryable<ReportRow> data, IServiceScope scope)
: IReport, IDisposable
{
public void Dispose() => scope.Dispose();
}
Why the fix works: Instead of injecting the DbContext directly, the factory now injects IServiceScopeFactory — a safe singleton service that can create new DI scopes on demand. Every time CreateReport() is called, it opens a fresh scope, gets a brand-new DbContext from that scope (with the correct tenant connection), and passes both to the Report. When the Report is disposed, it disposes the scope, which in turn disposes the DbContext. Fresh context per call, no stale data, no cross-tenant leaks.
How to Spot This in Your Code
Search for any singleton-registered class whose constructor takes a scoped or transient service (like DbContext, HttpClient, or anything request-specific). In development, enable builder.Host.UseDefaultServiceProvider(o => o.ValidateScopes = true) — .NET will throw an exception at startup if it detects a singleton capturing a scoped service.
Lesson Learned
Singleton factories must never inject scoped/transientA DI lifetime where a new instance is created every time the service is requested. No sharing between consumers. services directly. Use IServiceScopeFactoryA .NET interface that creates new DI scopes on demand — the safe way for singletons to access scoped services without captive dependency bugs. to create a fresh scope per product creation. Enable ValidateScopes in development to catch this early.
Bug 3: Missing Family Member
The Incident
2023, .NET 8 multi-platform app. The team added a new CreateDropdown() method to the factory interface. One developer updated the Windows factory, but nobody remembered to update the macOS factory. The app compiled without errors, shipped to production, and crashed with a NullReferenceException — but only for users on macOS.
Here is the sneaky detail. When the developer added CreateDropdown() to IUIFactory, they gave it a default interface methodA C# 8+ feature that lets interfaces provide a method body. Classes that implement the interface don't have to override it — they silently inherit the default. Useful for evolving interfaces without breaking existing code, but dangerous when the default hides missing implementations. that returned null!. In C#, a default interface method means existing classes that implement the interface do not have to add the new method — they just silently use the default. That is great for backward compatibility, but terrible when the "default" is a null that crashes at runtime.
The Windows factory (WinFactory) was updated in the same pull request that added the interface method. It properly returned a WinDropdown. But MacFactory was in a different file, maintained by a different team member. Nobody noticed it was missing the override. The compiler was happy — the default method satisfied the contract. All tests on the Windows path passed.
When a macOS user navigated to a page with a dropdown, the app called factory.CreateDropdown() on the Mac factory. The default method returned null!. The next line tried to call a method on that null dropdown — boom, NullReferenceException. The bug only affected one platform and one UI element, making it particularly tricky to catch in testing if you only tested on Windows.
Think of it like a restaurant adding a new dessert to the menu. The head chef at the Downtown location creates the recipe. The Uptown chef never gets the memo. When an Uptown customer orders the dessert, the waiter walks to the kitchen and comes back with nothing. The menu (interface) promises the dessert exists, but the kitchen (factory) cannot actually make it.
Time to Diagnose
30 minutes — NullReferenceException in production, but only on macOS clients.
MissingMember.cs
public interface IUIFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
IDropdown CreateDropdown() => null!; // ❌ Default implementation hides the bug
}
public sealed class WinFactory : IUIFactory
{
public IButton CreateButton() => new WinButton();
public ICheckbox CreateCheckbox() => new WinCheckbox();
public IDropdown CreateDropdown() => new WinDropdown(); // ✅ Implemented
}
public sealed class MacFactory : IUIFactory
{
public IButton CreateButton() => new MacButton();
public ICheckbox CreateCheckbox() => new MacCheckbox();
// ❌ CreateDropdown() NOT overridden — returns null! at runtime
}
Walking through the buggy code: The interface defines CreateDropdown() with a default body of => null!. The null! tells the compiler "trust me, this will not be null" — but it is a lie. WinFactory properly overrides the method and returns a real dropdown. MacFactory does not override it, so when someone calls macFactory.CreateDropdown(), they get the default null! back. The compiler sees no issue because the default satisfies the interface contract. The bug only surfaces at runtime, on the macOS code path, when someone actually tries to use the null dropdown.
FixedMember.cs
// ✅ Fix 1: Remove default — compiler forces all factories to implement
public interface IUIFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
IDropdown CreateDropdown(); // ✅ No default — compile error if missing
}
// ✅ Fix 2: Add a startup validation test
[Fact]
public void All_Factories_Implement_All_Products()
{
var factoryTypes = typeof(IUIFactory).Assembly.GetTypes()
.Where(t => typeof(IUIFactory).IsAssignableFrom(t) && !t.IsInterface);
foreach (var type in factoryTypes)
{
var factory = (IUIFactory)Activator.CreateInstance(type)!;
Assert.NotNull(factory.CreateButton());
Assert.NotNull(factory.CreateCheckbox());
Assert.NotNull(factory.CreateDropdown());
}
}
Why the fix works: Fix 1 removes the default body from CreateDropdown(). Now the interface has an abstract method that every implementing class must provide. The moment you try to compile MacFactory without CreateDropdown(), the compiler screams with an error. You physically cannot ship the app with a missing implementation. Fix 2 adds a belt-and-suspenders test that loops through every factory in the assembly and verifies it produces non-null products. Even if someone reintroduces a default method later, this test catches the null.
How to Spot This in Your Code
Search your factory interfaces for default method bodies — especially ones that return null, null!, or throw NotImplementedException. These are ticking time bombs. Also look for interface methods added in recent commits and cross-reference with all implementing classes to make sure every factory got the update.
Lesson Learned
Avoid default interface methods on factory interfaces — they hide missing implementations. Let the compiler enforce completeness. Add integration tests that verify all factories produce non-null products.
Bug 4: Keyed Service String Mismatch
The Incident
2024, .NET 8 API. A factory was registered under the key "postgres", but a consumer on the other side of the codebase tried to resolve it with "postgresql". One letter off. No compiler warning, no startup error — the app booted fine. But the moment a user hit the reporting endpoint, it blew up with InvalidOperationException: No service for type IDbFactory with key 'postgresql'.
This is the classic "magic string" problem. When you use raw strings as keys to register and resolve services, the compiler cannot check if the strings match. You can spell the key differently in two places and the compiler will not blink. The connection between the registration and the consumer is purely conventional — it only exists in the developer's head.
In this case, the developer who registered the factory wrote "postgres" (the short name). A different developer, working on the reporting feature weeks later, typed "postgresql" (the full name). Both strings are reasonable names for PostgreSQL. Neither developer did anything obviously wrong. But the key mismatch meant the DI container could not find a factory registered under "postgresql" because none existed — the factory was registered under "postgres".
The bug only triggered in production because the reporting endpoint was rarely used during development. Manual testing focused on the main CRUD operations, which did not use keyed services. The reporting feature sat quietly broken for two weeks before a customer hit it.
Think of it like labeling boxes in a warehouse. You label a box "DB-Postgres" and put it on shelf A. Your coworker comes looking for "DB-PostgreSQL." They search every shelf and find nothing — even though the box they need is right there, labeled slightly differently.
Time to Diagnose
15 minutes once triggered — but the bug sat in production for 2 weeks in a rarely-used code path.
KeyMismatch.cs
// ❌ Magic strings — typo causes runtime failure
builder.Services.AddKeyedSingleton<IDbFactory, PostgresFactory>("postgres");
// Somewhere else in the codebase...
public class ReportService(
[FromKeyedServices("postgresql")] IDbFactory factory) // ❌ TYPO!
{ }
Walking through the buggy code: The registration uses the string "postgres". The consumer uses "postgresql" in the [FromKeyedServices] attribute. These are two completely independent strings with no compile-time connection. The DI container registers the factory under one key and the consumer asks for it under a different key. At startup, the container does not validate that all [FromKeyedServices] attributes have matching registrations (unless you explicitly enable validation). So the mismatch hides until the consumer code actually runs.
FixedKeys.cs
// ✅ Use constants — single source of truth
public static class DbKeys
{
public const string Postgres = "postgres";
public const string SqlServer = "sqlserver";
}
builder.Services.AddKeyedSingleton<IDbFactory, PostgresFactory>(DbKeys.Postgres);
public class ReportService(
[FromKeyedServices(DbKeys.Postgres)] IDbFactory factory) // ✅ Compile-safe
{ }
// ✅ Even better: use an enum
public enum DbProvider { Postgres, SqlServer }
builder.Services.AddKeyedSingleton<IDbFactory, PostgresFactory>(DbProvider.Postgres);
Why the fix works: By defining keys as constants in a shared static class (or as an enum), both the registration and the consumer reference the same symbol. If you rename the constant, every usage updates automatically. If you typo the constant name, the compiler catches it immediately. Enums are even safer because they also give you IntelliSense autocomplete and refactoring support. The string is defined in exactly one place — no more copy-paste mismatches.
How to Spot This in Your Code
Search your codebase for raw string literals inside AddKeyed* and [FromKeyedServices(...)]. If the same key string appears in more than one place as a raw literal (not a constant reference), it is a mismatch waiting to happen. Also add builder.Host.UseDefaultServiceProvider(o => o.ValidateOnStart = true) to catch unresolvable services at startup instead of at request time.
Lesson Learned
Never use raw magic strings for keyed service registration. Define keys as const string in a shared static class or use enums. One source of truth, zero typo bugs.
Bug 5: Circular Dependency
The Incident
2023, .NET 7 modular monolith. The app would not start. Every time the team deployed, they got a circular dependency exception at startup. OrderFactory needed IPaymentFactory to create orders with payment info. PaymentFactory needed IOrderFactory to validate that an order exists. Each factory required the other to be constructed first — a chicken-and-egg problem.
Imagine two people trying to introduce each other. Alice says "I can't introduce myself until Bob goes first." Bob says "I can't introduce myself until Alice goes first." Neither can ever start. That is exactly what happened with these two factories.
The DI container tried to build OrderFactory. To do that, it needed to inject IPaymentFactory, which meant building PaymentFactory. But PaymentFactory required IOrderFactory, which is what we were trying to build in the first place. The container detected the infinite loop and threw an exception. Unlike the other bugs on this page, this one at least fails loudly at startup — you cannot accidentally ship it to production. But it still blocked the team for two hours because untangling the design required real thought.
The root problem was a design flaw, not a coding mistake. The PaymentFactory did not actually need to create orders — it just needed to look up an existing order to validate it. That "look up" responsibility belonged in a simple repository, not in a factory. By giving PaymentFactory a dependency on the entire IOrderFactory, the team gave it far more power (and coupling) than it needed.
Time to Diagnose
2 hours — clear error message, but untangling the design took thought.
CircularDependency.cs
// ❌ Circular dependency — startup crash
public sealed class OrderFactory(IPaymentFactory payments) : IOrderFactory
{
public IOrder Create(OrderRequest req)
{
var payment = payments.CreatePayment(req.PaymentInfo);
return new Order(req, payment);
}
}
public sealed class PaymentFactory(IOrderFactory orders) : IPaymentFactory
{
public IPayment CreatePayment(PaymentInfo info)
{
var order = orders.GetOrder(info.OrderId); // ❌ Needs IOrderFactory!
return new Payment(info, order);
}
}
Walking through the buggy code:OrderFactory takes IPaymentFactory in its constructor because it needs to create a payment when building an order. PaymentFactory takes IOrderFactory because it needs to look up an order when creating a payment. Each class requires the other to exist first. The DI container cannot construct either one without the other already being available — a deadlock at the dependency graph level.
FixedCircular.cs
// ✅ Break the cycle — use Lazy<T> or extract shared concern
// Option 1: Lazy injection (quick fix)
public sealed class PaymentFactory(Lazy<IOrderFactory> orders) : IPaymentFactory
{
public IPayment CreatePayment(PaymentInfo info)
{
var order = orders.Value.GetOrder(info.OrderId); // ✅ Resolved lazily
return new Payment(info, order);
}
}
// Option 2 (better): Extract the shared concern into a separate service
public interface IOrderRepository
{
Order GetOrder(Guid id);
}
public sealed class PaymentFactory(IOrderRepository repo) : IPaymentFactory
{
public IPayment CreatePayment(PaymentInfo info)
{
var order = repo.GetOrder(info.OrderId); // ✅ No factory dependency
return new Payment(info, order);
}
}
Why the fix works: Option 1 (Lazy) is a band-aid: wrapping the dependency in Lazy<T> defers resolution until the first time .Value is accessed, breaking the construction-time cycle. But Option 2 is the real fix: the PaymentFactory never needed the full IOrderFactory — it just needed to read an order from storage. By extracting that into IOrderRepository, both factories can depend on the repository without depending on each other. The cycle vanishes because you removed the unnecessary coupling.
How to Spot This in Your Code
If your app crashes at startup with a "circular dependency detected" error, trace the dependency chain in the error message. Ask yourself: does Factory A really need to create via Factory B, or does it just need to read data that B also reads? If it is just reading, extract a shared repository or service that both factories can depend on without depending on each other.
Lesson Learned
Circular dependencies between factories almost always signal a design flaw. The right fix is extracting the shared concern into its own service, not working around the cycle with Lazy<T>.
Bug 6: Thread-Unsafe Lazy Factory Cache
The Incident
2022, .NET 6 high-traffic API. A singleton factory was caching products in a regular Dictionary to avoid re-creating them on every call. Under heavy traffic, the dictionary started throwing random InvalidOperationException errors. The internal data structure was getting corrupted because multiple threads were reading and writing to it at the same time.
To understand this bug, you need to know one important rule: a regular Dictionary<TKey, TValue> is not safe to use from multiple threads at the same time. It is like a filing cabinet designed for one person. If two people try to file documents simultaneously, papers get mixed up, folders get misfiled, and the whole system breaks down.
The developer's intention was reasonable: creating UI products can be expensive (maybe it involves loading resources, reading configs, etc.), so they wanted to cache the result. The first time CreateButton() is called, it creates a WinButton and stores it in the dictionary. Every subsequent call just returns the cached button. This works perfectly in single-threaded code.
But the factory was registered as a singleton — shared across all threads. When 50 requests hit simultaneously, 50 threads all called CreateButton() at the same time. Multiple threads found the cache empty (the TryGetValue check), created new buttons, and then tried to write to the dictionary simultaneously. Dictionary is not designed for this — its internal hash buckets got corrupted, leading to race conditionsA bug where concurrent threads access shared data and the outcome depends on execution timing — one of the hardest bugs to diagnose.: infinite loops in bucket chains, lost entries, and eventually exceptions.
The bug was intermittent. In dev (one user, low traffic), it worked perfectly. In staging (moderate load), it occasionally threw a strange exception. In production (peak traffic), it crashed repeatedly. This is the hallmark of a concurrency bug — it only appears under load and is nearly impossible to reproduce on a developer's machine.
Time to Diagnose
6 hours — intermittent, only under load, dictionary corruption caused random exceptions.
UnsafeCache.cs
// ❌ Non-thread-safe cache in singleton factory
public sealed class CachedUIFactory : IUIFactory
{
private readonly Dictionary<string, IButton> _cache = new(); // ❌
public IButton CreateButton()
{
if (!_cache.TryGetValue("btn", out var btn))
{
btn = new WinButton();
_cache["btn"] = btn; // ❌ Race condition under concurrent access
}
return btn;
}
}
Walking through the buggy code: The factory uses a plain Dictionary as its cache. The pattern looks innocent: check if the key exists, and if not, create the product and store it. But between the TryGetValue check and the _cache["btn"] = btn write, another thread can jump in and do the same thing. Two threads write to the same dictionary simultaneously, corrupting its internal structure. Once corrupted, even simple reads can infinite-loop or throw. The fact that this only happens under high concurrency makes it extremely hard to reproduce in development.
SafeCache.cs
// ✅ Use ConcurrentDictionary or FrozenDictionary
public sealed class CachedUIFactory : IUIFactory
{
private readonly ConcurrentDictionary<string, IButton> _cache = new();
public IButton CreateButton() =>
_cache.GetOrAdd("btn", _ => new WinButton()); // ✅ Thread-safe
// ✅ Or pre-build the cache at startup with FrozenDictionary
private static readonly FrozenDictionary<string, Func<IButton>> _factories =
new Dictionary<string, Func<IButton>>
{
["win"] = () => new WinButton(),
["mac"] = () => new MacButton()
}.ToFrozenDictionary();
}
Why the fix works:ConcurrentDictionary is designed for multi-threaded access. Its GetOrAdd() method atomically checks if the key exists and creates the value if it does not — all in one thread-safe operation. No gap between "check" and "write" for another thread to exploit. The FrozenDictionary approach is even better for factories: you build the entire registry at startup (when only one thread is running), then freeze it. A frozen dictionary is completely read-only at runtime, so thread safety is guaranteed with zero locking overhead.
How to Spot This in Your Code
Search for Dictionary< fields in any class registered as a singleton. If the dictionary is written to after construction (anything other than read-only initialization), it is a potential race condition. Replace with ConcurrentDictionary for read/write caches, or use FrozenDictionary (or ImmutableDictionary) if the data is built once at startup and never modified.
Lesson Learned
Singleton factories that cache products must use thread-safe collections. ConcurrentDictionary for dynamic caches, FrozenDictionary for static registries built at startup. Never use Dictionary in a singleton that receives concurrent traffic.
Section 13
Pitfalls & Anti-Patterns
1. God Factory — One Factory Creates Everything
Mistake: Cramming all product families into a single class with switch statements.
Why This Happens: It starts innocently. You have two families — Windows and macOS. You write one factory class with a switch on the platform name. It works, it is simple, and it fits in one file. Then Linux support arrives. Then a "high contrast" accessibility family. Each new family adds another case to every switch statement in the class. Suddenly your "simple" factory is 500 lines long with 4 switch blocks, and every single change risks breaking all other families.
The deeper reason developers do this is that creating a separate class for each family feels like "too many files." But the trade-off is wrong: a few extra files is far cheaper than one monster file that every developer on the team needs to edit simultaneously.
GodFactory.cs
// ❌ God Factory — one class handles ALL families
public class UIFactory
{
public IButton CreateButton(string platform) => platform switch
{
"win" => new WinButton(),
"mac" => new MacButton(),
"linux" => new LinuxButton(), // keeps growing...
_ => throw new ArgumentException(nameof(platform))
};
public ICheckbox CreateCheckbox(string platform) => platform switch
{
"win" => new WinCheckbox(),
"mac" => new MacCheckbox(),
"linux" => new LinuxCheckbox(), // same switch, again...
_ => throw new ArgumentException(nameof(platform))
};
// ... repeat for every product type
}
SeparateFactories.cs
// ✅ Each family gets its own sealed factory
public sealed class WinFactory : IUIFactory
{
public IButton CreateButton() => new WinButton();
public ICheckbox CreateCheckbox() => new WinCheckbox();
}
public sealed class MacFactory : IUIFactory
{
public IButton CreateButton() => new MacButton();
public ICheckbox CreateCheckbox() => new MacCheckbox();
}
// Adding Linux? Just add a new class — no existing code changes.
public sealed class LinuxFactory : IUIFactory { ... }
The good version follows the Open/Closed PrincipleSoftware entities should be open for extension (add new families by adding classes) but closed for modification (existing factory classes never change).: adding a new family means adding a new class, not editing an existing one. No switch statements, no risk of breaking Windows when you add Linux.
2. Product Family Explosion
Mistake: Creating too many families with too many products each — 5 families x 8 products = 40 concreteImplementation classes as opposed to abstract types — the actual classes that get instantiated (e.g., WinButton vs IButton). product classes + 5 factory classes.
Why This Happens: The pattern works so well for 2 families and 3 products that developers apply it to everything. "We have 5 platforms and 8 UI controls — let's make all 40 combinations!" But often only 2-3 of those products actually differ between families. A scrollbar might look identical on Windows and macOS. A text label might render the same everywhere. Creating family-specific versions of products that do not actually vary is pure waste — 40 classes where 20 would suffice.
The fix is to ask a simple question for each product: Does this product actually behave differently across families? If not, share a single implementation. Only the products that truly vary need family-specific classes. This can dramatically shrink your class count.
SharedProducts.cs
// ✅ Only vary what actually differs per family
public sealed class WinFactory : IUIFactory
{
public IButton CreateButton() => new WinButton(); // Different per family
public ICheckbox CreateCheckbox() => new WinCheckbox(); // Different per family
public ILabel CreateLabel() => new StandardLabel(); // ✅ Shared — same everywhere
public IScrollbar CreateScrollbar() => new StandardScrollbar(); // ✅ Shared
}
3. Leaking Concrete Types
Mistake: Factory method returns WinButton instead of IButton, or client casts the result to a concrete type.
Why This Happens: Developers know the concrete type and reach for it out of habit. "I know it is a WinButton, so why not use the concrete type? I get IntelliSense for the Windows-specific properties." The problem is that once client code depends on WinButton, you cannot swap in MacButton without changing that code. You have just hard-wired the family into the consumer — the entire point of Abstract Factory was to avoid that.
Sometimes the leak is not the return type itself, but a (WinButton) cast buried deep in application code. That cast is a time bomb: it works today because you only have Windows, but it explodes the moment you add a second family.
LeakedTypes.cs
// ❌ Leaking concrete types
IButton btn = factory.CreateButton();
var winBtn = (WinButton)btn; // ❌ Cast couples client to Windows family
winBtn.ShowWinTaskbar(); // ❌ Windows-only method — breaks on Mac
CleanAbstraction.cs
// ✅ Work entirely through the interface
IButton btn = factory.CreateButton();
btn.Paint(); // ✅ Works for every family
btn.OnClick(); // ✅ Defined on IButton — family-agnostic
Rule of thumb: if you see a cast to a concrete product type anywhere in code that uses a factory, something is wrong. The client should never know or care which family produced the product. Use sealedC# keyword preventing a class from being inherited. Sealed classes improve performance (devirtualization) and clarify design intent. on concrete products to discourage downcasting chains.
4. Ignoring DI — Manually Newing Factories
Mistake: Writing new WinFactory() in application code instead of resolving from the DI container.
Why This Happens: Developers sometimes skip DI because "it is just one factory, I will hard-code it for now." Or they are not familiar with how to register multiple implementations. The result is tight coupling: the code that creates new WinFactory() is permanently married to the Windows family. You cannot swap families via configuration, environment variables, or test doubles.
This also defeats testability. In unit tests, you want to inject a TestFactory that returns lightweight stub products. If the code hard-codes new WinFactory(), the only way to substitute it is to change the source code itself.
ManualNew.cs
// ❌ Hard-coded factory — cannot swap families
public class Dashboard
{
private readonly IUIFactory _factory = new WinFactory(); // ❌ Tight coupling!
}
DIInjected.cs
// ✅ Let the DI container decide which factory to inject
public class Dashboard(IUIFactory factory)
{
// Family is decided at registration time — Dashboard doesn't know or care
}
// In Program.cs — change this ONE line to switch families:
builder.Services.AddSingleton<IUIFactory, MacFactory>();
The fix is simple: register the factory in IServiceCollection and inject it via the constructor. The family selection happens once at the composition root — every consumer is blissfully unaware of which family it received.
5. Abstract Factory for a Single Product
Mistake: Creating an Abstract Factory with only one Create method.
Why This Happens: Someone reads about Abstract Factory and gets excited. They create INotificationFactory with a single CreateNotification() method. But there is no "family" here — just one product with multiple variants. That is literally what Factory Method does. Abstract Factory exists specifically for the case where you have multiple products that must stay in sync across families. With just one product, the "family consistency" guarantee is meaningless.
Using Abstract Factory for a single product adds an unnecessary layer of indirection: an extra interface, an extra class, and extra complexity — all for zero benefit over Factory Method.
SingleProduct.cs
// ❌ Overkill — Abstract Factory for a single product
public interface INotificationFactory
{
INotification CreateNotification(); // Only one method = no family!
}
// ✅ Just use Factory Method instead
public interface INotification { void Send(string message); }
// Register directly — no factory interface needed
builder.Services.AddSingleton<INotification, EmailNotification>();
Rule: only escalate to Abstract Factory when you have 2 or more related products per family. If it is a single product with multiple implementations, Factory Method or plain DI registration is simpler and cleaner.
6. String-Keyed Factories Without Validation
Mistake: Using raw strings for keyed service resolution without constants or enums.
Why This Happens: Keyed services in .NET 8 are so easy to set up that developers jump straight to raw strings: "postgres", "sqlserver", "redis". It works fine initially. But as the codebase grows and multiple developers register and resolve services in different files, typos creep in. One file says "postgres", another says "postgresql", a third says "pg". All are reasonable names — and all must match exactly.
Without compile-time validation, these mismatches only surface at runtime. And if the mismatched code is behind a rarely-used feature flag, it might sit in production for weeks before a user triggers it.
RawStrings.cs
// ❌ Raw strings — typos hide until runtime
builder.Services.AddKeyedSingleton<IDbFactory, PgFactory>("postgres");
builder.Services.AddKeyedSingleton<IDbFactory, SqlFactory>("sqlserver");
// In a different file, weeks later...
public class ReportService(
[FromKeyedServices("postgresql")] IDbFactory db) { } // ❌ Typo! Boom.
Constants.cs
// ✅ Constants or enums — single source of truth
public static class DbKeys
{
public const string Postgres = "postgres";
public const string SqlServer = "sqlserver";
}
builder.Services.AddKeyedSingleton<IDbFactory, PgFactory>(DbKeys.Postgres);
public class ReportService(
[FromKeyedServices(DbKeys.Postgres)] IDbFactory db) { } // ✅ Compile-safe
Define keys in one place (a static class with const string fields, or an enum). Reference that constant everywhere. The compiler catches typos, IntelliSense auto-completes, and renaming propagates automatically.
7. Not Sealing Concrete Factories
Mistake: Leaving concrete factories inheritable, leading to unintended inheritance hierarchies.
Why This Happens: A developer needs a "slightly different" Windows factory — maybe one that caches products. Instead of creating a new factory class, they inherit from WinFactory and override just CreateButton(). Seems harmless, right? But now the child class returns a cached WinButton from CreateButton() and a fresh WinCheckbox from the parent's CreateCheckbox(). If the cached button holds stale state, the family's products are no longer consistent.
Inheritance also makes the factory hierarchy brittle. Every change to WinFactory might break its children. The Abstract Factory pattern is designed for extension through new classes, not through inheritance of existing ones.
UnsealedFactory.cs
// ❌ Unsealed — allows fragile inheritance
public class WinFactory : IUIFactory { ... }
public class CachedWinFactory : WinFactory // ❌ Inherits and partially overrides
{
public override IButton CreateButton() => _cached; // Stale state risk
// CreateCheckbox() inherited — mismatch with cached button!
}
SealedFactory.cs
// ✅ Sealed — extend by adding, not inheriting
public sealed class WinFactory : IUIFactory { ... }
public sealed class CachedWinFactory : IUIFactory { ... } // Own class, full control
Mark all concrete factories as sealed. When you need a variation, create a brand new class that implements the interface from scratch. This keeps each factory self-contained and predictable.
8. Mixing Families at the Boundary
Mistake: A method accepts IUIFactory but also takes individual IButton and ICheckbox parameters from elsewhere.
Why This Happens: Performance optimization or convenience. A developer thinks "I already have this button from earlier — why create a new one from the factory?" So they pass the pre-created button alongside the factory. But the pre-created button might be from a different family than the factory. Now you have a Windows button sitting next to a macOS checkbox again — the same "mixed families" problem from Bug 1, just hidden behind a method signature.
The root issue is that the method signature promises family consistency (by accepting IUIFactory) but then breaks that promise by also accepting individual products from unknown sources.
MixedBoundary.cs
// ❌ Mixed sources — button might be from a different family
public void RenderForm(IUIFactory factory, IButton externalButton)
{
var chk = factory.CreateCheckbox(); // MacCheckbox
externalButton.Paint(); // ❌ Could be WinButton!
}
ConsistentBoundary.cs
// ✅ Let the factory create everything — guaranteed same family
public void RenderForm(IUIFactory factory)
{
var btn = factory.CreateButton(); // ✅ Same family
var chk = factory.CreateCheckbox(); // ✅ Same family
btn.Paint();
chk.Paint();
}
Pick one approach: either use the factory to create all products, or accept all pre-created products as parameters. Never mix both in the same method — it creates a hole in the family consistency guarantee.
9. Fat Factory Interface (10+ Create Methods)
Mistake: A factory interface with CreateButton(), CreateCheckbox(), CreateDropdown(), CreateSlider(), CreateRadio(), ... 10+ methods.
Why This Happens: The team keeps adding products to the same factory interface as the UI grows. "We need a datepicker? Add CreateDatePicker() to IUIFactory." After a year, the interface has 12 methods. Every concrete factory must implement all 12, even if most clients only use 2-3. This violates the Interface Segregation PrincipleClients should not be forced to depend on interfaces they don't use. If your interface has 12 methods and most consumers use 3, the interface is too broad. — clients are forced to depend on factory methods they never call.
Adding a new product also forces every concrete factory to change (add the new method), which means editing files across multiple assemblies and risking merge conflicts.
// ✅ Split by cohesion — each client depends only on what it uses
public interface IInputFactory { IButton CreateButton(); ICheckbox CreateCheckbox(); }
public interface ILayoutFactory { IModal CreateModal(); IToast CreateToast(); }
public class FormRenderer(IInputFactory inputs) { ... } // Needs only inputs
public class NotificationService(ILayoutFactory layout) { ... } // Needs only layout
Split the fat interface into smaller, cohesive factory interfaces. Group products by what is used together. A form renderer needs input controls; a notification service needs modals and toasts. Each client depends only on the interface it actually uses.
10. Forgetting to Register All Products
Mistake: Registering the factory in DI but not its product dependencies. Factory creates products that need services not in the container.
Why This Happens: The factory itself is registered fine. But the products it creates have their own dependencies — maybe WinButton needs an IThemeService, or MacCheckbox requires an IAccessibilityProvider. If those dependencies are not also registered in the DI container, the factory's Create method fails with a resolution exception buried deep in the call stack. The error message says "Cannot resolve IThemeService" but gives no hint that the problem started because you forgot to register it alongside the factory.
This is especially sneaky when different families have different product dependencies. The Windows factory might work perfectly (all its product deps are registered) while the macOS factory crashes because IAccessibilityProvider was never added.
// ✅ Bundle factory + all dependencies in one extension method
public static class WinUIExtensions
{
public static IServiceCollection AddWinUI(this IServiceCollection services)
{
services.AddSingleton<IUIFactory, WinFactory>();
services.AddSingleton<IThemeService, WinTheme>();
services.AddSingleton<IIconProvider, WinIcons>();
return services;
}
}
// In Program.cs — one call registers everything
builder.Services.AddWinUI();
// ✅ Add startup validation to catch anything missing
builder.Host.UseDefaultServiceProvider(o => o.ValidateOnStart = true);
Create an extension method (like AddWinUI()) that registers the factory and all its product dependencies as a bundle. This way, adding a family is a single method call that cannot be partially applied. Enable ValidateOnStart to catch any missing registrations the moment the app boots, not when a user triggers the code path.
Section 14
Testing Strategies
Strategy 1: Test Double Factory
Create a FakeUIFactory that returns in-memory stubs. Use it in unit tests to isolate client logic from product implementations.
TestDoubleFactory.cs
// Test doubleA generic term for any object that replaces a real dependency in testing — includes stubs (return canned data), mocks (verify interactions), fakes (simplified implementations), and spies. factory — all products are simple stubs
public sealed class FakeUIFactory : IUIFactory
{
public IButton CreateButton() => new FakeButton();
public ICheckbox CreateCheckbox() => new FakeCheckbox();
}
public sealed class FakeButton : IButton
{
public string LastPaintCall { get; private set; } = "";
public string Paint() { LastPaintCall = "painted"; return "fake-btn"; }
}
[Fact]
public void Dialog_Renders_Both_Controls()
{
var factory = new FakeUIFactory();
var dialog = new Dialog(factory);
dialog.Render();
// Assert products were used — no real UI, no platform dependency
Assert.NotNull(factory.CreateButton());
Assert.NotNull(factory.CreateCheckbox());
}
Strategy 2: Verify Family Consistency
Ensure all products from one factory are from the same family. Catch the "mixed families" bug at test time.
FamilyConsistency.cs
[TheoryAn xUnit test attribute that runs the same test method with different input data — each [InlineData] or [MemberData] becomes a separate test case.]
[InlineData(typeof(WinFactory), "Win")]
[InlineData(typeof(MacFactory), "Mac")]
public void Factory_Creates_Consistent_Family(Type factoryType, string prefix)
{
var factory = (IUIFactory)Activator.CreateInstance(factoryType)!;
var button = factory.CreateButton();
var checkbox = factory.CreateCheckbox();
// All products should be from the same family
Assert.StartsWith(prefix, button.GetType().Name);
Assert.StartsWith(prefix, checkbox.GetType().Name);
}
[Fact]
public void All_Factories_Create_NonNull_Products()
{
var factories = typeof(IUIFactory).Assembly.GetTypes()
.Where(t => typeof(IUIFactory).IsAssignableFrom(t)
&& t.IsClass && !t.IsAbstract);
foreach (var type in factories)
{
var factory = (IUIFactory)Activator.CreateInstance(type)!;
Assert.NotNull(factory.CreateButton());
Assert.NotNull(factory.CreateCheckbox());
}
}
Strategy 3: DI Container Tests
Verify that keyed services resolve correctly and the container is wired up properly.
ContainerTests.cs
[Fact]
public void DI_Resolves_Correct_Factory_By_Key()
{
var services = new ServiceCollection();
services.AddKeyedSingleton<IUIFactory, WinFactory>("windows");
services.AddKeyedSingleton<IUIFactory, MacFactory>("mac");
var sp = services.BuildServiceProvider();
var win = sp.GetRequiredKeyedService<IUIFactory>("windows");
var mac = sp.GetRequiredKeyedService<IUIFactory>("mac");
Assert.IsType<WinFactory>(win);
Assert.IsType<MacFactory>(mac);
Assert.IsType<WinButton>(win.CreateButton());
Assert.IsType<MacButton>(mac.CreateButton());
}
Strategy 4: Integration Test with WebApplicationFactory
Override the production factory with a test factory in integration tests to control the entire product family.
IntegrationTest.cs
public class UITests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public UITests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// ✅ Replace production factory with test factory
services.RemoveAll<IUIFactory>();
services.AddSingleton<IUIFactory, FakeUIFactory>();
});
}).CreateClient();
}
[Fact]
public async Task Dialog_Endpoint_Uses_Test_Factory()
{
var response = await _client.GetStringAsync("/dialog");
Assert.Contains("fake-btn", response);
}
}
Section 15
Performance Considerations
Abstract Factory overhead is negligible in most applications. The factory itself is typically a singleton — the real cost is product creation, which is the same with or without the pattern.
Approach
Time per call
Allocations
Notes
new WinButton()
~2 ns
1 object
Direct instantiation — baseline
Factory Method
~4 ns
1 object
Virtual dispatchThe process of calling a method through an interface or virtual method — the CLR looks up the actual method to call at runtime via a vtable, costing ~2ns extra. overhead (~2 ns for vtableVirtual method table — a mechanism used by the CLR to implement runtime polymorphism. Each class has a vtable mapping interface methods to concrete implementations. lookup)
Abstract Factory (3 products)
~12 ns
3 objects
3× virtual dispatch — factory itself is free if singleton
DI GetService<T>
~50 ns
1 object + scope
Container resolution overhead
Keyed DI GetKeyedService
~60 ns
1 object + scope
Key lookup + container resolution
Dictionary<string, IFactory>
~15 ns
0 (lookup only)
Hash lookup to find factory, then virtual dispatch for products
FrozenDictionary lookup
~8 ns
0 (lookup only)
Optimized hash — 2× faster than regular Dictionary for reads
Activator.CreateInstance
~500 ns
1 object + reflection
Reflection — avoid on hot pathsCode that executes very frequently in a tight loop — even small overhead matters here (e.g., per-request processing in a high-traffic API).
Benchmarks.cs
// BenchmarkDotNet — measuring Abstract Factory overhead
[MemoryDiagnoser]
public class AbstractFactoryBenchmarks
{
private readonly IUIFactory _directFactory = new WinFactory();
private readonly IServiceProvider _sp;
private readonly FrozenDictionary<string, IUIFactory> _frozen;
public AbstractFactoryBenchmarks()
{
var services = new ServiceCollection();
services.AddKeyedSingleton<IUIFactory, WinFactory>("win");
services.AddKeyedSingleton<IUIFactory, MacFactory>("mac");
_sp = services.BuildServiceProvider();
_frozen = new Dictionary<string, IUIFactory>
{
["win"] = new WinFactory(),
["mac"] = new MacFactory()
}.ToFrozenDictionary();
}
[Benchmark(Baseline = true)]
public IButton DirectNew() => new WinButton();
[Benchmark]
public IButton ViaFactory() => _directFactory.CreateButton();
[Benchmark]
public IButton ViaKeyedDI() =>
_sp.GetRequiredKeyedService<IUIFactory>("win").CreateButton();
[Benchmark]
public IButton ViaFrozenDict() =>
_frozen["win"].CreateButton();
[Benchmark]
public (IButton, ICheckbox, ITextBox) CreateFamily() =>
(_directFactory.CreateButton(),
_directFactory.CreateCheckbox(),
_directFactory.CreateTextBox());
}
// Results: Factory overhead is ~2ns/call — invisible next to
// any I/O, HTTP, or database operation (ms-scale)
Key Takeaways
Factory singleton: Register Abstract Factory as singleton — zero allocation overhead per resolve. Products are the only allocations.
FrozenDictionary for registries: If you maintain a dictionary mapping keys to factories, use FrozenDictionary (.NET 8+) for O(1) lookups optimized for read-heavy workloads — 2× faster than regular Dictionary.
GC pressureThe rate at which your application allocates objects that the garbage collector must later reclaim — high GC pressure causes frequent pauses and degrades throughput.: Abstract Factory itself adds zero GC pressure (singleton). Product objects follow normal allocation patterns — profile your specific products, not the factory.
AOT-friendly: If targeting Native AOTPublishing .NET apps as self-contained native executables — no JIT, faster startup, but reflection and dynamic code generation won't work., avoid reflection-based factory discovery. Use source generators or static registration instead of Assembly.GetTypes().
When Performance Actually Matters
For 99% of applications, Abstract Factory overhead is irrelevant — your database queries take 1-50ms, dwarfing the ~12ns factory cost. Only optimize factory performance if you're creating millions of product families per second (game engines, real-time trading). Profile first, optimize second.
Section 16
How to Explain in an Interview
Your Script (90 seconds)
Opening: "Abstract Factory provides an interface for creating families of related objects that must work together — not just one product, but an entire coordinated set."
Analogy: "Think of IKEA furniture collections — when you pick 'Modern', you get a matching Modern chair, table, and lamp. The factory ensures you never accidentally mix styles. That's Abstract Factory."
.NET Example: "In .NET, DbProviderFactory is the textbook example — call GetFactory("Npgsql") and it creates a related Connection, Command, and DataAdapter all guaranteed to work with PostgreSQL. Switch the string to Microsoft.Data.SqlClient and the entire family changes to SQL Server."
vs Factory Method: "The key difference from Factory Method: Factory Method creates one product type. Abstract Factory creates an entire family. It adds a second dimension — you're varying the product family, not just the product type."
Modern .NET: "In .NET 8+, keyed services let you register multiple factory implementations by key — AddKeyedSingleton<IUIFactory, WinFactory>("windows"). You get the same Abstract Factory semantics with less boilerplate."
Close: "I'd reach for Abstract Factory when products from different families are incompatible and must not be mixed — cross-platform UI, multi-database support, or multi-cloud abstractions. If products don't need to be related, plain Factory Method is simpler."
Section 17
Interview Q&As
Easy (5)
Q1: What is the Abstract Factory pattern?
Easy
Think First Can you explain it in one sentence without using the word "factory"?
Think of a furniture store that sells "collections" — a Modern collection (modern sofa + modern table + modern lamp) and a Victorian collection (Victorian sofa + Victorian table + Victorian lamp). You pick a collection, and everything in it is guaranteed to match. You never accidentally get a Victorian lamp with a Modern table. That is the Abstract Factory pattern in a nutshell: you pick a family, and the factory gives you matching pieces.
In code terms, it is a creational design pattern that provides an interface for creating families of related objects without specifying their concrete classes. The client works only with abstract interfaces — it never knows whether it got a Windows button or a macOS button. It just knows it got "a button" that works with the rest of the family.
Key participants: AbstractFactory (the interface with create methods — like the furniture store catalog), ConcreteFactory (one per family — the "Modern" section, the "Victorian" section), AbstractProduct (interface per product type — "a sofa," "a table"), ConcreteProduct (specific to each family — "Modern Sofa," "Victorian Table"), and Client (the customer who shops using abstractions only).
Great Answer Bonus "It enforces family consistency at the type level — if you have a WinFactory, every product it creates is guaranteed to be Windows-compatible."
Q2: How is Abstract Factory different from Factory Method?
Easy
Think First How many products does each pattern create? How many dimensions of variation?
Here is a simple analogy. Factory Method is like a bakery that makes one thing — bread — but in different varieties (sourdough, rye, whole wheat). You ask the bakery for bread, and the specific bakery decides which kind you get. Abstract Factory is like a meal kit service that gives you a complete set of matching items — an appetizer, main course, and dessert that all go together. You pick Italian or Japanese, and the service gives you a coordinated meal, not a random mix.
Factory Method creates one product via one method — subclasses decide which variant. Abstract Factory creates an entire family of related products via multiple create methods — the factory decides which family.
Factory Method: One factory method, one product type, multiple variants (vertical variation)
Think "one product" vs "coordinated product set." Abstract Factory often uses Factory Methods internally — each CreateButton() is essentially a Factory Method inside the larger factory.
Great Answer Bonus "Abstract Factory is open for new families (add a class) but closed for new products (interface change). Factory Method is the opposite — easy to add new product variants."
Q3: Name a real Abstract Factory in .NET.
Easy
Think First Think of a .NET class that creates multiple related objects you use together...
The most famous example is DbProviderFactory in ADO.NET. Think of it this way: when you talk to a database, you need several related objects that must all "speak the same language" — a connection, a command, a data adapter, and parameters. You cannot use a SQL Server connection with a PostgreSQL command. DbProviderFactory solves this by being an Abstract Factory: each provider (SQL Server, PostgreSQL, SQLite) ships its own concrete factory that creates a family of compatible objects. You ask for a connection, you get a SqlConnection. You ask for a command, you get a SqlCommand. Everything matches.
Other real examples in .NET:
CultureInfo — creates NumberFormatInfo + DateTimeFormatInfo + TextInfo as a locale family. Pick "en-US" and all formatting objects follow American conventions
EF Core database providers — UseSqlServer() vs UseNpgsql() swaps 30+ related services at once
Great Answer Bonus Mention that IHttpClientFactory with named clients is AF-like (creates HttpClient + handler pipeline + policies as a family), while plain IHttpClientFactory.CreateClient() is closer to Factory Method.
Q4: When should you use Abstract Factory?
Easy
Think First What makes Abstract Factory necessary vs just using multiple Factory Methods?
The trigger is simple: you have multiple objects that must come from the same family, and mixing families would break things. Think of it like buying car parts — you need tires, brake pads, and an oil filter that are all compatible with your specific car model. You would not want a Honda oil filter paired with Toyota brake pads. Abstract Factory ensures the "parts" all come from the same "manufacturer."
Use Abstract Factory when:
Cross-platform UI — Windows controls must not mix with Mac controls on the same screen
Multi-database support — SQL Server connections must not mix with PostgreSQL commands
Multi-cloud abstraction — AWS storage must not mix with Azure queue services
White-label theming — brand A components must not mix with brand B's color scheme
If products from different families can safely mix without problems, you probably do not need Abstract Factory. The pattern adds complexity, so only use it when family consistency is genuinely important.
Great Answer Bonus "The trigger is incompatible families — if products from different families can safely mix, you don't need Abstract Factory."
Q5: What problem does Abstract Factory solve?
Easy
Think First What goes wrong if you don't use Abstract Factory in a multi-family system?
Imagine plugging a UK appliance into a US outlet — the plug fits (the code compiles), but you fry the device when you turn it on (runtime failure). That is what happens without Abstract Factory: you can easily create a SQL Server connection paired with a PostgreSQL command. They both implement the same interfaces, so the compiler sees no issue. But at runtime, the connection speaks one protocol and the command speaks another — and everything breaks.
Abstract Factory solves this by guaranteeing that all products in a set come from the same family. You cannot accidentally mix incompatible pieces because the factory only knows how to create its own family's products.
Secondary problems it solves:
Client coupling — without it, client code references concrete types (new SqlConnection()) and becomes impossible to change
Swapping families — switching from Windows to Mac UI requires finding and changing every new WinButton() in the entire codebase
Testing — you cannot substitute a lightweight test family if the code hard-codes production types
Medium (7)
Q6: How would you implement Abstract Factory with .NET DI?
Medium
Think First How do you register multiple implementations of the same interface in .NET DI?
The challenge is that .NET's built-in DI container normally maps one interface to one implementation. But Abstract Factory often needs multiple factories (one per family) behind the same interface. Here are three approaches depending on your .NET version:
.NET 6-7: Register a Func<string, IUIFactory> delegate that acts as a manual switcher
.NET 8+: Use keyed services — the container itself can hold multiple implementations under different keys
Single family (simplest): Just services.AddSingleton<IUIFactory, WinFactory>() — done
PreNet8Registration.cs
// .NET 6-7: Register a delegate that resolves by key
builder.Services.AddSingleton<WinFactory>();
builder.Services.AddSingleton<MacFactory>();
builder.Services.AddSingleton<Func<string, IUIFactory>>(sp => key => key switch
{
"win" => sp.GetRequiredService<WinFactory>(),
"mac" => sp.GetRequiredService<MacFactory>(),
_ => throw new ArgumentException($"Unknown platform: {key}")
});
// Consumer injects the resolver delegate
public class Dashboard(Func<string, IUIFactory> factoryResolver)
{
public void Render(string platform)
{
var factory = factoryResolver(platform);
var btn = factory.CreateButton();
}
}
Net8KeyedServices.cs
// .NET 8+: Built-in keyed services — clean and native
builder.Services.AddKeyedSingleton<IUIFactory, WinFactory>("win");
builder.Services.AddKeyedSingleton<IUIFactory, MacFactory>("mac");
// Static key — inject with attribute
public class Dashboard([FromKeyedServices("win")] IUIFactory factory)
{
public void Render() => factory.CreateButton().Paint();
}
// Dynamic key — resolve at runtime
public class DynamicDashboard(IServiceProvider sp)
{
public void Render(string platform)
{
var factory = sp.GetRequiredKeyedService<IUIFactory>(platform);
factory.CreateButton().Paint();
}
}
In all cases, the client works with IUIFactory and never knows which family it received. The .NET 8 approach is cleaner because it eliminates the manual delegate and switch statement — the DI container handles the mapping natively.
Great Answer Bonus Show both the pre-.NET 8 Func workaround and the .NET 8 keyed services approach to demonstrate evolution awareness.
Q7: How does DbProviderFactory work internally?
Medium
Think First How does GetFactory("Npgsql") know which class to return?
Think of DbProviderFactories as a phone book for database providers. Each provider registers itself under a unique name (like "Npgsql" or "Microsoft.Data.SqlClient"). When you call GetFactory("Npgsql"), it looks up that name in the registry and hands you the PostgreSQL factory. From there, every object the factory creates — connection, command, adapter, parameter — is guaranteed to be PostgreSQL-compatible.
Here is how it works step by step:
At startup, providers register: DbProviderFactories.RegisterFactory("Npgsql", NpgsqlFactory.Instance)
The static registry stores a dictionary of provider names to factory instances
Client code calls GetFactory("Npgsql") to get the concrete factory
The factory's CreateConnection(), CreateCommand(), etc. return Npgsql-specific types
DbProviderFactoryUsage.cs
// Register the provider at startup
DbProviderFactories.RegisterFactory("Npgsql", NpgsqlFactory.Instance);
// Later — client code doesn't know which database it's talking to
var factory = DbProviderFactories.GetFactory(providerName); // From config
using var conn = factory.CreateConnection(); // NpgsqlConnection
using var cmd = factory.CreateCommand(); // NpgsqlCommand
cmd.Connection = conn;
// Everything is guaranteed to be from the same provider family
In .NET Core (unlike .NET Framework), providers must explicitly call RegisterFactory() — there is no machine.config to auto-discover them. This is conceptually identical to how keyed services work: register under a key, resolve by key.
Great Answer Bonus Mention that in .NET Core, providers must explicitly register (unlike .NET Framework where machine.config handled it), and this is similar to how keyed services work.
Q8: How do you add a new product to an existing Abstract Factory?
Medium
Think First What happens to all existing ConcreteFactory classes when you add CreateDropdown() to the interface?
This is the pattern's Achilles' heel. Adding a new family (like Linux) is easy — you just create a new LinuxFactory class. No existing code changes. But adding a new product (like CreateDropdown()) is painful — you must modify the interface and update every single concrete factory. If you have 5 factories, that is 5 files to edit, 5 tests to update, and 5 chances for merge conflicts.
Mitigations when you must add a product:
Default interface methodsC# 8+ feature allowing interfaces to provide method implementations — existing implementing classes aren't forced to add the new method. (C# 8+):IDropdown CreateDropdown() => new NullDropdown(); — existing factories compile without changes (but beware: the default might hide missing implementations, as we saw in Bug 3)
Split the interface: Separate IInputFactory from ILayoutFactory — new products go in a new interface
Generic create:T Create<T>() where T : IProduct — but loses compile-time safety for which products each factory supports
Great Answer Bonus "Abstract Factory is open for new families (add a class) but closed for new products (interface change). Factory Method is the opposite — this is the key architectural trade-off."
Q9: How do you test Abstract Factory implementations?
Medium
Think First How do you unit test a Dialog class without real platform controls?
The beauty of Abstract Factory for testing is that you can create a TestFactory that returns lightweight stub products — no real UI, no platform dependencies, no heavy resources. The class under test does not know or care that it received fake products.
Four testing approaches:
Test double factory: Create a TestFactory with stub products — fast, no platform deps
Family consistency tests: Verify all products from one factory belong to the same family
DI registration tests: Resolve keyed services and assert correct concrete types
Integration tests:WebApplicationFactory replacing production factory with test factory
FactoryTests.cs
// 1. Test double factory — lightweight stubs
public sealed class TestFactory : IUIFactory
{
public IButton CreateButton() => new StubButton();
public ICheckbox CreateCheckbox() => new StubCheckbox();
}
[Fact]
public void Dialog_Renders_With_Factory_Products()
{
var dialog = new Dialog(new TestFactory()); // No real UI needed
dialog.Render();
Assert.True(dialog.IsRendered);
}
// 2. Parameterized family consistency test
[Theory]
[InlineData("win")]
[InlineData("mac")]
public void All_Products_Are_Non_Null(string key)
{
var factory = _sp.GetRequiredKeyedService<IUIFactory>(key);
Assert.NotNull(factory.CreateButton());
Assert.NotNull(factory.CreateCheckbox());
}
Great Answer Bonus "I'd write a parameterized test that iterates all registered factory keys, resolves each, creates all products, and verifies type consistency — catches the 'forgot to implement CreateDropdown in LinuxFactory' bug."
Q10: When does Abstract Factory violate ISP?
Medium
Think First What if a client only needs buttons but the factory also creates checkboxes, textboxes, and dropdowns?
When the factory interface has many create methods (e.g., 8+ products) and most clients only use a subset. The client is forced to depend on factory methods it never calls — classic ISPInterface Segregation Principle — clients should not be forced to depend on interfaces they don't use. Split fat interfaces into smaller, focused ones. violation.
Fix: split into focused factory interfaces (IInputFactory for Button + Checkbox, ILayoutFactory for Panel + Grid) and compose them when needed. Each client depends only on the factory interface it actually uses.
Q11: How do you handle optional products in a family?
Medium
Think First What if macOS has no system tray icon but Windows does? How does the Mac factory handle CreateTrayIcon()?
Not every product makes sense in every family. Maybe Windows has a system tray icon but macOS does not. Maybe a mobile factory cannot produce a multi-window layout. You need a strategy for "this family does not support this product." There are four options:
Null Object: Return a no-op implementation that does nothing when called. NullTrayIcon silently ignores Show(). Safe when the product is truly optional and skipping it does not break anything.
NotSupportedException: Throw a clear exception. Fails fast if someone calls the unsupported method. Good when mixing is a bug, not a feature.
Split the factory (best): Move optional products into their own interface. ITrayFactory exists only for platforms that support it. Aligns with ISP — clients only depend on what they use.
TryCreate pattern:bool TryCreateTrayIcon(out ITrayIcon? icon) — the caller checks before using.
OptionalProducts.cs
// Option 1: Null Object — safe when product is truly optional
public sealed class MacFactory : IUIFactory
{
public ITrayIcon CreateTrayIcon() => NullTrayIcon.Instance; // Does nothing
}
// Option 3: Split interface — cleanest
public interface IUIFactory { IButton CreateButton(); ICheckbox CreateCheckbox(); }
public interface ITrayFactory { ITrayIcon CreateTrayIcon(); }
// Windows supports both, macOS only implements IUIFactory
public sealed class WinFactory : IUIFactory, ITrayFactory { ... }
public sealed class MacFactory : IUIFactory { ... } // No ITrayFactory
Option 3 (splitting) is the cleanest approach because it makes the capability explicit in the type system. If a class needs a tray icon, it injects ITrayFactory — and only platforms that support it will provide one. No nulls, no exceptions, no surprises.
Q12: What are the thread safety concerns with Abstract Factory?
Medium
Think First A singleton factory is shared across all threads. What could go wrong?
If the factory is a singleton (common), its create methods must be thread-safeCode that can be safely executed by multiple threads simultaneously without data corruption, race conditions, or other concurrency issues.. Stateless factories (no fields, just => new Product()) are naturally safe.
Danger zones:
Factory caches products → use ConcurrentDictionary or Lazy<T>
Factory holds mutable state (counter, last-created) → race conditions
Products hold state → create new instances per request, never share
Great Answer Bonus "The safest pattern: stateless singleton factory, transient products. This matches DI lifetimes naturally — factory is Singleton, products are created fresh each call."
Hard (17)
Q13: How do keyed services in .NET 8 change Abstract Factory implementation?
Hard
Think First Before .NET 8, how did you register multiple implementations of the same interface?
Before .NET 8, registering multiple implementations of the same interface was awkward. You needed workarounds like Func<string, IFactory> delegates or hand-built dictionaries. .NET 8 changed the game with keyed services — the DI container natively supports multiple registrations under different keys.
Keyed services let you register multiple IUIFactory implementations with different keys. Resolve with [FromKeyedServices("win")] for compile-time keys, or GetRequiredKeyedService<T>(key) for runtime-dynamic keys.
This eliminates:
Custom factory resolver classes
Func<string, IFactory> delegate workarounds
Manual Dictionary<string, IFactory> registries
The DI container itself becomes the Abstract Factory registry.
KeyedServices.cs
// Registration — one line per family
builder.Services.AddKeyedSingleton<IUIFactory, WinFactory>("win");
builder.Services.AddKeyedSingleton<IUIFactory, MacFactory>("mac");
// Constructor injection — declarative, clean
public class Renderer([FromKeyedServices("mac")] IUIFactory factory) { }
// Dynamic resolution — when key is runtime data
public class DynamicRenderer(IServiceProvider sp)
{
public void Render(string platform) =>
sp.GetRequiredKeyedService<IUIFactory>(platform)
.CreateButton().Paint();
}
Great Answer Bonus "Keyed services make the DI container the Abstract Factory. The trade-off: you lose the explicit factory interface, which means compile-time verification of family completeness disappears — you trade type safety for convenience."
Q14: How do you make Abstract Factory AOT-friendly?
Hard
Think First What parts of Abstract Factory rely on reflection, and how would you eliminate them?
Source generators: Emit factory registrations at compile time — scan for [Family("win")] attributes and generate AddFactories() extension methods
Explicit registration: Manually register all concrete types in IServiceCollection
FrozenDictionary: Static initialization — structure known at build time, AOT-safe
[DynamicallyAccessedMembers]: If reflection is unavoidable, annotate to preserve types during trimming
Great Answer Bonus "Source generators are the ideal solution — you get the convenience of assembly scanning with the performance and AOT-compatibility of static registration."
Q15. Can Abstract Factory use covariant return types?
Hard
Think First What changed in C# 9 regarding return types? Does it work on interfaces or only class overrides?
C# 9+ supports covariantA type relationship where a subtype can be used where a supertype is expected — in C#, covariant return types allow overrides to return a more derived type than the base method. return types on override methods — but only on classes, not interfaces.
How it works:ConcreteFactory can override CreateButton() to return WinButton instead of IButton
The catch: If the client holds an IUIFactory reference (which is the entire point), it still sees IButton
When useful: Internal factory consumers that know the concrete type — e.g., unit tests that construct WinFactory directly and want WinButton without casting
CovariantFactory.cs
public abstract class UIFactoryBase
{
public abstract IButton CreateButton();
}
public class WinFactory : UIFactoryBase
{
// Covariant return — returns WinButton (more specific)
public override WinButton CreateButton() => new();
}
// Client using base type — still sees IButton
UIFactoryBase factory = new WinFactory();
IButton btn = factory.CreateButton();
// Client using concrete type — sees WinButton directly
WinFactory winFactory = new();
WinButton winBtn = winFactory.CreateButton(); // No cast needed!
Great Answer Bonus "Covariant returns are useful for testing and internal code, but they don't change the Abstract Factory contract — clients still program to the interface."
Q16. How would you implement an open generic Abstract Factory?
Hard
Think First How does .NET DI handle open generics? What's the trade-off with compile-time safety?
Define a generic factory interface and register open generics so the DI container closes them at resolve time:
OpenGenericFactory.cs
// Generic factory interface
public interface IFactory<TFamily> where TFamily : IFamily
{
T Create<T>() where T : IProduct;
}
// Concrete implementation per family
public class WinFactory<TFamily> : IFactory<TFamily>
where TFamily : IWindowsFamily
{
public T Create<T>() where T : IProduct
=> (T)_registry[typeof(T)](); // Dictionary<Type, Func<IProduct>>
}
// Register open generic — DI closes it at resolve time
services.AddSingleton(typeof(IFactory<>), typeof(WinFactory<>));
Benefit: One factory interface for any number of product types
Trade-off: Loses compile-time enforcement of which products each family must produce — runtime errors for unsupported products
Mitigation: Add a bool CanCreate<T>() check, or throw a meaningful exception with the missing product type
Great Answer Bonus "Open generic factories trade compile-time safety for flexibility — use them only when the set of products isn't known at compile time, like plugin systems."
Q17. How do you switch product families at runtime?
Hard
Think First What mechanism selects which factory to use when the choice depends on user settings or request context?
Four strategies depending on when the switch happens:
Keyed services (.NET 8+): Resolve by a runtime key — provider.GetRequiredKeyedService<IUIFactory>(userPreference)
Enumerable injection: Inject IEnumerable<IUIFactory> and select with .First(f => f.SupportsTheme(theme))
Factory selector: Dedicated service wraps selection logic — IFactorySelector.GetFactory(criteria)
Scoped per-request: Middleware reads the request and registers the correct factory as scoped
RuntimeSwitch.cs
// Per-request factory switching via middleware
app.Use(async (context, next) =>
{
var theme = context.Request.Cookies["theme"] ?? "light";
var factory = context.RequestServices
.GetRequiredKeyedService<IUIFactory>(theme);
context.Items["UIFactory"] = factory;
await next();
});
Great Answer Bonus "For per-request switching, keyed services + middleware is the cleanest approach — the factory resolves from DI without any Service Locator smell."
Q18. How does Abstract Factory apply in microservices?
Hard
Think First What "families" of infrastructure do microservices typically need? Why must the pieces within a family be compatible?
Each microservice may need consistent infrastructure families:
Message broker family: RabbitMQ producer + consumer + health check vs Kafka producer + consumer + health check
Cache family: Redis client + serializer + health check vs Memcached equivalents
Abstract Factory ensures each service uses a consistent infrastructure family — you never accidentally mix a Kafka producer with a RabbitMQ health check. In practice, the family selection is often driven by environment variables at deployment time:
InfraFactory.cs
var broker = builder.Configuration["Messaging:Provider"]; // "rabbitmq" or "kafka"
builder.Services.AddKeyedSingleton<IMessagingFactory>("rabbitmq", new RabbitMqFactory());
builder.Services.AddKeyedSingleton<IMessagingFactory>("kafka", new KafkaFactory());
// Resolve per environment
builder.Services.AddScoped<IMessagingFactory>(sp =>
sp.GetRequiredKeyedService<IMessagingFactory>(broker));
Great Answer Bonus "Abstract Factory in microservices prevents the 'infrastructure mismatch' bug — where one component talks to Kafka but the health check monitors RabbitMQ."
Q19. What's the performance cost of Abstract Factory vs direct instantiation?
Hard
Think First What's the actual overhead of interface dispatch? When does it matter vs when is it irrelevant?
Interface dispatch: ~2ns per call (vtable lookup) — essentially free
Factory singleton: Zero per-call allocation for the factory itself
DI resolution: ~50ns if resolving the factory each time — cache the reference instead
Product allocation: Normal heap allocation — same cost as new
FrozenDictionary lookup: O(1), optimized at build time — ideal for factory registries on hot paths
For 99.9% of applications, the overhead is unmeasurable. The real cost is in what the products do (DB calls, HTTP requests), not in factory dispatch.
Great Answer Bonus "The performance cost of Abstract Factory is ~2ns of interface dispatch — your average DB query takes 2ms. Optimize the query, not the factory."
Q20. How do you resolve circular dependencies between factories?
Hard
Think First If FactoryA needs FactoryB and FactoryB needs FactoryA, what does that tell you about the design?
Three approaches, in order of preference:
Extract shared concern (best fix): If two factories depend on each other, they share a responsibility — extract it into its own service both factories depend on
Lazy<T> injection: One factory takes Lazy<IOtherFactory> to defer resolution and break the cycle
Mediator / events: Decouple via IMediator so factories never reference each other directly
LazyBreakCycle.cs
// Option 2: Lazy<T> breaks the cycle
public class OrderFactory(Lazy<IPaymentFactory> paymentFactory)
: IOrderFactory
{
public IOrder Create(Cart cart)
{
var order = new Order(cart);
// Lazy resolves only when accessed — cycle broken
order.Payment = paymentFactory.Value.CreatePayment(order);
return order;
}
}
Great Answer Bonus "Circular factory dependencies almost always signal a design flaw — the right fix is extracting the shared concern, not working around it with Lazy<T>."
Q21. What is a "factory of factories" and when would you use it?
Hard
Think First What if the choice of which Abstract Factory to use is itself complex — multi-criteria, configuration-based, or dynamic?
A meta-factory that selects which Abstract Factory to use based on runtime criteria:
FactoryProvider.cs
public interface ICloudFactoryProvider
{
ICloudFactory GetFactory(string region);
}
public class CloudFactoryProvider(IServiceProvider sp) : ICloudFactoryProvider
{
public ICloudFactory GetFactory(string region) => region switch
{
"us-east-1" or "us-west-2" => sp.GetRequiredKeyedService<ICloudFactory>("aws"),
"westeurope" or "northeurope" => sp.GetRequiredKeyedService<ICloudFactory>("azure"),
"asia-east1" => sp.GetRequiredKeyedService<ICloudFactory>("gcp"),
_ => throw new ArgumentException($"No cloud provider for region '{region}'")
};
}
Use when: Family selection is complex (multi-criteria, user preferences, A/B tests)
Avoid when: Keyed services with a single key can handle the selection — don't over-engineer
Great Answer Bonus "In modern .NET, keyed services often eliminate the need for a meta-factory — only add this layer when selection logic is genuinely complex."
Q22. How do you combine Abstract Factory with Builder?
Hard
Think First Abstract Factory picks the family, Builder constructs complex products — how do these compose?
The factory selects the family, the builder handles step-by-step construction within that family:
FactoryPlusBuilder.cs
public interface IUIFactory
{
IFormBuilder CreateFormBuilder(); // Returns a builder, not a product
IButton CreateButton();
}
public class WinFactory : IUIFactory
{
public IFormBuilder CreateFormBuilder() => new WinFormBuilder();
public IButton CreateButton() => new WinButton();
}
// Builder handles complex construction
IFormBuilder builder = factory.CreateFormBuilder();
IForm form = builder
.AddField("Name", FieldType.Text)
.AddField("Email", FieldType.Email)
.SetLayout(Layout.TwoColumn)
.AddValidation(new RequiredRule())
.Build(); // Returns WinForm or MacForm depending on factory
Abstract Factory: Decides which family of products
Builder: Decides how to construct a complex product within that family
Great Answer Bonus "Factory picks the family, Builder handles construction — they solve orthogonal problems and compose naturally."
Q23. How would you use source generators to create Abstract Factories?
Hard
Think First What if you could get the convenience of assembly scanning with zero runtime reflection cost?
A Roslyn source generator scans for product types at compile time and emits factory code:
Step 1: Mark products with [Family("win")] and [Product(typeof(IButton))] attributes
Step 2: Generator scans for these attributes and groups by family
Step 3: Emits concrete factory classes — one per family, with all create methods
// Auto-generated by source generator
[GeneratedCode("FactoryGenerator", "1.0")]
public sealed class WinFactory : IUIFactory
{
public IButton CreateButton() => new WinButton();
public ICheckbox CreateCheckbox() => new WinCheckbox();
}
public static class FactoryRegistration
{
public static IServiceCollection AddUIFactories(this IServiceCollection services)
{
services.AddKeyedSingleton<IUIFactory, WinFactory>("win");
services.AddKeyedSingleton<IUIFactory, MacFactory>("mac");
return services;
}
}
Great Answer Bonus "Source generators give you the convenience of assembly scanning with zero runtime cost — plus compile-time errors if a family is missing a product."
Q24. When should you decompose an Abstract Factory?
Hard
Think First What are the signs that your factory interface has grown too large? How does ISP apply here?
Decompose when any of these signals appear:
Fat interface: 5+ create methods and growing — violates ISP
Independent variation: Some products change independently — not all products vary by family
Disjoint consumers: Different clients use different subsets of products
Frequent churn: New products added often, causing changes across all concrete factories
Split into smaller cohesive factories and compose them:
DecomposedFactories.cs
// Instead of one fat factory:
// IUIFactory { CreateButton(); CreateCheckbox(); CreateTextBox();
// CreateDropdown(); CreateModal(); CreateToast(); }
// Split by cohesion:
public interface IInputFactory { IButton CreateButton(); ICheckbox CreateCheckbox(); }
public interface ILayoutFactory { IModal CreateModal(); IToast CreateToast(); }
// Each client depends only on what it uses
public class FormRenderer(IInputFactory inputs) { ... }
public class NotificationService(ILayoutFactory layout) { ... }
Great Answer Bonus "Apply ISP to your factories — if no single client uses all the create methods, the factory is too broad."
Q25. How does Abstract Factory support plugin architectures?
Hard
Think First How do you add a new product family without recompiling the host application?
Each plugin ships an assembly containing a ConcreteFactory + all its products. The host discovers and loads them at runtime:
PluginLoader.cs
// Host scans plugin directory and loads factories
var pluginDir = Path.Combine(AppContext.BaseDirectory, "plugins");
foreach (var dll in Directory.GetFiles(pluginDir, "*.dll"))
{
var context = new AssemblyLoadContext(dll, isCollectible: true);
var assembly = context.LoadFromAssemblyPath(dll);
var factoryTypes = assembly.GetTypes()
.Where(t => typeof(IUIFactory).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in factoryTypes)
{
var attr = type.GetCustomAttribute<FamilyAttribute>();
services.AddKeyedSingleton(typeof(IUIFactory), attr!.Name,
(sp, _) => (IUIFactory)Activator.CreateInstance(type)!);
}
}
Add family: Drop a new DLL in the plugins folder — zero host code changes
Hot reload: Use isCollectible: true to unload/reload plugin assemblies
AOT caveat: This approach uses reflection and is NOT AOT-compatible — use source generators for AOT scenarios
Great Answer Bonus "Plugin-based factories let you add product families by deploying DLLs — the host never changes. This is how Visual Studio extensions and MEF-based apps work."
Q26. How do you handle cross-assembly Abstract Factories?
Hard
Think First Where do the interfaces live vs the implementations? How do you avoid coupling the host to concrete assemblies?
Family assemblies (UI.Windows.dll, UI.Mac.dll): Contain concrete factories + products — referenced only by the composition root
Host assembly: References only contracts; concrete assemblies loaded via DI registration or plugins
CrossAssembly.csproj
// In Program.cs (composition root) — the ONLY place that knows concrete types
builder.Services.AddKeyedSingleton<IUIFactory, WinFactory>("win"); // from UI.Windows
builder.Services.AddKeyedSingleton<IUIFactory, MacFactory>("mac"); // from UI.Mac
// All other code depends only on UI.Abstractions
public class Dashboard(IUIFactory factory) { ... } // No idea which family
Great Answer Bonus "Interfaces in a shared contracts assembly, implementations in family assemblies, wired up only at the composition root — clean dependency inversion."
Q27. Should an Abstract Factory hold state?
Hard
Think First What happens if a singleton factory holds mutable state? What about immutable configuration?
Generally no — stateless factories are simpler, thread-safe, and safely registered as singletons. But sometimes configuration is necessary:
Immutable config is OK: Connection strings, API keys injected via constructor — the factory is still effectively immutable
Mutable state is dangerous: Counters, caches, last-created references — race conditions in multi-threaded scenarios
Per-call context: Pass as method parameter, not factory state — CreateButton(Theme theme) not factory.Theme = theme
StatefulFactory.cs
// ✅ OK — immutable configuration via constructor
public sealed class AzureStorageFactory(AzureOptions options) : IStorageFactory
{
public IBlobClient CreateBlobClient()
=> new AzureBlobClient(options.ConnectionString);
}
// ❌ BAD — mutable state on a singleton factory
public class BadFactory : IUIFactory
{
public Theme CurrentTheme { get; set; } // Race condition!
public IButton CreateButton() => new Button(CurrentTheme);
}
Great Answer Bonus "Inject immutable configuration, never mutate after construction, and pass per-call context as method parameters."
Q28. How do you create products in parallel across a family?
Hard
Think First When product creation involves I/O (network, disk), how do you avoid sequential bottlenecks?
When product creation is expensive (API calls, I/O), define async create methods and use Task.WhenAll:
ParallelCreation.cs
public interface ICloudFactory
{
ValueTask<IStorage> CreateStorageAsync(CancellationToken ct = default);
ValueTask<IQueue> CreateQueueAsync(CancellationToken ct = default);
ValueTask<ICache> CreateCacheAsync(CancellationToken ct = default);
}
// Create all products in parallel — 3 API calls, ~1 RTT total
var storageTask = factory.CreateStorageAsync(ct);
var queueTask = factory.CreateQueueAsync(ct);
var cacheTask = factory.CreateCacheAsync(ct);
await Task.WhenAll(storageTask.AsTask(), queueTask.AsTask(), cacheTask.AsTask());
var storage = storageTask.Result;
var queue = queueTask.Result;
var cache = cacheTask.Result;
Use ValueTask: Avoids heap allocation when creation is synchronous (cached products)
CancellationToken: Always accept it — lets the caller cancel all creations on timeout
When it matters: Cloud provisioning, external service initialization, database connections
Great Answer Bonus "Use ValueTask for create methods that might be synchronous (cached) and async (first call). Task.WhenAll parallelizes the expensive first calls."
Q29. How do you migrate from a switch-based factory to Abstract Factory?
Hard
Think First How do you incrementally replace a switch statement without breaking everything at once?
Use the Strangler FigA migration pattern where you incrementally replace an old system by routing new functionality to the new implementation while the old system handles remaining cases — named after strangler fig trees that gradually envelop their host. pattern — migrate one family at a time:
Step 1: Extract product interfaces from existing concrete types
Step 2: Extract factory interface from the switch method signatures
Step 3: Create one ConcreteFactory per switch case, moving creation logic into each
Step 4: Register factories in DI with keyed services
Step 5: Replace switch callers with injected IFactory
Step 6: Delete the original switch class once all cases are migrated
StranglerMigration.cs
// BEFORE: God switch factory
public static class UIFactory
{
public static IButton CreateButton(string platform) => platform switch
{
"win" => new WinButton(),
"mac" => new MacButton(),
_ => throw new ArgumentException(nameof(platform))
};
// ... same pattern for every product
}
// AFTER: One ConcreteFactory per case
public sealed class WinFactory : IUIFactory
{
public IButton CreateButton() => new WinButton();
public ICheckbox CreateCheckbox() => new WinCheckbox();
}
// DI registration replaces the switch
services.AddKeyedSingleton<IUIFactory, WinFactory>("win");
services.AddKeyedSingleton<IUIFactory, MacFactory>("mac");
Great Answer Bonus "Use the Strangler Fig approach — migrate one family at a time while the old switch handles remaining cases. This way you never break production."
Section 18
Practice Exercises
Exercise 1: Notification System
Easy
Create an INotificationFactory with CreateSender() and CreateFormatter(). Implement two families: Email (SmtpSender + HtmlFormatter) and SMS (TwilioSender + PlainTextFormatter). Write a NotificationService that uses the factory to send formatted messages.
Hints
Define ISender with Send(string to, string body) and IFormatter with Format(string message): string. The service should call formatter.Format(msg) then sender.Send(to, formatted).
Solution
NotificationFactory.cs
public interface ISender { void Send(string to, string body); }
public interface IFormatter { string Format(string message); }
public interface INotificationFactory
{
ISender CreateSender();
IFormatter CreateFormatter();
}
public sealed class EmailFactory : INotificationFactory
{
public ISender CreateSender() => new SmtpSender();
public IFormatter CreateFormatter() => new HtmlFormatter();
}
public sealed class SmsFactory : INotificationFactory
{
public ISender CreateSender() => new TwilioSender();
public IFormatter CreateFormatter() => new PlainTextFormatter();
}
public class NotificationService(INotificationFactory factory)
{
public void Notify(string to, string message)
{
var formatted = factory.CreateFormatter().Format(message);
factory.CreateSender().Send(to, formatted);
}
}
Exercise 2: Cross-Database Factory
Medium
Implement IDbFactory with CreateConnection(), CreateCommand(), and CreateAdapter(). Create two families: SqlServer and Postgres. Register both with keyed services and write an endpoint that accepts a provider query parameter.
Hints
Use AddKeyedSingleton<IDbFactory, SqlServerFactory>("sqlserver"). In the endpoint, resolve with sp.GetRequiredKeyedService<IDbFactory>(provider). You can use wrapper classes around the real ADO.NET types or simple stubs.
Solution
DbFactory.cs
public interface IDbConnection { string ConnectionInfo { get; } }
public interface IDbCommand { string Execute(string sql); }
public interface IDbFactory
{
IDbConnection CreateConnection();
IDbCommand CreateCommand();
}
public sealed class SqlServerFactory : IDbFactory
{
public IDbConnection CreateConnection() => new SqlConn();
public IDbCommand CreateCommand() => new SqlCmd();
}
public sealed class PostgresFactory : IDbFactory
{
public IDbConnection CreateConnection() => new PgConn();
public IDbCommand CreateCommand() => new PgCmd();
}
// Registration
builder.Services.AddKeyedSingleton<IDbFactory, SqlServerFactory>("sqlserver");
builder.Services.AddKeyedSingleton<IDbFactory, PostgresFactory>("postgres");
// Endpoint
app.MapGet("/query/{provider}", (string provider, IServiceProvider sp) =>
{
var factory = sp.GetRequiredKeyedService<IDbFactory>(provider);
var conn = factory.CreateConnection();
var cmd = factory.CreateCommand();
return $"Connected: {conn.ConnectionInfo}, Result: {cmd.Execute("SELECT 1")}";
});
Exercise 3: UI Theme System
Hard
Build IThemeFactory creating IButton, ITextBox, and IDropdown. Implement Dark and Light themes. Register with keyed services. Create a Blazor/Razor endpoint that renders UI based on user's theme preference stored in a cookie.
Hints
Read the theme cookie in middleware, set a scoped ThemeContext service. In the endpoint, resolve the factory by key from ThemeContext.Current. Each product's Render() method returns HTML with theme-specific CSS classes.
Solution
ThemeFactory.cs
// Product interfaces
public interface IButton { string Render(); }
public interface ITextBox { string Render(); }
public interface IDropdown { string Render(string[] options); }
// Factory interface
public interface IThemeFactory
{
IButton CreateButton();
ITextBox CreateTextBox();
IDropdown CreateDropdown();
}
// Dark theme family
public sealed class DarkButton : IButton
{
public string Render() => "<button class='btn-dark'>Submit</button>";
}
public sealed class DarkTextBox : ITextBox
{
public string Render() => "<input class='input-dark' />";
}
public sealed class DarkDropdown : IDropdown
{
public string Render(string[] options)
=> $"<select class='select-dark'>{string.Join("", options.Select(o => $"<option>{o}</option>"))}</select>";
}
public sealed class DarkThemeFactory : IThemeFactory
{
public IButton CreateButton() => new DarkButton();
public ITextBox CreateTextBox() => new DarkTextBox();
public IDropdown CreateDropdown() => new DarkDropdown();
}
// Light theme family (same structure, different CSS classes)
public sealed class LightThemeFactory : IThemeFactory
{
public IButton CreateButton() => new LightButton();
public ITextBox CreateTextBox() => new LightTextBox();
public IDropdown CreateDropdown() => new LightDropdown();
}
// Registration with keyed services
builder.Services.AddKeyedSingleton<IThemeFactory, DarkThemeFactory>("dark");
builder.Services.AddKeyedSingleton<IThemeFactory, LightThemeFactory>("light");
// Middleware reads cookie and sets scoped factory
app.Use(async (ctx, next) =>
{
var theme = ctx.Request.Cookies["theme"] ?? "light";
var factory = ctx.RequestServices
.GetRequiredKeyedService<IThemeFactory>(theme);
ctx.Items["ThemeFactory"] = factory;
await next();
});
// Endpoint renders themed UI
app.MapGet("/form", (HttpContext ctx) =>
{
var factory = (IThemeFactory)ctx.Items["ThemeFactory"]!;
var html = $"""
{factory.CreateTextBox().Render()}
{factory.CreateDropdown().Render(["Option A", "Option B"])}
{factory.CreateButton().Render()}
""";
return Results.Content(html, "text/html");
});
Exercise 4: Cloud Provider Abstraction
Expert
Create ICloudFactory with CreateStorage(), CreateQueue(), and CreateCache(). Implement three families: AWS, Azure, and GCP. Add an InMemoryFactory for testing. Write integration tests using WebApplicationFactory that swap the production factory for InMemoryFactory.
Hints
Define ICloudStorage (Upload/Download), ICloudQueue (Send/Receive), ICloudCache (Get/Set). InMemoryFactory uses Dictionary for storage, ConcurrentQueue for queue, and MemoryCache for cache. In WebApplicationFactory, call services.RemoveAll<ICloudFactory>() then services.AddSingleton<ICloudFactory, InMemoryFactory>().
public sealed class AwsFactory(IConfiguration config) : ICloudFactory
{
public ICloudStorage CreateStorage()
=> new S3Storage(config["AWS:BucketName"]!);
public ICloudQueue CreateQueue()
=> new SqsQueue(config["AWS:QueueUrl"]!);
public ICloudCache CreateCache()
=> new ElastiCacheClient(config["AWS:CacheEndpoint"]!);
}
// Registration
builder.Services.AddKeyedSingleton<ICloudFactory, AwsFactory>("aws");
builder.Services.AddKeyedSingleton<ICloudFactory, AzureFactory>("azure");
builder.Services.AddKeyedSingleton<ICloudFactory, GcpFactory>("gcp");
// Resolve by config
var provider = builder.Configuration["Cloud:Provider"]; // "aws"
builder.Services.AddSingleton<ICloudFactory>(sp =>
sp.GetRequiredKeyedService<ICloudFactory>(provider!));
InMemoryFactory.cs
public sealed class InMemoryFactory : ICloudFactory
{
public ICloudStorage CreateStorage() => new InMemoryStorage();
public ICloudQueue CreateQueue() => new InMemoryQueue();
public ICloudCache CreateCache() => new InMemoryCache();
}
public sealed class InMemoryStorage : ICloudStorage
{
private readonly ConcurrentDictionary<string, byte[]> _store = new();
public Task UploadAsync(string key, byte[] data) { _store[key] = data; return Task.CompletedTask; }
public Task<byte[]?> DownloadAsync(string key) => Task.FromResult(_store.GetValueOrDefault(key));
}
public sealed class InMemoryQueue : ICloudQueue
{
private readonly ConcurrentQueue<string> _queue = new();
public Task SendAsync(string msg) { _queue.Enqueue(msg); return Task.CompletedTask; }
public Task<string?> ReceiveAsync() => Task.FromResult(_queue.TryDequeue(out var m) ? m : null);
}
public sealed class InMemoryCache : ICloudCache
{
private readonly ConcurrentDictionary<string, string> _cache = new();
public Task SetAsync(string key, string value, TimeSpan _) { _cache[key] = value; return Task.CompletedTask; }
public Task<string?> GetAsync(string key) => Task.FromResult(_cache.GetValueOrDefault(key));
}
CloudIntegrationTests.cs
public class CloudTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public CloudTests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Swap production factory for in-memory test double
services.RemoveAll<ICloudFactory>();
services.AddSingleton<ICloudFactory, InMemoryFactory>();
});
}).CreateClient();
}
[Fact]
public async Task Upload_ThenDownload_RoundTrips()
{
var payload = new { key = "test.txt", data = "SGVsbG8=" };
await _client.PostAsJsonAsync("/api/storage/upload", payload);
var response = await _client.GetAsync("/api/storage/download/test.txt");
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("SGVsbG8=", content);
}
}
Section 19
Cheat Sheet
Classic Abstract Factory
interface IUIFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
}
sealed class WinFactory : IUIFactory
{
IButton CreateButton()
=> new WinButton();
ICheckbox CreateCheckbox()
=> new WinCheckbox();
}
// Client uses factory, never
// sees concrete types
class App(IUIFactory f)
{
void Run()
=> f.CreateButton().Paint();
}
DI + Keyed Services (.NET 8+)
// Register both families
services.AddKeyedSingleton<
IUIFactory, WinFactory>("win");
services.AddKeyedSingleton<
IUIFactory, MacFactory>("mac");
// Inject by key
class Page(
[FromKeyedServices("win")]
IUIFactory factory)
{ }
// Resolve at runtime
var f = sp
.GetRequiredKeyedService<
IUIFactory>(userPref);
Decision Rules
One product type?
→ Factory Method
Multiple related products?
→ Abstract Factory
Must products be compatible?
→ Yes = Abstract Factory
→ No = Separate factories
One family only?
→ Concrete classes directly
Simple creation?
→ Keyed services
Complex creation (async/config)?
→ Full factory hierarchy
Factory returns interface,
never concrete type ✓
Factories are sealed ✓
Test all families produce
all products ✓
Section 20
Deep Dive: Plugin Architectures
One of the most powerful real-world uses of Abstract Factory is in plugin architecturesA software design where functionality can be extended by loading external modules (plugins) at runtime, without modifying the host application. — loading factory implementations from external assemblies at runtime.
Assembly-Based Plugin Loading
Each plugin ships as a DLL containing a ConcreteFactory + all its products. The host scans a plugins directory, loads assemblies via AssemblyLoadContext.NET mechanism for loading assemblies in isolation — enables plugin loading, unloading, and version isolation without affecting the host application., discovers factories, and registers them in DI.
PluginLoader.cs
// Host: discover and load plugin factories at startup
public static class PluginLoader
{
public static void LoadPlugins(IServiceCollection services, string pluginDir)
{
foreach (var dll in Directory.GetFiles(pluginDir, "*.Plugin.dll"))
{
var context = new AssemblyLoadContext(Path.GetFileName(dll), true);
var assembly = context.LoadFromAssemblyPath(Path.GetFullPath(dll));
var factoryTypes = assembly.GetTypes()
.Where(t => typeof(IUIFactory).IsAssignableFrom(t)
&& t.IsClass && !t.IsAbstract);
foreach (var type in factoryTypes)
{
var keyAttr = type.GetCustomAttribute<FactoryKeyAttribute>();
if (keyAttr is null) continue;
services.AddKeyedSingleton(
typeof(IUIFactory), keyAttr.Key, type);
}
}
}
}
// Plugin assembly: UI.Linux.Plugin.dll
[FactoryKey("linux")]
public sealed class LinuxFactory : IUIFactory
{
public IButton CreateButton() => new GtkButton();
public ICheckbox CreateCheckbox() => new GtkCheckbox();
}
// Usage in Program.cs
PluginLoader.LoadPlugins(builder.Services, "./plugins");
// Now "linux" key resolves to LinuxFactory from the plugin DLL
Hot-Reload Factories
For applications that need to swap factories without restarting (e.g., feature flags, A/B testing), use a factory proxy that delegates to the current active factory.
HotReloadFactory.cs
// Proxy factory that can swap the underlying implementation at runtime
public sealed class HotSwapFactory : IUIFactory
{
private volatile IUIFactory _current;
public HotSwapFactory(IUIFactory initial) => _current = initial;
public IButton CreateButton() => _current.CreateButton();
public ICheckbox CreateCheckbox() => _current.CreateCheckbox();
// Call this from an admin endpoint or feature flag callback
public void SwapTo(IUIFactory newFactory) => _current = newFactory;
}
// Registration
builder.Services.AddSingleton<HotSwapFactory>(sp =>
new HotSwapFactory(sp.GetRequiredKeyedService<IUIFactory>("default")));
builder.Services.AddSingleton<IUIFactory>(sp =>
sp.GetRequiredService<HotSwapFactory>());
// Admin endpoint to swap
app.MapPost("/admin/swap-theme/{key}", (string key, HotSwapFactory proxy, IServiceProvider sp) =>
{
var newFactory = sp.GetRequiredKeyedService<IUIFactory>(key);
proxy.SwapTo(newFactory);
return Results.Ok($"Swapped to {key}");
});
Section 21
Mini-Project: Multi-Database Report Generator
Build a report generator that supports multiple database backends. See how the implementation evolves from junior to senior level.
Attempt 1: The Junior Way
JuniorReport.cs — ❌
// ❌ Everything in one class, hardcoded strings, no abstractions
public class ReportGenerator
{
public string Generate(string dbType, string query)
{
object connection = dbType switch
{
"sqlserver" => new SqlConnection("Server=..."),
"postgres" => new NpgsqlConnection("Host=..."),
_ => throw new NotSupportedException()
};
object command = dbType switch // ❌ Duplicated switch
{
"sqlserver" => new SqlCommand(query),
"postgres" => new NpgsqlCommand(query),
_ => throw new NotSupportedException()
};
// ❌ Casting everywhere, could mix families
// ❌ New database = modify this class
return $"Report from {dbType}";
}
}
Problems
Returns object, duplicated switch, could mix SqlConnection with NpgsqlCommand, no testabilityThe ease with which code can be tested in isolation — high testability means dependencies can be swapped for test doubles without changing the code under test., OCP violation.
Attempt 2: The Mid-Level Way
MidReport.cs — Better
// Proper interfaces but manual factory instantiation
public interface IDbConnection { void Open(); }
public interface IDbCommand { string Execute(string sql); }
public interface IDbFactory
{
IDbConnection CreateConnection();
IDbCommand CreateCommand();
}
public sealed class SqlServerFactory : IDbFactory { /* ... */ }
public sealed class PostgresFactory : IDbFactory { /* ... */ }
public class ReportGenerator
{
public string Generate(string dbType, string query)
{
// ⚠️ Still a switch, but at least uses interfaces
IDbFactory factory = dbType switch
{
"sqlserver" => new SqlServerFactory(), // ⚠️ Manual new
"postgres" => new PostgresFactory(),
_ => throw new NotSupportedException()
};
var conn = factory.CreateConnection();
conn.Open();
return factory.CreateCommand().Execute(query);
}
}
Attempt 3: The Senior Way
Interfaces.cs
// Abstract Products
public interface IDbConnection : IDisposable.NET interface that provides a mechanism for releasing unmanaged resources (database connections, file handles, network sockets) deterministically via the Dispose() method.
{
Task OpenAsync(CancellationToken ct = default);
string Provider { get; }
}
public interface IDbCommand
{
Task<string> ExecuteAsync(string sql, CancellationToken ct = default);
}
public interface IDbHealthCheck
{
Task<bool> IsHealthyAsync(CancellationToken ct = default);
}
// Abstract Factory
public interface IDbFactory
{
IDbConnection CreateConnection();
IDbCommand CreateCommand(IDbConnection connection);
IDbHealthCheck CreateHealthCheck();
}
SqlServerFamily.cs
public sealed class SqlServerConnection(string connStr) : IDbConnection
{
public string Provider => "SqlServer";
public Task OpenAsync(CancellationToken ct) =>
Task.CompletedTask; // Real: open actual connection
public void Dispose() { /* close connection */ }
}
public sealed class SqlServerCommand(SqlServerConnection conn) : IDbCommand
{
public Task<string> ExecuteAsync(string sql, CancellationToken ct) =>
Task.FromResult($"[SqlServer] Executed: {sql}");
}
public sealed class SqlServerHealthCheck(string connStr) : IDbHealthCheck
{
public Task<bool> IsHealthyAsync(CancellationToken ct) =>
Task.FromResult(true); // Real: SELECT 1
}
public sealed class SqlServerFactory(IConfiguration config) : IDbFactory
{
private readonly string _connStr = config.GetConnectionString("SqlServer")!;
public IDbConnection CreateConnection() => new SqlServerConnection(_connStr);
public IDbCommand CreateCommand(IDbConnection conn) =>
new SqlServerCommand((SqlServerConnection)conn);
public IDbHealthCheck CreateHealthCheck() => new SqlServerHealthCheck(_connStr);
}
PostgresFamily.cs
public sealed class PostgresConnection(string connStr) : IDbConnection
{
public string Provider => "PostgreSQL";
public Task OpenAsync(CancellationToken ct) => Task.CompletedTask;
public void Dispose() { }
}
public sealed class PostgresCommand(PostgresConnection conn) : IDbCommand
{
public Task<string> ExecuteAsync(string sql, CancellationToken ct) =>
Task.FromResult($"[PostgreSQL] Executed: {sql}");
}
public sealed class PostgresHealthCheck(string connStr) : IDbHealthCheck
{
public Task<bool> IsHealthyAsync(CancellationToken ct) =>
Task.FromResult(true);
}
public sealed class PostgresFactory(IConfiguration config) : IDbFactory
{
private readonly string _connStr = config.GetConnectionString("Postgres")!;
public IDbConnection CreateConnection() => new PostgresConnection(_connStr);
public IDbCommand CreateCommand(IDbConnection conn) =>
new PostgresCommand((PostgresConnection)conn);
public IDbHealthCheck CreateHealthCheck() => new PostgresHealthCheck(_connStr);
}
Program.cs
var builder = WebApplication.CreateBuilder(args);
// Register both families by key
builder.Services.AddKeyedSingleton<IDbFactory, SqlServerFactory>("sqlserver");
builder.Services.AddKeyedSingleton<IDbFactory, PostgresFactory>("postgres");
// Register the report service
builder.Services.AddScoped<ReportService>();
var app = builder.Build();
app.MapGet("/report/{provider}", async (
string provider, IServiceProvider sp, CancellationToken ct) =>
{
var factory = sp.GetRequiredKeyedService<IDbFactory>(provider);
var svc = new ReportService(factory);
return await svc.GenerateAsync("SELECT * FROM sales", ct);
});
app.MapGet("/health/{provider}", async (
string provider, IServiceProvider sp, CancellationToken ct) =>
{
var factory = sp.GetRequiredKeyedService<IDbFactory>(provider);
var healthy = await factory.CreateHealthCheck().IsHealthyAsync(ct);
return healthy ? Results.Ok("Healthy") : Results.StatusCode(503);
});
app.Run();
ReportService.cs
// Clean service — no knowledge of concrete DB products
public sealed class ReportService(IDbFactory factory)
{
public async Task<string> GenerateAsync(string sql, CancellationToken ct)
{
using var conn = factory.CreateConnection();
await conn.OpenAsync(ct);
var cmd = factory.CreateCommand(conn);
var result = await cmd.ExecuteAsync(sql, ct);
return $"Report ({conn.Provider}): {result}";
}
}
ReportTests.cs
// In-memory test factory — zero DB dependency
public sealed class InMemoryFactory : IDbFactory
{
public IDbConnection CreateConnection() => new InMemoryConnection();
public IDbCommand CreateCommand(IDbConnection conn) => new InMemoryCommand();
public IDbHealthCheck CreateHealthCheck() => new InMemoryHealthCheck();
}
[Fact]
public async Task Report_Uses_Injected_Factory()
{
var svc = new ReportService(new InMemoryFactory());
var result = await svc.GenerateAsync("SELECT 1", CancellationToken.None);
Assert.Contains("InMemory", result);
}
// Integration test — swap factory in WebApplicationFactory
public class ReportApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ReportApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(b =>
b.ConfigureServices(s =>
{
s.RemoveAll<IDbFactory>();
s.AddKeyedSingleton<IDbFactory, InMemoryFactory>("test");
})).CreateClient();
}
}
Section 22
Migration Guide
How to migrate from switch-based creation to a proper Abstract Factory — step by step:
Step 1: Identify Product Families
Look for switch/if-else chains that create multiple objects based on the same discriminator. If the same key ("windows", "postgres") appears in multiple switches creating different types, those types form a product family.
Signs You Need Abstract Factory
Grep for patterns like switch (provider) appearing in multiple methods, each creating a different but related type. That's your family waiting to be extracted.
Step 2: Extract Product & Factory Interfaces
Create an interface for each product type and a factory interface with one create method per product. Keep the interfaces minimal — only methods the client actually calls.
ExtractedInterfaces.cs
// Product interfaces — extracted from concrete types
public interface IDbConnection
{
void Open();
void Close();
}
public interface IDbCommand
{
string Execute(string sql);
}
// Factory interface — one create method per product
public interface IDbFactory
{
IDbConnection CreateConnection();
IDbCommand CreateCommand();
}
Step 3: Create Concrete Factories
One sealed class per switch case. Move the creation logic from each switch branch into the corresponding factory's create method. Use the Strangler Fig pattern — migrate one family at a time while the old switch handles the remaining cases.
ConcreteFactories.cs
// One factory per former switch case
public sealed class SqlServerFactory : IDbFactory
{
public IDbConnection CreateConnection()
=> new SqlServerConnection("Server=.;Database=App;");
public IDbCommand CreateCommand()
=> new SqlServerCommand();
}
public sealed class PostgresFactory : IDbFactory
{
public IDbConnection CreateConnection()
=> new PostgresConnection("Host=localhost;Database=App;");
public IDbCommand CreateCommand()
=> new PostgresCommand();
}
Strangler Fig Tip
Keep the old switch factory alive during migration. Route new callers to the injected IDbFactory while old callers still use the switch. Delete the switch only after all callers are migrated and tests pass.
Step 4: Register in DI & Delete the Switch
Register all factories with keyed services. Replace callers: inject IFactory instead of calling the switch method. Once all callers are migrated, delete the original switch class.
Run tests to verify no regressions.
Migration.cs
// Before: switch-based (delete this after migration)
IConnection conn = provider switch {
"sql" => new SqlConn(), "pg" => new PgConn(), ...
};
// After: DI-based
public class Service(IDbFactory factory) // ✅ Injected
{
public void Run() => factory.CreateConnection().Open();
}
// Registration
builder.Services.AddKeyedSingleton<IDbFactory, SqlFactory>("sql");
builder.Services.AddKeyedSingleton<IDbFactory, PgFactory>("pg");
Section 23
Code Review Checklist
Print it, bookmark it, tattoo it on your forearm. Use this checklist when reviewing Abstract Factory implementations in PRs:
#
Check
Why It Matters
Red Flag
1
Factory returns interfaces, never concrete types
Prevents client coupling to specific family
public WinButton CreateButton()
2
All families implement all products
Prevents runtime NotImplementedException
throw new NotImplementedException() in any create method
3
Factories registered in DI, not manually created
Enables config-based family selection + testing
new WinFactory() in business code
4
No magic strings for keys
Prevents runtime mismatch — typo = null factory
GetKeyedService("widnows") — typo
5
Concrete factories are sealed
Prevents inheritance breaking family consistency
public class WinFactory — missing sealed
6
Products from same factory used together
The entire point of Abstract Factory — no mixing
Different IUIFactory instances used in same scope
7
Factory interface ≤ 5 create methods
Avoid ISP violation; split if larger
IUIFactory with 8+ Create methods
8
Thread safety verified for singletons
Shared factories must handle concurrent calls
Mutable _lastCreated field in singleton factory
9
Unit tests use test double factory
Isolate client logic from product implementations
Tests using real WinFactory for Dialog tests
10
No Service LocatorAn anti-pattern where a class resolves its own dependencies from a container at runtime, hiding dependencies and making code harder to test. inside factories
No test asserting all families create all products
Automate it: Enable these RoslynThe .NET compiler platform — provides APIs for code analysis, refactoring, and source generation. Roslyn analyzers run at compile time to catch issues before runtime. analyzers to catch Abstract Factory issues at compile time:
Modern .NET's DI container often replaces manual Abstract Factory hierarchies. Keyed services are the latest evolution.
Senior Fix: One Factory Per Family
Each platform gets its own sealed factory class implementing IUIFactory. The God Factory's switch statements disappear entirely. Adding a new platform (Linux, Web) means creating a new class — zero modifications to WinFactory or MacFactory.
Fix.cs
// Before (junior) — one class, N switch statements
public class UIFactory {
public object CreateButton() => _platform switch { ... };
public object CreateCheckbox() => _platform switch { ... };
}
// After (senior) — one class per family, no switch
public sealed class WinFactory : IUIFactory {
public IButton CreateButton() => new WinButton();
public ICheckbox CreateCheckbox() => new WinCheckbox();
}
public sealed class MacFactory : IUIFactory {
public IButton CreateButton() => new MacButton();
public ICheckbox CreateCheckbox() => new MacCheckbox();
}
Senior Fix: Type-Safe Family Guarantee
Each factory returns IButton, ICheckbox, etc. — not object.
Since WinFactory only creates Win products, it's structurally impossible to mix families.
The compiler catches type mismatches, not runtime crashes.
TypeSafety.cs
// Before (junior) — returns object, mixing is easy
object btn = factory.CreateButton(); // Could be any platform
object chk = factory.CreateCheckbox(); // Could be different platform!
((WinButton)btn).Click(); // Runtime cast — boom if wrong
// After (senior) — one factory instance = one family
IUIFactory factory = sp.GetRequiredKeyedService<IUIFactory>("mac");
IButton btn = factory.CreateButton(); // MacButton guaranteed
ICheckbox chk = factory.CreateCheckbox(); // MacCheckbox guaranteed
// Mixing impossible — factory only creates Mac products
Senior Fix: Interface-Based Testability
Dialog depends on IUIFactory (interface).
In tests, create an InMemoryFactory that returns lightweight test doubles — no real Win32/Cocoa controls needed.
Fast, deterministic, platform-independent.
DialogTests.cs
// Test factory — no real platform controls
public sealed class TestFactory : IUIFactory
{
public IButton CreateButton() => new FakeButton();
public ICheckbox CreateCheckbox() => new FakeCheckbox();
public ITextBox CreateTextBox() => new FakeTextBox();
}
[Fact]
public void Dialog_renders_all_controls()
{
var dialog = new Dialog(new TestFactory());
dialog.Render(); // Fast, no platform dependencies
// Assert output contains all three control types
}
[Fact]
public void DI_resolves_correct_factory_by_key()
{
var sp = BuildTestServiceProvider();
var factory = sp.GetRequiredKeyedService<IUIFactory>("windows");
Assert.IsType<WinFactory>(factory);
Assert.IsType<WinButton>(factory.CreateButton());
}