Criando uma API RESTful com o Carter e .NET Core

No início deste ano comentei o quanto o ASP.NET Web API é quase uma unanimidade na criação de APIs no .NET, mas que haviam outras opções, tão boas quanto ele.

No artigo em questão abordei a criação de uma API com o framework NancyFX. E ao longo deste ano ganhou destaque outro framework para criação de APIs, que podemos chamar de um derivado do Nancy, o Carter.

C# (C Sharp) - APIs REST com ASP.NET Web API
Curso de C# (C Sharp) - APIs REST com ASP.NET Web API
CONHEÇA O CURSO

Carter

O Carter foi criado por Jonathan Channon, e é mantido por ele e a Carter Community, seguindo a mesma filosofia do Nancy (onde o Jonathan Channon também contribui), facilitar a vida do desenvolvedor.

Implementado como uma extensão do ASP.NET Core, o Carter permite que o código seja mais direto, elegante e agradável. Seu principal objetivo é facilitar a definição das rotas e controllers.

Para compreendê-lo, vamos ver um exemplo simples de API.

Criando a aplicação para utilizar o Carter

O Carter pode ser adicionado em qualquer tipo de aplicação ASP.NET Core, mesmo uma aplicação existente. Caso queria trabalhar apenas com este framework é necessário criar uma aplicação web vazia:

dotnet new web -n CarterAPI

E adicionar o pacote carter:

dotnet add package carter

Em seguida ele deve ser configurado na classe Startup:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCarter();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints => endpoints.MapCarter());
    }
}

Também é possível instalar o CarterTemplate:

dotnet new -i CarterTemplate

Com isso, é possível criar a aplicação utilizando este template:

dotnet new Carter -n CarterAPI

Nesta opção a aplicação é criada com o Carter configurado e um módulo definido. Como não optamos por esta opção, vamos criar o módulo na nossa aplicação.

Criando um módulo

Assim como no Nancy, no Carter os endpoints devem ser definidos em módulos. Um módulo nada mais é que uma classe que herde a classe CarterModule. Esta classe pode ser definida em qualquer ponto da aplicação. O importante é que seja pública, por exemplo:

using Carter;

namespace CarterAPI.Modules
{
    public class HomeModule : CarterModule
    {
        public HomeModule()
        {
            Get("/", (req, res) => res.WriteAsync("Hello from Carter!"));
        }
    }
}

Como é possível supor, acima estamos definindo uma solicitação GET, que será invocada quando o usuário acessar a rota /. Ao executar a aplicação, teremos o resultado:

Para cada verbo do HTTP há um método disponível:

  • Delete = DELETE;
  • Get = GET;
  • Head = HEAD;
  • Options = OPTIONS;
  • Post = POST;
  • Put = PUT;
  • Patch = PATCH.

Criando o CRUD

Para exemplificar uma API REST completa, vamos definir os métodos CRUD.

Inicialmente defina uma entidade:

public class Pessoa
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
}

E um repositório:

public class PessoaRepository
{
    private static Dictionary<int, Pessoa> pessoas = new Dictionary<int, Pessoa>();

    public List<Pessoa> GetAll(){
        return pessoas.Values.ToList();
    }

    public Pessoa Get(int id){
        return pessoas.GetValueOrDefault(id);
    }

    public void Add(Pessoa pessoa){
        pessoas.Add(pessoa.Id, pessoa);
    }

    public void Edit(Pessoa pessoa){
        pessoas.Remove(pessoa.Id);
        pessoas.Add(pessoa.Id, pessoa);
    }

    public void Delete(int id){
        pessoas.Remove(id);
    }
}

Por fim, podemos definir os endpoints:

public class PessoaModule : CarterModule
{
    public readonly PessoaRepository repository;
    public PessoaModule()
    {
        repository = new PessoaRepository();
        Get("/pessoa/", (req, res) => {
            var pessoas = repository.GetAll();
            return res.AsJson(pessoas);
        });
        Get("/pessoa/{id:int}", (req, res) =>
        {
            var id = req.RouteValues.As<int>("id");
            var pessoa = repository.Get(id);
            return res.Negotiate(pessoa);
        });
        Post("/pessoa/", async (req, res) =>
        {
            var pessoa = await req.Bind<Pessoa>();

            repository.Add(pessoa);
            res.StatusCode = 201;
            await res.Negotiate(pessoa);
            return;
        });
        Put("/pessoa/{id:int}", async (req, res) =>
        {
            var pessoa = await req.Bind<Pessoa>();

            pessoa.Id = req.RouteValues.As<int>("id");

            repository.Edit(pessoa);

            res.StatusCode = 204;
            return;
        });
        Delete("/pessoa/{id:int}", (req, res) =>
        {
            var id = req.RouteValues.As<int>("id");
            repository.Delete(id);
            res.StatusCode = 204;
            return Task.CompletedTask;
        });
    }
}

Nestes endpoints podemos notar algumas particularidades do framework, como o fato dos parâmetros da URL definem um tipo de dado:

Get("/pessoa/{id:int}", (req, res) =>

Isso permite que o valor seja validado no momento que a solicitação está sendo processada. Este valor é obtido através do método do RouteValues:

var id = req.RouteValues.As<int>("id");

Já os dados passados no corpo da solicitação serão obtidos através do Bind:

var pessoa = await req.Bind<Pessoa>();

Podemos testar este endpoint pelo Postman, mas outro recurso do Carter é a geração do JSON definido pelo OpenAPI, que abordei no meu artigo sobre o Swagger.

OpenAPI

O JSON do OpenAPI criado pelo Carter pode ser acessado pela URL /openapi, mas infelizmente ele não configura os endpoints automaticamente. Cada endpoint precisa ser configurado em uma classe que herde RouteMetaData.

Para os endpoints deste artigo, defini as classes abaixo:

public class GetPessoa : RouteMetaData
{
    public override string Tag { get; } = "Pessoa";
    public override string Description { get; } = "Retorna uma lista de pessoas";

    public override RouteMetaDataResponse[] Responses { get; } =
    {
        new RouteMetaDataResponse
        {
            Code = 200,
            Description = $"Uma lista de Pessoas",
            Response = typeof(IEnumerable<Pessoa>)
        }
    };

    public override string OperationId { get; } = "Pessoa_GetPessoa";
}
public class GetPessoaById: RouteMetaData
{
    public override string Tag { get; } = "Pessoa";
    public override string Description { get; } = "Obtém pessoa pelo id";

    public override RouteMetaDataResponse[] Responses { get; } =
    {
        new RouteMetaDataResponse
        {
            Code = 200, 
            Description = $"Uma Pessooa",
            Response = typeof(Pessoa)
        },
        new RouteMetaDataResponse
        {
            Code = 404,
            Description = $"Pessoa não encontrada"
        }
    };

    public override string OperationId { get; } = "Pessoa_GetPessoaById";
}
public class PostPessoa: RouteMetaData
{
    public override string Tag { get; } = "Pessoa";
    public override string Description { get; } = "Adiciona uma pessoa";

    public override RouteMetaDataRequest[] Requests { get; } =
    {
        new RouteMetaDataRequest
        {
            Request = typeof(Pessoa),
        }
    };

    public override RouteMetaDataResponse[] Responses { get; } = 
    { 
        new RouteMetaDataResponse 
        { 
            Code = 204, 
            Description = "Pessoa adicionada" 
        }
    };
    public override string OperationId { get; } = "Pessoa_PostPessoa";
}
public class PutPessoa: RouteMetaData
{
    public override string Tag { get; } = "Pessoa";
    public override string Description { get; } = "Atualiza uma pessoa";

    public override RouteMetaDataRequest[] Requests { get; } =
    {
        new RouteMetaDataRequest
        {
            Request = typeof(Pessoa),
        }
    };

    public override RouteMetaDataResponse[] Responses { get; } = 
    { 
        new RouteMetaDataResponse 
        { 
            Code = 204, 
            Description = "Pessoa atualizada" 
        }
    };
    public override string OperationId { get; } = "Pessoa_PutPessoa";
}
public class DeletePessoa: RouteMetaData
{
    public override string Tag { get; } = "Pessoa";
    public override string Description { get; } = "Exclui uma pessoa";

    public override RouteMetaDataResponse[] Responses { get; } = 
    { 
        new RouteMetaDataResponse 
        { 
            Code = 204, 
            Description = "Exclui Pessoa" 
        }
    };

    public override string OperationId { get; } = "Pessoa_DeletePessoa";
}

Agora é necessário definir esses meta dados em cada endpoint:

public class PessoaModule : CarterModule
{
    public readonly PessoaRepository repository;
    public PessoaModule()
    {
        repository = new PessoaRepository();
        Get<GetPessoa>("/pessoa/", (req, res) => {
            var pessoas = repository.GetAll();
            return res.AsJson(pessoas);
        });
        Get<GetPessoaById>("/pessoa/{id:int}", (req, res) =>
        {
            var id = req.RouteValues.As<int>("id");
            var pessoa = repository.Get(id);
            if(pessoa == null)
            {
                res.StatusCode = 404;
                return Task.CompletedTask;
            }
            return res.Negotiate(pessoa);
        });
        Post<PostPessoa>("/pessoa/", async (req, res) =>
        {
            var pessoa = await req.Bind<Pessoa>();

            repository.Add(pessoa);
            Console.WriteLine(pessoa);
            res.StatusCode = 201;
            await res.Negotiate(pessoa);
            return;
        });
        Put<PutPessoa>("/pessoa/{id:int}", async (req, res) =>
        {
            var pessoa = await req.Bind<Pessoa>();

            pessoa.Id = req.RouteValues.As<int>("id");

            repository.Edit(pessoa);

            res.StatusCode = 204;
            return;
        });
        Delete<DeletePessoa>("/pessoa/{id:int}", (req, res) =>
        {
            var id = req.RouteValues.As<int>("id");
            repository.Delete(id);
            res.StatusCode = 204;
            return Task.CompletedTask;
        });
    }
}

Note que eles são especificados no método do endpoint:

Get<GetPessoa>("/pessoa/", (req, res) => {

Ao defini-los e acessar /openapi, será retornado o JSON:

Para visualizarmos em uma interface gráfica, é possível adicionar o SwaggerUi da biblioteca Swashbuckle:

dotnet add package Swashbuckle.AspNetCore.SwaggerUi --version 5.0.0-rc4

Configurá-lo no projeto:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();

    app.UseSwaggerUI(opt =>
    {
        opt.RoutePrefix = "openapi/ui";
        opt.SwaggerEndpoint("/openapi", "Carter OpenAPI Sample");
    });

    app.UseEndpoints(endpoints => endpoints.MapCarter());
}

Desta forma, ao acessar a URL /openapi/ui teremos acesso a interface gráfica:

E por ela é possível testar todos os endpoints.

F# (F Sharp) - Fundamentos
Curso de F# (F Sharp) - Fundamentos
CONHEÇA O CURSO

Conclusão

Devido a sua simplicidade e grande gama de recursos o Carter é um framework que merece ser acompanhado. Caso tenha se interessado por ele, recomendo que dê uma olhada no seu repositório.

Infelizmente no momento ele não possui nenhuma documentação detalhada.

Você pode obter os códigos da aplicação demonstrada neste artigo, no meu Github.

Deixe seu comentário

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200