How to Implement Factory Method Pattern in C#: Step-by-Step Guide
The Factory Method pattern is a creational design pattern that provides an interface for creating objects without specifying their exact classes. Implementing this pattern in C# involves defining a product interface, creating concrete products, establishing a creator abstract class, and implementing concrete creators. This step-by-step guide will walk you through implementing the Factory Method pattern with practical C# examples.
Understanding how to implement the Factory Method pattern in C# is crucial for developers who need flexible object creation mechanisms. This pattern is particularly useful when you have multiple related types that share a common interface but require different instantiation logic. The Factory Method pattern in C# enables you to decouple object creation from object usage, making your code more maintainable and extensible.
Step 1: Define the Product Interface for Factory Method Pattern
The first step in implementing Factory Method is defining the product interface. This interface represents the common contract that all products must follow.
// Product interface - defines the common contract
public interface ILogger
{
void LogInfo(string message);
void LogWarning(string message);
void LogError(string message);
void LogDebug(string message);
}
The product interface should:
- Define methods that all concrete products will implement
- Be focused on behavior, not implementation details
- Follow interface segregation principles (keep interfaces small and focused)
Why this matters: The product interface is what client code depends on, ensuring loose coupling between clients and concrete implementations.
Step 2: Create Concrete Product Classes
Next, create concrete implementations of the product interface. Each concrete product represents a specific type of object that can be created.
// Concrete Product 1: Console Logger
public class ConsoleLogger : ILogger
{
public void LogInfo(string message)
{
Console.WriteLine($"[INFO] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
}
public void LogWarning(string message)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"[WARNING] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
Console.ResetColor();
}
public void LogError(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[ERROR] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
Console.ResetColor();
}
public void LogDebug(string message)
{
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine($"[DEBUG] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
Console.ResetColor();
}
}
// Concrete Product 2: File Logger
public class FileLogger : ILogger
{
private readonly string _filePath;
public FileLogger(string filePath)
{
_filePath = filePath;
}
public void LogInfo(string message)
{
WriteToFile("INFO", message);
}
public void LogWarning(string message)
{
WriteToFile("WARNING", message);
}
public void LogError(string message)
{
WriteToFile("ERROR", message);
}
public void LogDebug(string message)
{
WriteToFile("DEBUG", message);
}
private void WriteToFile(string level, string message)
{
var logEntry = $"[{level}] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}";
File.AppendAllText(_filePath, logEntry + Environment.NewLine);
}
}
// Concrete Product 3: Database Logger
public class DatabaseLogger : ILogger
{
private readonly string _connectionString;
public DatabaseLogger(string connectionString)
{
_connectionString = connectionString;
}
public void LogInfo(string message)
{
SaveToDatabase("INFO", message);
}
public void LogWarning(string message)
{
SaveToDatabase("WARNING", message);
}
public void LogError(string message)
{
SaveToDatabase("ERROR", message);
}
public void LogDebug(string message)
{
SaveToDatabase("DEBUG", message);
}
private void SaveToDatabase(string level, string message)
{
// Simplified database logging - in production, use proper data access
Console.WriteLine($"Saving to database [{level}]: {message}");
// Actual implementation would use Entity Framework, Dapper, etc.
}
}
When implementing concrete products, ensure each concrete product fully implements the interface, products can have different constructors and internal state as needed, and implementation details are hidden behind the interface. This encapsulation allows you to change product implementations without affecting client code.
Step 3: Create the Creator Abstract Class
The third step in implementing the Factory Method pattern is creating the creator abstract class. This class is the foundation of the Factory Method pattern. It declares the factory method that subclasses will implement and optionally provides template methods that use the factory method to perform common operations. This abstract class defines the structure that all concrete creators will follow in the Factory Method pattern.
// Creator abstract class
public abstract class LoggerFactory
{
// Factory Method - subclasses must implement this
public abstract ILogger CreateLogger();
// Template method using the factory method
public void LogApplicationStart()
{
var logger = CreateLogger();
logger.LogInfo("Application started");
logger.LogInfo($"Environment: {Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Unknown"}");
}
public void LogApplicationStop()
{
var logger = CreateLogger();
logger.LogInfo("Application stopping");
}
// Another template method
public void LogException(Exception ex)
{
var logger = CreateLogger();
logger.LogError($"Exception occurred: {ex.Message}");
logger.LogDebug($"Stack trace: {ex.StackTrace}");
}
}
Important considerations:
- The factory method is declared as
abstract, forcing subclasses to implement it - Template methods (
LogApplicationStart,LogException) use the factory method but don't know which concrete product is created - The creator can contain common logic that works with any product type
Step 4: Implement Concrete Creator Classes for Factory Method
The fourth step in implementing the Factory Method pattern is creating concrete creator classes. Concrete creators override the factory method to return specific concrete products. Each concrete creator is responsible for creating one type of product in the Factory Method pattern.
// Concrete Creator 1: Console Logger Factory
public class ConsoleLoggerFactory : LoggerFactory
{
public override ILogger CreateLogger()
{
return new ConsoleLogger();
}
}
// Concrete Creator 2: File Logger Factory
public class FileLoggerFactory : LoggerFactory
{
private readonly string _filePath;
public FileLoggerFactory(string filePath)
{
_filePath = filePath;
}
public override ILogger CreateLogger()
{
return new FileLogger(_filePath);
}
}
// Concrete Creator 3: Database Logger Factory
public class DatabaseLoggerFactory : LoggerFactory
{
private readonly string _connectionString;
public DatabaseLoggerFactory(string connectionString)
{
_connectionString = connectionString;
}
public override ILogger CreateLogger()
{
return new DatabaseLogger(_connectionString);
}
}
When implementing concrete creators, remember that each concrete creator returns a specific concrete product, creators can accept parameters needed for product construction through their constructors, and the factory method encapsulates object creation logic, keeping it separate from business logic.
Step 5: Use the Factory Method Pattern
Now you can use the factory method pattern in your application code:
class Program
{
static void Main(string[] args)
{
// Example 1: Using Console Logger Factory
LoggerFactory consoleFactory = new ConsoleLoggerFactory();
consoleFactory.LogApplicationStart();
var consoleLogger = consoleFactory.CreateLogger();
consoleLogger.LogInfo("This is an info message");
consoleLogger.LogWarning("This is a warning message");
// Example 2: Using File Logger Factory
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "app.log");
LoggerFactory fileFactory = new FileLoggerFactory(filePath);
fileFactory.LogApplicationStart();
var fileLogger = fileFactory.CreateLogger();
fileLogger.LogError("This error will be written to file");
// Example 3: Using Database Logger Factory
var connectionString = "Server=localhost;Database=Logs;Integrated Security=true;";
LoggerFactory dbFactory = new DatabaseLoggerFactory(connectionString);
dbFactory.LogException(new InvalidOperationException("Something went wrong"));
// Example 4: Polymorphic usage
ProcessLogging(consoleFactory);
ProcessLogging(fileFactory);
ProcessLogging(dbFactory);
}
// Method works with any LoggerFactory - polymorphism in action
static void ProcessLogging(LoggerFactory factory)
{
var logger = factory.CreateLogger();
logger.LogInfo("Processing logs through factory");
factory.LogApplicationStop();
}
}
Advanced Implementation: Factory Method with Parameters
While basic Factory Method implementations create products without parameters, real-world scenarios often require factory methods that accept parameters to determine which product variant to create. This approach provides more flexibility while maintaining the pattern's benefits.
public abstract class NotificationFactory
{
// Factory method with parameters
public abstract INotification CreateNotification(string recipient, string message);
public void SendNotification(string recipient, string message)
{
var notification = CreateNotification(recipient, message);
notification.Send();
}
}
public class EmailNotificationFactory : NotificationFactory
{
private readonly string _smtpServer;
public EmailNotificationFactory(string smtpServer)
{
_smtpServer = smtpServer;
}
public override INotification CreateNotification(string recipient, string message)
{
// Could create different email types based on message content
if (message.Length > 1000)
{
return new DetailedEmailNotification(recipient, message, _smtpServer);
}
return new SimpleEmailNotification(recipient, message, _smtpServer);
}
}
Implementation Best Practices
Following best practices when you implement Factory Method pattern in C# ensures your code is maintainable, testable, and follows SOLID principles. These practices help you avoid common mistakes and create robust implementations of the Factory Method pattern in C#.
1. Use Interfaces for Products
When defining products in Factory Method implementations, prefer interfaces over abstract classes whenever possible. Interfaces provide maximum flexibility and allow products to inherit from other classes while implementing the product interface, giving you more design options.
// Good: Interface-based
public interface ILogger { }
// Less flexible: Abstract class
public abstract class Logger { }
2. Keep Factory Methods Focused
Maintaining focus in factory methods ensures clarity and prevents pattern misuse. Each factory method should create one type of product. If you need multiple products, consider Abstract Factory pattern instead, which is designed for creating families of related objects.
3. Handle Dependencies Properly
Proper dependency management ensures clean Factory Method implementations. If products need dependencies, pass them through the creator's constructor rather than through factory method parameters. This keeps factory methods focused on creation logic while allowing products to receive necessary dependencies.
public class DatabaseLoggerFactory : LoggerFactory
{
private readonly IDbConnectionFactory _connectionFactory;
public DatabaseLoggerFactory(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public override ILogger CreateLogger()
{
return new DatabaseLogger(_connectionFactory);
}
}
4. Consider Using Dependency Injection
Combine Factory Method with dependency injection for better testability:
public interface ILoggerFactory
{
ILogger CreateLogger();
}
public class ConsoleLoggerFactory : ILoggerFactory
{
public ILogger CreateLogger() => new ConsoleLogger();
}
// Register in DI container
services.AddScoped<ILoggerFactory, ConsoleLoggerFactory>();
Common Pitfalls to Avoid
Understanding common pitfalls helps you avoid mistakes when implementing Factory Method. These pitfalls can lead to bugs, poor maintainability, and violations of design principles. Being aware of these issues helps you create robust Factory Method implementations.
Pitfall 1: Returning Null from Factory Method
Bad:
public override ILogger CreateLogger()
{
if (someCondition)
return new ConsoleLogger();
return null; // Don't do this!
}
Good:
public override ILogger CreateLogger()
{
if (someCondition)
return new ConsoleLogger();
return new DefaultLogger(); // Always return a valid product
}
Pitfall 2: Mixing Creation Logic with Business Logic
Maintaining clear separation of concerns is essential in Factory Method implementations. Factory methods should only handle object creation, while business logic belongs in template methods or client code. Mixing these concerns makes code harder to test and maintain.
Bad:
public override ILogger CreateLogger()
{
var logger = new ConsoleLogger();
logger.LogInfo("Logger created"); // Business logic in factory method
return logger;
}
Good:
public override ILogger CreateLogger()
{
return new ConsoleLogger(); // Only creation logic
}
// Business logic in template methods or client code
public void InitializeLogger()
{
var logger = CreateLogger();
logger.LogInfo("Logger created");
}
Pitfall 3: Over-Engineering Simple Scenarios
Don't use Factory Method when a simple constructor is sufficient:
// Unnecessary Factory Method for simple case
public class SimpleFactory
{
public static string CreateString() => "Hello";
}
// Better: Just use the constructor or direct instantiation
var message = "Hello";
Testing Factory Method Implementation
Testability is one of the key benefits when you implement Factory Method pattern in C#. Testing factory methods is straightforward because you can create test doubles and mock factories that return test implementations, allowing you to verify behavior without depending on real product implementations. This makes testing implementations of the Factory Method pattern in C# much easier.
[Test]
public void ConsoleLoggerFactory_CreatesConsoleLogger()
{
// Arrange
var factory = new ConsoleLoggerFactory();
// Act
var logger = factory.CreateLogger();
// Assert
Assert.IsInstanceOf<ConsoleLogger>(logger);
Assert.IsAssignableFrom<ILogger>(logger);
}
[Test]
public void FileLoggerFactory_CreatesFileLoggerWithCorrectPath()
{
// Arrange
var filePath = "test.log";
var factory = new FileLoggerFactory(filePath);
// Act
var logger = factory.CreateLogger();
// Assert
Assert.IsInstanceOf<FileLogger>(logger);
var fileLogger = (FileLogger)logger;
// Verify file path is set correctly (would need to expose property or use reflection)
}
Integration with Modern C# Features
Modern C# features can enhance Factory Method implementations, making them more expressive and type-safe. Understanding how to leverage these features helps you create more modern and maintainable code.
Using Records for Products
C# 9+ records work well with Factory Method:
public record LogEntry(string Level, string Message, DateTime Timestamp);
public interface ILogger
{
void Log(LogEntry entry);
}
Using Generic Factory Methods
Generic factory methods provide type safety and reduce code duplication when creating similar products. You can create generic factory methods for type-safe creation, allowing you to define factories that work with multiple product types while maintaining compile-time type checking.
public abstract class Factory<T> where T : class
{
public abstract T Create();
}
public class StringFactory : Factory<string>
{
public override string Create() => "Default String";
}
Conclusion
Implementing the Factory Method pattern in C# involves four main steps: defining the product interface, creating concrete products, establishing the creator abstract class, and implementing concrete creators. The Factory Method pattern provides flexibility, reduces coupling, and makes your code more maintainable. By following this step-by-step guide, you can successfully implement the Factory Method pattern in your C# applications.
Key implementation points:
- Always define products as interfaces when possible
- Keep factory methods focused on creation logic only
- Use template methods in creators for common operations
- Combine with dependency injection for better testability
- Avoid over-engineering simple scenarios
For more guidance on when to use this pattern and best practices, explore decision frameworks and code organization strategies for Factory Method implementations.
Frequently Asked Questions
Do I need to use abstract classes for creators?
No, you can use interfaces for creators too. However, abstract classes are useful when you want to provide template methods with default implementations.
Can factory methods be static?
Factory methods can be static, but this reduces flexibility. Static factory methods are more like Simple Factory pattern. Instance factory methods allow for dependency injection and better testability.
How do I handle product initialization that requires parameters?
Pass parameters through the creator's constructor or through the factory method itself. The factory method can accept parameters to determine which product variant to create.
Should I use Factory Method for every object creation?
No! Factory Method adds complexity. Use it when you have multiple related types, complex creation logic, or need to vary creation based on context. For simple cases, direct instantiation is better.
How does Factory Method differ from Abstract Factory?
Factory Method creates single objects through inheritance (one factory method per creator). Abstract Factory creates families of related objects through composition (multiple factory methods in one factory).
Can I combine Factory Method with other patterns?
Yes! Factory Method works well with Template Method (as shown in examples), Strategy, and Dependency Injection patterns. It's common to see these patterns used together.
What if I need to create products based on runtime configuration?
You can use a parameterized factory method or create a factory selector that chooses the appropriate concrete creator based on configuration. This is a common pattern in application frameworks.

