How to Implement Template Method Pattern in C#: Step-by-Step Guide
Knowing the theory behind a design pattern is one thing. Knowing how to wire it up in production-quality C# is something else entirely. The template method pattern is one of those patterns that looks simple on the surface -- define an algorithm skeleton in a base class and let subclasses fill in the details -- but the implementation choices you make determine whether the pattern stays clean or becomes a maintenance headache. If you want to implement template method pattern in C# the right way, this guide walks you through five concrete steps, from defining the abstract base class to testing the result.
The template method pattern is a behavioral design pattern that lets you define the fixed structure of an algorithm in a base class while allowing subclasses to customize specific steps. Unlike the strategy pattern, which uses composition to swap entire algorithms at runtime, the template method pattern uses inheritance to vary individual steps within a fixed sequence. This distinction matters for implementation because it directly affects how you structure your classes, what you seal, and what you leave open for extension.
In this guide, we'll build a document processing pipeline that reads, transforms, and saves documents. Each step of that pipeline is a method in the base class. Subclasses override only the steps that vary -- like how transformations work or where the output goes -- while the pipeline structure itself stays locked down.
Prerequisites
Before jumping in, make sure you're comfortable with these C# fundamentals:
- Abstract classes and methods: The template method pattern relies on abstract classes to define the algorithm skeleton. You need to understand the difference between
abstract,virtual, andsealedmethod modifiers. - Inheritance: Subclasses extend the base class and override specific methods. Understanding method resolution in C# inheritance hierarchies is important.
- Dependency injection basics: We'll register template method classes in IServiceCollection and resolve them at runtime.
- .NET 8 or later: The code examples use modern C# syntax. Any recent .NET SDK will work.
Step 1: Define the Abstract Base Class
The first step to implement template method pattern in C# is creating the abstract base class that owns the algorithm. This class has three categories of methods:
- The template method itself -- a non-virtual (or sealed) method that defines the algorithm's step sequence. Subclasses cannot override this.
- Abstract methods -- steps that subclasses must implement because the base class cannot provide a meaningful default.
- Virtual hook methods -- optional extension points that subclasses can override but don't have to.
Here's our document processing base class:
using System;
using Microsoft.Extensions.Logging;
public abstract class DocumentProcessor
{
private readonly ILogger _logger;
protected DocumentProcessor(ILogger logger)
{
_logger = logger;
}
public void ProcessDocument(string inputPath)
{
_logger.LogInformation(
"Starting document processing for {Path}",
inputPath);
var rawContent = ReadDocument(inputPath);
OnBeforeTransform(rawContent);
var transformed = TransformContent(rawContent);
var outputPath = GenerateOutputPath(inputPath);
SaveDocument(outputPath, transformed);
OnAfterSave(outputPath);
_logger.LogInformation(
"Finished document processing for {Path}",
inputPath);
}
protected abstract string ReadDocument(string path);
protected abstract string TransformContent(string content);
protected abstract string GenerateOutputPath(
string inputPath);
protected abstract void SaveDocument(
string path,
string content);
protected virtual void OnBeforeTransform(string content)
{
}
protected virtual void OnAfterSave(string outputPath)
{
}
}
A few things to notice here:
- The
ProcessDocumentmethod is the template method. It is not markedvirtual, which means subclasses cannot override it. In C#, non-virtual instance methods are effectively sealed by default -- subclasses can hide them withnew, but they cannot override the base behavior through polymorphism. If you want to be even more explicit, you could move the template method into a sealed wrapper or mark the method in an intermediate class assealed override. - The four
abstractmethods (ReadDocument,TransformContent,GenerateOutputPath,SaveDocument) represent the steps that every concrete processor must implement. The base class cannot know how to read a PDF versus a CSV, so it forces subclasses to provide that logic. - The two
virtualhook methods (OnBeforeTransform,OnAfterSave) have empty default implementations. Subclasses can override them to inject additional behavior -- like validation or notification -- without being required to. - The constructor accepts an
ILogger, which we'll use to add cross-cutting logging in Step 4. This is a key advantage of the template method pattern: the base class can wrap step execution with shared concerns.
This structure enforces inversion of control at the class level. The base class controls the algorithm flow, and subclasses fill in the blanks. The control is inverted because the subclass doesn't call the steps -- the base class calls the subclass's implementations at the right time.
Step 2: Create Concrete Subclasses
With the base class defined, let's create concrete subclasses that implement the abstract steps differently. Each subclass handles a different document type, but the processing pipeline stays identical.
Markdown Processor
This processor reads markdown files, converts them to HTML, and saves the result:
using System;
using System.IO;
using Microsoft.Extensions.Logging;
public class MarkdownProcessor : DocumentProcessor
{
private readonly ILogger<MarkdownProcessor> _logger;
public MarkdownProcessor(
ILogger<MarkdownProcessor> logger)
: base(logger)
{
_logger = logger;
}
protected override string ReadDocument(string path)
{
return File.ReadAllText(path);
}
protected override string TransformContent(string content)
{
var html = content
.Replace("# ", "<h1>")
.Replace("
## ", "
<h2>")
.Replace("
", "<br/>
");
return $"<html><body>{html}</body></html>";
}
protected override string GenerateOutputPath(
string inputPath)
{
return Path.ChangeExtension(inputPath, ".html");
}
protected override void SaveDocument(
string path,
string content)
{
File.WriteAllText(path, content);
}
}
CSV Report Processor
This processor reads CSV data, transforms it into a summary report, and saves it. It also uses the OnAfterSave hook to log a notification:
using System;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Logging;
public class CsvReportProcessor : DocumentProcessor
{
private readonly ILogger<CsvReportProcessor> _logger;
public CsvReportProcessor(
ILogger<CsvReportProcessor> logger)
: base(logger)
{
_logger = logger;
}
protected override string ReadDocument(string path)
{
return File.ReadAllText(path);
}
protected override string TransformContent(string content)
{
var lines = content.Split('
',
StringSplitOptions.RemoveEmptyEntries);
var header = lines.FirstOrDefault() ?? "";
var dataRowCount = lines.Length - 1;
return $"Report Summary
" +
$"Columns: {header}
" +
$"Total Data Rows: {dataRowCount}";
}
protected override string GenerateOutputPath(
string inputPath)
{
var directory = Path.GetDirectoryName(inputPath)
?? ".";
var fileName = Path.GetFileNameWithoutExtension(
inputPath);
return Path.Combine(
directory,
$"{fileName}-report.txt");
}
protected override void SaveDocument(
string path,
string content)
{
File.WriteAllText(path, content);
}
protected override void OnAfterSave(string outputPath)
{
_logger.LogInformation(
"CSV report generated at {Path}. " +
"Ready for distribution.",
outputPath);
}
}
Both processors follow the same pipeline: read, optionally hook, transform, generate output path, save, optionally hook. The algorithm structure never changes. What changes is how each step executes. This is the template method pattern doing exactly what it was designed to do.
Notice that the hooks are used sparingly. The CsvReportProcessor sends a notification after saving via OnAfterSave. The MarkdownProcessor skips both hooks entirely. This is the right balance: hooks provide flexibility without forcing every subclass to implement them.
Step 3: Wire Up with Dependency Injection
In a real application, you won't instantiate processors directly. You'll register them in your DI container and resolve the right one at runtime. Here's how to wire things up with IServiceCollection.
Register Concrete Processors
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddLogging(builder =>
builder.AddConsole());
services.AddTransient<MarkdownProcessor>();
services.AddTransient<CsvReportProcessor>();
Use a Factory to Select at Runtime
When the right processor depends on runtime data -- like a file extension -- a factory method gives you clean selection logic without coupling callers to concrete types:
using System;
using Microsoft.Extensions.DependencyInjection;
public class DocumentProcessorFactory
{
private readonly IServiceProvider _serviceProvider;
public DocumentProcessorFactory(
IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public DocumentProcessor Create(string filePath)
{
var extension = Path.GetExtension(filePath)
.ToLowerInvariant();
return extension switch
{
".md" => _serviceProvider
.GetRequiredService<MarkdownProcessor>(),
".csv" => _serviceProvider
.GetRequiredService<CsvReportProcessor>(),
_ => throw new NotSupportedException(
$"No processor registered for " +
$"'{extension}' files.")
};
}
}
Register the factory itself:
services.AddTransient<DocumentProcessorFactory>();
Now callers resolve the factory and let it pick the right processor:
var provider = services.BuildServiceProvider();
var factory = provider
.GetRequiredService<DocumentProcessorFactory>();
var processor = factory.Create("report.csv");
processor.ProcessDocument("report.csv");
This approach embraces inversion of control -- the caller doesn't know or care which concrete processor handles the file. The factory encapsulates that decision, and the DI container manages object lifetimes.
If you're familiar with the adapter pattern, you'll notice the factory plays a similar role -- bridging between what the caller needs and what the system provides.
Step 4: Add Logging and Cross-Cutting Concerns
One of the strongest practical advantages of the template method pattern is that the base class can wrap step execution with shared concerns. Subclasses never need to think about logging, timing, or error handling around individual steps -- the base class handles it. Let's enhance the ProcessDocument method to add timing around each step:
using System;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
public abstract class DocumentProcessor
{
private readonly ILogger _logger;
protected DocumentProcessor(ILogger logger)
{
_logger = logger;
}
public void ProcessDocument(string inputPath)
{
var totalStopwatch = Stopwatch.StartNew();
_logger.LogInformation(
"Starting document processing for {Path}",
inputPath);
var rawContent = ExecuteStep(
"ReadDocument",
() => ReadDocument(inputPath));
OnBeforeTransform(rawContent);
var transformed = ExecuteStep(
"TransformContent",
() => TransformContent(rawContent));
var outputPath = ExecuteStep(
"GenerateOutputPath",
() => GenerateOutputPath(inputPath));
ExecuteStep(
"SaveDocument",
() =>
{
SaveDocument(outputPath, transformed);
return string.Empty;
});
OnAfterSave(outputPath);
totalStopwatch.Stop();
_logger.LogInformation(
"Finished processing {Path} in {Elapsed}ms",
inputPath,
totalStopwatch.ElapsedMilliseconds);
}
private T ExecuteStep<T>(
string stepName,
Func<T> step)
{
_logger.LogDebug(
"Executing step: {StepName}",
stepName);
var stopwatch = Stopwatch.StartNew();
try
{
var result = step();
stopwatch.Stop();
_logger.LogDebug(
"Step {StepName} completed in {Elapsed}ms",
stepName,
stopwatch.ElapsedMilliseconds);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(
ex,
"Step {StepName} failed after {Elapsed}ms",
stepName,
stopwatch.ElapsedMilliseconds);
throw;
}
}
protected abstract string ReadDocument(string path);
protected abstract string TransformContent(
string content);
protected abstract string GenerateOutputPath(
string inputPath);
protected abstract void SaveDocument(
string path,
string content);
protected virtual void OnBeforeTransform(string content)
{
}
protected virtual void OnAfterSave(string outputPath)
{
}
}
The ExecuteStep helper wraps each abstract method call with logging, timing, and error handling. Every subclass automatically gets this behavior without writing a single line of cross-cutting code. If you later need to add metrics, tracing, or retry logic, you change it in one place -- the base class -- and every processor benefits.
This is a significant advantage over the strategy pattern. With strategies, each implementation is a separate object, and wrapping them with cross-cutting concerns requires the decorator pattern or middleware. With the template method, the base class is the natural place for shared infrastructure.
Step 5: Testing Template Method Classes
Testing template method classes requires a different approach than testing composition-based patterns. When you implement template method pattern in C#, you need to verify that the algorithm executes steps in the correct order, that abstract methods are called with the right arguments, and that hooks fire at the right time.
Test Each Concrete Class Independently
The simplest tests exercise a concrete subclass through the public ProcessDocument method. Use temporary files and verify the output:
using System;
using System.IO;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
public class MarkdownProcessorTests
{
[Fact]
public void ProcessDocument_ValidMarkdown_CreatesHtmlFile()
{
var processor = new MarkdownProcessor(
NullLogger<MarkdownProcessor>.Instance);
var inputPath = Path.GetTempFileName();
File.WriteAllText(inputPath, "# Hello World");
processor.ProcessDocument(inputPath);
var outputPath = Path.ChangeExtension(
inputPath, ".html");
Assert.True(File.Exists(outputPath));
var content = File.ReadAllText(outputPath);
Assert.Contains("<h1>", content);
File.Delete(inputPath);
File.Delete(outputPath);
}
}
Verify Algorithm Order with a Test Spy
To confirm the template method calls steps in the right sequence, create a test-specific subclass that records each call:
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
public class StepOrderTests
{
private class SpyProcessor : DocumentProcessor
{
public List<string> CallLog { get; } = new();
public SpyProcessor()
: base(NullLogger.Instance)
{
}
protected override string ReadDocument(string path)
{
CallLog.Add("ReadDocument");
return "test content";
}
protected override string TransformContent(
string content)
{
CallLog.Add("TransformContent");
return "transformed";
}
protected override string GenerateOutputPath(
string inputPath)
{
CallLog.Add("GenerateOutputPath");
return "output.txt";
}
protected override void SaveDocument(
string path,
string content)
{
CallLog.Add("SaveDocument");
}
protected override void OnBeforeTransform(
string content)
{
CallLog.Add("OnBeforeTransform");
}
protected override void OnAfterSave(
string outputPath)
{
CallLog.Add("OnAfterSave");
}
}
[Fact]
public void ProcessDocument_StepsExecuteInOrder_CorrectSequence()
{
var spy = new SpyProcessor();
spy.ProcessDocument("test.txt");
Assert.Equal(
new[]
{
"ReadDocument",
"OnBeforeTransform",
"TransformContent",
"GenerateOutputPath",
"SaveDocument",
"OnAfterSave"
},
spy.CallLog);
}
}
Verify Hooks Are Optional
Confirm that the base class works correctly even when hooks are not overridden:
public class HookTests
{
private class MinimalProcessor : DocumentProcessor
{
public MinimalProcessor()
: base(NullLogger.Instance)
{
}
protected override string ReadDocument(string path)
=> "content";
protected override string TransformContent(
string content)
=> "transformed";
protected override string GenerateOutputPath(
string inputPath)
=> "output.txt";
protected override void SaveDocument(
string path,
string content)
{
}
}
[Fact]
public void ProcessDocument_NoHooksOverridden_CompletesSuccessfully()
{
var processor = new MinimalProcessor();
var exception = Record.Exception(
() => processor.ProcessDocument("input.txt"));
Assert.Null(exception);
}
}
The spy pattern is particularly powerful here because it lets you test the base class's control flow without depending on file I/O or external systems. You're testing the template -- the algorithm structure -- in isolation from the concrete implementations. This same test spy approach works well for the command pattern and other patterns where execution order matters.
Common Implementation Mistakes
Even experienced developers stumble on these pitfalls when they implement template method pattern in C# for the first time. Recognizing them early saves you from refactoring later.
Making the template method virtual or overridable: The entire point of the template method is that the algorithm structure is fixed. If you mark ProcessDocument as virtual, a subclass can override the entire sequence, which defeats the purpose of the pattern. Keep the template method non-virtual. If you're defining it in an intermediate class that inherits from another, use sealed override to prevent further overrides.
Too many abstract methods: If your base class has eight or ten abstract methods, every subclass is forced to implement all of them. This creates a heavy contract that discourages reuse. Aim for a small number of abstract methods that represent genuinely variable steps. Use virtual hooks with sensible defaults for optional customization points.
God base class: Cramming too much logic into the base class -- business rules, validation, data access, error handling -- turns it into an untestable monolith. The base class should own the algorithm flow and cross-cutting concerns. Business logic belongs in the abstract methods that subclasses implement. If the base class grows beyond a hundred lines, consider whether some responsibilities should move to injected services instead.
Tight coupling between steps: If TransformContent needs data that ReadDocument produced but that wasn't returned or stored properly, you end up with implicit state shared through fields. This makes subclasses fragile because they depend on side effects from other methods. Pass data explicitly through return values and parameters wherever possible. The template method's parameter and return types are your contract.
Ignoring the Hollywood Principle: The template method pattern follows "don't call us, we'll call you." If subclasses start calling the template method's helper methods in their own custom order, or if they call each other's methods, you've lost the control flow guarantees. Each abstract method should be self-contained and called only by the base class.
Frequently Asked Questions
What is the template method pattern and when should I use it?
The template method pattern defines an algorithm's skeleton in a base class and lets subclasses override specific steps without changing the overall structure. Use it when you have multiple classes that follow the same high-level workflow but differ in specific details. If you need to implement template method pattern in C# for a real project, document processing, ETL pipelines, and report generation are common use cases where the sequence is fixed but the individual operations vary.
How is the template method pattern different from the strategy pattern?
The template method pattern uses inheritance -- subclasses override methods in a base class. The strategy pattern uses composition -- you inject interchangeable algorithm objects at runtime. Template method is better when the algorithm structure is fixed and only individual steps vary. Strategy is better when you need to swap entire algorithms dynamically or when you want to avoid deep inheritance hierarchies.
Should I mark the template method as sealed in C#?
In C#, non-virtual methods cannot be overridden through polymorphism, so a regular method in the base class is already non-overridable in the traditional sense. However, if your base class inherits from another class that declares the method as virtual, you should use sealed override to prevent further subclasses from changing the algorithm flow. Being explicit about sealing communicates intent clearly to other developers.
Can I use the template method pattern with dependency injection?
Yes, and you should. Register your concrete subclasses in IServiceCollection and use a factory to resolve the right implementation at runtime. The base class constructor can accept injected services like ILogger, and each concrete subclass can inject its own dependencies as well. This keeps the pattern compatible with modern .NET architecture.
How do I add cross-cutting concerns like logging to the template method pattern?
Place logging, timing, and error handling in the base class's template method. Because the base class controls the algorithm flow, it can wrap each step with shared infrastructure. Subclasses automatically inherit this behavior without any extra code. This is one of the template method pattern's strongest practical benefits compared to composition-based patterns like strategy or decorator.
What is a hook method in the template method pattern?
A hook method is a virtual method in the base class with an empty or default implementation. Subclasses can override hooks to inject optional behavior at specific points in the algorithm -- like validation before a transformation or notification after a save. Hooks differ from abstract methods because they're optional: subclasses only override them when they need to.
How do I test the algorithm order in a template method class?
Create a test spy -- a subclass that records each method call into a list instead of performing real work. After calling the template method, assert that the list contains the expected method names in the expected order. This approach lets you verify the algorithm's control flow without depending on external resources like files or databases.

