- Published on
Factory Design Pattern in C#
- Authors
- Name
- Remigiusz Zalewski

Introduction
In the ever-changing world of software development, design patterns act as guides to efficiency and good practices. These well-tested answers to common coding challenges have become essential tools for developers.
They offer a organized way to build strong, expandable, and easy-to-maintain software systems. Design patterns aren’t strict rules. Instead, they’re flexible templates that can be adjusted to solve recurring design issues in different situations. They capture the shared knowledge of seasoned software engineers, creating a common language that works across different programming languages and technologies.
The Factory Pattern in C# is a creational design pattern that provides a way to create objects without specifying the exact class of object that will be created. This pattern is particularly useful in scenarios where the instantiation logic is complex or varies based on certain conditions. In this article, we will explore an example of using the Factory Pattern to create invoices in different formats based on a provided document type.
Let’s first examine the code below, which does not use the Factory Pattern, and highlight its drawbacks in terms of violating SOLID principles and clean code practices:
app.MapGet("/api/invoice/{id}/{format}", (Guid id, InvoiceFormat format) =>
{
var generator = new InvoiceGenerator();
var invoiceData = generator.GenerateInvoice(id, format);
var contentType = generator.GetContentType(format);
string fileName = $"Invoice_{id}.{format.ToString().ToLower()}";
return Results.File(invoiceData, contentType, fileName);
})
.WithName("GenerateInvoice")
.WithOpenApi();
public class InvoiceGenerator
{
public byte[] GenerateInvoice(Guid invoiceId, string format)
{
var invoice = MockInvoiceProvider.CreateMockInvoice(invoiceId);
format = format.ToLower();
switch (format)
{
case "pdf":
return GeneratePdfInvoice(invoice);
case "txt":
return GenerateTxtInvoice(invoice);
case "csv":
return GenerateCsvInvoice(invoice);
default:
throw new ArgumentException("Invalid format", nameof(format));
}
}
public string GetContentType(string format)
{
format = format.ToLower();
switch (format)
{
case "pdf":
return "application/pdf";
case "txt":
return "text/plain";
case "csv":
return "text/csv";
default:
throw new ArgumentException("Invalid format", nameof(format));
}
}
}
- Violation of the Single Responsibility Principle (SRP): The InvoiceGenerator class is responsible for too many things: fetching invoice data, determining content types, and generating invoices in multiple formats. This makes the class harder to maintain and extend.
- Open/Closed Principle (OCP) Violation: The
switch
statements in bothGenerateInvoice
andGetContentType
violate OCP because adding a new format (e.g., JSON or XML) requires modifying these methods. This increases the risk of introducing bugs and makes the code less extensible. - Tight Coupling: The
InvoiceGenerator
class is tightly coupled to specific invoice generation logic (GeneratePdfInvoice
,GenerateTxtInvoice
, etc.). This makes it difficult to test or replace individual components without affecting the entire class. - Code Duplication: The
switch
statement logic is duplicated across methods (GenerateInvoice
andGetContentType
). This redundancy increases maintenance overhead and can lead to inconsistencies if one part is updated but not the other. - Lack of Scalability: As more formats are added, the class becomes bloated with additional cases in the
switch
statements, making it harder to read and maintain.
Let’s refactor the code to apply the Factory design pattern step-by-step and examine the benefits we achieve:
- Define the invoice generator interface: All types of formats are using two methods:
GenerateInvoice
andGetContentType
. We can create an interface calledIInvoiceGenerator
that later on could be implemented by concrete classes.
public interface IInvoiceGenerator
{
byte[] GenerateInvoice(Guid invoiceId);
string GetContentType();
}
- Create Concrete Classes: Now I will create three classes —
PdfInvoiceGenerator
,TxtInvoiceGenerator
andCsvInvoiceGenerator
that will implement IInvoiceGenerator interface and it will generate the invoice based on their format. At the same time I will remove the InvoiceGenerator class (as it won’t be needed anymore).
public class PdfInvoiceGenerator : IInvoiceGenerator
{
public byte[] GenerateInvoice(Guid invoiceId)
{
var invoice = MockInvoiceProvider.CreateMockInvoice(invoiceId);
var document = Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.Header().Text($"Invoice #{invoice.Id}").SemiBold().FontSize(20);
page.Content().PaddingVertical(1, Unit.Centimetre).Column(column =>
{
column.Item().Text($"Date: {invoice.Date:yyyy-MM-dd}");
column.Item().Text($"Customer: {invoice.CustomerName}");
column.Item().Text($"Amount: ${invoice.Amount:F2}").FontSize(14);
});
page.Footer().AlignCenter().Text(x =>
{
x.Span("Page ");
x.CurrentPageNumber();
});
});
});
return document.GeneratePdf();
}
public string GetContentType() => "application/pdf";
}
public class TxtInvoiceGenerator : IInvoiceGenerator
{
public byte[] GenerateInvoice(Guid invoiceId)
{
var invoice = MockInvoiceProvider.CreateMockInvoice(invoiceId);
string content = $"Invoice #{invoice.Id}\n" +
$"Date: {invoice.Date:yyyy-MM-dd}\n" +
$"Customer: {invoice.CustomerName}\n" +
$"Amount: ${invoice.Amount:F2}";
return Encoding.UTF8.GetBytes(content);
}
public string GetContentType() => "text/plain";
}
public class CsvInvoiceGenerator : IInvoiceGenerator
{
public byte[] GenerateInvoice(Guid invoiceId)
{
var invoice = MockInvoiceProvider.CreateMockInvoice(invoiceId);
string csvContent = "Invoice ID,Date,Customer,Amount\n" +
$"{invoice.Id},{invoice.Date:yyyy-MM-dd},{invoice.CustomerName},{invoice.Amount:F2}";
return Encoding.UTF8.GetBytes(csvContent);
}
public string GetContentType() => "text/csv";
}
- Define enum to cover the supported invoice formats: I will get rid of handling the string as an input and make it more concise:
public enum InvoiceFormat
{
Pdf,
Txt,
Csv
}
- Define the factory class interface: I will define the IInvoiceGeneratorFactory interface that will be implemented later on by concrete factory class
public interface IInvoiceGeneratorFactory
{
IInvoiceGenerator CreateInvoiceGenerator(InvoiceFormat invoiceFormat);
}
- Create Factory class: After completion of all the steps we are ready to create the class that will implement the IInvoiceGeneratorFactory interface and based on InvoiceFormat enum that I have created, will decide which InvoiceGenerator class will return as the result
public class InvoiceGeneratorFactory : IInvoiceGeneratorFactory
{
public IInvoiceGenerator CreateInvoiceGenerator(InvoiceFormat invoiceFormat)
{
return invoiceFormat switch
{
InvoiceFormat.Pdf => new PdfInvoiceGenerator(),
InvoiceFormat.Txt => new TxtInvoiceGenerator(),
InvoiceFormat.Csv => new CsvInvoiceGenerator(),
_ => throw new ArgumentException("Invalid/Unsupported InvoiceFormat", nameof(invoiceFormat))
};
}
}
- You can register your classes in DI container and resolve the dependencies using IServiceProvider to return the concrete class from the container instead of creating the class with the new operator:
public class InvoiceGeneratorFactory : IInvoiceGeneratorFactory
{
private readonly IServiceProvider _serviceProvider;
public InvoiceGeneratorFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IInvoiceGenerator CreateInvoiceGenerator(InvoiceFormat invoiceFormat)
{
return invoiceFormat switch
{
InvoiceFormat.Pdf => _serviceProvider.GetRequiredService<PdfInvoiceGenerator>(),
InvoiceFormat.Txt => _serviceProvider.GetRequiredService<TxtInvoiceGenerator>(),
InvoiceFormat.Csv => _serviceProvider.GetRequiredService<CsvInvoiceGenerator>(),
_ => throw new ArgumentException("Invalid/Unsupported InvoiceFormat", nameof(invoiceFormat))
};
}
}
- Register factory class in DI container: We need to register our factory class together with an interface to inject it to the place where it will be used (here minimal api endpoint)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IInvoiceGeneratorFactory, InvoiceGeneratorFactory>();
Finally we can use our factory in the minimal api endpoint as follows:
app.MapGet("/api/invoice/{id}/{format}", (Guid id, InvoiceFormat format,
IInvoiceGeneratorFactory invoiceGeneratorFactory) =>
{
var generator = invoiceGeneratorFactory.CreateInvoiceGenerator(format);
var invoiceData = generator.GenerateInvoice(id);
var contentType = generator.GetContentType();
string fileName = $"Invoice_{id}.{format.ToString().ToLower()}";
return Results.File(invoiceData, contentType, fileName);
})
.WithName("GenerateInvoice")
.WithOpenApi();
Now we don’t need to pass the invoice format to the main InvoiceGenerator class, because the InvoiceGeneratorFactory class based on the format will return for us the concrete class that handles only this specific invoice format.
Summary
Applying the Factory Pattern addresses these issues by delegating object creation to a dedicated factory class. Here’s how it helps:
- Adherence to Single Responsibility Principle: The responsibility for creating specific invoice formats is moved out of InvoiceGenerator class, allowing it to focus solely on orchestrating invoice generation.
- Compliance with Open/Closed Principle: New formats can be added by creating new classes that implement a common interface (e.g.,
IInvoiceGenerator
) without modifying existing code. - Improved Testability: Each format-specific logic is encapsulated in its own class, making unit testing more straightforward.
- Reduced Code Duplication: The factory centralizes format handling logic, eliminating redundant
switch
statements. - Enhanced Maintainability: The separation of concerns makes it easier to understand, extend, and modify individual parts of the codebase.
By refactoring this code with the Factory Pattern, we can create a cleaner, more modular design that adheres to SOLID principles and other clean code practices.