ASP.NET Core - Usando OData no .NET 5.0


 Neste artigo veremos como usar os recursos do OData em APIs ASP .NET Core com o NET 5.0.

A partir da ASP .NET Core 2.0 foi disponiblizado o suporte a OData (Open Data Protocol) como um pacote nuget permitindo a criação de endpoints com OData v4.0 em múltiplas plataformas.

Mas o que é OData ?

O OData (Open Data Protocol) é um padrão OASIS aprovado pela ISO/IEC que define um conjunto de práticas recomendadas para criar e consumir Web APIs.

A implementação dos padrões OData torna mais fácil consumir uma API pelos seus clientes e permite a criação de consultas flexíveis e legíveis por meio das convenções da URL do OData.

Usando as convenções da URL OData, você pode expor uma API muito mais limpa e genérica e permitir que o cliente da API especifique suas necessidades por meio de requisições.

Assim o OData é um protocolo aberto que permite a criação e consumo de APIs RESTful consultáveis e interoperáveis de forma simples e padronizada. Ele descreve coisas como qual método HTTP usar para qual tipo de solicitação, quais códigos de status retornar quando e também convenções de URL. Inclui informações sobre como consultar dados - filtragem e paginação, mas também como chamar funções e ações personalizadas e muito mais.

A seguir temos algumas opções de consultas OData que podemos usar com Web API da ASP .NET Core:

A seguir veremos um exemplo prático do uso de OData em uma API ASP .NET Core.

Recursos usados:

Criando a API ASP .NET Core

Abra o VS 2019 e clique em New Project e selecione o template ASP .NET Core Web API e clique em Next;

Informe o nome ApiOData e clique em Next;

A seguir selecione o Target Framework, Authentication Type e demais configurações conforme mostrada na figura:

Não vamos usar o Swagger neste projeto.

Clique em Create.

Vamos incluir no projeto os seguintes pacotes Nuget:

Isso pode ser feito via menu Tools-> Manage Nuget Packages for Solution, selecionando a guia Browse e informando o nome do pacote ou digitando na janela do Console Package Manager:

Crie uma pasta Models no projeto e nesta pasta crie a classe Cliente:

    public class Cliente
    {
        public int Id { get; set; }
        public string Nome { get; set; }
        public string Email { get; set; }
        public string Cidade { get; set; }
        public int Idade { get; set; }
    }

A seguir crie uma pasta Services e nesta pasta crie a classe ClienteService:

using APIOData.Models;
using System.Collections.Generic;
using System.Linq;

namespace APIOData.Services
{
    public class ClienteService
    {
        public List<Cliente> CriaDados()
        {
            List<Cliente> listaClientes = new(); // C# 9 

            listaClientes.Add(new Cliente { Id = 1, Nome = "Maria Silveira", Email = "maria@email.com", Cidade = "Santos", Idade = 22 });
            listaClientes.Add(new Cliente { Id = 2, Nome = "Carina Sanches", Email = "carina@email.com", Cidade = "São Paulo", Idade = 27 });
            listaClientes.Add(new Cliente { Id = 3, Nome = "Benedito Juarez", Email = "bene@email.com", Cidade = "Limeira", Idade = 34 });
            listaClientes.Add(new Cliente { Id = 4, Nome = "Pedro Altivo", Email = "pedro@email.com", Cidade = "Campinas", Idade = 43 });
            listaClientes.Add(new Cliente { Id = 5, Nome = "Norberto Santos", Email = "norba@email.com", Cidade = "Lins", Idade = 21 });
            listaClientes.Add(new Cliente { Id = 6, Nome = "Yuri Rodrigues", Email = "yuri@email.com", Cidade = "Bauru", Idade = 18 });
            listaClientes.Add(new Cliente { Id = 7, Nome = "Paulo Sabino", Email = "paulo@email.com", Cidade = "São Paulo", Idade = 54 });
            listaClientes.Add(new Cliente { Id = 8, Nome = "Jose Macoratti", Email = "jose@email.com", Cidade = "Campinas", Idade = 22 });
            listaClientes.Add(new Cliente { Id = 9, Nome = "Ana Limeira", Email = "ana@email.com", Cidade = "Lins", Idade = 33 });
            listaClientes.Add(new Cliente { Id = 10, Nome = "Marta Ribeiro", Email = "marta@email.com", Cidade = "Santos", Idade = 41 });
            listaClientes.Add(new Cliente { Id = 11, Nome = "Carlos Duarte", Email = "carlos@email.com", Cidade = "Catanduva", Idade = 61 });
            listaClientes.Add(new Cliente { Id = 12, Nome = "Samuel Somero ", Email = "samuel@email.com", Cidade = "Tanabi", Idade = 55 });
            listaClientes.Add(new Cliente { Id = 13, Nome = "João Bueno", Email = "joao@email.com", Cidade = "Sorocaba", Idade = 48 });
            listaClientes.Add(new Cliente { Id = 14, Nome = "Elisa Silva", Email = "elisa@email.com", Cidade = "Limeira", Idade = 68 });
            listaClientes.Add(new Cliente { Id = 15, Nome = "Nilda Almeida", Email = "nilda@email.com", Cidade = "Barueri", Idade = 25 });
            return listaClientes;
        }

        public List<Cliente> GetClientes() => CriaDados().ToList();
    }
}

Neste serviço estamos definindo alguns dados para poder usar nas consultas.

Registrando os serviços e o endpoint OData

No método ConfigureServices da classe Startup vamos registrar o serviço OData , o serviço ClienteService e o serviço do NewtonsoftJson:]

public void ConfigureServices(IServiceCollection services)
{
            services.AddControllers().AddNewtonsoftJson();
         
  services.AddControllers();
            services.AddScoped<ClienteService>();
            services.AddOData();

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "APIOData", Version = "v1" });
            });
 }
...

A seguir precisamos definir quais as convenções vamos permitir para criar as consultas customizadas que desejamos realizar em nossa API de forma a poder usar a API já pronta sem ter que alterar nada no seu código.

No método Configure vamos habilitar a injeção de dependência para suportar rotas HTTP e a seguir definir quais opções de consultas desejamos habilitar em nossas rotas.

No exemplo estamos habilitando as opções de consulta:  Expand(), Select(), Count(), OrderBy() e Filter().

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

      app.UseEndpoints(endpoints =>
      {
                endpoints.EnableDependencyInjection();
                endpoints.Select().Count().Filter().OrderBy().MaxTop(100).SkipToken().Expand();

                endpoints.MapControllers();
                endpoints.MapControllers();
       });
}
...

Com isso temos tudo pronto para usar o OData na API.

Criando a API ClientesController

Vamos criar o controlador ClientesController na pasta Controllers que representa a nossa API:

using APIOData.Services;
using Microsoft.AspNet.OData;
using Microsoft.AspNetCore.Mvc;

namespace APIOData.Controllers
{
    [Route("api/[controller]")]
    [ApiController]

    public class ClientesController : ControllerBase
    {
        private readonly ClienteService _clienteService;

        public ClientesController(ClienteService clienteService)
        {
            _clienteService = clienteService;
        }

        [HttpGet(nameof(GetData))]
        [EnableQuery]
        public IActionResult GetData() => Ok(_clienteService.GetClientes());
    }
}

O atributo [EnableQuery] permite que os clientes modifiquem a consulta usando opções de consulta, como $Filter, $Select e $Expand, etc. 

Testando a API usando OData

Vamos executar a API e podemos usar o Postman ou o navegador para realizar consultas.

1- Acessando o endpoint : api/clientes teremos a exibição da lista de clientes

2- https://localhost:44330/api/clientes?$select=id,nome ($select)

2- https://localhost:44330/api/clientes?$select=id,nome&$skip=2 ($skip)

2- https://localhost:44330/api/clientes?$select=id,nome&$top=2 ($top)

2- https://localhost:44330/api/clientes?$select=id,nome,idade&$top=5&$filter=idade gt 50 ($filter)

Filtrando para exibir clientes com idade maior que (gt) 50.

Os operadores padrão que podemos usar com $filter são:

Operator Description Example
Comparison Operators
eq Equal $filter=valor  eq 100000
ne Not Equal $filter=valor  ne 100000
gt Greater than $filter=valor  gt 100000
ge Greater than or equal $filter=valor  ge 100000
lt Less than $filter=valor  lt 100000
le Less than or equal $filter=valor  le 100000
Logical Operators
and Logical and $filter=valor  lt 100000 and valor  gt 2000
or Logical or $filter=contains(name,'(sample)') or contains(nome,'teste')
not Logical not $filter=not contains(name,'sample')
Grouping Operators
() Precedence grouping (contains(name,'sample') or contains(name,'test')) and valor  gt 5000

2- https://localhost:44330/api/clientes?$select=id,nome,idade&$top=5&$filter=startswith(Nome,'P')

Usando $filter com startswith : Filtrar pelos nomes iniciando com a letra 'P'

2- https://localhost:44330/api/clientes?$select=id,nome,idade&$top=5&$orderby=nome desc  ($orderby)

Podemos inclusive realizar a paginação usando os recursos do OData e vamos mostrar isso na próxima parte do artigo.

Paginação com OData

Podemos criar um endpoint habilitado para realizar a paginação de dados, o que significa que se você tem muitos dados em um banco de dados, e o requisito é que o cliente precisa mostrar os dados como 10 registros por página, é aconselhável que o próprio servidor envie esses 10 registros por request para que toda a carga de dados não trafegue na rede,  e com isso estamos melhorando o desempenho dos serviços.

Suponha que você tenha 10.000 registros no banco de dados, então você pode habilitar seu endpoint para retornar 10 registros e considerar o request para o registro inicial e o número de registros a serem enviados. Nesse caso, o cliente fará uma solicitação toda vez que a opção de paginação de busca do próximo conjunto de registros for usada ou o usuário navegar para a próxima página.

Para habilitar a paginação, basta mencionar a contagem de páginas no atributo [Queryable].

Ex:   [EnableQuery(PageSize=5)]

Observe que informamos o pagesize no atributo indicando o tamanho da página.

 [HttpGet]
 [EnableQuery(PageSize = 5)]
  public IActionResult GetData() => Ok(_clienteService.GetClientes());

 

Além disso podemos também colocar restrições em suas opções de consulta. Suponha que você não queira que o cliente acesse opções de filtragem ou opções de ignorar; então, no nível de ação, você pode colocar restrições para ignorar esse tipo de request na API. 

As Query Option ou opções de restrições podem ser dos seguintes tipos:

  1. AllowedQueryOptions
  2. AllowedLogicalOperators
  3. AllowedArithmeticOperators

Pegue o projeto implementado aqui:   APIOData.zip (sem as referências)

"Que diremos, pois, a estas coisas? Se Deus é por nós, quem será contra nós?"
Romanos 8:31

Referências:


José Carlos Macoratti