APIs Data Driven com ASP.NET Core 3.0 e EF Core 3.0 - Part 1

APIs data driven são APIs que expõem operações simples sobre os dados, quase um CRUD direto sobre o banco, sem camadas de domínio elaboradas. Nesta primeira parte, você vai criar uma API desse tipo com ASP.NET Core 3.0 e Entity Framework Core 3.0, passando por modelos, contexto de dados, injeção de dependência e o primeiro controller.

Por que criar APIs data driven

Houve uma cena na minha carreira que nunca esqueço. Em 2012, eu apresentava algumas novidades do ASP.NET Web API no TDC Floripa e, no fim da palestra, um dos espectadores veio falar comigo.

"Achei bem legal isto, mas consigo o mesmo resultado, com metade do código e o dobro da velocidade em Node."

Chegando em casa, corri para testar, e não é que o tal do Node era bom mesmo? Rapaz, que facilidade para criar APIs e que rapidez na execução.

Claro que boa parte dessa velocidade de desenvolvimento vem do fato de a stack ser toda JavaScript. Ou seja, você não precisa ficar criando objetos para serializar ou desserializar nada.

Não existe bala de prata

Eu sou um defensor da orientação a objetos e de domínios ricos. Mesmo assim, gosto de pontuar que não existe bala de prata no desenvolvimento de software, ou seja, o DDD não atende a tudo.

Em determinados momentos, você quer algo data driven, apenas um CRUD básico sobre o banco, sem criar complexidade desnecessária. Caso contrário, não sobra tempo para focar no seu domínio ou mesmo para testá-lo.

Se quiser entender melhor quando escolher cada abordagem, leia o artigo sobre Data Driven vs Domain Driven. Em resumo, cadastros simples, como categorias ou unidades de medida, raramente justificam um domínio rico.

Por isso, neste artigo, vou mostrar como criar APIs data driven de forma simples, prática e objetiva. Com isso, você economiza um belo tempo de desenvolvimento nas partes do sistema que não têm regras de negócio relevantes.

Preparando o ambiente com o .NET Core 3.0

Para executar os códigos a seguir, você precisa do SDK do .NET Core 3.0 instalado. Então, acesse a página oficial de download do .NET e faça a instalação antes de prosseguir.

Depois de instalar, confira a versão no terminal com o comando dotnet --version. Assim, você garante que os templates e os pacotes vão corresponder ao que mostramos aqui.

Criando o projeto da API

O primeiro passo é criar a API. Em muitos casos, eu uso o template vazio do ASP.NET Core, mas aqui o foco é ganhar tempo, então opto por um template pronto e já configurado.

Para criar o projeto, execute o comando:

dotnet new webapi -o Shop

Em seguida, acesse a pasta criada com cd Shop e instale os pacotes do EF Core 3.0:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.InMemory

Neste caso, vamos usar um banco de dados em memória por questões didáticas. Você pode optar pelo SQL Server local, mas o mapeamento objeto relacional fica para outro post.

Vale um alerta importante: a própria Microsoft avisa que o provedor em memória não serve para produção. Ele não suporta transações nem SQL puro, e por isso só faz sentido em estudos e protótipos como este.

O template já vem com um exemplo pronto. Você pode deixá-lo como está ou remover os arquivos WeatherForecast* da aplicação.

Esse template já traz a estrutura de controllers e o Startup.cs configurados, o que acelera bastante a criação de APIs data driven. Afinal, a ideia aqui é escrever o mínimo de código de infraestrutura.

Modelando os dados da API data driven

Mesmo sendo data driven, começamos pelo modelo da aplicação, que representa as tabelas que teríamos no banco de dados. Primeiro, vamos criar a categoria do produto, que contém apenas o Id e o Title, com as devidas validações.

Para isso, crie uma pasta chamada Models e, dentro dela, o arquivo Category.cs com o seguinte código:

using System.ComponentModel.DataAnnotations;

namespace Shop.Models
{
    public class Category
    {
        [Key]
        public int Id { get; set; }

        // Título obrigatório, entre 3 e 60 caracteres
        [Required(ErrorMessage = "This field is required")]
        [MaxLength(60, ErrorMessage = "This field must contain between 3 and 60 characters")]
        [MinLength(3, ErrorMessage = "This field must contain between 3 and 60 characters")]
        public string Title { get; set; }
    }
}

Note que já usamos os Data Annotations, aquelas anotações acima das propriedades. Elas servem tanto para gerar o banco quanto para validar as requisições que chegam à API.

Se você já acompanhou algum curso meu sobre DDD, sabe que eu não recomendo Data Annotations em cenários domain driven, por causa das dependências que eles trazem ao domínio. Neste cenário data driven, porém, não há problema algum, afinal não vamos compartilhar o domínio.

Em APIs data driven, o modelo é praticamente um espelho da tabela. Por isso, concentrar mapeamento e validação na mesma classe simplifica o código sem grandes prejuízos.

A entidade de produto

A próxima entidade é o Product.cs, que se parece com a categoria. A diferença é que ele contém duas propriedades para referenciá-la: o CategoryId e o Category.

using System.ComponentModel.DataAnnotations;

namespace Shop.Models
{
    public class Product
    {
        [Key]
        public int Id { get; set; }

        [Required(ErrorMessage = "This field is required")]
        [MaxLength(60, ErrorMessage = "This field must contain between 3 and 60 characters")]
        [MinLength(3, ErrorMessage = "This field must contain between 3 and 60 characters")]
        public string Title { get; set; }

        [MaxLength(1024, ErrorMessage = "This field must contain at most 1024 characters")]
        public string Description { get; set; }

        [Required(ErrorMessage = "This field is required")]
        [Range(1, int.MaxValue, ErrorMessage = "Price must be greater than zero")]
        public decimal Price { get; set; }

        // Chave estrangeira da categoria
        [Required(ErrorMessage = "This field is required")]
        [Range(1, int.MaxValue, ErrorMessage = "Invalid category")]
        public int CategoryId { get; set; }

        // Propriedade de navegação, opcional neste cenário
        public Category Category { get; set; }
    }
}

Se você não usa outras informações da categoria além do Id, a propriedade de navegação Category não é necessária. Por outro lado, se quiser carregar os dados da categoria junto com o produto, ela passa a ser obrigatória.

Resumo das anotações

Você deve estar se perguntando o que significa esse monte de anotações sobre as propriedades. Então, aqui vai um guia rápido:

  • Key: define que a propriedade é uma chave.
  • Required: define que o campo é obrigatório.
  • MaxLength: define um tamanho máximo, em caracteres, para o campo.
  • MinLength: define um tamanho mínimo, em caracteres, para o campo.
  • Range: define um limite numérico mínimo e máximo para o campo.

Nas APIs data driven, essas anotações fazem boa parte do trabalho que um domínio rico faria.

Além disso, o parâmetro ErrorMessage personaliza a mensagem que o cliente recebe quando a validação falha. Dessa forma, o front-end consegue exibir um retorno claro para o usuário.

Criando o DataContext da API data driven

O DataContext é a representação do banco de dados dentro da aplicação. Por meio dele, temos acesso às operações de criação, leitura, atualização e exclusão, entre outras interações com a fonte de dados.

Em suma, tudo o que precisamos para trabalhar com o Entity Framework está nesse arquivo. É nele que ficam as configurações de como a aplicação interage com o banco.

Em APIs data driven, o contexto costuma ser o centro de tudo. Na prática, os controllers conversam diretamente com ele, sem repositórios ou serviços intermediários.

Um DataContext nada mais é do que uma classe que herda de DbContext e contém as coleções (DbSet) que manipulamos no banco. Então, crie uma pasta chamada Data na raiz da aplicação e adicione o arquivo DataContext.cs:

using Microsoft.EntityFrameworkCore;
using Shop.Models;

namespace Shop.Data
{
    public class DataContext : DbContext
    {
        // As opções chegam pela injeção de dependência
        public DataContext(DbContextOptions<DataContext> options)
            : base(options)
        {
        }

        public DbSet<Product> Products { get; set; }
        public DbSet<Category> Categories { get; set; }
    }
}

O construtor que recebe DbContextOptions<DataContext> é essencial. É por ele que a configuração definida no Startup chega até o contexto.

Como não vamos usar o SQL Server aqui, não precisamos configurar uma connection string. No entanto, ao trocar para um banco real, essa string entra no appsettings.json.

Registrando o contexto no Startup

Por fim, precisamos informar à aplicação que usamos o EF Core, um banco em memória e o nosso DataContext. Fazemos isso no método ConfigureServices do arquivo Startup.cs, com o método AddDbContext.

O ConfigureServices configura os serviços que a API utiliza, como autenticação, logs e banco de dados. Assim, basta chamar o AddDbContext, informar o contexto e dizer, nas opções, que queremos um banco em memória:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shop.Data;

namespace Shop
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            // Banco em memória chamado "Database"
            services.AddDbContext<DataContext>(opt => opt.UseInMemoryDatabase("Database"));
            services.AddControllers();
        }

        // ...
    }
}

Ao usar um banco em memória, precisamos dar um nome a ele. Neste caso, chamei de "Database", mas qualquer nome funciona.

Quando você migrar para o SQL Server, basta trocar o UseInMemoryDatabase pelo UseSqlServer com a connection string. O restante das APIs data driven continua igual, porque o contexto isola essa decisão.

Injeção de dependência nas APIs data driven

A injeção de dependência (DI, de Dependency Injection) não é um assunto básico. Mesmo assim, dá para resumi-la aqui da seguinte forma.

Ao trabalhar com bancos relacionais, não ficamos conectados o tempo todo, porque o banco tem um limite de conexões. Se você esquecer uma conexão aberta, por exemplo, terá problemas graves em pouco tempo de uso.

No modelo web, o front-end também não fica conectado à API. Cada informação que o cliente precisa gera uma nova requisição, que se conecta ao banco, recupera ou grava as informações e devolve o resultado em JSON.

O custo de abrir conexões

No .NET, temos o using, que garante o descarte de um objeto logo após o uso. Porém, o using trabalha em blocos, então várias chamadas ao banco significam conectar e desconectar diversas vezes.

Cada conexão com a API ou com o banco tem um custo mínimo. Afinal, há a abertura, o estabelecimento de um canal seguro, a transferência e a conferência dos dados e, por fim, o fechamento.

Portanto, quanto menos idas desnecessárias ao banco fizermos, mais rápida fica a API data driven. Mas, se não podemos usar o using em todo lugar nem correr o risco de deixar conexões abertas, o que fazer?

Um contexto por requisição

A solução é criar um objeto do DataContext a cada requisição à API. Inicialmente, ele não se conecta ao banco, apenas existe na memória.

Quando a requisição precisa executar uma operação no banco, o contexto já está pronto. Então, o EF Core abre a conexão quando necessário e executa a instrução desejada.

Pouco depois, ainda na mesma requisição, precisamos ir ao banco de novo. Adivinha?

O mesmo contexto continua disponível para nos atender.

Ao terminar a requisição, o ASP.NET Core descarta esse objeto e libera os recursos associados. Tudo isso acontece sem a nossa intervenção, graças ao recurso de injeção de dependência do framework.

Declarando e resolvendo a dependência

Injetar dependência significa deixar explícito que dependemos de algo para trabalhar. Por exemplo, para criar um produto, precisamos do DataContext, afinal ele é a ponte entre a API e o banco.

Em outras palavras, o controller diz: "Para cadastrar um produto, eu preciso do DataContext; como você vai me prover ele, eu não sei".

No código, basta declarar o objeto no construtor da classe:

public class ProductController
{
    // O ASP.NET Core entrega o contexto pronto para uso
    public ProductController(DataContext context) { /* ... */ }
}

Assim, criamos uma dependência do DataContext no ProductController, o que faz todo sentido em APIs data driven. Sem um contexto, não conseguimos fazer nenhuma operação de produto no banco.

Uma coisa que você aprende em orientação a objetos é que devemos depender de abstrações, e não de implementações. O curso 1975 do balta.io aprofunda esse tema.

Agora, precisamos resolver a dependência, e isso também acontece no Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<DataContext>(opt => opt.UseInMemoryDatabase("Database"));
    // Um DataContext por requisição
    services.AddScoped<DataContext, DataContext>();
    services.AddControllers();
}

A linha com AddScoped resolve a dependência do contexto por requisição. Na prática, segundo a documentação do EF Core, o próprio AddDbContext já registra o contexto com esse tempo de vida, então a linha deixa a intenção explícita, mas não muda o comportamento.

Para saber mais sobre o tema, veja o artigo sobre injeção de dependência no ASP.NET Core.

Criando os controllers da API data driven

O controller responde às requisições da API. Esse nome vem do padrão MVC (Models, Views, Controllers), muito comum no ASP.NET.

Um controller é uma classe que herda de ControllerBase e recebe atributos de rotas e de verbos HTTP. Para criar o primeiro, adicione uma pasta Controllers na raiz e, dentro dela, o arquivo CategoryController.cs, já que o sufixo Controller é uma convenção:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Shop.Controllers
{
    [ApiController]
    [Route("v1/categories")]
    public class CategoryController : ControllerBase
    {
        // ...
    }
}

Primeiro, herdamos de ControllerBase. Em seguida, adicionamos os atributos ApiController e Route.

O ApiController indica que o controller é uma API, lembrando que o ASP.NET não serve apenas para APIs. Além disso, ele ativa a validação automática: quando o modelo é inválido, a API devolve um HTTP 400 sem que você precise checar o ModelState.

Já o Route define a URL que leva até o controller da API data driven. Eu costumo colocar sempre a versão na rota, o que permite versionar os endpoints sem quebrar o front-end no futuro.

Listando as categorias na API data driven

Para listar as categorias, precisamos ir ao banco e, portanto, precisamos do DataContext. Como vimos na seção de injeção de dependência, uma opção é recebê-lo no construtor:

using Microsoft.AspNetCore.Mvc;
using Shop.Data;

namespace Shop.Controllers
{
    [ApiController]
    [Route("v1/categories")]
    public class CategoryController : ControllerBase
    {
        private readonly DataContext _context;

        public CategoryController(DataContext context)
        {
            _context = context;
        }
    }
}

Porém, existe uma forma ainda mais simples. O ASP.NET Core oferece o atributo FromServices, que injeta um objeto já registrado diretamente no parâmetro do método.

Dessa forma, o método fica assim:

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Shop.Data;
using Shop.Models;

// ...

[HttpGet]
[Route("")]
public async Task<ActionResult<List<Category>>> Get([FromServices] DataContext context)
{
    // Lê todas as categorias de forma assíncrona
    var categories = await context.Categories.ToListAsync();
    return categories;
}

Para ler os dados na API data driven, basta chamar o contexto seguido da coleção. Como usamos métodos assíncronos, não podemos esquecer do await.

Note também que decoramos o método com HttpGet e deixamos a rota vazia. Para testar, rode a aplicação com dotnet run e acesse https://localhost:5001/v1/categories.

Como o banco começa vazio, a resposta inicial é uma lista vazia em JSON.

Repare que o endpoint inteiro cabe em poucas linhas, e essa é justamente a proposta das APIs data driven. Na próxima parte, vamos criar os métodos de cadastro para popular esses dados.

Conclusão sobre APIs data driven

Por enquanto é isso. Nesta primeira parte, vimos como criar APIs data driven com ASP.NET Core 3.0, configurar o EF Core com um banco em memória, montar o DataContext e aplicar a injeção de dependência.

Também criamos o primeiro controller e o endpoint de listagem de categorias. No próximo artigo, vamos terminar os métodos restantes do CRUD.

Lembre-se de que APIs data driven não substituem o DDD. Na verdade, elas complementam o design de domínio nas partes do sistema que não têm regras de negócio relevantes.

FAQ

O que são APIs data driven?

São APIs centradas nos dados, que oferecem operações de CRUD praticamente diretas sobre o banco. Elas funcionam bem em cadastros simples, sem regras de negócio complexas.

Quando usar data driven em vez de DDD?

Use data driven quando a funcionalidade não tem regras de negócio relevantes. Para cálculos complexos e domínios ricos, o DDD continua sendo a melhor escolha.

Posso usar o banco em memória do EF Core em produção?

Não. A Microsoft deixa claro que o provedor em memória não foi projetado para produção e não suporta transações nem SQL puro.

Preciso chamar AddScoped depois do AddDbContext?

Não é obrigatório, porque o AddDbContext já registra o contexto com tempo de vida Scoped. A chamada extra apenas deixa a intenção explícita.

O que o atributo ApiController faz?

Ele exige rotas por atributo, infere a origem dos parâmetros e devolve automaticamente um HTTP 400 quando o modelo é inválido.

O .NET Core 3.0 ainda recebe suporte?

Não, o .NET Core 3.0 já chegou ao fim do suporte. O conteúdo continua útil para entender os conceitos, mas projetos novos devem usar uma versão suportada.