Fluent API

Uma fluent API é uma forma de compor o código para que ele seja fácil de usar e de ler, encadeando chamadas de métodos como se fossem frases. Neste artigo, vamos entender o que são e como podemos criar Fluent APIs no .NET com C#.

Ao longo do texto, você vai construir um exemplo real e descobrir como as interfaces garantem que as chamadas aconteçam na ordem correta.

O que é uma fluent API

Fluent APIs, ou APIs fluentes, são uma forma de compor o código para facilitar o seu uso. Em vez de criar vários objetos soltos e conectá-los manualmente, você encadeia métodos em uma única expressão.

O termo "fluent interface" foi cunhado por Martin Fowler e Eric Evans. Segundo o artigo original de Martin Fowler, publicado em 2005, o objetivo é criar algo parecido com uma pequena linguagem de domínio dentro do próprio código.

Vamos supor que queremos compor um código para executar algumas tarefas, e que elas precisam rodar em uma determinada ordem. Nesse caso, as Fluent APIs se encaixam perfeitamente.

Isso acontece porque a própria API conduz o desenvolvedor. Cada método disponível depois de uma chamada indica qual é o próximo passo possível.

Fluent API não é Web API nem Fluent Mapping

Não podemos confundir Fluent APIs com o Fluent Mapping utilizado no Entity Framework. Ainda assim, o Fluent Mapping faz uso da técnica de fluent API.

No EF Core, por exemplo, a configuração do modelo dentro do OnModelCreating usa chamadas encadeadas sobre o ModelBuilder. A documentação do EF Core explica que o nome "fluent" vem justamente desse encadeamento de chamadas em uma única instrução:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // configuração do mapeamento usando a técnica fluente
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

Ou seja, o EF Core oferece uma fluent API para mapeamento, mas a técnica é bem mais ampla do que esse uso específico.

Também não devemos confundir Fluent APIs com APIs Web, ou Web APIs, que criamos no ASP.NET, por exemplo. Neste artigo, "API" se refere à superfície pública de uma classe, ou seja, aos métodos que outros desenvolvedores chamam.

Exemplo de uso: gerando um workflow do GitHub Actions

Vamos tomar como base um exemplo que trabalhei há algum tempo. Havia a necessidade de gerar um arquivo yml para o GitHub Actions fazer o deployment da aplicação.

Um arquivo yml é basicamente um texto, que deve ser escrito em uma ordem, com determinados parâmetros e respeitando o espaçamento.

name: Build and deploy package

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@main

    - name: Set up .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: '5.0.x'

    - name: Build with dotnet
      run: dotnet build --configuration Release

    - name: Test
      run: dotnet test

    - name: Create the Package
      run: dotnet pack --configuration Release

    - name: Publish
      run: dotnet nuget push "./Gravatar/bin/Release/Gravatar.1.1.0.nupkg" -k ${{ secrets.NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json

O problema é que um simples espaço fora de lugar quebra o deployment. Por isso, criamos uma espécie de gerador de yml que podemos validar via código, com testes de unidade.

Dessa forma, chegamos ao uso das Fluent APIs para estruturar o código de maneira limpa e prevenir possíveis erros:

var workflow = GitHubActionBuilder
    .Configure()
    .WithName("My workflow name")
    .OnBranch("main")
    .WithJob("Build and deploy", x =>
    {
        x.Key = "MyEnvVar";
        x.Value = "MyValue";
    })
    .RunsOn("ubuntu-latest")
    .AddStep("Setup .NET", "actions/setup-dotnet@v1")
        .With(x =>
        {
            x.Key = "dotnet-version";
            x.Value = "5.0.x";
        })
    .AddStep("Add GitHub Packages Source", "dotnet nuget add source ...")
    .AddStep("Build with dotnet", "dotnet build ...")
    .Build("main-workflow.yml");

Repare como o código se lê quase como uma descrição do workflow. Além disso, cada etapa tem um método com nome claro, o que reduz a chance de erro na montagem do arquivo.

Observe também os métodos WithJob e With. Eles recebem uma expressão lambda que configura um objeto de chave e valor, em vez de exigir vários parâmetros soltos.

Esse padrão aparece com frequência em APIs fluentes. Afinal, ele mantém a cadeia legível mesmo quando uma etapa precisa de várias informações.

Se você quer conhecer melhor o GitHub Actions, o artigo sobre deploy de aplicações ASP.NET com GitHub Actions e Azure mostra um pipeline completo.

Iniciando uma fluent API

A primeira coisa que precisamos para iniciar uma fluent API é criar uma classe com um construtor. Normalmente, mantemos esse construtor privado, já que não vamos precisar dele fora da classe.

public class GitHubActionBuilder
{
    private GitHubActionBuilder()
    {
    }
}

Com o construtor privado, ninguém consegue usar new GitHubActionBuilder() de fora da classe. Assim, garantimos que todo uso da API comece pelo ponto de entrada que vamos definir a seguir.

Adicionando ações

Com a classe criada, o próximo passo é adicionar algumas ações. Normalmente, começamos com um método que não tem muito significado externo ou que realiza apenas uma configuração.

public static GitHubActionBuilder Configure()
{
    return new GitHubActionBuilder();
}

Entretanto, todos os métodos de uma fluent API precisam retornar a própria API. Para isso, usamos um new GitHubActionBuilder() (o nome da nossa classe) ou this.

No Flunt, por exemplo, o primeiro método chama-se Requires e não tem nada nele, sendo apenas um syntax sugar. Para conhecer a biblioteca, veja o artigo sobre o Flunt.

Dessa forma, já podemos iniciar nosso código com a chamada ao GitHubActionBuilder.Configure():

var workflow = GitHubActionBuilder
    .Configure();

Problemas neste código

Por mais que este código funcione, há um problema nele. Vamos adicionar mais um método na classe GitHubActionBuilder para analisar.

public class GitHubActionBuilder
{
    private GitHubActionBuilder()
    {
    }

    public static GitHubActionBuilder Configure()
    {
        return new GitHubActionBuilder();
    }

    public static GitHubActionBuilder WithName()
    {
        return new GitHubActionBuilder();
    }
}

Mesmo com um novo método, não conseguimos encadear as chamadas. Como os dois métodos são estáticos, não é possível fazer uma chamada como a abaixo:

var workflow = GitHubActionBuilder
    .WithName()
    .Configure();

Pior ainda: e se houver a necessidade explícita de o método WithName vir somente depois do Configure? Como resolvemos isso?

Interfaces como guia da ordem das chamadas

A solução são as interfaces. Com elas, podemos limitar os retornos e permitir que o GitHubActionBuilder implemente várias delas ao mesmo tempo.

Vale lembrar que, desde o C# 8, as interfaces podem ter implementações padrão de membros. Na prática, isso permite compartilhar comportamento entre interfaces, algo próximo de uma herança múltipla de comportamento.

Criando as interfaces

Vamos criar duas interfaces para entender o processo. Ele se repete quantas vezes você precisar, então não vamos escrever todo o código aqui.

public interface IWorkflow
{
    public IWorkflowName WithName(string name);
}

public interface IWorkflowName
{
}

Como podemos notar, temos duas interfaces, IWorkflow e IWorkflowName. A primeira tem um método chamado WithName que retorna a segunda interface.

Aqui está o segredo: uma interface retorna ela mesma ou alguma outra interface. Assim, conseguimos restringir o uso na ordem que os retornos dos métodos determinam.

Isso funciona porque o compilador enxerga apenas os membros do tipo declarado. Mesmo que o objeto real seja um GitHubActionBuilder completo, quem recebe um IWorkflow só consegue chamar os métodos dessa interface.

Em outras palavras, a classe concentra toda a implementação, enquanto as interfaces funcionam como janelas. Cada janela mostra apenas o que faz sentido naquele ponto da cadeia.

Uma interface também pode retornar ela mesma quando um passo pode se repetir. É o caso de AddStep, que pode aparecer várias vezes seguidas antes do Build.

Implementando as interfaces na classe

Seguindo adiante, vamos adicionar as duas interfaces à classe principal, separadas por vírgula e precedidas por :.

public class GitHubActionBuilder :
    IWorkflow,
    IWorkflowName
{
    ...

Para finalizar, vamos implementar as interfaces. O método Configure continua o mesmo, com exceção do retorno, que agora é um IWorkflow.

public class GitHubActionBuilder :
    IWorkflow,
    IWorkflowName
{
    private GitHubActionBuilder()
    {
    }

    public static IWorkflow Configure()
    {
        return new GitHubActionBuilder();
    }

    public IWorkflowName WithName(string name)
    {
        return this;
    }
}

Note que o método WithName retorna this. Tanto new GitHubActionBuilder() quanto this funcionam aqui, pois a classe implementa essas interfaces.

Consumindo a fluent API

Agora, ao tentar consumir a fluent API, não conseguimos chamar o método WithName antes do Configure. O código abaixo nem compila:

var workflow = GitHubActionBuilder
    .WithName("Build and Deploy")
    .Configure();

Somos obrigados a seguir a ordem correta de execução, conforme definimos previamente nas interfaces:

var workflow = GitHubActionBuilder
    .Configure()
    .WithName("Build and Deploy");

Esse é o grande ganho da abordagem. O compilador passa a validar a sequência das chamadas, e o IntelliSense mostra apenas os métodos permitidos em cada etapa.

Evoluindo o builder com estado e um método final

Até aqui, os métodos apenas retornam a própria instância. Em um cenário real, porém, cada chamada precisa guardar alguma informação para o resultado final.

Para isso, a classe mantém campos privados que cada método preenche. Em seguida, um método final, como Build, usa esses dados para gerar o resultado:

public interface IWorkflow
{
    IWorkflowName WithName(string name);
}

public interface IWorkflowName
{
    IWorkflowBranch OnBranch(string branch);
}

public interface IWorkflowBranch
{
    string Build();
}

public class GitHubActionBuilder :
    IWorkflow,
    IWorkflowName,
    IWorkflowBranch
{
    private string _name = string.Empty;
    private string _branch = string.Empty;

    private GitHubActionBuilder()
    {
    }

    public static IWorkflow Configure() => new GitHubActionBuilder();

    public IWorkflowName WithName(string name)
    {
        _name = name;
        return this;
    }

    public IWorkflowBranch OnBranch(string branch)
    {
        _branch = branch;
        return this;
    }

    // gera o trecho inicial do arquivo yml com o espaçamento correto
    public string Build()
        => $"name: {_name}\n\non:\n  push:\n    branches:\n      - {_branch}\n";
}

Repare que Build só fica disponível depois de OnBranch. Dessa forma, é impossível gerar um workflow sem nome ou sem branch, porque o compilador bloqueia essa sequência.

Note também que Build retorna uma string, e não outra interface. Esse é o ponto em que a cadeia termina e o resultado sai da fluent API.

Testando a fluent API com testes de unidade

Lembra que o objetivo inicial era validar o yml via código? Com o método Build retornando texto, fica simples escrever um teste de unidade para isso:

[Fact]
public void Build_ShouldGenerateWorkflowHeader()
{
    var result = GitHubActionBuilder
        .Configure()
        .WithName("Build and deploy package")
        .OnBranch("main")
        .Build();

    // garante nome, gatilho e indentação esperados
    Assert.Contains("name: Build and deploy package", result);
    Assert.Contains("      - main", result);
}

Assim, um espaço fora do lugar aparece no teste, e não no pipeline quebrado. Se quiser se aprofundar no tema, o artigo Fundamentos dos testes de unidade cobre a base.

Fluent API e o padrão Builder

O exemplo deste artigo combina duas ideias: o encadeamento de métodos e o padrão Builder. O Builder separa a construção de um objeto complexo da sua representação final.

Uma fluent API não precisa ser sempre um Builder, mas os dois combinam muito bem. Afinal, o Builder monta algo passo a passo, e a fluência deixa esses passos legíveis.

Também é importante notar a diferença entre encadear métodos e ser realmente fluente. Fowler destaca que method chaining é uma técnica comum, mas a fluência de verdade está em fazer o código soar como a linguagem do domínio.

Se você quer revisar o conceito de padrões de projeto, o artigo O que é um design pattern apresenta a base.

Onde a fluent API aparece no .NET

Mesmo sem perceber, você provavelmente já usa uma fluent API todos os dias. O próprio ecossistema .NET adota esse estilo em várias bibliotecas.

O LINQ é o exemplo mais conhecido. Na sintaxe de métodos, chamadas como Where, OrderBy e Select se encadeiam sobre uma coleção e formam uma consulta legível:

// consulta encadeada com a sintaxe de métodos do LINQ
var activeNames = customers
    .Where(c => c.IsActive)
    .OrderBy(c => c.Name)
    .Select(c => c.Name)
    .ToList();

O EF Core, como vimos, também usa esse estilo no ModelBuilder. Nesse caso, cada chamada refina a configuração de uma entidade ou propriedade.

Bibliotecas de validação seguem a mesma linha. No Flunt, por exemplo, você encadeia regras de validação a partir de um contrato, e cada regra devolve o próprio contrato para a próxima chamada.

Portanto, ao criar a sua fluent API, você segue um estilo que os desenvolvedores .NET já reconhecem. Isso reduz a curva de aprendizado de quem vai consumir o seu código.

Boas práticas de nomenclatura

Em uma API fluente, o nome dos métodos importa tanto quanto a estrutura. Afinal, a cadeia de chamadas precisa ser lida quase como uma frase.

No exemplo do workflow, os prefixos ajudam bastante. With indica uma configuração, como em WithName e WithJob, enquanto On descreve um gatilho, como em OnBranch.

Da mesma forma, Add sugere que a etapa pode se repetir, como em AddStep. Já Build deixa claro que a cadeia termina ali e que um resultado será produzido.

Em resumo, escolha verbos e preposições que façam sentido na sequência. Assim, quem lê a chamada entende o resultado sem abrir a implementação de cada método.

Vantagens e cuidados ao criar uma fluent API

As vantagens aparecem logo no primeiro uso. O código fica mais legível, a ordem das chamadas fica garantida pelo compilador e o IntelliSense guia quem consome a API.

Além disso, a abordagem reduz erros de configuração. No caso do yml, por exemplo, a formatação passa a ser responsabilidade de um único lugar testável.

Por outro lado, existem cuidados. Fowler aponta que construir uma API fluente exige mais esforço de design do que uma API comum.

Ele também lembra que, isolados, os métodos podem perder clareza, porque fazem sentido apenas dentro da cadeia. Por isso, a documentação de cada método tende a ser mais difícil.

Outro ponto é o número de interfaces. Quanto mais etapas a API tem, mais interfaces você cria, então vale manter cada uma pequena e focada em um único passo.

Portanto, use a técnica quando a ordem das operações realmente importa ou quando a legibilidade traz ganho claro. Para APIs simples, métodos comuns continuam sendo a melhor escolha.

Conclusão

O C# é muito poderoso no quesito de organização e restrição de código. Com uma fluent API, podemos facilitar o consumo das nossas APIs e guiar os usuários para a forma correta de uso.

Neste artigo, você viu como iniciar a API com um construtor privado e um método estático, por que métodos estáticos não bastam e como as interfaces resolvem o problema da ordem.

Por fim, com estado interno e um método final, a mesma estrutura gera um resultado concreto e testável. Assim, a fluent API deixa de ser apenas estética e passa a prevenir erros reais.

Agora, experimente aplicar a técnica em um cenário do seu dia a dia, como a montagem de um relatório, de uma consulta ou de um arquivo de configuração. Comece com poucas interfaces e evolua conforme a necessidade aparecer.

FAQ

Qual a diferença entre fluent API e Web API?

A fluent API é um estilo de design de classes com métodos encadeados. Já uma Web API é um serviço acessado via HTTP, como as que criamos no ASP.NET.

O Fluent Mapping do Entity Framework é uma fluent API?

Ele usa a técnica de fluent API para configurar o mapeamento. Porém, a técnica é mais ampla e pode ser aplicada em qualquer classe.

Por que usar interfaces em uma fluent API?

Cada método retorna uma interface que expõe apenas o próximo passo permitido. Assim, o compilador impede chamadas fora de ordem.

Todo método de uma fluent API precisa retornar this?

Os métodos intermediários retornam a própria instância, com this ou new, tipada pela interface do próximo passo. Já o método final, como Build, retorna o resultado.

Quando não vale a pena criar uma fluent API?

Quando a classe é simples e a ordem das chamadas não importa. Nesse caso, o esforço extra de design não compensa.

Glossário

Termo Significado
Fluent API Estilo de API em que métodos encadeados formam expressões legíveis, próximas da linguagem do domínio.
Method chaining Técnica de chamar vários métodos em sequência, cada um sobre o retorno do anterior.
Builder Padrão de projeto que constrói um objeto complexo passo a passo.
Interface Contrato que define os membros que uma classe deve implementar.
Default interface member Implementação padrão de um membro declarada na própria interface, disponível desde o C# 8.
Syntax sugar Recurso que facilita a escrita do código sem mudar o comportamento.
GitHub Actions Plataforma de automação do GitHub usada para build, testes e deploy.
YAML Formato de texto baseado em indentação, usado nos workflows do GitHub Actions.

Fontes