Pipeline Pattern¶
Padrão para fluxos complexos com múltiplas etapas sequenciais.
Quando Usar¶
- Fluxos com 3+ etapas que modificam estado compartilhado
- Lógica complexa que precisa ser testada em partes
- Operações que podem crescer (novos steps no futuro)
Não usar para: operações simples, CRUD básico, queries.
Estrutura Base¶
Localização: Ciba.Api/_Shared/Pipelines/
// IPipelineContext.cs
public interface IPipelineContext { }
// IPipelineStep.cs
public interface IPipelineStep<TContext> where TContext : IPipelineContext
{
Task ExecuteAsync(TContext context, CancellationToken ct = default);
}
// IPipeline.cs
public interface IPipeline<TContext> where TContext : IPipelineContext
{
IPipeline<TContext> AddStep<TStep>() where TStep : IPipelineStep<TContext>;
Task ExecuteAsync(TContext context, CancellationToken ct = default);
}
// Pipeline.cs
public class Pipeline<TContext> : IPipeline<TContext> where TContext : IPipelineContext
{
private readonly IServiceProvider _sp;
private readonly List<Type> _steps = [];
public Pipeline(IServiceProvider sp) => _sp = sp;
public IPipeline<TContext> AddStep<TStep>() where TStep : IPipelineStep<TContext>
{
_steps.Add(typeof(TStep));
return this;
}
public async Task ExecuteAsync(TContext context, CancellationToken ct = default)
{
foreach (var stepType in _steps)
{
var step = (IPipelineStep<TContext>)_sp.GetRequiredService(stepType);
await step.ExecuteAsync(context, ct);
}
}
}
Organização de Pastas¶
Ciba.Api/
└── Features/
└── {Recurso}/
└── {Ação}/
└── Pipelines/
└── {NomePipeline}/
├── {NomePipeline}Context.cs
├── Step1.cs
├── Step2.cs
└── Step3.cs
Exemplo Prático¶
Contexto¶
public class ProcessarMensagemContext : IPipelineContext
{
// Inputs (imutáveis)
public required MensagemRecebida Mensagem { get; init; }
public required Guid TenantId { get; init; }
// Outputs (modificados pelos steps)
public Conversa? Conversa { get; set; }
public string? RespostaIA { get; set; }
public bool MensagemEnviada { get; set; }
// Erros
public List<string> Erros { get; } = [];
public bool TemErros => Erros.Count > 0;
}
Step¶
public class ValidarConversaStep(IConversaRepository repo)
: IPipelineStep<ProcessarMensagemContext>
{
public async Task ExecuteAsync(ProcessarMensagemContext ctx, CancellationToken ct)
{
var conversa = await repo.ObterPorContatoAsync(ctx.Mensagem.ContatoId, ct);
if (conversa is null)
{
ctx.Erros.Add("Conversa não encontrada");
return;
}
ctx.Conversa = conversa;
}
}
Handler¶
public class ProcessarMensagemHandler(IPipeline<ProcessarMensagemContext> pipeline)
: IRequestHandler<ProcessarMensagemCommand, Result>
{
public async Task<Result> Handle(ProcessarMensagemCommand cmd, CancellationToken ct)
{
var ctx = new ProcessarMensagemContext
{
Mensagem = cmd.Mensagem,
TenantId = cmd.TenantId
};
await pipeline
.AddStep<ValidarConversaStep>()
.AddStep<EnriquecerContextoStep>()
.AddStep<GerarRespostaIAStep>()
.AddStep<EnviarMensagemStep>()
.ExecuteAsync(ctx, ct);
return ctx.TemErros
? Result.Failure(ctx.Erros)
: Result.Success();
}
}
Registro de DI¶
Em PipelineServiceCollectionExtensions.cs:
// Pipeline genérico
services.AddTransient(typeof(IPipeline<>), typeof(Pipeline<>));
// Steps (via Scrutor)
services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(c => c.AssignableTo(typeof(IPipelineStep<>)))
.AsSelf()
.WithTransientLifetime());
Requer:
dotnet add package Scrutor
Regras¶
- Um step = uma responsabilidade — não misture validação com envio de email
- Early return — se condição não se aplica, retorne sem fazer nada
- Context carrega tudo — inputs são
init, outputs sãoset - Steps não conhecem outros steps — comunicam-se apenas via contexto
- Testável isoladamente — cada step pode ser testado unitariamente