Behavioral Beginner 8 min read

Template Method in C#: Inheritance Hooks vs Strategy

Template Method pattern in C# / .NET 10: when an abstract base with hook methods is right, when Strategy injection wins, and how to avoid inheritance lock-in.

Table of contents
  1. What problem does the Template Method pattern solve in C#?
  2. How does the textbook Template Method look in .NET 10?
  3. How does ASP.NET Core's BackgroundService use Template Method?
  4. When should you replace Template Method with Strategy?
  5. When does the Template Method pattern misfire?
  6. How does Template Method compare to Strategy and State?
  7. What does a real .NET 10 example look like?
  8. Where should you read next in this series?

Three checkout flows: web, mobile, in-store kiosk. All three load the cart, validate inventory, charge a card, persist the order, and send an email — in that order. The differences are small. Web shows a confirmation page; mobile pushes a notification; the kiosk prints a receipt. The temptation is three near-identical methods, eighty per cent shared, copy-pasted.

The Template Method pattern is the answer the GoF gave for this. Put the shared skeleton in an abstract base class, define hook methods for the parts that vary, and let each subclass fill in its hook. The pattern was the bread-and-butter of 1990s object orientation. In modern C# it competes with composition (Strategy) and the choice is non-trivial — this article is about when each wins.

What problem does the Template Method pattern solve in C#?

The pattern earns its place when several variants share a fixed skeleton and differ in one or two well-named steps. Three concrete shapes:

  1. Lifecycle methods. ASP.NET Core's BackgroundService defines start/stop/execute; you override only ExecuteAsync.
  2. Configurable algorithms with required hooks. A base Importer reads files, parses each row, validates, persists — only the parser changes per file format.
  3. Inheritable templates with optional steps. A base ReportBuilder defines header/body/footer; subclasses override only what they need.

What is not a Template Method problem: "I want to swap an algorithm at runtime" — that is Strategy. "I want different behaviour based on lifecycle stage" — that is State. Template Method is specifically about fixed skeleton, variable hooks, via inheritance.

How does the textbook Template Method look in .NET 10?

A public non-virtual Run() (the template), one or more protected abstract or protected virtual hooks:

public abstract class CheckoutFlow
{
    protected readonly ICartRepository Carts;
    protected readonly IPaymentProcessor Payment;
    protected readonly IOrderRepository Orders;

    protected CheckoutFlow(ICartRepository carts, IPaymentProcessor payment, IOrderRepository orders)
        => (Carts, Payment, Orders) = (carts, payment, orders);

    // The template — non-virtual, defines the algorithm.
    public async Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct)
    {
        var cart   = await Carts.GetAsync(req.CartId, ct);
        var charge = await Payment.PayAsync(cart.Total, cart.CustomerId, req.CartId, ct);
        if (charge.Status != "succeeded")
            throw new CheckoutFailedException(charge.Error ?? "Unknown");

        var order = await Orders.CreateAsync(cart, charge.TransactionId!, ct);

        await NotifyCustomerAsync(order, ct);     // hook
        return BuildResult(order);                // hook
    }

    protected abstract Task NotifyCustomerAsync(Order order, CancellationToken ct);
    protected abstract CheckoutResult BuildResult(Order order);
}

public sealed class WebCheckoutFlow : CheckoutFlow
{
    private readonly IEmailService _emails;
    public WebCheckoutFlow(ICartRepository carts, IPaymentProcessor payment,
        IOrderRepository orders, IEmailService emails)
        : base(carts, payment, orders) => _emails = emails;

    protected override Task NotifyCustomerAsync(Order order, CancellationToken ct)
        => _emails.SendOrderConfirmationAsync(order.CustomerEmail, order.Id, ct);

    protected override CheckoutResult BuildResult(Order order)
        => new(order.Id, order.Total, "/checkout/done?id=" + order.Id);
}

public sealed class KioskCheckoutFlow : CheckoutFlow
{
    private readonly IReceiptPrinter _printer;
    public KioskCheckoutFlow(ICartRepository carts, IPaymentProcessor payment,
        IOrderRepository orders, IReceiptPrinter printer)
        : base(carts, payment, orders) => _printer = printer;

    protected override Task NotifyCustomerAsync(Order order, CancellationToken ct)
        => _printer.PrintReceiptAsync(order, ct);

    protected override CheckoutResult BuildResult(Order order)
        => new(order.Id, order.Total, ConfirmationUrl: null);
}

The structural picture:

classDiagram
    class CheckoutFlow {
        <<abstract>>
        +CheckoutAsync(req)
        #NotifyCustomerAsync(order)*
        #BuildResult(order)*
    }
    class WebCheckoutFlow
    class KioskCheckoutFlow
    class MobileCheckoutFlow
    CheckoutFlow <|-- WebCheckoutFlow
    CheckoutFlow <|-- KioskCheckoutFlow
    CheckoutFlow <|-- MobileCheckoutFlow

The * marks abstract members; the public CheckoutAsync is the template. Adding a fourth flow is one new subclass plus two hook implementations.

How does ASP.NET Core's BackgroundService use Template Method?

Microsoft.Extensions.Hosting.BackgroundService is the canonical Template Method in modern .NET:

public sealed class OrderRetryService : BackgroundService
{
    private readonly IOrderRepository _orders;
    private readonly IPaymentProcessor _payment;
    public OrderRetryService(IOrderRepository orders, IPaymentProcessor payment)
        => (_orders, _payment) = (orders, payment);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var failed = await _orders.GetFailedRecentAsync(stoppingToken);
            foreach (var order in failed)
                await _payment.PayAsync(order.Total, order.CustomerId, order.Id, stoppingToken);
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

You override exactly one method — ExecuteAsync. The base class handles StartAsync, StopAsync, cancellation token wiring, and lifetime registration. The pattern is invisible because it is so familiar; recognising it tells you what MichelinController, DbContext.OnConfiguring, and dozens of other framework hooks are.

When should you replace Template Method with Strategy?

Template Method is inheritance; Strategy is composition. Inheritance has known problems:

Replace with Strategy when:

The composition equivalent of the checkout example:

public sealed class CheckoutFlow
{
    private readonly ICartRepository _carts;
    private readonly IPaymentProcessor _payment;
    private readonly IOrderRepository _orders;
    private readonly INotificationStrategy _notify;
    private readonly IResultBuilder _builder;

    public CheckoutFlow(ICartRepository carts, IPaymentProcessor payment, IOrderRepository orders,
        INotificationStrategy notify, IResultBuilder builder)
        => (_carts, _payment, _orders, _notify, _builder) = (carts, payment, orders, notify, builder);

    public async Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct)
    {
        var cart   = await _carts.GetAsync(req.CartId, ct);
        var charge = await _payment.PayAsync(cart.Total, cart.CustomerId, req.CartId, ct);
        if (charge.Status != "succeeded") throw new CheckoutFailedException(charge.Error ?? "Unknown");
        var order = await _orders.CreateAsync(cart, charge.TransactionId!, ct);
        await _notify.NotifyAsync(order, ct);
        return _builder.Build(order);
    }
}

One concrete class, two strategies, no inheritance. Wiring is DI registrations: web flow gets EmailNotification + WebResultBuilder, kiosk gets PrintNotification + KioskResultBuilder.

The trade-off is real. Template Method's inheritance gives you one class per variant — readable, easy to navigate, and clearly named. Composition gives you flexibility and testability at the cost of a few more files and DI lines.

When does the Template Method pattern misfire?

Three traps:

How does Template Method compare to Strategy and State?

Pattern Variation mechanism Coupling Modern .NET
Template Method Inheritance (base + subclass hooks) Tight (subclass knows base's algorithm) BackgroundService, ControllerBase
Strategy Composition (inject implementation) Loose DI, Func<>
State Self-replacing classes per lifecycle stage Internal State machine

The cleanest split: Template Method is we share a skeleton, you fill the gaps; Strategy is we share an interface, you swap implementations; State is we change which class we are based on what happened. All three vary behaviour; the axis differs.

What does a real .NET 10 example look like?

A pragmatic shape that combines Template Method (for the fixed skeleton) with strategies (for parts that need rich configuration). The base class owns the algorithm; the hooks delegate to strategies when the variant has a real abstraction worth naming:

public abstract class CheckoutFlow
{
    protected ICartRepository Carts { get; }
    protected IPaymentProcessor Payment { get; }
    protected IOrderRepository Orders { get; }

    protected CheckoutFlow(ICartRepository carts, IPaymentProcessor payment, IOrderRepository orders)
        => (Carts, Payment, Orders) = (carts, payment, orders);

    public async Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct)
    {
        var cart   = await Carts.GetAsync(req.CartId, ct)
            ?? throw new NotFoundException();
        var charge = await Payment.PayAsync(cart.Total, cart.CustomerId, req.CartId, ct);
        if (charge.Status != "succeeded")
            throw new CheckoutFailedException(charge.Error ?? "Unknown");

        var order = await Orders.CreateAsync(cart, charge.TransactionId!, ct);

        await OnPaidAsync(order, ct);                  // optional hook
        await NotifyCustomerAsync(order, ct);          // required hook
        return BuildResult(order);                     // required hook
    }

    protected virtual Task OnPaidAsync(Order order, CancellationToken ct) => Task.CompletedTask;
    protected abstract Task NotifyCustomerAsync(Order order, CancellationToken ct);
    protected abstract CheckoutResult BuildResult(Order order);
}

public sealed class WebCheckoutFlow : CheckoutFlow
{
    private readonly IEmailService _emails;
    public WebCheckoutFlow(ICartRepository carts, IPaymentProcessor payment,
        IOrderRepository orders, IEmailService emails)
        : base(carts, payment, orders) => _emails = emails;

    protected override Task NotifyCustomerAsync(Order order, CancellationToken ct)
        => _emails.SendOrderConfirmationAsync(order.CustomerEmail, order.Id, ct);

    protected override CheckoutResult BuildResult(Order order)
        => new(order.Id, order.Total, $"/checkout/done?id={order.Id}");
}

public sealed class KioskCheckoutFlow : CheckoutFlow
{
    private readonly IReceiptPrinter _printer;
    public KioskCheckoutFlow(ICartRepository carts, IPaymentProcessor payment,
        IOrderRepository orders, IReceiptPrinter printer)
        : base(carts, payment, orders) => _printer = printer;

    protected override async Task OnPaidAsync(Order order, CancellationToken ct)
        => await _printer.PrintReceiptAsync(order, ct);    // print *before* notifying

    protected override Task NotifyCustomerAsync(Order order, CancellationToken ct)
        => Task.CompletedTask;                             // already printed

    protected override CheckoutResult BuildResult(Order order)
        => new(order.Id, order.Total, ConfirmationUrl: null);
}

// Composition root
builder.Services.AddScoped<WebCheckoutFlow>();
builder.Services.AddScoped<KioskCheckoutFlow>();
// per-context: inject the right one based on caller

// Test the template via a minimal subclass
public sealed class TestFlow : CheckoutFlow
{
    public TestFlow(ICartRepository c, IPaymentProcessor p, IOrderRepository o) : base(c, p, o) {}
    public string? Notified { get; private set; }
    protected override Task NotifyCustomerAsync(Order order, CancellationToken ct)
        { Notified = order.Id; return Task.CompletedTask; }
    protected override CheckoutResult BuildResult(Order order)
        => new(order.Id, order.Total, null);
}

[Fact]
public async Task Template_runs_hooks_in_order()
{
    var sut = new TestFlow(/* mocks */);
    var result = await sut.CheckoutAsync(new CheckoutRequest("c1", "stripe"), default);
    Assert.NotNull(sut.Notified);
}

What is on the page: a fixed skeleton. Two real subclasses. One test subclass to exercise the template. Adding a fourth flow is one subclass and two methods. Not on the page: a switch over flow type, copy-pasted skeleton, or a five-axis Strategy zoo.

A practical rule: default to composition; reach for Template Method when the skeleton genuinely is the abstraction worth naming. ASP.NET Core uses Template Method for BackgroundService because "thing that runs in the background" is a real concept. Your application code mostly is not — and Strategy is the lighter, more testable shape.

Frequently asked questions

When should I prefer Template Method over Strategy?
Prefer Template Method when the skeleton is the abstraction worth naming and the variations are small steps inside it; subclasses are genuine variants of one concept. Prefer Strategy when the variation is the abstraction — the skeleton is incidental and the algorithm is the interesting part. Strategy composes; Template Method inherits.
Is the ASP.NET Core BackgroundService class a Template Method?
Yes — it is a textbook example. BackgroundService defines StartAsync/StopAsync/ExecuteAsync lifecycle, and you override only ExecuteAsync. The base class controls the algorithm; you fill in one hook. Same shape as ControllerBase, IdentityUserStore, DbContext.OnModelCreating — the framework uses Template Method extensively.
How do I avoid inheritance lock-in with Template Method?
Make the public surface a single concrete Run() and make the hooks protected abstract. Document which hooks are required and which are optional with protected virtual. The day a subclass needs to vary something the base did not anticipate, refactor to inject a Strategy for that step rather than carving a new protected virtual method as an afterthought.
Should hook methods throw if not overridden?
If the hook is logically required, mark it abstract — the compiler enforces. If the hook is optional, give it a virtual no-op default. Throwing NotImplementedException from a virtual is a sign the contract is unclear; convert to abstract or provide a real default.