Cutting Edge

Serviços que podem ser consultados

Dino Esposito

Dino EspositoEstá ficando mais comum para as empresas expor seus serviços de negócios de back-end como pontos de extremidade HTTP antigos simples. Esse tipo de arquitetura não exige nenhuma informação sobre bancos de dados e modelos de dados físicos. Aplicativos clientes ainda não precisam de referências para bibliotecas específicas do banco de dados como o Entity Framework. A localização física dos serviços é irrelevante e você pode manter o back-end no local ou movê-lo de forma transparente para a nuvem.

Como um arquiteto de soluções ou desenvolvedor chefe, há dois cenários que você deve estar preparado para enfrentar ao adotar essa estratégia. O primeiro é quando você não tem nenhum acesso ao funcionamento interno dos serviços. Nesse caso, você ainda não está em uma condição de solicitar por mais ou menos dados para ajustar o desempenho do seu aplicativo cliente.

O segundo cenário é quando você também é responsável por manter esses serviços de back-end e pode influenciar a API pública até certo ponto. Neste artigo, me concentrarei principalmente no segundo cenário. Falarei sobre a função de tecnologias específicas para implementar os serviços que podem ser consultados de forma flexível. A tecnologia que usarei é serviços OData além da API da Web do ASP.NET. Quase tudo abordado neste artigo se aplica às plataformas existentes do ASP.NET e vNext do ASP. NET 5.

Serviços de back-end lacrados

Antes de entrar no design de serviço consultáveis, eu explorarei brevemente o primeiro cenário no qual você não tem controle sobre os serviços disponíveis. Você recebe todos os detalhes de que você precisa para fazer chamadas para esses serviços, mas não há como modificar a quantidade e a forma da resposta.

Esses serviços lacrados são lacrados por um motivo. Eles fazem parte do back-end de TI oficial da sua empresa. Esses serviços fazem parte da arquitetura geral e não vão mudar facilmente. Conforme mais aplicativos clientes dependem desses serviços, há uma probabilidade de que sua empresa esteja considerando o controle de versão. Em geral, porém, deve haver um motivo convincente antes de qualquer nova versão desses serviços ser implementada.

Se a API lacrada é um problema para o aplicativo cliente que você está desenvolvendo, a única coisa que você pode fazer é encapsular os serviços originais em uma camada adicional de proxy. Em seguida, você pode usar qualquer truque que serve para esse fim, inclusive armazenamento em cache, agregações de novos dados e inserir dados adicionais. Do ponto de vista arquitetônico, o conjunto resultante de serviços muda da camada de infraestrutura para a camada de serviços de domínio. Pode ser até mais alto na camada de aplicativo (consulte a Figura 1).

De serviços lacrados para serviços de aplicativos mais flexíveis
Figura 1 De serviços lacrados para serviços de aplicativos mais flexíveis

O lado de leitura de uma API

Aplicativos da Web modernos são criados ao redor de uma API interna. Em alguns casos, essa API se torna pública. É impressionante considerar que o ASP.NET 5 vNext envia uma arquitetura na qual o ASP.NET MVC e o mecanismo Razor fornecem a infraestrutura necessária para gerar exibições HTML.

A API da Web do ASP.NET representa a infraestrutura ideal para tratar as solicitações do cliente provenientes de clientes que não sejam navegadores e páginas HTML. Em outras palavras, um novo site do ASP.NET é o ideal desenvolvido como uma camada fina de HTML em torno de um conjunto possivelmente lacrado de serviços de back-end. A equipe responsável pelo aplicativo da Web, no entanto, agora também é o proprietário da API do back-end, em vez do consumidor. Se alguém tiver um problema com isso, você ouvirá suas reclamações ou sugestões.

A maioria dos problemas de API de consumidor estão relacionados à quantidade e qualidade dos dados retornados. O lado de consulta de uma API é geralmente mais complicado de criar porque nunca se sabe que forma os dados estão sendo solicitados e usados a longo prazo. O lado de comando de uma API é geralmente muito mais estável porque ele depende do domínio corporativo e serviços. Algumas vezes os serviços de domínio mudam, mas pelo menos eles mantêm um ritmo diferente e mais lento.

Geralmente, você tem um modelo por trás de uma API. O lado da consulta da API tende a refletir o modelo se a API tem um REST ou um tipo RPC. Por fim, o ponto fraco de uma API de leitura é o formato de dados que ela retorne e as agregações de dados que oferece suporte. Esse problema tem um nome amigável — DTOs (objetos de transferência de dados).

Ao criar um serviço de API, você o compila de um modelo de dados existente e o expõe para o mundo exterior através do mesmo modelo personalizado ou nativo. Durante anos, arquitetos de software desenvolveram aplicativos de uma maneira de baixo para cima. Eles sempre começam na parte inferior de um modelo de dados relacional normal. Esse modelo percorre até a camada de apresentação.

Dependendo das necessidades dos vários aplicativos cliente, algumas classes DTO foram criadas juntamente para garantir que a apresentação pode lidar com os dados certos no formato correto. Esse aspecto do desenvolvimento e arquitetura de software está mudando atualmente sob o efeito da função de cada vez maior de aplicativos cliente. Embora a criação do lado de comando de uma API de back-end seja um trabalho relativamente fácil, elaborar um modelo de dados único e suficientemente geral que atenda a todos os clientes possíveis é um trabalho muito mais difícil. A flexibilidade da API de leitura é um fator vencedor hoje porque nunca se sabe qual aplicativo cliente sua API encontrará.

Serviços que podem ser consultados

Na última edição do livro, "Microsoft .NET: Architecting Applications for the Enterprise" (Microsoft Press, 2014), eu e Andrea Saltarello formalizamos um conceito que chamamos de árvores de expressão em camadas (LET). A ideia por trás da LET é a camada de aplicativos e os serviços de domínio trocando objetos IQueryable<T> sempre que possível. Isso geralmente ocorre sempre que as camadas estão no mesmo espaço de processo e não exigem a serialização. Trocando IQueryable<T>, você pode adiar quaisquer resultados de consulta necessários da composição de filtro e projeção de dados para o último minuto. Você pode ter eles adaptados em uma base por aplicativo, em vez de alguma forma codificada de API de nível de domínio.

A ideia do LET vai lado a lado com o padrão CQRS emergente. Isso envia a separação da pilha de leitura de pilha de comando. A Figura 2 ilustra os pontos de ter um padrão LET e vários serviços que podem ser consultados em sua arquitetura.

Objetos que podem ser consultados são consultados no último minuto
Figura 2 Objetos que podem ser consultados são consultados no último minuto

A principal vantagem do LET é que não é necessário que qualquer DTOs transporte dados em camadas. Você ainda precisa ter classes de modelo de exibição em algum momento, mas essa é uma história diferente. Você precisa lidar com classes de modelo de exibição, contanto que tenha uma interface de usuário para preencher com dados. Classes de modelo de exibição expressam o layout de dados desejado que os usuários esperam. É o único conjunto de classes DTO que você vai ter. Todo o resto do nível no qual você pode consulta fisicamente por dados e até ser atendido através de referências IQueryable.

Outro benefício do LET e dos serviços que podem ser consultados é que as consultas resultantes são consultas de nível de aplicativo. Sua lógica segue muito a linguagem especializada do domínio. Isso facilita mapear requisitos para código e discutir problemas suspeitos ou mal-entendidos com os clientes. Na maioria das vezes, uma rápida olhada no código ajuda a explicar a lógica. Por exemplo, aqui está o que uma consulta LET pode se parecer:

var model = from i in db.Invoices
                        .ForBusinessUnit(buId)
                        .UnpaidInLast(30.Days)
  orderby i.PaymentDueDate
  select new UnpaidViewModel
    {
      ...
    };

No contexto do banco de dados de um objeto de raiz do Entity Framework, você consulta todas as faturas de entrada e seleciona aquelas relevantes para uma determinada unidade de negócios. Entre elas, você encontra aquelas ainda a pagar vários dias após o prazo de vencimento.

A boa notícia é que as referências IQueryable não são dados reais. A consulta é executada na fonte de dados somente quando você troca IQueryable por alguns IList. Os filtros que você adiciona ao longo do caminho são simplesmente cláusulas WHERE adicionadas à consulta real que está sendo executada em algum ponto. Enquanto você estiver no mesmo espaço de processo, a quantidade de dados transferidos e mantidos na memória é o mínimo necessário.

Como essa escalabilidade afeta? A tendência emergente para os quais a plataforma vNext foi otimizada é você manter seu back-end da Web o mais compacto possível. Idealmente, ele poderá ser uma única camada. Você pode obter escalabilidade, replicando a camada exclusiva através de uma variedade de funções da Web do Microsoft Azure. Ter uma única camada para o back-end da Web permite usar IQueryable em todos os lugares e economiza muitas classes DTO.

Implementar serviços que podem ser consultados

No trecho de código anterior, presumi que os serviços são implementados como uma camada em torno de um contexto de banco de dados do Entity Framework. Mas isso é apenas um exemplo. Você também pode encapsular totalmente o provedor de dados reais em uma fachada de API da Web do ASP.NET. Dessa forma, você tem a vantagem de uma API que expressa os recursos de serviços de domínio e ainda pode acessar via HTTP, desacoplar clientes de uma plataforma específica e tecnologia.

Em seguida, você pode criar uma biblioteca de classes da API da Web e hospedá-la em algum site ASP.NET MVC, serviço do Windows ou até mesmo um aplicativo de host personalizado. No projeto de API da Web, você pode criar classes do controlador que derivam do ApiController e expõe métodos que retornam IQueryable<T>. Por fim, você decora cada método IQueryable com o atributo EnableQuery. O atributo Queryable agora obsoleto também funciona. O principal fator é o atributo EnableQuery permitir adicionar consultas OData para a URL solicitada — algo assim:

[EnableQuery]
public IQueryable<Customer> Get()
{
  return (from c in db.Customers select c);
}

Essa parte básica do código representa o coração da sua API. Ele não retorna dados por si só. Ele permite que os clientes modelem quaisquer dados de retorno desejados. Examine o código na Figura 3 e considere como o código em um aplicativo cliente.

Figura 3 Aplicativos clientes pode moldar os dados retornados

var api = "api/customers?$select=LastName";
var request = new HttpRequestMessage()
{
  RequestUri = new Uri(api)),
    Method = HttpMethod.Get,
};
var client = new HttpClient();
var response = client.SendAsync(request).Result;
if (response.IsSuccessStatusCode)
{
  var list = await
    response.Content.ReadAsAsync<IEnumerable<Customer>>();
  // Build view model object here
}

A convenção $select na URL determina a projeção de dados que o cliente recebe. Você pode usar o poder da sintaxe de consulta OData para moldar a consulta. Para obter mais informações sobre isso, consulte bit.ly/15PVBXv.

Por exemplo, um cliente só pode solicitar um pequeno subconjunto de colunas. Um outro cliente ou uma tela diferente no mesmo cliente pode consultar uma parte maior de dados. Tudo isso pode acontecer sem tocar a API e criar toneladas de DTOs ao longo do caminho. Tudo o que você precisa é um serviço de API da Web que podem ser consultado OData e as classes de modelo de exibição final a serem consumidas. Dados transferidos são mantidos em um mínimo como somente campos filtrados são retornados.

Há alguns aspectos notáveis para isso. Primeiro, o OData é um protocolo detalhado e não pôde ser dada a sua função. Isso significa que quando você aplica uma projeção $select, a carga JSON ainda listará todos os campos no IQueryable original<T> (todos os campos da classe Customer original no método Get). No entanto, somente os campos especificados conterão um valor.

Outro ponto a considerar é a diferenciação de maiúsculas e minúsculas. Isso afeta o elemento de consulta $filter que você usar para adicionar uma cláusula WHERE à consulta. Você talvez queira chamar as funções do OData tolower ou toupper (se houver suporte pela biblioteca cliente OData que você está usando) para normalizar comparações entre cadeias de caracteres.

Conclusão

Francamente, nunca senti que o OData merecia uma consideração séria até que me encontrei — como proprietário de uma API de back-end — no meio de uma tempestade de solicitações de DTOs diferentes seja retornado do mesmo modelo de dados. Cada solicitação parecia legítima e pronta para uma causa muito nobre — melhoria de desempenho.

Em algum momento, pareceu que todos os clientes queriam fazer a "consulta" no back-end em grande parte da mesma maneira que a consulta de uma tabela de banco de dados regular. Em seguida, que atualizei o serviço de back-end para expor pontos de extremidade OData, atribuindo para cada cliente a flexibilidade para baixar somente os campos em que estava interessado.

O tipo T de cada método IQueryable é a chave. Ele pode ou não pode ser do mesmo tipo T que tiver no modelo físico. Pode corresponder a uma tabela de banco de dados simples ou resultado de agregações de dados feitas no lado do servidor e transparente para os clientes. Quando você aplica o OData, no entanto, você permite que os clientes consultem conjuntos de dados de uma única entidade conhecida, T, então por que não experimentar?


Dino Esposito é o co-autor do "Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014) e “Programming ASP.NET MVC 5” (Microsoft Press, 2014). Um evangelista técnico para as plataformas Microsoft .NET Framework e Android na JetBrains e palestrante frequente em eventos do setor em todo mundo, Esposito compartilha sua visão do software em software2cents.wordpress.com e no Twitter em twitter.com/despos.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Jon Arne Saeteras