Neste novo artigo continuo a série de dicas úteis para o dia a dia dos desenvolvedores ASP.NET Core. Caso não tenha consultado os dois primeiros artigos ou queira revê-los, acesse os links a seguir:
- ASP.NET Core: dicas úteis para o dia a dia de um desenvolvedor – Parte 01
- ASP.NET Core: dicas úteis para o dia a dia de um desenvolvedor – Parte 02
Removendo valores nulos do retorno de APIs REST
Ao utilizar bancos de dados relacionais, sobretudo bases de sistemas legados, é extremamente comum que diversos campos retornados por uma consulta apresentem conteúdo nulo.
Em se tratando do retorno de um volume grande dessas informações a partir de uma API REST, a presença de inúmeros campos nulos certamente produzirá um impacto na transmissão de dados.
Uma solução neste caso seria a remoção de tais valores nulos, contribuindo assim para uma redução no tamanho das respostas produzidas pela API considerada.
Já abordei esse tipo de prática no seguinte artigo:
Esse tipo de ajuste pode ser realizado a nível de projeto, através do método AddJsonOptions em ConfigureServices na classe Startup:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace APIProdutos
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(opcoes =>
{
opcoes.SerializerSettings.NullValueHandling =
Newtonsoft.Json.NullValueHandling.Ignore;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
E também pode ser usado através do tipo JsonResult (namespace Microsoft.AspNetCore.Mvc), configurando também aqui a propriedade NullValueHandling para que sejam ignorados valores nulos. É o que demonstra o método GetJsonProdutos na listagem a seguir:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using APIProdutos.Models;
using APIProdutos.Repository;
namespace APIProdutos.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProdutosController : ControllerBase
{
[HttpGet]
public ActionResult<IEnumerable<Produto>> Get()
{
return ProdutosRepository.Listar();
}
private JsonResult GetJsonProdutos()
{
return new JsonResult(
ProdutosRepository.Listar(),
new JsonSerializerSettings()
{
NullValueHandling = NullValueHandling.Ignore
}
);
}
[HttpGet("sem-nulos")]
public IActionResult GetRemocaoNulls()
{
return GetJsonProdutos();
}
[HttpGet("comprimir")]
[MiddlewareFilter(typeof(CompressaoGZipPipeline))]
public IActionResult GetComCompressao()
{
return GetJsonProdutos();
}
}
}
A seguir temos um resultado obtido ao empregarmos essa técnica (os valores foram registrados por meio do utilitário Fiddler), com uma redução bastante significativa no tamanho da resposta gerada:
Associando a execução de um middleware a uma Action ou Controller específico
Middlewares representam um dos pilares da arquitetura sob a qual o ASP.NET Core foi construído, permitindo adicionar novos comportamentos ou, até mesmo, customizar o funcionamento de uma aplicação baseada nessa plataforma.
O uso mais comum de uma estrutura desse tipo consiste na sua ativação a partir da classe Startup, como indicado no exemplo a seguir envolvendo uma chamada ao método UseResponseCompression:
using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace APIProdutos
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Configura o modo de compressão
services.Configure<GzipCompressionProviderOptions>(
options => options.Level = CompressionLevel.Optimal);
services.AddResponseCompression(options =>
{
options.Providers.Add<GzipCompressionProvider>();
options.EnableForHttps = true;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(opcoes =>
{
opcoes.SerializerSettings.NullValueHandling =
Newtonsoft.Json.NullValueHandling.Ignore;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
// Ativa a compressão
app.UseResponseCompression();
app.UseMvc();
}
}
}
Esse ajuste ativará a compressão para todas as respostas produzidas pela aplicação (uma API REST neste exemplo), empregando, para isso, o padrão GZip. Mas e se precisássemos aplicar essa técnica apenas a um Controller ou Action específica dentro do projeto em questão?
A resposta para esta pergunta passa pela codificação de uma classe com um método chamado Configure, com este último recebendo como parâmetro uma instância do tipo IApplicationBuilder (namespace Microsoft.AspNetCore.Builder) e acionando a partir desta referência o método responsável pela ativação de um middleware.
Na próxima listagem temos um exemplo dessa implementação envolvendo o tipo CompressaoGZipPipeline, o qual invocará em Configure o método UseResponseCompression. Importante destacar que o middleware desejado não deverá ser acionado a partir da classe Startup.
using System.IO.Compression;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.DependencyInjection;
namespace APIProdutos
{
public class CompressaoGZipPipeline
{
public void Configure(IApplicationBuilder applicationBuilder)
{
applicationBuilder.UseResponseCompression();
}
}
}
A Action ou Controller em que será aplicado o middleware precisará ser marcada com o atributo MiddlewareFilter (CompressaoGZipPipeline), recebendo como parâmetro a classe definida no passo anterior. É o que demonstra a listagem a seguir, em que MiddlewareFilter foi associado ao método GetComCompressao:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using APIProdutos.Models;
using APIProdutos.Repository;
namespace APIProdutos.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProdutosController : ControllerBase
{
[HttpGet]
public ActionResult<IEnumerable<Produto>> Get()
{
return ProdutosRepository.Listar();
}
private JsonResult GetJsonProdutos()
{
return new JsonResult(
ProdutosRepository.Listar(),
new JsonSerializerSettings()
{
NullValueHandling = NullValueHandling.Ignore
}
);
}
[HttpGet("sem-nulos")]
public IActionResult GetRemocaoNulls()
{
return GetJsonProdutos();
}
[HttpGet("comprimir")]
[MiddlewareFilter(typeof(CompressaoGZipPipeline))]
public IActionResult GetComCompressao()
{
return GetJsonProdutos();
}
}
}
Outros exemplos envolvendo a utilização e, até mesmo a implementação de middlewares customizados podem ser encontrados na seguinte gravação de um evento online recente do Canal .NET:
Utilizando ActionResult<T> no retorno de APIs REST
Uma das novidades integrando o ASP.NET Core 2.1 foi a classe ActionResult<T>: essa estrutura simplifica a implementação de Actions em APIs REST permitindo tanto o retorno de um objeto, quanto de uma mensagem de erro sem que isso implique na necessidade de transformações adicionais.
Antes desse recurso seríamos obrigados a empregar a interface IActionResult, precisando ainda transformar um resultado por meio de uma instância do tipo ObjectResult:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using APITemperaturas.Models;
namespace APITemperaturas.Controllers
{
[Route("api/[controller]")]
public class ConversorTemperaturasController : ControllerBase
{
[HttpGet("Fahrenheit/{temperatura}")]
public IActionResult GetConversaoFahrenheit(double temperatura)
{
if (temperatura < -459.67)
return BadRequest(new { MensagemErro = "Temperatura inválida!" });
Temperatura dados = new Temperatura();
dados.ValorFahrenheit = temperatura;
dados.ValorCelsius =
Math.Round((temperatura - 32.0) / 1.8, 2);
dados.ValorKelvin = dados.ValorCelsius + 273.15;
return new ObjectResult(dados);
}
}
}
Com ActionResult<T> essa transformação não é mais necessária, o que contribui para um código mais simplificado (como indicado na próxima listagem):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using APITemperaturas.Models;
namespace APITemperaturas.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ConversorTemperaturasController : ControllerBase
{
[HttpGet("Fahrenheit/{temperatura}")]
public ActionResult<Temperatura> GetConversaoFahrenheit(double temperatura)
{
if (temperatura < -459.67)
return BadRequest(new { MensagemErro = "Temperatura inválida!" });
Temperatura dados = new Temperatura();
dados.ValorFahrenheit = temperatura;
dados.ValorCelsius =
Math.Round((temperatura - 32.0) / 1.8, 2);
dados.ValorKelvin = dados.ValorCelsius + 273.15;
return dados;
}
}
}
Também abordei o uso de ActionResult<T> no seguinte artigo:
Retornando XML em APIs REST
Embora o formato JSON tenha se tornado um padrão de mercado, há ainda a possibilidade de surgimento de demandas relacionadas à integração com sistemas legados e que necessitem que as APIs REST envolvidas retornem suas respostas em XML.
No artigo a seguir demonstrei como habilitar esse padrão como retorno em APIs REST criadas com o ASP.NET Core:
Ativar a produção de respostas empregando XML requer que o método AddXmlSerializerFormatters seja acionado na classe Startup em ConfigureServices:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace APITemperaturas
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddXmlSerializerFormatters(); // Habilitando o uso de XML
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
Requisições enviadas à API devem possuir o header Accept com o seu valor configurado como application/xml:
Cache com Redis
Embora usual, implementações empregando cache em memória representam um sério problema durante a tentativa de se escalar uma aplicação Web. Requisições enviadas a diferentes instâncias do projeto, cada uma com seu próprio cache, resultarão em estados inválidos e consequentes falhas.
O Redis, solução NoSQL open source, é uma alternativa performática e capaz de solucionar com eficiência a questão do cache em cenários que envolvam escalabilidade. Funcionando como um repositório centralizado, a utilização do Redis pode ser facilmente habilitada em uma aplicação ASP.NET Core.
Para isso, será necessário adicionar o package Microsoft.Extensions.Caching.Redis ao projeto que fará uso do Redis, bem como acionar o método AddDistributedRedisCache a fim de configurar o acesso a uma instância deste NoSQL em ConfigureServices (classe Startup):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace SiteImagemNASA
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Ativando o uso de cache via Redis
services.AddDistributedRedisCache(options =>
{
options.Configuration =
Configuration.GetConnectionString("ConexaoRedis");
options.InstanceName = "SiteImagemNASA-";
});
// Configurando o client de acesso à API da NASA
services.AddHttpClient<APINasaClient>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMvc();
}
}
}
Instâncias de IDistributedCache (namespace Microsoft.Extensions.Caching.Distributed) poderão então ser obtidas via injeção de dependências, permitindo assim a interação com o servidor do Redis:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using SiteImagemNASA.Models;
namespace SiteImagemNASA.Pages
{
public class IndexModel : PageModel
{
public void OnGet(
[FromServices]IDistributedCache cache,
[FromServices]APINasaClient client)
{
ImagemNASA imagemNASA = null;
string valorJSON = cache.GetString("DadosImagemNASA");
if (valorJSON == null)
{
imagemNASA = client.ObterDadosImagem();
DistributedCacheEntryOptions opcoesCache =
new DistributedCacheEntryOptions();
opcoesCache.SetAbsoluteExpiration(
TimeSpan.FromMinutes(1));
valorJSON = JsonConvert.SerializeObject(imagemNASA);
cache.SetString("DadosImagemNASA", valorJSON, opcoesCache);
}
if (imagemNASA == null && valorJSON != null)
{
imagemNASA = JsonConvert
.DeserializeObject<ImagemNASA>(valorJSON);
}
TempData["ImagemNASA"] = imagemNASA;
}
}
}