Template Method in C#: Stable Skeletons, Custom Steps

Fixing the Algorithm Flow

If you've ever needed to ensure a process always happens in the same order while letting different implementations customize individual steps, you've needed the Template Method pattern. Without it, you end up duplicating the workflow sequence in every implementation or letting subclasses accidentally skip critical steps.

Template Method defines the skeleton of an algorithm in a base class but lets subclasses override specific steps. The base class controls the flow and calls methods that subclasses implement. This guarantees the sequence stays consistent while behavior varies.

You'll build a data processing pipeline where the overall flow is fixed but individual format parsers customize how they read and validate data. By the end, you'll know when template methods enforce consistency and when they create rigid hierarchies.

Defining the Template

The template method orchestrates the algorithm by calling abstract or virtual methods in a specific order. Subclasses provide the implementations but can't change the sequence.

DataProcessor.cs
namespace TemplateDemo;

public abstract class DataProcessor
{
    public void Process(string filePath)
    {
        var rawData = LoadData(filePath);
        var validated = ValidateData(rawData);
        var transformed = TransformData(validated);
        SaveData(transformed);
    }

    protected abstract string LoadData(string path);
    protected abstract bool ValidateData(string data);
    protected abstract string TransformData(string data);
    protected abstract void SaveData(string data);
}

Process is the template method. It defines the workflow that all subclasses follow. The abstract methods are the customization points where each subclass provides its specific logic.

Implementing Concrete Processors

Each concrete subclass implements the abstract steps with format-specific behavior. The template guarantees they all follow the same load, validate, transform, save sequence.

CsvProcessor.cs
namespace TemplateDemo;

public class CsvProcessor : DataProcessor
{
    protected override string LoadData(string path)
    {
        Console.WriteLine($"Loading CSV from {path}");
        return File.ReadAllText(path);
    }

    protected override bool ValidateData(string data)
    {
        Console.WriteLine("Validating CSV format");
        return !string.IsNullOrEmpty(data) && data.Contains(",");
    }

    protected override string TransformData(string data)
    {
        Console.WriteLine("Transforming CSV data");
        return data.Replace(",", "|");
    }

    protected override void SaveData(string data)
    {
        Console.WriteLine("Saving transformed CSV");
        File.WriteAllText("output.csv", data);
    }
}

public class JsonProcessor : DataProcessor
{
    protected override string LoadData(string path)
    {
        Console.WriteLine($"Loading JSON from {path}");
        return File.ReadAllText(path);
    }

    protected override bool ValidateData(string data)
    {
        Console.WriteLine("Validating JSON format");
        return data.TrimStart().StartsWith("{") || data.TrimStart().StartsWith("[");
    }

    protected override string TransformData(string data)
    {
        Console.WriteLine("Transforming JSON data");
        return data.ToUpperInvariant();
    }

    protected override void SaveData(string data)
    {
        Console.WriteLine("Saving transformed JSON");
        File.WriteAllText("output.json", data);
    }
}

Both processors implement the same four methods but with different logic. When you call Process, the base class ensures the workflow runs identically. Subclasses can't accidentally skip validation or reorder steps.

Adding Optional Hooks

Hooks are virtual methods with default empty implementations. Subclasses can override them to add behavior, but they're not required to. This gives flexibility without forcing every subclass to implement unnecessary steps.

ProcessorWithHooks.cs
namespace TemplateDemo;

public abstract class EnhancedDataProcessor
{
    public void Process(string filePath)
    {
        BeforeLoad();
        var rawData = LoadData(filePath);
        AfterLoad(rawData);

        var validated = ValidateData(rawData);
        if (!validated)
        {
            HandleValidationFailure();
            return;
        }

        var transformed = TransformData(rawData);
        SaveData(transformed);
        AfterSave();
    }

    protected virtual void BeforeLoad() { }
    protected virtual void AfterLoad(string data) { }
    protected virtual void HandleValidationFailure() =>
        Console.WriteLine("Validation failed");
    protected virtual void AfterSave() { }

    protected abstract string LoadData(string path);
    protected abstract bool ValidateData(string data);
    protected abstract string TransformData(string data);
    protected abstract void SaveData(string data);
}

Hooks provide extension points without forcing implementation. Subclasses override them when needed but can ignore them otherwise. This is cleaner than requiring every subclass to implement methods that do nothing.

Try It Yourself

Build a simple report generator with a template method that defines formatting steps. Different reports customize headers and footers while following the same structure.

Steps

  1. Create: dotnet new console -n TemplateDemo
  2. Enter: cd TemplateDemo
  3. Edit Program.cs
  4. Update .csproj
  5. Run: dotnet run
Program.cs
var sales = new SalesReport();
sales.Generate();

Console.WriteLine();

var inventory = new InventoryReport();
inventory.Generate();

abstract class Report
{
    public void Generate()
    {
        PrintHeader();
        PrintBody();
        PrintFooter();
    }

    protected abstract void PrintHeader();
    protected abstract void PrintFooter();

    protected virtual void PrintBody() =>
        Console.WriteLine("Report body content");
}

class SalesReport : Report
{
    protected override void PrintHeader() =>
        Console.WriteLine("=== SALES REPORT ===");

    protected override void PrintFooter() =>
        Console.WriteLine("End of Sales Report");
}

class InventoryReport : Report
{
    protected override void PrintHeader() =>
        Console.WriteLine("*** INVENTORY REPORT ***");

    protected override void PrintFooter() =>
        Console.WriteLine("--- Report Complete ---");
}
TemplateDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Output

=== SALES REPORT ===
Report body content
End of Sales Report

*** INVENTORY REPORT ***
Report body content
--- Report Complete ---

Design Trade-offs

Choose Template Method when you have a stable algorithm skeleton that applies across variations. If the workflow sequence is fixed but individual steps differ, template methods enforce consistency. They work well for processes like data import pipelines, report generation, or test fixtures where the structure stays constant.

Choose composition with Strategy pattern when the entire algorithm varies, not just individual steps. If you need to swap the whole process at runtime or avoid deep inheritance hierarchies, strategies provide more flexibility. Template Method locks you into inheritance, which can be rigid.

If unsure, start with template methods for workflows you control completely. If you find subclasses wanting to change the algorithm flow itself, refactor to strategies. Watch for base classes with too many hooks or abstract methods. That signals the template is too flexible and might need decomposition.

FAQ

When should I use Template Method vs Strategy?

Use Template Method when algorithm structure stays fixed but individual steps vary. Use Strategy when the entire algorithm swaps out. Template Method uses inheritance and locks in the skeleton. Strategy uses composition and swaps the whole process.

What's the gotcha with sealed template methods?

Always mark the template method itself as sealed or final to prevent subclasses from changing the algorithm flow. Let subclasses override individual steps, not the orchestration. This ensures the workflow stays consistent across all implementations.

How do I test template methods effectively?

Test the base template with a concrete test subclass that tracks which methods were called. Verify call order and parameters. Then test each real subclass to ensure it implements steps correctly. Mock dependencies but keep the template flow intact.

Does Template Method work with async methods?

Yes, make the template method async and await each step. Override methods should return Task or Task<T>. The pattern works identically, just ensure all steps properly await their work. Use ConfigureAwait(false) in library code.

Is it safe to use virtual methods in constructors?

No, avoid calling virtual methods from constructors. Subclass overrides run before the subclass constructor finishes, accessing uninitialized fields. Use initialization methods or lazy evaluation instead. This is a common source of NullReferenceException bugs.

Back to Articles