OData com ASP.Net Core WebAPI

OData Logo

A criação de APIs hoje em dia é parte primordial do desenvolvimento de software, principalmente com o conceito Mobile First, que, embora seja mais voltado para UI/UX, também se aplica ao desenvolvimento Backend. Afinal de contas, a maioria dos projetos mobile vai consumir uma API.

Pesquisando as melhores práticas, é visto de maneira praticamente unânime que APIs devem ser RESTful, evitando verbos, utilizando mais substantivos. Isso se traduz em chamadas como

GET http://enderecoapi.com/api/Entidade

Ao invés de

GET http://enderecoapi.com/api/Entidade/RetornarTodas

Mas aí veio o desafio: como fazer consultas customizadas, mantendo a não-verbosidade da API e tendo flexibilidade na construção de métodos?

OData

De acordo com o site oficial, é um protocolo aberto, proposto pela Microsoft, e que define um conjunto de melhores práticas para a construção e consumo de APIs RESTful. É algo ousado, que se define como “a melhor maneira de usar REST” (e vai além com um trocadilho em inglês, afirmando ser “a melhor maneira de ‘descansar’”.

Em termos técnicos, o OData permite que façamos consultas em cima de um endereço que utiliza substantivo de maneira muito similar ao que já fazemos com SQL. Um exemplo disso, seria o retorno de pessoas com nome que contém a palavra ‘Maria’.

GET http://enderecoapi.com/api/People?$filter=contains(Name, 'Maria')

Isso tira a complexidade de consultas do Backend e leva ela pro frontend, permitindo que o frontend (mobile, web, ou qualquer outro cliente que consuma a API) possa definir o que irá retornar.

OData com ASP.Net Core

Nos meus estudos, criei um projeto de exemplo que mostra como habilitar o OData em WebAPIs construídas com ASP.Net Core. O projeto WebAPIOData pode ser acessado no meu GitHub, e o seguiremos para exemplo nesse artigo. Esse projeto expõe uma API para consulta de jogos (Games) e suas respectivas Plataformas (Platforms).

Pacotes NuGet e Startup.cs

Como o OData é uma proposta da própria Microsoft, seu apoio no ambiente .Net é total. Infelizmente, o apoio para o .Net Core está um pouco atrasado, mas temos um pacote NuGet oficial em beta (versão 7.0.0-beta2) que está em pleno desenvolvimento, o Microsoft.AspNetCore.OData.

Após a instalação desse pacote, é necessário configurarmos o arquivo Startup.cs para que habilite o OData.

public void ConfigureServices(IServiceCollection services)
{
services.AddOData();
services.AddSingleton(sp => new ODataUriResolver { EnableCaseInsensitive = true });
    services.AddMvc().AddJsonOptions(opt =>
{
if (opt.SerializerSettings.ContractResolver != null)
{
var resolver = opt.SerializerSettings.ContractResolver as DefaultContractResolver;
resolver.NamingStrategy = null;
}
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvc(ConfigureODataRoutes);
}
private static void ConfigureODataRoutes(IRouteBuilder routes)
{
var model = GetEdmModel();
routes.MapODataServiceRoute("ODataRoute", "odata", model);
routes.Filter(QueryOptionSetting.Allowed);
routes.OrderBy();
routes.Count();
routes.Select();
}
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
var games = builder.EntitySet<Game>("Games").EntityType;
games.ComplexProperty(y => y.Platform);

return builder.GetEdmModel();
}

Primeiro de tudo, é necessário habilitar o OData no método ConfigureServices (linha 3). Na outra linha, adicionamos o ODataUriResolver habilitando o Case Insensitive para URLs.

A chamada ao método AddJsonOptions se dá para que o JsonFormatter não altere o nome das propriedades de retorno e os mantenha em Upper Case.

No método Configure, é chamada a Action ConfigureODataRoutes, onde definimos a rota ODataRoute, com prefixo “odata” (o prefixo que será chamado na URL). Essa rota é baseada em um Model pré definido, sendo criado no método GetEdmModel. Tal método informa quais conjuntos de entidade (EntitySet) serão disponibilizados. Caso haja uma propriedade complexa (um subtipo agregado, como Platform, por exemplo), é necessário definir que ela também será retornada.

As outras definições (Filter, OrderBy, Count e Select) são informadas para que as respectivas operações sejam suportadas.

Arquivo GamesController.cs

[EnableQuery(PageSize = 20)]
[Route("api/[controller]")]
public class GamesController : Controller
{
// GET odata/Games
[HttpGet]
public IQueryable<Game> Get()
{
return Games.AsQueryable();
}
    // GET odata/Games(5)
[HttpGet]
public Game Get(int key)
{
return Games.SingleOrDefault(x => x.Id == key);
}
    // POST odata/Games
[HttpPost]
public IActionResult Post([FromBody]Game value)
{
value.Id = Games.Max(g => g.Id) + 1;
value.Platform = Platforms.SingleOrDefault(p => p.Id == value.Platform.Id);
        Games.Add(value);
        return Created($"?key={value.Id}", value);
}
    // PUT odata/Games(5)
[HttpPut]
public IActionResult Put(int key, [FromBody]Game value)
{
var existingGame = Games.SingleOrDefault(x => x.Id == key);
Games.Remove(existingGame);
        value.Id = key;
value.Platform = Platforms.SingleOrDefault(p => p.Id == value.Platform.Id);
Games.Add(value);
        return Ok(existingGame);
}
    [HttpPatch]
public IActionResult Patch(int key, [FromBody]JsonPatchDocument<Game> valuePatch)
{
var existingGame = Games.SingleOrDefault(x => x.Id == key);
Games.Remove(existingGame);
        valuePatch.ApplyTo(existingGame);
Games.Add(existingGame);
        return Ok(existingGame);
}
    // DELETE odata/Games(5)
[HttpDelete]
public IActionResult Delete(int key)
{
var existingGame = Games.SingleOrDefault(x => x.Id == key);
Games.Remove(existingGame);
        return Ok(key);
}
    private static Platform GetPlatform(int id)
{
return Platforms.SingleOrDefault(x => x.Id == id);
}
}

No arquivo GamesController.cs temos as actions que serão disponibilizadas pela API. Vale notar o atributo [EnableQuery], definindo o PageSize = 20. Isso permite que a paginação já venha habilitada por padrão.

Os métodos que utilizam Id (GET/Id, PUT e PATCH) possuem uma peculiaridade. É necessário substituir o nome de parâmetro ‘id’ (padrão do WebAPI) por ‘key’, caso contrário o OData não reconhece a Action.

Executando chamadas à API

Rodando o projeto, é possível retornar todos os jogos através do endereço

http://localhost:53760/odata/Games

Até aí nenhuma novidade, mas vale prestar atenção no JSON de retorno

{
"@odata.context": "http://localhost:53760/odata/$metadata#Games",
"value": [
{
"Id": 1,
"Name": "The Legend of Zelda: Breath of the Wild",
"Genre": "ActionAdventure",
"Year": 2017,
"Platform": {
"Id": 2,
"Name": "Switch"
}
},
{
"Id": 2,
"Name": "Persona 4 Golden",
"Genre": "RPG",
"Year": 2013,
"Platform": {
"Id": 3,
"Name": "PSVita"
}
},
{
"Id": 3,
"Name": "Destiny",
"Genre": "FPS",
"Year": 2014,
"Platform": {
"Id": 1,
"Name": "PS4"
}
},
{
"Id": 4,
"Name": "Need for Speed: Most Wanted",
"Genre": "Sport",
"Year": 2005,
"Platform": {
"Id": 4,
"Name": "PC"
}
}
]
}

Na linha 2 agora temos a chave @odata.context com um link para metadados de Games. Seguindo esse link, é possível ver os metadados referentes à classe Game.

A coisa começa a melhorar quando aplicamos o atributo $filter na QueryString. Por exemplo, retornando os jogos que contém a palavra ‘Need’ em seu nome:

GET http://localhost:53760/odata/Games?$filter=contains(Name, 'Need')

Devido ao uso de IQueryable no retorno da consulta, o ASP.Net Core faz uma compilação de atributos de pesquisa (os do OData e qualquer um que você passe no código), e mostra o resultado indo apenas uma vez ao banco, por isso não é necessário se preocupar com uma chamada extra.

Mais exemplos de chamada GET e demais verbos, podem ser encontrados na documentação do OData e no GitHub do Projeto WebAPIOData.