Factory Method and Abstract Factory in C#
Learn Factory Method and Abstract Factory patterns in C#: create objects without specifying exact types, support extensibility, and decouple construction from usage.
Factory Method and Abstract Factory
Factory patterns decouple the creation of objects from their usage. Instead of new ConcreteClass(), you ask a factory for an instance — the factory decides what to create.
Why Factories?
// Without factory — tightly coupled to concrete type
var notifier = new EmailNotifier(); // what if we need Slack? SMS? Push?
notifier.Send("Order shipped!");
// With factory — decoupled
var notifier = notifierFactory.Create(user.Preferences);
notifier.Send("Order shipped!"); // factory decides which typeFactory Method
A method (or class) that creates objects. Subclasses override it to change what gets created.
public interface INotifier
{
void Send(string message, string recipient);
}
public class EmailNotifier : INotifier
{
public void Send(string message, string recipient)
=> Console.WriteLine($"Email to {recipient}: {message}");
}
public class SmsNotifier : INotifier
{
public void Send(string message, string recipient)
=> Console.WriteLine($"SMS to {recipient}: {message}");
}
public class SlackNotifier : INotifier
{
public void Send(string message, string recipient)
=> Console.WriteLine($"Slack to #{recipient}: {message}");
}
// Factory method — static factory
public static class NotifierFactory
{
public static INotifier Create(NotificationType type) => type switch
{
NotificationType.Email => new EmailNotifier(),
NotificationType.Sms => new SmsNotifier(),
NotificationType.Slack => new SlackNotifier(),
_ => throw new ArgumentException($"Unknown type: {type}")
};
}
// Usage
var notifier = NotifierFactory.Create(user.PreferredNotification);
notifier.Send("Your order has shipped!", user.Contact);Registration-Based Factory (Open/Closed)
The switch statement above requires modifying the factory to add new types. A registration-based factory is open for extension.
public class NotifierFactory
{
private readonly Dictionary<NotificationType, Func<INotifier>> _registry = new();
public void Register(NotificationType type, Func<INotifier> creator)
=> _registry[type] = creator;
public INotifier Create(NotificationType type)
{
if (!_registry.TryGetValue(type, out var creator))
throw new KeyNotFoundException($"No notifier for {type}");
return creator();
}
}
// At startup — register all types
var factory = new NotifierFactory();
factory.Register(NotificationType.Email, () => new EmailNotifier());
factory.Register(NotificationType.Sms, () => new SmsNotifier());
// Adding a new type: just register it — no factory code changes!
factory.Register(NotificationType.Slack, () => new SlackNotifier());Factory with DI in ASP.NET Core
// Register multiple implementations
builder.Services.AddScoped<EmailNotifier>();
builder.Services.AddScoped<SmsNotifier>();
builder.Services.AddScoped<SlackNotifier>();
// Factory that uses DI to create instances
builder.Services.AddScoped<INotifierFactory, NotifierFactory>();
public class NotifierFactory(IServiceProvider provider) : INotifierFactory
{
public INotifier Create(NotificationType type) => type switch
{
NotificationType.Email => provider.GetRequiredService<EmailNotifier>(),
NotificationType.Sms => provider.GetRequiredService<SmsNotifier>(),
NotificationType.Slack => provider.GetRequiredService<SlackNotifier>(),
_ => throw new ArgumentException($"Unknown type: {type}")
};
}Abstract Factory — Families of Objects
Abstract Factory creates families of related objects without specifying concrete types. Use it when objects must be compatible with each other.
// Abstract factory — creates a family of UI components
public interface IUiFactory
{
IButton CreateButton();
ITextField CreateTextField();
IDialog CreateDialog();
}
// Concrete factory for each theme/platform
public class MaterialUiFactory : IUiFactory
{
public IButton CreateButton() => new MaterialButton();
public ITextField CreateTextField() => new MaterialTextField();
public IDialog CreateDialog() => new MaterialDialog();
}
public class WindowsUiFactory : IUiFactory
{
public IButton CreateButton() => new WindowsButton();
public ITextField CreateTextField() => new WindowsTextField();
public IDialog CreateDialog() => new WindowsDialog();
}
// Application uses the abstract factory — doesn't know which theme
public class FormBuilder(IUiFactory factory)
{
public Form Build()
{
var button = factory.CreateButton();
var textField = factory.CreateTextField();
return new Form(button, textField); // all components from the same family
}
}Real-World: Notification Strategy
public class NotificationService(INotifierFactory factory)
{
public async Task NotifyAsync(User user, string message)
{
var notifier = factory.Create(user.PreferredChannel);
notifier.Send(message, user.ContactInfo);
// Also notify via secondary channel if configured
if (user.SecondaryChannel.HasValue)
{
var secondary = factory.Create(user.SecondaryChannel.Value);
secondary.Send(message, user.SecondaryContact!);
}
}
}Key Takeaways
- Factory Method encapsulates
new— callers get an instance without knowing the concrete type - Use a registration-based factory to follow Open/Closed — add new types without modifying the factory
- Abstract Factory ensures that a family of objects (all Material, all Windows) are compatible
- In ASP.NET Core, use
IServiceProviderin factories to get DI-resolved instances - The goal is decoupling — the caller shouldn't need to know what it's creating or how
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.