BehavioralC#verifiedVerified
Template Method Pattern in C#
Defines the skeleton of an algorithm in a base class, deferring certain steps to subclasses without changing the algorithm's overall structure.
How to Implement the Template Method Pattern in C#
1Step 1: Define abstract class with the template method
public abstract class DataParser
{
// Template method defines the algorithm skeleton
public string Parse(string rawData)
{
var opened = OpenFile(rawData);
var extracted = ExtractData(opened);
var parsed = ParseData(extracted);
return FormatOutput(parsed);
}2Step 2: Abstract steps to be implemented by subclasses
protected abstract string OpenFile(string path);
protected abstract string[] ExtractData(string raw);
protected abstract object[] ParseData(string[] rows);
// Hook method with default behavior
protected virtual string FormatOutput(object[] data) =>
string.Join(", ", data);
}3Step 3: Concrete implementation
public class CsvParser : DataParser
{
protected override string OpenFile(string path) =>
$"Contents of {path}";
protected override string[] ExtractData(string raw) =>
raw.Split('\n');
protected override object[] ParseData(string[] rows) =>
rows.Select(r => (object)r.Split(',')).ToArray();
}using Microsoft.Extensions.Logging;
// [step] Define abstract ETL pipeline as a template method
public abstract class EtlPipeline<TRaw, TTransformed, TResult>(
ILogger logger)
{
// [step] Template method defines the ETL algorithm
public async Task<EtlResult<TResult>> RunAsync(
string source, CancellationToken ct = default)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var errors = new List<string>();
try
{
logger.LogInformation("Starting ETL for {Source}", source);
// Step 1: Extract
var rawData = await ExtractAsync(source, ct);
logger.LogDebug("Extracted {Count} records", rawData.Count);
// Step 2: Validate (hook)
var validData = await ValidateAsync(rawData, errors, ct);
// Step 3: Transform
var transformed = await TransformAsync(validData, ct);
logger.LogDebug("Transformed {Count} records", transformed.Count);
// Step 4: Load
var result = await LoadAsync(transformed, ct);
// Step 5: Post-process (hook)
await OnCompletedAsync(result, ct);
return new EtlResult<TResult>(
true, result, rawData.Count, transformed.Count,
errors.AsReadOnly(), sw.Elapsed);
}
catch (Exception ex)
{
logger.LogError(ex, "ETL pipeline failed for {Source}", source);
errors.Add(ex.Message);
return new EtlResult<TResult>(
false, default, 0, 0, errors.AsReadOnly(), sw.Elapsed);
}
}
// [step] Abstract steps subclasses must implement
protected abstract Task<IReadOnlyList<TRaw>> ExtractAsync(
string source, CancellationToken ct);
protected abstract Task<IReadOnlyList<TTransformed>> TransformAsync(
IReadOnlyList<TRaw> data, CancellationToken ct);
protected abstract Task<TResult> LoadAsync(
IReadOnlyList<TTransformed> data, CancellationToken ct);
// [step] Hook methods with defaults
protected virtual Task<IReadOnlyList<TRaw>> ValidateAsync(
IReadOnlyList<TRaw> data, List<string> errors,
CancellationToken ct) =>
Task.FromResult(data);
protected virtual Task OnCompletedAsync(
TResult result, CancellationToken ct) =>
Task.CompletedTask;
}
public record EtlResult<T>(
bool Success, T? Result, int ExtractedCount,
int TransformedCount, IReadOnlyList<string> Errors,
TimeSpan Duration);
// [step] Concrete ETL pipeline
public class CsvToDbPipeline(ILogger<CsvToDbPipeline> logger)
: EtlPipeline<string[], Dictionary<string, object>, int>(logger)
{
protected override Task<IReadOnlyList<string[]>> ExtractAsync(
string source, CancellationToken ct)
{
// Simulate CSV parsing
IReadOnlyList<string[]> rows = new[]
{
new[] { "id", "name", "value" },
new[] { "1", "Alice", "100" },
};
return Task.FromResult(rows);
}
protected override Task<IReadOnlyList<Dictionary<string, object>>>
TransformAsync(IReadOnlyList<string[]> data, CancellationToken ct)
{
var headers = data[0];
var result = data.Skip(1)
.Select(row => headers.Zip(row)
.ToDictionary(p => p.First, p => (object)p.Second))
.ToList();
return Task.FromResult<IReadOnlyList<Dictionary<string, object>>>(result);
}
protected override Task<int> LoadAsync(
IReadOnlyList<Dictionary<string, object>> data, CancellationToken ct)
{
// Simulate DB insert
return Task.FromResult(data.Count);
}
}Template Method Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Template Method Pattern in the Real World
“Consider a recipe for baking bread. The overall process—mix, knead, let rise, bake, cool—is fixed. But the specific flour blend, kneading technique, and baking temperature are decisions left to the baker. The cookbook provides the invariant sequence; individual bakers customize the steps that can vary without disrupting the overall process.”