Learnixo

.NET & C# Development · Lesson 50 of 229

Template Method — Skeleton Algorithm, Custom Steps

Template Method — Skeleton Algorithm, Custom Steps

The Template Method pattern defines the skeleton of an algorithm in a base class and lets subclasses fill in the variable steps. The overall structure is fixed; the details are customisable.


Classic Implementation

C#
// Base class — defines the algorithm skeleton
public abstract class DataExporter
{
    // Template method — the fixed algorithm
    public async Task ExportAsync(string outputPath)
    {
        var data    = await ExtractDataAsync();
        var cleaned = TransformData(data);
        var content = FormatOutput(cleaned);
        await WriteOutputAsync(outputPath, content);
        await OnExportCompletedAsync(outputPath);   // optional hook
    }

    // Steps subclasses MUST implement
    protected abstract Task<IEnumerable<object>> ExtractDataAsync();
    protected abstract string FormatOutput(IEnumerable<object> data);

    // Steps subclasses MAY override (hooks with defaults)
    protected virtual IEnumerable<object> TransformData(IEnumerable<object> data) => data;
    protected virtual Task OnExportCompletedAsync(string path)
    {
        Console.WriteLine($"Export complete: {path}");
        return Task.CompletedTask;
    }

    private async Task WriteOutputAsync(string path, string content)
        => await File.WriteAllTextAsync(path, content);
}

// CSV exporter — implements required steps for CSV format
public class CsvOrderExporter(AppDbContext db) : DataExporter
{
    protected override async Task<IEnumerable<object>> ExtractDataAsync()
        => await db.Orders.Include(o => o.Items).ToListAsync();

    protected override string FormatOutput(IEnumerable<object> data)
    {
        var orders = data.Cast<Order>();
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("Id,CustomerId,Total,Status");
        foreach (var o in orders)
            sb.AppendLine($"{o.Id},{o.CustomerId},{o.Total},{o.Status}");
        return sb.ToString();
    }
}

// JSON exporter — same algorithm, different format
public class JsonOrderExporter(AppDbContext db) : DataExporter
{
    protected override async Task<IEnumerable<object>> ExtractDataAsync()
        => await db.Orders.ToListAsync();

    protected override string FormatOutput(IEnumerable<object> data)
        => System.Text.Json.JsonSerializer.Serialize(data,
            new System.Text.Json.JsonSerializerOptions { WriteIndented = true });

    protected override async Task OnExportCompletedAsync(string path)
    {
        await base.OnExportCompletedAsync(path);
        // Additional hook: send notification after JSON export
        Console.WriteLine("JSON export notification sent");
    }
}

// Usage — same API, different behaviour
DataExporter exporter = new CsvOrderExporter(db);
await exporter.ExportAsync("/exports/orders.csv");

exporter = new JsonOrderExporter(db);
await exporter.ExportAsync("/exports/orders.json");

Template Method with Interfaces (Modern Alternative)

C#
// Instead of abstract classes, use a strategy-like composition
// This avoids inheritance and is more testable

public interface IDataSource<T>
{
    Task<IEnumerable<T>> ExtractAsync();
}

public interface IDataFormatter<T>
{
    string Format(IEnumerable<T> data);
}

// Template is now a generic class using composition
public class DataExporterPipeline<T>(
    IDataSource<T>    source,
    IDataFormatter<T> formatter)
{
    public async Task ExportAsync(string outputPath)
    {
        var data    = await source.ExtractAsync();
        var content = formatter.Format(data);
        await File.WriteAllTextAsync(outputPath, content);
    }
}

xUnit Test Base (Template Method in Test Frameworks)

C#
// xUnit's fixture setup IS Template Method
public abstract class DatabaseTestBase : IAsyncLifetime
{
    protected AppDbContext Db { get; private set; } = null!;

    // Template method — xUnit calls these
    public async Task InitializeAsync()
    {
        Db = CreateDbContext();
        await SeedDataAsync();    // subclass implements
    }

    public async Task DisposeAsync()
        => await Db.DisposeAsync();

    protected abstract AppDbContext CreateDbContext();
    protected virtual Task SeedDataAsync() => Task.CompletedTask;
}

public class OrderRepositoryTests : DatabaseTestBase
{
    protected override AppDbContext CreateDbContext()
        => new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase("TestDb").Options);

    protected override async Task SeedDataAsync()
    {
        Db.Orders.Add(new Order { CustomerId = 1, Total = 99.99m });
        await Db.SaveChangesAsync();
    }

    [Fact]
    public async Task GetById_Returns_SeededOrder()
    {
        var repo = new OrderRepository(Db);
        var order = await repo.GetByIdAsync(1);
        Assert.NotNull(order);
    }
}

Template Method vs Strategy

Template Method: algorithm skeleton in base class, steps overridden in subclasses
  — uses inheritance, compile-time binding
  — tight coupling between base and subclass

Strategy: algorithm selected at runtime by injecting a strategy object
  — uses composition, runtime binding
  — more flexible and testable

Prefer Strategy when you want to swap algorithms at runtime or test them independently.
Use Template Method for frameworks and base classes where the skeleton is truly fixed.

Interview Answer

"Template Method defines an algorithm's structure in a base class and delegates variable steps to subclasses via abstract methods (required overrides) or virtual methods (optional hooks). It's the foundation of most framework base classes: ASP.NET Core's BackgroundService (you override ExecuteAsync), xUnit's IAsyncLifetime (you override InitializeAsync/DisposeAsync). The key trade-off versus Strategy: Template Method uses inheritance (fixed skeleton, variable steps by subclassing) while Strategy uses composition (algorithm object injected at runtime, swappable). Modern C# prefers Strategy over Template Method for new code — it's more testable, avoids deep inheritance hierarchies, and allows runtime flexibility. Template Method is still correct for framework extension points where subclasses need a guaranteed setup/teardown contract."