Back-End

26 fev, 2019

ASP.NET Core: dicas úteis para o dia a dia de um desenvolvedor – Parte 03

Publicidade

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:

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;
        }
    }
}

Referências