Por que eu devo ler este artigo:As aplicações Monolíticas hoje em dia, em determinados projetos, tornam-se um tanto difíceis de serem mantidas, principalmente em grandes sistemas que precisam ter “partes móveis” e autônomas mantendo sua alta disponibilidade e facilitando também a sua escalabilidade seja ela vertical ou horizontal.

Este artigo é útil pois guiará o leitor, a partir de um exemplo prático, sobre como migrar uma aplicação monolítica para uma arquitetura de microserviços, mais flexível e escalável.


Guia do artigo:

Nesta série, temos uma aplicação monolítica de Controle de Contatos, desenvolvida em camadas com uma forte dependência entre si. Nessa aplicação, a Camada de Autenticação será transformada em uma API, reduzindo o seu acoplamento com a aplicação e permitindo que possa ser disponibilizada a terceiros.

O mesmo será feito com o controle dos contatos. A Camada de Apresentação então será totalmente desacoplada do sistema, de forma a se tornar mais autônoma e sem contrato algum com as camadas inferiores e isso irá mudar por completo a forma de interação do sistema, mas não mudará seu comportamento.

Uma aplicação monolítica (BOX 1), como a deste artigo, segue o padrão de outras aplicações: apenas em uma instância de servidor todas as camadas são publicadas juntas, pois são dependentes entre si.

Caso, por exemplo, a Camada de Negócio tenha algum problema, seja ele de performance, de quebra de código fonte ou erro fatal, o elo entre as camadas subsequentes é quebrado e isso causa a indisponibilidade da aplicação.

Além disso se tem a desvantagem de manter um código fonte repetitivo e trabalhoso causando um grande dispêndio, pois a equipe tende a trabalhar sempre em grandes correções e melhorias crescentes, aumentando assim o prazo para a entrega do produto completamente livre de erros.

Porém, aplicações monolíticas também têm suas vantagens: podem ser utilizadas em sistemas pequenos com pouca exigência de recursos, pois ela tem facilidade para build e o deploy da aplicação não requer muitos passos para colocá-la disponível no Servidor Web ou Servidor de Aplicação.

BOX 1. Aplicação Monolítica

O termo original é Aplicação com Arquitetura Monolítica e segundo Martin Fowler é uma aplicação onde há uma forte dependência entre seus módulos ou camadas. Quando um desses módulos para de funcionar, para toda a aplicação.

Tais módulos são disponibilizados em uma mesma instância no Servidor de Aplicação, não dando margem ao uso de Clusters ou balanceadores de carga, o que significa que quando cresce a demanda por ele, causa um grande ônus à infraestrutura e ao desenvolvimento de uma forma geral.

A aplicação que trataremos neste artigo e que será decomposta tem sua estrutura mostrada na Figura 1.

Arquitetura de Microserviços em .NET
Figura 1. Aplicação com Arquitetura Monolítica.

Motivação para a mudança de Arquitetura

A principal motivação para ir de uma arquitetura monolítica para uma arquitetura de Microserviços é a descentralização do código e a quebra da dependência, pois o microserviço abarca o acesso aos dados e a regra de negócio em um só lugar como um bloco.

Também é possível desenvolver um melhor redimensionamento da equipe de desenvolvedores, uma vez que com o microserviço podemos dividir a equipe por funcionalidades ou por contexto, como na Figura 2.

Arquitetura de Microserviços em .NET
Figura 2. Divisão da Equipes por Funcionalidade.

Normalmente, de acordo com o Scrum, uma Equipe Ágil precisa ter entre seis a 10 membros, logo, se nosso time de desenvolvimento tiver 20 profissionais, podemos dividir a equipe em dois e com isso trabalhar em duas sprints com funcionalidades distintas para um mesmo produto, que ao final da iteração é entregue funcionando e testável através de suas interfaces gráficas, por exemplo.

Em geral, quando tratamos de uma refatoração deste tamanho ou uma nova implementação, é uma boa prática se criar inicialmente uma sprint apenas de Spikes, onde a equipe irá verificar as necessidades de negócio e como isso será portado para microserviços.

Dessa forma a entrega do produto poderá seguir uma linha uniforme e as lições aprendidas poderão apoiar tanto o Product Owner quanto o Time no momento em que forem quantificar um trabalho, ou o Product Owner priorizar este ou aquele Backlog de produto ou História de Usuário.

Aplicação Monolítica de Exemplo

Nossa aplicação de exemplo é dividida em camadas e a persistência é feita pelo Entity Framework 6 com Fluent API, com tipos genéricos e interfaces seguindo todos os princípios SOLID. Na Figura 3 temos a estrutura dessa aplicação.

Arquitetura de Microserviços em .NET
Figura 3. Aplicação Monolítica.

Podemos notar nitidamente que as camadas aplicação têm forte dependência entre si. A ideia então é transformar essa aplicação de arquitetura monolítica em uma aplicação servida pela arquitetura de microserviços, e o resultado deve ficar como na Figura 4.

Arquitetura de Microserviços em .NET
Figura 4. Aplicação Monolítica aprimorada para Microserviços.

Diminuímos bastante a dependência entre as camadas. O que fizemos foi concentrar os Bounded Contexts de Contato e Autenticação em microserviços, ou seja, encapsulamos o acesso aos dados e expusemos as regras de negócio como serviços RESTFul através da WebAPI 2.2. Na Figura 5, demonstramos como é a estrutura de cada microserviço.

Arquitetura de Microserviços em .NET

Figura 5. Estrutura do Microserviço de exemplo.

Separando as Regras de Negócio para conversão

O que precisamos expor em nosso microserviço é justamente a camada que fica exposta para uso da Camada de Apresentação, ou seja, a Camada de Regra de Negócios, pois está entre a camada mais baixa, de Acesso a Dados, e a camada mais alta, a fronteira do sistema com o usuário. Na Tabela 1 temos todos os métodos que vamos transportar para o microserviço.

Nota: O código fonte completo da aplicação não será abordado neste artigo, mas encontra-se disponível para download no link de publicação desta revista.

Assinatura do Método

Pertence à Regra de Negócio

Descrição

CadastrarUsuario(UsuarioDto dto)

Autenticação

Cadastra os dados mais importantes do usuário como e-mail válido, senha e login.

AutenticarUsuario(UsuarioDto dto)

Autenticação

Efetua a operação de confrontar a senha digitada e convertida para o respectivo algoritmo é a mesma digitada em texto puro pelo usuário do sistema.

TrocarSenha(UsuarioDto dto)

Autenticação

Permite que o usuário troque a sua senha atual por outra.

BuscarUsuarioPorLogin(UsuarioDto dto)

Autenticação

Como o login é tão único quanto o e-mail, é interessante fazer a busca de um usuário por seu login.

CadastrarContato(ContatoDto dto)

Contato

Cadastra o contato juntamente com o Id do usuário logado no sistema.

AlterarContato(ContatoDto dto)

Contato

Modifica os dados do contato

ApagarContato(ContatoDto dto)

Contato

Exclui o contato da base de dados

ListarContatosDoUsuario(ContatoDto dto)

Contato

Busca todos os contatos cadastrados pelo usuário

ListarContatoPorId(ContatoDto)

Contato

Busca um contato por sua chave numérica.

Tabela 1. Métodos extraídos da aplicação monolítica para ser convertida em método REST.

Já separamos os métodos para expô-los via REST, agora vamos explicar o motivo da escolha da Arquitetura. Anteriormente nesse artigo, informamos o que utilizaremos no que tange a arquitetura, mas nas próximas seções vamos aprofundar o motivo dessa escolha.

Porque a Performance importa

Quando trabalhamos com a arquitetura de microserviços, precisamos criar um banco de dados para cada um, mesmo que seja necessária a duplicação das tabelas entre microserviços.

Microserviço não se conecta diretamente a outro microserviço, porém, é importante que haja uma certa integração, não no exemplo deste artigo, mas em outro caso como um microserviço de controle de estoque e um microserviço de checkout.

Existe a necessidade de as informações trafegarem entre esses dois microserviços e para fazer essa interligação utilizamos mensageria assíncrona, normalmente com o protocolo AMQP.

Porém, caso não precisemos integrar os microserviços com informações em tempo real, podemos utilizar o Quartz.NET para tarefas agendadas e atualizações durante períodos em que as requisições são menores, normalmente na parte da madrugada.

Essas abordagens ajudam muito no quesito performance, mas um outro detalhe é também utilizar programação paralela.

Na internet existem muitos artigos, em sua grande maioria, explicando como criar serviços através da programação paralela em .NET, porém, não é o escopo deste artigo, e deixamos o código fonte do artigo disponível para que o leitor faça o download e o refatore à vontade.

Outro quesito importante sobre performance é qual o framework de persistência que iremos utilizar e neste artigo vamos utilizar o ADO.NET, este framework é muito performático no que tange a acesso direto ao banco de dados, e nos microserviços a curva de refactoring é muito pequena, ou seja, não há a necessidade de se criar uma arquitetura muito grande para reuso, pois utilizamos as inserções, seleções e atualizações simples, sem algo muito complexo. O ADO.NET é nestes casos é uma excelente saída para a performance.

Web API 2.2

De acordo com a própria Microsoft, o protocolo HTTP não é apenas para operar páginas web. É também uma plataforma poderosa para construir APIs que exponham serviços e dados. HTTP é simples, flexível e único.

Quase todas as plataformas que existem têm uma biblioteca que manipule o HTTP, então serviços baseados em HTTP podem atingir uma maior gama de clientes, incluindo navegadores, celulares e as aplicações de desktop.

Diante disso, o ASP.NET Web API é um framework para construir APIs baseadas em Web com o .NET Framework. Do ponto de vista dos antigos Web Services ASMX e do WCF, a Web API se torna mais simples de ser manipulada, criamos um código fonte menor e conseguimos fazer muitas operações.

Como a Web API funciona não é o escopo deste artigo, para saber como que a Web API funciona, mas se o leitor tiver algum conhecimento de ASP.NET MVC, irá compreender os fundamentos da Web API.

Neste artigo não vamos utilizar os Models, pois de acordo com a Teoria da Arquitetura de Microserviços, faz-se necessária a passagem de objetos transitórios apenas para fazer o fluxo de entrada e saída, e com isso criaremos uma camada de DTOs (BOX 2) para que possamos receber e devolver os objetos.

BOX 2. DTO - Data Transfer Object

A tradução livre é Objeto para Transferência de Dados e é uma classe que normalmente é réplica de outra do Modelo de Domínio, contendo apenas suas propriedades e que atua, como o nome já diz, como entrada e saída, não tendo nenhum processamento, apenas recebendo os valores da Classe de Domínio original e transportando a outra classe ou camada.

Normalmente DTOs são utilizados na interação entre as Regras de Negócios e as Interfaces de Usuário (UI) e nos Microserviços têm a função de "universalizar" os valores que são passados.

Uma boa prática para compartilhar os DTOs e evitar problemas por causa de seu namespace é criar uma camada só de DTOs e compartilhar com o Cliente para que o mesmo possa fazer a integração com facilidade. O DTO não guarda nenhum comportamento, logo, pode ser compartilhado sem qualquer problema.

Criando a Solução e Estrutura

Criaremos agora uma solução no Visual Studio com um projeto do tipo Class Library e no diretório que é criado com ela, devemos criar um novo diretório chamado Bin, que é onde colocaremos o assemblies após a compilação dos nossos projetos.

Para que o compilador coloque os assemblies em um diretório diferente dos Debug e Release habituais, devemos inserir instruções nos Build Events da Class Library em questão.

Após a criação de qualquer Class Library, devemos digitar o seguinte comando dentro do campo Build Events, com mostra a Figura 6: Copy “$(TargetPath)” “$SolutionDir)bin” /y.

Arquitetura de Microserviços em .NET
Figura 6. Centralizando os Assemblies do Projeto.

Criando a Camada de DTOs

Vamos então criar a camada de DTO, que será disponibilizada aos clientes para facilitar a integração do sistema. Crie uma Class Library na solution com o nome de Dto no Namespace e Microservice.Dto no Assembly name. De acordo com os padrões de desenvolvimento, é uma boa prática criar uma classe em cada arquivo, na Listagem 1 está o código de todos os DTOs para fins didáticos.

Listagem 1. Camada de Dtos para Integração.

   public class AutenticacaoDto
   {

       public Nullable<int> IdUsuario { get; set; }
       public String Login { get; set; }
       public String Senha { get; set; }
       public String NovaSenha { get; set; }
       public String PerguntaSecreta { get; set; }
       public String RespostaSecreta { get; set; }
       public String Email { get; set; }

   }

 public class ContatoDto
   {
       public Nullable<int> Id { get; set; }
       public Nullable<int> IdUsuario { get; set; }
       public String Nome { get; set; }
       public String Email { get; set; }
       public Nullable<long> TelefoneFixo { get; set; }
       public Nullable<long> TelefoneCelular { get; set; }
       public Nullable<DateTime> DataDeCriacao { get; set; }
       public Nullable<DateTime> DataDeAlteracao { get; set; }
       public String Excecao;
   }

   public enum BloqueadoEnum
   {
       Nao = 0,
       Sim = 1
   }

   public class UsuarioDto
   {

       public Nullable<int> Id { get; set; }
       public String NomeCompleto { get; set; }
       public String PerguntaSecreta { get; set; }
       public String RespostaSecreta { get; set; }
       public String Senha { get; set; }
       public String Conta { get; set; }
       public BloqueadoEnum Bloqueado { get; set; }
       public Nullable<DateTime> DataDaUltimaTrocaDeSenha { get; set; }
       public Nullable<DateTime> DataDeAlteracao { get; set; }
       public Nullable<DateTime> DataDeCriacao { get; set; }
       public String Email { get; set; }
  47    }

Utilizamos Nullable nos Ids, como mostrado nas linhas 04, 16 e 36. Nullable é muito útil para assertivas em testes, pois tem a propriedade booleana HasValue e assim podemos montar um Assert(id.HasValue) que é muito mais limpo do que criar igualdades que possam realmente falhar, pois o teste nesse sentido é implementado para passar.

Vamos na próxima seção criar o serviço REST que irá interagir com o Dto e o Cliente.

Criando o Microserviço de Autenticação

Para criar nosso microserviço, precisamos criar um projeto ASP.NET MVC com um controller Web API. Neste Projeto precisamos instalar alguns pacotes via NuGet, e os pacotes selecionados são os da Figura 7.

Seus nomes são autoexplicativos e eles nos darão o suporte para que possamos utilizar a Web API sem quaisquer imprevistos.

Arquitetura de Microserviços em .NET
Figura 7. Pacotes instalados via NuGet.

Agora, criamos um controller Web API com o nome de AutenticacaoController e é aí que começamos a fazer com que o microserviço tome forma. Nos microserviços a conexão é feita diretamente no próprio método que é acessado via protocolo HTTP. Como foi dito anteriormente, estamos utilizando ADO.NET para efetuar as operações.

Criamos uma propriedade que recebe o valor da String de Conexão para que possamos utilizá-la em todos os métodos para a acesso ao banco de dados. Na Listagem 2 temos a criação dessa propriedade.

Listagem 2. Propriedade para String de Conexão.

  01 public class AutenticacaoController : ApiController
  02 {
  03     public String ConnectionString
  04     {
  05       get
  06        {
  07          return ConfigurationManager.ConnectionStrings["Autenticacao"].ConnectionString;
  08        }
  09      }     
  10 
  11   // Continuação...
  12 }

Devemos lembrar que o nome presente na linha 05 é o mesmo nome da string de conexão do arquivo app.config.

Vamos criar os métodos para persistência de nosso microserviço e vamos fazer um paralelo com o nosso sistema monolítico, começando pelo método de cadastro dos usuários, na Listagem 3.

Listagem 3. Cadastro de Usuário.

[HttpPost]
public HttpResponseMessage CadastrarUsuario([FromBody]UsuarioDto dto)
{

    //Para o rastreador
    Configuration.Services.GetTraceWriter().Info(
                 Request, "AutenticacaoController", "Cadastra um Usuário");

    HttpResponseMessage resposta;
    int id;
    IDbConnection conexao;
    IDbCommand comando;
    IDbDataParameter parametro;
    IDbTransaction transacao;
    StringBuilder query; 

    conexao = new SqlConnection(ConnectionString);

    conexao.Open();

    transacao = conexao.BeginTransaction();

    comando = conexao.CreateCommand();

    comando.Transaction = transacao;

    try
    {
          if (dto == null)
          {
                 throw new AutenticacaoException("Não é possível se passar um objeto vazio");
          }

          query = new StringBuilder();
          query.Append("INSERT INTO USUARIO (USUARIO,SENHA,EMAIL,NOMECOMPLETO,DATACRIACAO) ");
          query.AppendLine(" VALUES ");
          query.AppendLine("(@usuario,@senha,@email,@nomecompleto,@datacriacao);");
          query.AppendLine("SELECT CAST(scope_identity() AS int)");

          comando.CommandText = query.ToString();

          
          parametro = new SqlParameter("@usuario", dto.Conta);                
          comando.Parameters.Add(parametro);

          parametro = new SqlParameter("@senha", Hermes.GeraValorHash(dto.Senha, "SHA3", null));
          comando.Parameters.Add(parametro);

          parametro = new SqlParameter("@email", dto.Email);
          comando.Parameters.Add(parametro);
          parametro = new SqlParameter("@nomecompleto", dto.NomeCompleto);
          comando.Parameters.Add(parametro);

          parametro = new SqlParameter("@datacriacao", DateTime.Now);
          comando.Parameters.Add(parametro);

          id = (int)comando.ExecuteScalar();                                        
          transacao.Commit();

          dto.Id = id;
          dto.Senha = Hermes.GeraValorHash(dto.Senha, "SHA3", null);

          resposta = Request.CreateResponse<UsuarioDto>(HttpStatusCode.Created, dto);

          return resposta;
    }
    catch (Exception ex)
    {
          resposta = Request.CreateResponse<String>(HttpStatusCode.NotAcceptable, ex.Message);
          transacao.Rollback();
    }
    finally
    {
          conexao.Close();
    }

    return resposta;
}

Podemos notar que essa classe está ferindo já uma sugestão feita no padrão dos microserviços que solicita que cada microserviço deve ter no máximo 100 linhas de código. Porém, isso é uma recomendação, em sistemas mais complexos retiramos tal complexidade das camadas inferiores e os organizamos melhor em um microserviço. Mesmo assim conseguimos um melhor controle e melhor organização do projeto.

Na linha 01 utilizamos o attribute HttpPost pois não estamos utilizando prefixos que tenham o mesmo nome das ações do HTTP como Getxxx, Postxxx, Putxxx, Deletexxx.

Quando utilizamos quaisquer destes prefixos, automaticamente a Web API os identifica como tais ações do protocolo HTTP. O POST significa que vamos gravar os dados no destino onde essa requisição está sendo enviada, porém, não basta isso, pois também vamos precisar modificar a rota em seu lugar apropriado e será o que veremos mais adiante nesse artigo.

Na linha 02 passamos o attribute FromBody junto com o Dto como um parâmetro, isso significa que não passaremos um valor pela URL, mas sim dentro da requisição, ou seja, um dado encapsulado.

Mostraremos mais adiante essa abordagem quando efetuarmos testes com o microserviço.

Ainda na linha 02 utilizamos HttpResponseMessage, pois assim retornaremos os códigos numéricos de status do protocolo HTTP, que é o seu padrão, como retornar o código 200 para informar que a requisição foi executada com sucesso, 500 para erro interno do servidor ou 404 para não encontrado.

Porém, podemos utilizar os outros códigos caso precisemos, pois essa classe detém alguns códigos de saída padrão através de Enums, inclusive validação, se o cadastro do usuário foi feito com sucesso através do Enum Success, por exemplo.

Na linha 06 configuramos uma saída para que possamos ver o que está sendo feito dentro da Web API através do log na saída do Console. Para habilitar este rastreamento precisamos configurar na mesma classe responsável também pelo controle das rotas (veremos isso mais adiante no artigo).

Na linha 30 lançamos uma exceção personalizada para informar qual o erro que foi retornado. Diante disso, como o retorno é um valor numérico este será 500, porém, informará qual será o erro retornado e tipado com essa exceção personalizada.

O conteúdo dessa classe pode ser visto no código fonte deste artigo.

Na linha 37 informamos o Id que é retornado após o INSERT, isso pode ser útil caso precisemos devolver o DTO devidamente preenchido e com o novo Id retornado após a criação do usuário.

Na linha 45 criptografamos a senha utilizando métodos próprios, porém, criamos um Salt aleatório e o algoritmo de criptografia é o SHA3, que é o último algoritmo de criptografia seguro criado.

Para que possamos utilizá-lo, precisamos instalá-lo via NuGet, fazendo uma busca por SHA3 e instalar o pacote.

Na Listagem 4 mostramos como foi criado o método Autenticar, que é o que nos dará permissão para a entrada em qualquer sistema, pois faz o confronto da senha de texto puro com essa mesma senha transformada em hash.

Se a validação da senha retornar positivo, então o usuário teria o acesso ao sistema e o retorno desta é um DTO que retorna o código do usuário e outras informações que o cliente pode utilizar para gravar em sessão ou persistir de outra maneira.

Listagem 4. Método Autenticar.

[ResponseType(typeof(AutenticacaoDto))]
[HttpGet]
public AutenticacaoDto AutenticarUsuario([FromUri]String usuario, [FromUri]String senha)
{
    //Para o rastreador
    Configuration.Services.GetTraceWriter().Info(
                 Request, "AutenticacaoController", "Autenticando um Usuario");            

    Boolean autenticado;

    UsuarioDto usuarioChallenging = new UsuarioDto();

    usuarioChallenging = BuscarUsuarioPorLogin(usuario);

    //autenticado = Hermes.ValidaHashSenhaComSalt(senha, usuarioChallenging.Senha);
    autenticado = Hermes.ValidaSenha(senha, "SHA3", usuarioChallenging.Senha);


    if (autenticado)
    {               
          AutenticacaoDto dto = new AutenticacaoDto()
          {
                 Email = usuarioChallenging.Email,
                 Login = usuarioChallenging.Conta,
                 IdUsuario = usuarioChallenging.Id,
                 Senha = usuarioChallenging.Senha
          };

          return dto;

    } else {
          throw new AutenticacaoException("A senha do usuário " + usuario + " não confere!");
    }

    return null;
}

Na linha 1 temos um attribute ResponseType(typeof(AutenticacaoDto)) que informa à Web API que o objeto retornado ao executar esse método é um objeto AutenticacaoDTO.

A resposta para o cliente será esse objeto convertido em JSON e com os valores preenchidos, pois essa é a saída padrão da Web API, mas se quisermos utilizar outra saída como XML ou outro parser personalizado, a Web API também o permite.

A linha 02 mostra como qual o tipo de método HTTP a Web API irá tratar essa action. Utilizamos o GET pois precisamos consultar e retornar os valores e o HttpGet equivale à uma consulta retornando apenas um objeto ou uma lista deles.

Na linha 03 passamos os parâmetros de forma distinta da Listagem 3, pois utilizamos o attribute FromUri, o que significa que podemos passar esses parâmetros pela URL sem a necessidade de preencher algum objeto para envio.

Na linha 13 utilizamos um outro método dentro de nosso próprio microserviço para que possamos confrontar os valores recebidos.

Se a validação da linha 16 retornar positivo, então o objeto preenchido com alguns dados do usuário é retornado para tratamento.

Na Listagem 5 vemos a criação do método para a troca de senha.

Listagem 5. Método para a troca de senha.

[ResponseType(typeof(UsuarioDto))]
[HttpPut]
public HttpResponseMessage TrocarSenha([FromBody]AutenticacaoDto dto)
{

    //Para o rastreador
    Configuration.Services.GetTraceWriter().Info(
                 Request, "AutenticacaoController", "Trocando a senha");
    IDbConnection conexao;
    IDbCommand comando;
    IDbDataParameter parametro;
    IDbTransaction transacao;
    List<String> errosDeValidacao = new List<String>();
    HttpResponseMessage resposta;
    Boolean senhaAntigaValida;

    if (dto.IdUsuario == null)
    {
          errosDeValidacao.Add("O id do Usuário não pode ser vazio");
    }else if (dto.Login == null)
    {
          errosDeValidacao.Add("A conta do Usuário não pode ser vazia");
    }else if (dto.Login == null)
    {
          errosDeValidacao.Add("A conta do Usuário não pode ser vazia");
    }else if (dto.Senha == null)
    {
          errosDeValidacao.Add("A senha do Usuário não pode ser vazia");
    }if (dto.NovaSenha == null)
    {
          errosDeValidacao.Add("A nova senha do Usuário não pode ser vazia");
    }

    if (errosDeValidacao != null && errosDeValidacao.Count > 0)
    {
          StringBuilder builder = new StringBuilder();
          foreach (string linha in errosDeValidacao)
          {
                 builder.Append(linha).Append(", ");
          }

          string resultado = builder.ToString();

          throw new AutenticacaoException(resultado);
    }
    
    String senhaAntiga = BuscarUsuarioPorLogin(dto.Login).Senha;

    senhaAntigaValida = Hermes.ValidaSenha(dto.Senha,"SHA3",senhaAntiga);

    if (senhaAntigaValida)
    {

          conexao = new SqlConnection(ConnectionString);

          conexao.Open();

          transacao = conexao.BeginTransaction();

          comando = conexao.CreateCommand();

          comando.Transaction = transacao;

          try
          {
                 StringBuilder query = new StringBuilder();

                 query.Append(" UPDATE USUARIO ");
                 query.AppendLine(" SET SENHA = @novasenha, ");
                 query.AppendLine(" DATAULTIMATROCADESENHA = GETDATE() ");
                 query.AppendLine(" WHERE ");
                 query.AppendLine(" ID = @idusuario ");
                 query.AppendLine(" AND USUARIO = @conta ");
                 query.AppendLine(" AND SENHA = @senhaatual ");

                 comando.CommandText = query.ToString();

                 parametro = new SqlParameter("@novasenha", Hermes.GeraValorHash(dto.NovaSenha, "SHA3", null));
                 comando.Parameters.Add(parametro);

                 parametro = new SqlParameter("@idUsuario", dto.IdUsuario);
                 comando.Parameters.Add(parametro);

                 parametro = new SqlParameter("@conta", dto.Login);
                 comando.Parameters.Add(parametro);

                 parametro = new SqlParameter("@senhaatual", senhaAntiga);
                 comando.Parameters.Add(parametro);

                 comando.ExecuteNonQuery();

                 transacao.Commit();

                 resposta = Request.CreateResponse<AutenticacaoDto>(HttpStatusCode.OK, dto);

          }
          catch (Exception ex)
          {

                 transacao.Rollback();
                 resposta = Request.CreateResponse<String>(HttpStatusCode.NotAcceptable, ex.Message);

          }
          finally
          {
                 conexao.Close();
          }

          return resposta;

    }
    else
    {
          throw new AutenticacaoException("A senha antiga do usuário " + dto.Login + " não é valida!");
    }                   
 }

Como podemos ver, o uso do ADO.NET aumentou o número de linhas de código, mas manteve o acesso a dados bastante coeso e em um ponto único.

Também pode ser feita uma refatoração como forma de melhorar e diminuir a extensão do código, mas isso deixamos como desafio ao leitor.

Na Listagem 6 temos o último método deste contexto, o que faz a busca de um usuário por seu Login.

Listagem 6. Método de Busca de usuário por seu Login.

  [ResponseType(typeof(UsuarioDto))]
  [HttpGet]
  public UsuarioDto BuscarUsuarioPorLogin([FromUri]String login)
  {
      //Para o rastreador
      Configuration.Services.GetTraceWriter().Info(
                   Request, "AutenticacaoController", "Busca um usuário por Login");
 
      IDbConnection conexao;
      IDbCommand comando;
      IDbDataParameter parametro;                    
 
      conexao = new SqlConnection(ConnectionString);
      comando = new SqlCommand();
 
      comando.Connection = conexao;
 
      conexao.Open();
 
      StringBuilder query = new StringBuilder();
 
      query.Append("SELECT ],[USUARIO],[SENHA],[EMAIL],[NOMECOMPLETO],[BLOQUEADO],[DATACRIACAO],[DATAALTERACAO],[DATAULTIMATROCADESENHA] ");
      query.AppendLine("  FROM UsuarioDto ");
      query.AppendLine(" WHERE USUARIO = @usuario");
 
      parametro = new SqlParameter("@Usuario", login);                                      
 
      comando.Parameters.Add(parametro);                                 
 
      comando.CommandText = query.ToString();
 
      UsuarioDto usuario = new UsuarioDto();
 
      try
      {
            using (SqlDataReader leitorDeDados = (SqlDataReader)comando.ExecuteReader())
            {
                   while (leitorDeDados.Read())
                   {                                 
 
                          if (!leitorDeDados["ID"].Equals(DBNull.Value))
                                 usuario.Id = Convert.ToInt32(leitorDeDados["ID"].ToString());
 
                          if (!leitorDeDados["USUARIO"].Equals(DBNull.Value))
                                 usuario.Conta = leitorDeDados["USUARIO"].ToString();
 
                          if (!leitorDeDados["SENHA"].Equals(DBNull.Value))
                                 usuario.Senha = leitorDeDados["SENHA"].ToString();
 
                          if (!leitorDeDados["EMAIL"].Equals(DBNull.Value))
                                 usuario.Email = leitorDeDados["EMAIL"].ToString();
 
                          if (!leitorDeDados["NOMECOMPLETO"].Equals(DBNull.Value))
                                 usuario.NomeCompleto = leitorDeDados["NOMECOMPLETO"].ToString();
 
                          if (!leitorDeDados["BLOQUEADO"].Equals(DBNull.Value))
                                 usuario.Bloqueado = (BloqueadoEnum)Convert.ToInt32(leitorDeDados["BLOQUEADO"].ToString());
 
                          if (!leitorDeDados["DATACRIACAO"].Equals(DBNull.Value))
                                 usuario.DataDeCriacao = Convert.ToDateTime(leitorDeDados["DATACRIACAO"].ToString());
 
                          if (!leitorDeDados["DATAALTERACAO"].Equals(DBNull.Value))
                                 usuario.DataDeAlteracao = Convert.ToDateTime(leitorDeDados["DATAALTERACAO"].ToString());
 
                          if (!leitorDeDados["DATAULTIMATROCADESENHA"].Equals(DBNull.Value))
                                 usuario.DataDeCriacao = Convert.ToDateTime(leitorDeDados["DATAULTIMATROCADESENHA"].ToString());
            
                   }                  
            }
      }
      catch (Exception)
      {
 
            throw;
      }
      finally
      {
            conexao.Close();
      }
 
      return usuario;
  }  

Agora podemos configurar as rotas para que nosso serviço funcione corretamente. Se quisermos publicá-lo em nosso IIS, Microsoft Azure ou Microsoft Katana, ele irá publicar sem qualquer problema, mas no momento em que fizermos o teste de consumo, receberemos sempre o erro 404, dando a entender que o método não foi encontrado.

Diante disso, precisamos configurar uma nova rota. A forma é muito similar ao ASP.NET MVC, porém é uma outra classe, pois estamos na Web API, que se chama WebApiConfig em App_Start. A Listagem 7 nos mostra o conteúdo dessa classe.

Listagem 7. Conteúdo de WebApiConfig.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
          //Configurando o Trace para diagnóstico
          config.EnableSystemDiagnosticsTracing();

          SystemDiagnosticsTraceWriter traceWriter = config.EnableSystemDiagnosticsTracing();
          traceWriter.IsVerbose = true;
          traceWriter.MinimumLevel = TraceLevel.Debug;

          config.Services.Replace(typeof(ITraceWriter), new GoldenRetriever());

          /*
          config.Routes.MapHttpRoute(
                 name: "DefaultApi",
                 routeTemplate: "api/{controller}/{id}",
                 defaults: new { id = RouteParameter.Optional }
          );
          */
          config.Routes.MapHttpRoute(
                 name: "ApiComActions",
                 routeTemplate: "api/{controller}/{action}/{id}",
                 defaults: new { id = RouteParameter.Optional}
          );
    }
}

Nas linhas 06 até 10 temos a configuração do Logger para a console que vimos no início dos métodos do microserviço de autenticação.

Na linha 21 temos uma nova configuração de rota padrão. Esta nova configuração faz com que a nossa Uri fique da seguinte forma: http://dominio.com.br/api/autenticacao/BuscarUsuarioPorLogin?id=1, pois a variável {id} não será confundida com a variável Id da action BuscarUusarioPorLogin.

Criando o Microserviço para Controle de Contatos

Vamos então criar os microserviços de contatos, o mesmo é muito similar ao microserviço de autenticação, tanto nos pacotes instalados quanto na sua sintaxe, diferindo-se apenas por ter de adicionar o método HTTP DELETE quando vamos excluir um contato.

O procedimento de criação do mesmo é idêntico ao de autenticação, apenas criamos um controller com o nome de ContatoController. Na Listagem 8 criamos o método ou action para cadastro de usuários utilizando o método HTTP POST, pois estamos fazendo o envio e gravação no destino.

Listagem 8. Action para Cadastro de Usuário.

[HttpPost]
public HttpResponseMessage CadastrarContato([FromBody]ContatoDto dto)
{
    HttpResponseMessage resposta;
    int id;
    IDbConnection conexao;
    IDbCommand comando;
    IDbDataParameter parametro;
    IDbTransaction transacao;

    conexao = new SqlConnection(ConnectionString);

    conexao.Open();

    transacao = conexao.BeginTransaction();

    comando = conexao.CreateCommand();

    comando.Transaction = transacao;

    try
    {
          if (dto == null){
                 throw new ContatoException("Não é possível se passar um objeto vazio");
          }
          comando.CommandText = "INSERT INTO CONTATO(IdUsuario,Nome,Email,TelefoneFixo,TelefoneCelular) 
          VALUES(@IdUsuario, @Nome, @Email,@TelefoneFixo,@TelefoneCelular);SELECT CAST(scope_identity() AS int)";

          if (dto.IdUsuario.HasValue)
          {
                 parametro = new SqlParameter("@IdUsuario", dto.IdUsuario);
          }
          else
          {
                 parametro = new SqlParameter("@IdUsuario", DBNull.Value);
          }
                                     
          comando.Parameters.Add(parametro);

          if (!String.IsNullOrWhiteSpace(dto.Nome))
          {
                 parametro = new SqlParameter("@Nome", dto.Nome);
          }
          else
          {
                 parametro = new SqlParameter("@Nome", DBNull.Value);
          }
          
          comando.Parameters.Add(parametro);

          if (!String.IsNullOrWhiteSpace(dto.Email))
          {
                 parametro = new SqlParameter("@Email", dto.Email);
          }
          else
          {
                 parametro = new SqlParameter("@Email", DBNull.Value);
          }
                                     
          comando.Parameters.Add(parametro);

          if (dto.TelefoneFixo != null)
          {
                 parametro = new SqlParameter("@TelefoneFixo", dto.TelefoneFixo);
          }
          else
          {
                 parametro = new SqlParameter("@TelefoneFixo", DBNull.Value);
          }

          comando.Parameters.Add(parametro);


          if (dto.TelefoneCelular != null)
          {
                 parametro = new SqlParameter("@TelefoneCelular", dto.TelefoneCelular);
          }
          else
          {
                 parametro = new SqlParameter("@TelefoneCelular", DBNull.Value);
          }
          
          comando.Parameters.Add(parametro);

          id =(int) comando.ExecuteScalar();
          
          dto.Id = id;                      

          transacao.Commit();
          resposta = Request.CreateResponse<ContatoDto>(HttpStatusCode.Created, dto);

          return resposta;
    }
    catch (Exception ex)
    {
          resposta = Request.CreateResponse<String>(HttpStatusCode.NotAcceptable, ex.Message);
          transacao.Rollback();
    }
    finally
    {
          conexao.Close();
    }

    return resposta;
 }

Na linha 85 utilizamos o ExecuteScalar do ADO.NET para que possamos receber o id gravado.

Nas linhas 91 e 97 informamos através dos códigos de retorno HTTP se a gravação foi executada com sucesso (código 200), se lançou alguma exceção (código 500) ou se foi feito alguma coisa errada e erramos o nome do action (código 404).

Na Listagem 9 mostramos como criar a action AlterarContato, na qual utilizaremos o método HTTP PUT, pois estamos fazendo uma modificação no destino.

Listagem 9. Action para alterar o Contato.

[HttpPut]
public HttpResponseMessage AlterarContato([FromBody]ContatoDto dto)
{   
    IDbConnection conexao;
    IDbCommand comando;
    IDbDataParameter parametro;
    IDbTransaction transacao;
    List<String> errosDeValidacao = new List<String>();
    HttpResponseMessage resposta;

    //Validações
    //Verifica se os Ids estão presentes no Dto
    if (dto.Id == null)
    {
          errosDeValidacao.Add("O id do Contato não pode ser vazio");                
    }

    if (dto.IdUsuario == null)
    {
          errosDeValidacao.Add("O id do Usuário não pode ser vazio");
    }

    if (errosDeValidacao != null && errosDeValidacao.Count > 0)
    {
          StringBuilder builder = new StringBuilder();
          foreach (string linha in errosDeValidacao)
          {
                 builder.Append(linha).Append(", ");
          }
          string resultado = builder.ToString();

          throw new ContatoException(resultado);                
    }

    conexao = new SqlConnection(ConnectionString);

    conexao.Open();

    transacao = conexao.BeginTransaction();

    comando = conexao.CreateCommand();

    comando.Transaction = transacao;

    try
    {
          StringBuilder query = new StringBuilder();

          query.Append("UPDATE CONTATO ");
          query.AppendLine(" SET  Nome = @Nome, ");
          query.AppendLine(" Email = @Email, ");
          query.AppendLine(" TELEFONEFIXO = @TelefoneFixo, ");
          query.AppendLine(" TELEFONECELULAR = @TelefoneCelular ");
          query.AppendLine(" WHERE idUsuario = @IdUsuario ");
          query.AppendLine(" AND id = @Id ");

          comando.CommandText = query.ToString();

          if (dto.IdUsuario != null)
          {
                 parametro = new SqlParameter("@IdUsuario", dto.IdUsuario);
          }
          else
          {
                 parametro = new SqlParameter("@IdUsuario", DBNull.Value);
          }

          comando.Parameters.Add(parametro);

          if (dto.Id != null)
          {
                 parametro = new SqlParameter("@Id", dto.Id);
          }
          else
          {
                 parametro = new SqlParameter("@Id", DBNull.Value);
          }

          comando.Parameters.Add(parametro);

          if (dto.Nome != null)
          {
                 parametro = new SqlParameter("@Nome", dto.Nome);
          }
          else
          {
                 parametro = new SqlParameter("@Nome", DBNull.Value);
          }

          comando.Parameters.Add(parametro);

          if (!String.IsNullOrWhiteSpace(dto.Email))
          {
                 parametro = new SqlParameter("@Email", dto.Email);
          }
          else
          {
                 parametro = new SqlParameter("@Email", DBNull.Value);
          }

          comando.Parameters.Add(parametro);

          if (dto.TelefoneFixo != null)
          {
                 parametro = new SqlParameter("@TelefoneFixo", dto.TelefoneFixo);
          }
          else
          {
                 parametro = new SqlParameter("@TelefoneFixo", DBNull.Value);
          }

          comando.Parameters.Add(parametro);

          if (dto.TelefoneCelular != null)
          {
                 parametro = new SqlParameter("@TelefoneCelular", dto.TelefoneCelular);
          }
          else
          {
                 parametro = new SqlParameter("@TelefoneCelular", DBNull.Value);
           }

          comando.Parameters.Add(parametro);

          comando.ExecuteNonQuery();

          transacao.Commit();

          resposta = Request.CreateResponse<ContatoDto>(HttpStatusCode.OK, dto);

    }
    catch (Exception ex)
    {                          
          transacao.Rollback();
          dto.Excecao = ex;
          resposta = Request.CreateResponse<ContatoDto>(HttpStatusCode.NotAcceptable, dto);
    }
    finally
    {
          conexao.Close();
    }

    return resposta;
 }

Essa action ficou bem extensa devido à quantidade de validações que são necessárias para que não haja nenhum problema no momento da gravação do contato.

Na Listagem 10 criamos a action para apagar um contato. Como vamos fazer uma exclusão, vamos utilizar o método HTTP DELETE.

Listagem 10. Método para a deleção de um contato.
 
 [HttpDelete]
 public HttpResponseMessage ApagarContato([FromUri] String id)
 {

     int linhas = 0;
     IDbConnection conexao;
     IDbCommand comando;
     IDbDataParameter parametro;
     IDbTransaction transacao;

     String errosDeValidacao = String.Empty;
     HttpResponseMessage resposta;

     //Validações
     //Verifica se os Ids estão presentes no Dto
     if (id == null)
     {
           errosDeValidacao = "O id do Contato não pode ser vazio";
     }
     
     if (!String.IsNullOrWhiteSpace(errosDeValidacao))
     {                          
           throw new ContatoException(errosDeValidacao);
     }

     conexao = new SqlConnection(ConnectionString);

     conexao.Open();

     transacao = conexao.BeginTransaction();

     comando = conexao.CreateCommand();

     comando.Transaction = transacao;

     try
     {                
           comando.CommandText = "DELETE FROM CONTATO WHERE Id=@id";

           if (id != null)
           {
                  parametro = new SqlParameter("@id", id);
           }
           else
           {
                  throw new ContatoException("É necessário que se passe o id a ser excluido");
           }
     
           comando.Parameters.Add(parametro);                

           linhas = comando.ExecuteNonQuery();              

           transacao.Commit();

           resposta = Request.CreateResponse(HttpStatusCode.OK);
     }
     catch (Exception ex)
     {               
           transacao.Rollback();
           resposta = Request.CreateResponse<String>(HttpStatusCode.NotAcceptable,ex.Message);
     }
     finally
     {
           conexao.Close();            
     }

     return resposta;
  }

Até o momento já vimos a maioria dos métodos utilizados nessa listagem, portanto não são necessários comentários adicionais nesse ponto. Na Listagem 11 temos uma consulta para listar contatos pelo IdUuario.

Listagem 11. Método para Listar Contatos do Usuário.

   [HttpGet]
   public IList<ContatoDto> ListarContatosDoUsuario([FromUri]String id)
   {                
                    IDbConnection conexao;
                    IDbCommand comando;
                    IDbDataParameter parametro;                    
  
                    conexao = new SqlConnection(ConnectionString);
                    comando = new SqlCommand();
  
                    comando.Connection = conexao;
  
                    conexao.Open();
  
                    StringBuilder query = new StringBuilder();
  
                    query.Append("SELECT [ID],[IDUSUARIO],[NOME],[EMAIL],[TELEFONEFIXO],[TELEFONECELULAR],[DATACRIACAO],[DATALTERACAO],[ATIVO]");
                    query.AppendLine(" FROM vContatoDto ");
                    query.AppendLine(" WHERE IDUSUARIO = @IdUsuario");
  
                    parametro = new SqlParameter("@IdUsuario", id);                                      
  
                    comando.Parameters.Add(parametro);                                 
  
                    comando.CommandText = query.ToString();
  
             IList<ContatoDto> lista = new List<ContatoDto>();
  
                    try
                    {
                           using (SqlDataReader leitorDeDados = (SqlDataReader)comando.ExecuteReader())
                           {
                                  while (leitorDeDados.Read())
                                  {
                                        ContatoDto contatoDto = new ContatoDto();36
  
                                  if (!leitorDeDados["ID"].Equals(DBNull.Value))
                                               contatoDto.Id = Convert.ToInt32(leitorDeDados["ID"].ToString());
  
                                        if (!leitorDeDados["IDUSUARIO"].Equals(DBNull.Value))
                                               contatoDto.IdUsuario = Convert.ToInt32(leitorDeDados["IDUSUARIO"].ToString());
  
                                        if (!leitorDeDados["NOME"].Equals(DBNull.Value))
                                               contatoDto.Nome = leitorDeDados["NOME"].ToString();
  
                                  if (!leitorDeDados["EMAIL"].Equals(DBNull.Value))
                                               contatoDto.Email = leitorDeDados["EMAIL"].ToString();
  
                                  if (!leitorDeDados["TELEFONEFIXO"].Equals(DBNull.Value))
                                               contatoDto.TelefoneFixo = Convert.ToInt64(leitorDeDados["TELEFONEFIXO"].ToString());
  
                               if (!leitorDeDados["TELEFONECELULAR"].Equals(DBNull.Value))
                                               contatoDto.TelefoneFixo = Convert.ToInt64(leitorDeDados["TELEFONECELULAR"].ToString());
  
                                        if (!leitorDeDados["DATACRIACAO"].Equals(DBNull.Value))
                                               contatoDto.DataDeCriacao = Convert.ToDateTime(leitorDeDados["DATACRIACAO"].ToString());
  
                                        if (!leitorDeDados["DATALTERACAO"].Equals(DBNull.Value))
                                               contatoDto.DataDeAlteracao = Convert.ToDateTime(leitorDeDados["DATALTERACAO"].ToString());
  
                                        lista.Add(contatoDto);
  
                                  }                  
                           }
  
                    }
                    catch (Exception)
                    {
  
                           throw;
                    }
                    finally
                    {
                           conexao.Close();
                    }
  
                    return lista;
   }

Para concluir esse conjunto de funcionalidades principais do nosso microserviços de contatos, na Listagem 12 temos uma action que faz a busca de um contato por seu Id.

Listagem 12. Buscando um contato por seu Id.

 [ResponseType(typeof(ContatoDto))]
 [HttpGet]
 public ContatoDto ListarContatoPorId([FromUri]String id)
 {
     ContatoDto contatoDto = new ContatoDto();

     IDbConnection conexao;
     IDbCommand comando;
     IDbDataParameter parametro;
     
     conexao = new SqlConnection(ConnectionString);
     comando = new SqlCommand();

     comando.Connection = conexao;

     conexao.Open();

     StringBuilder query = new StringBuilder();

     query.Append("SELECT [ID],[IDUSUARIO],[NOME],[EMAIL],[TELEFONEFIXO],
     [TELEFONECELULAR],[DATACRIACAO],[DATALTERACAO],[ATIVO]");
     query.AppendLine(" FROM vContatoDto ");
     query.AppendLine(" WHERE ID = @IdContato");

     parametro = new SqlParameter("@IdContato", id);

     comando.Parameters.Add(parametro);

     comando.CommandText = query.ToString();

     try
     {
           using (SqlDataReader leitorDeDados = (SqlDataReader)comando.ExecuteReader())
           {
                  while (leitorDeDados.Read())
                  {                        

                         if (!leitorDeDados["ID"].Equals(DBNull.Value))
                                contatoDto.Id = Convert.ToInt32(leitorDeDados["ID"].ToString());

                         if (!leitorDeDados["IDUSUARIO"].Equals(DBNull.Value))
                                contatoDto.IdUsuario = Convert.ToInt32(leitorDeDados["IDUSUARIO"].ToString());

                         if (!leitorDeDados["NOME"].Equals(DBNull.Value))
                                contatoDto.Nome = leitorDeDados["NOME"].ToString();

                         if (!leitorDeDados["EMAIL"].Equals(DBNull.Value))
                                contatoDto.Email = leitorDeDados["EMAIL"].ToString();

                         if (!leitorDeDados["TELEFONEFIXO"].Equals(DBNull.Value))
                                contatoDto.TelefoneFixo = Convert.ToInt64(leitorDeDados["TELEFONEFIXO"].ToString());

                         if (!leitorDeDados["TELEFONECELULAR"].Equals(DBNull.Value))
                                contatoDto.TelefoneFixo = Convert.ToInt64(leitorDeDados["TELEFONECELULAR"].ToString());

                         if (!leitorDeDados["DATACRIACAO"].Equals(DBNull.Value))
                                contatoDto.DataDeCriacao = Convert.ToDateTime(leitorDeDados["DATACRIACAO"].ToString());

                         if (!leitorDeDados["DATALTERACAO"].Equals(DBNull.Value))
                                contatoDto.DataDeAlteracao = Convert.ToDateTime(leitorDeDados["DATALTERACAO"].ToString());

                  }                    
           }
     }
     catch (Exception e)
     {
           contatoDto.Excecao = e;
     }
     finally
     {
           conexao.Close();
     }

     return contatoDto;                
 }

Arquiteturas baseadas em microserviços estão se tornando uma realidade e a tendência que passemos a encontrá-las com mais frequência. A própria Microsoft ao criar o ASP.NET vNext e o Projeto Katana estão transformando o ASP.NET de uma arquitetura monolítica para algo mais leve e bastante funcional.

Relacionado

Microserviços estão revolucionando cada vez mais a forma com vemos nossas aplicações, fazendo com que estas possam ser portadas, adicionadas a uma Web Farm com alta disponibilidade, clusters e outras partes de infraestrutura que outrora se mostravam impraticáveis pela forma estática como eram distribuídas as camadas e como suas dependências eram geridas dentro da aplicação.

Arquitetura de Microserviços em .NET – Parte 2

Nesta segunda parte da série o leitor terá a oportunidade de conhecer os processos de publicação dos microserviços, seja no IIS ou via Microsoft Katana, uma implementação da Microsoft para o padrão OWIN.

Poderá criar uma API e posteriormente seu Gateway, que será a camada intermediária entre o microserviço e o cliente.

Com o OWIN, o leitor terá um deploy rápido preparando suas aplicações para Integração e Entrega Contínuas.

Ao fim desse processo, o leitor terá uma aplicação com arquitetura mais flexível e escalável, diferente da arquitetura monolítica da qual partimos no início desta série.

Na primeira parte desta série vimos como escalar e dividir uma aplicação monolítica, mesmo com todas as suas limitações, para uma Arquitetura de Microserviços.

Selecionamos os métodos mais importantes e que poderíamos, inclusive, disponibilizar para que terceiros utilizem em seus sistemas, tal como uma antiga camada de autenticação que foi melhorada e convertida em microserviço.

Agora, veremos como consumir tais serviços, mas para que isso seja possível precisamos hospedar nossa aplicação.

Nessa segunda e última parte da série vamos hospedar nossos microserviços no IIS (Internet Information Service) e efetuar os spikes (BOX 1) de consumo dos microserviços rumo à criação de nossa Gateway API, que irá funcionar como um catálogo dos microserviços criados e centralizar o acesso pelo cliente.

Entendemos “cliente” como qualquer aplicação que fará uso do microserviço, ou seja, é a camada que faz a fronteira entre a aplicação e o usuário.

BOX 1. Spikes

Spikes, em XP (Extreme Programming), significa investigação. Normalmente, quando precisamos testar uma regra de negócios, uma história de usuário ou uma funcionalidade disponibilizada por terceiros, para evitar problemas de implementação na sprint e evitar perda de tempo, utilizamos os spikes.

Normalmente, quando existe um sistema totalmente novo para a equipe, o primeiro sprint normalmente é de "Spikes", como uma forma geral de investigação de escalabilidade, ferramentas de desenvolvimento e afins.

Publicando no IIS

A publicação no IIS requer um grau de complexidade baixo, uma vez que um projeto Web API é um projeto ASP.NET MVC, porém focado em métodos HTTP. Vamos criar um domínio fictício em nosso sistema operacional para que possamos utilizar um nome distinto de Localhost.

Abra o arquivo hosts do seu sistema operacional (localizado em C:\WINDOWS\System32\drivers\etc) e adicione a seguinte linha:

[Seu IP]          gateway.microservicos              #Microserviços

Em [Seu IP] deve ser colocado o IP da sua máquina e não o endereço de loopback que normalmente é 127.0.0.1 em IPv4 e ::1: em IPv6. Feito isso, para verificar se nossos endereços estão respondendo como devem, precisamos executar um ping no endereço e obter um resultado como o que é apresentado na Figura 1.

Arquitetura de Microserviços em .NET
Figura 1. Testando o endereço criado

.

Por questões de didática, a criação das demais classes não será apresentada neste artigo, mas o código fonte completo encontra-se para download junto com esse artigo no site da DevMedia.

Vamos abrir a nossa solução dos microserviços, como mostra a Figura 2.

Arquitetura de Microserviços em .NET
Figura 2. Estrutura inicial da Solution dos microserviços.

Devemos então clicar com a direita sobre o projeto Microservice.Autenticacao e em seguida clicar em Publish. Na tela que se abre, criamos o profile chamado Microservico, como vemos na Figura 3.

Arquitetura de Microserviços em .NET
Figura 3. Criação do Perfil Microserviços.

Clicamos em Next ou em Connection e marcamos o Publish Method como File System e informamos o caminho. Vemos na Figura 4 como ficou a nossa configuração.

Não é necessário que se copie a mesma localização, pode ser qualquer uma escolhida pelo leitor. Neste artigo, colocamos na raiz da solution em um diretório de nome Publish.

Arquitetura de Microserviços em .NET
Figura 4. Local da publicação do Perfil Microserviços.

Nas configurações de publicação, devemos selecionar as opções mostradas na Figura 5.

Arquitetura de Microserviços em .NET
Figura 5. Configuração de Deploy.

Agora podemos clicar em Publish e, caso tudo dê certo, o diretório informado na configuração deve estar como mostra a Figura 6.

Arquitetura de Microserviços em .NET
Figura 6. Deploy efetuado no diretório.

Já temos o nosso diretório publicado, agora vamos efetuar a configuração do IIS. Ao abrir o Console de Configuração do IIS, vamos criar um novo Web Site, como pode ser visto na Figura 7.

Arquitetura de Microserviços em .NET
Figura 7. Adicionando um novo site para o microserviço.

Na tela de configuração, colocamos o nome no site de autenticacao, selecionamos o mesmo endereço IP e o nome de host do arquivo de configuração descrito anteriormente. Na Figura 8 vemos essa configuração.

Arquitetura de Microserviços em .NET
Figura 8. Configuração do Site

.

Feito isso, nosso microserviço de autenticação já está hospedado no IIS. Vamos configurar então o Pool de aplicativos, pois sem essa configuração não vamos conseguir fazer o acesso necessário ao site configurado. A Figura 9 mostra como deve ser configurado e o nome de nosso Pool de aplicativo.

Arquitetura de Microserviços em .NET
Figura 9. Configuração do Pool de Aplicativos.

Após configurar o pool, devemos ir nas configurações avançadas de nosso site criado e selecionar o pool de aplicativos criado.

Configuração de permissão do Pool no SQL Server

Pode ocorrer um problema quando acessamos alguma action do microserviço, podemos receber um erro de falta de permissão para a conexão com o nosso banco de dados para o pool configurado, nesse caso, o pool Microservico.

Para evitar esse problema, precisamos configurar o acesso ao usuário do IIS que tem o mesmo nome do pool de aplicativo com o prefixo, IIS APPPOOLMicroservico, dentro dos bancos de dados dos nossos microserviços.

Normalmente, utilizamos o Microsoft SQL Server Management Studio, mas podemos utilizar qualquer console que acesse a base de dados do SQL Server (neste artigo foi utilizada a versão Express).

Para tal, selecionamos o nosso banco de dados do microserviço de autenticação, clicamos em Segurança e em seguida criamos o usuário do IIS e lhe aplicamos as permissões necessárias, como mostra a Figura 10.

Arquitetura de Microserviços em .NET
Figura 10. Permitindo o pool para conexão com o Banco de Dados.

Testando o Microserviço no navegador

Para efetuar o primeiro teste, vamos utilizar o browser e utilizar o método BuscarUsuarioPorLogin e em seguida vamos utilizar o Fiddler para todos os outros testes. Vamos acessar a seguinte URL no navegador e devemos obter o resultado mostrado na Listagem 1:

http://gateway.microservicos/api/Autenticacao/BuscarUsuarioPorLogin?login=gabrielsimas
Listagem 1. Retorno do teste do microserviço.

<UsuarioDto xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Microservice.Dto">
<Bloqueado>Nao</Bloqueado>
<Conta>gabrielsimas</Conta>
<DataDaUltimaTrocaDeSenha i:nil="true"/>
<DataDeAlteracao i:nil="true"/>
<DataDeCriacao>2015-05-03T11:47:54</DataDeCriacao>
<Email>autorgabrielsimas@gmail.com</Email>
<Id>1</Id>
<NomeCompleto>Luís Gabriel N. Simas</NomeCompleto>
<PerguntaSecreta i:nil="true"/>
<RespostaSecreta i:nil="true"/>
<Senha>
0f3Mi9lsUBa7QfU3SS6yaUjw4zDXHyP5IyO1l1bXqFTqR1OBirXg8IG4e
5JWiFPcAVoYOl4snk9/nTVZHD86sOujYhhRnTzmwvqatKXag/
dnsq71glCnOFJu58jy9DEEjrppJYkKC0hg+rkXLB97hCR02eM=
</Senha>
</UsuarioDto>

Testando o microserviço com Fiddler

O Fiddler é a ferramenta que utilizaremos para outros testes mais profundos, pois vão existir métodos em que precisamos enviar um objeto complexo populado para a aplicação e fazer isso diretamente do browser é difícil de se fazer.

Para tal, é necessário efetuar o download do Fiddler no site indicado na seção Links deste artigo. Em seguida, com o Fiddler instalado, efetuaremos o mesmo teste que efetuamos no navegador.

Montamos esse teste no Fiddler através de sua ferramenta chamada Composer, na qual selecionamos o método HTTP que enviaremos a requisição e também podemos enviar um objeto completo populado, caso seja essa a necessidade do método.

A action BuscarUsuarioPorLogin utiliza um método GET por se tratar de uma consulta para recuperação de informações, essa action retorna um DTO, mas com a Web API ele retorna um objeto JSON que é serializado e quando efetuamos o consumo desse microserviço, o desserializamos novamente para o DTO.

Isso faz da Web API seja uma solução bem robusta para tráfego de informações e excelente para uma arquitetura para microserviços, facilitando inclusive a integração entre microserviços utilizando um protocolo de mensageria, normalmente o AMQP.

Como já tem a experiência nativa de transmitir objetos através do corpo da mensagem, sua solução é facilmente acoplada ao protocolo AMQP.

Na Figura 11 temos o teste do action pelo Fiddler.

Arquitetura de Microserviços em .NET
Figura 11. Teste inicial do microserviço no Fiddler.

Observamos pelo Fiddler que recebemos um erro 500 que informa um erro interno no servidor. Tal erro é ocasionado pela falta de permissão de nosso pool de aplicativo ao SQL Server, como foi explicado anteriormente.

Para identificar o motivo do erro, clicamos sobre o resultado e depois clicamos em Raw e recebemos a seguinte mensagem:


{"Message":"An error has occurred.","ExceptionMessage":"Cannot open database \"Servico_Autenticacao\" requested by the login. 
The login failed.\r\nLogin failed for user 'IIS APPPOOL\\Microservicos'

Note que é a mesma informação comentada anteriormente sobre a criação de um usuário com permissão no SQL Server e esse usuário deve ter o mesmo nome do pool. Para resolver esse problema, basta seguirmos o que já foi explicado, porém para o usuário IIS APPPOOL\Microservicos.

Após essa configuração, já podemos repetir o teste no Fiddler e ver o sucesso da consulta, como mostra na Figura 12.

Arquitetura de Microserviços em .NET
Figura 12. Sucesso na resposta do Serviço.

O Fiddler é capaz de identificar o formato de retorno, que nesse caso é JSON e pode ser visto na Listagem 2.

Listagem 2. Resposta em JSON.
</p>
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/7.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Thu, 04 Jun 2015 23:21:27 GMT
Content-Length: 429

{"Id":1,
"NomeCompleto":"Luís Gabriel N.Simas",
"PerguntaSecreta":null,
"RespostaSecreta":null, 16“Senha":"0f3Mi9lsUBa7QfU3SS6yaUjw4zDXHyP5IyO1l1bXqFTqR1OBirXg8IG4e5JWiFPcAVoYOl4snk9/nTVZ17 HD86sOujYhhRnTzmwvqatKXag/dnsq71glCnOFJu58jy9DEEjrppJYkKC0hg+rkXLB97hCR02eM=",
"Conta":"gabrielsimas",
"Bloqueado":0,
"DataDaUltimaTrocaDeSenha":null,
"DataDeAlteracao":null,
"DataDeCriacao":"2015-05-03T11:47:54",
"Email":"autorgabrielsimas@gmail.com"}

Nosso microserviço já está hospedado e funcional, doravante, vamos criar os testes unitários para que possamos consumir esses microserviços para certificar de seu correto funcionamento antes de desenvolver o Gateway de nossa API, que disponibilizaremos para consumo por parte do cliente.

A partir do procedimento que realizamos, o leitor pode realizar a hospedagem do microserviço para controle dos contatos.

Criando os Testes unitários para Autenticação

Para os nossos testes unitários, vamos utilizar o MS Test Framework que vem com o próprio Visual Studio, apenas para fins didáticos, mas isso não impede o uso de qualquer outro framework de testes. Também não utilizaremos nenhum framework para Mock. Cada método criará a instância do objeto e a tratará localmente.

Para o teste do microserviço de autenticação utilizaremos o projeto Teste.Microservice.Autenticacao. Além de adicionarmos como referência o assembly do projeto Microservice.Dto.

A partir do Nuget devemos então instalar os pacotes ilustrados na Figura 13.

Arquitetura de Microserviços em .NET
Figura 13. Pacotes instalados para o Teste.

Crie então uma classe de testes chamada WebApiTeste e criamos um atributo como uma constante onde guardaremos o endpoint da API, conforme o código a seguir:

private const String endpoint = "http://gateway.microservicos"; 

Ainda na classe de teste criada, adicionaremos em cada método de teste o sufixo necessário para interação com o microserviço de autenticação.

Vamos então testar a primeira action, que valida o cadastro de usuário. Na Listagem 3 temos o código do método de teste, que é autoexplicativo.

Listagem 3. Método de teste da da Action CadastrarUsuario.

[TestMethod]
[Description("Cadastra um Contato")]
[TestCategory("Spike - Autenticacao - WebApi")]
public void CadastrarUsuario()
{
    String senhacriptografada = String.Empty;

    UsuarioDto dto = new UsuarioDto() { 
        
        Conta = "liviahelena",
        Email = "liviahsimas@familiasimas.net",
        NomeCompleto = "Livia Helena B. Simas",
        Senha  = "l1v1451m45,123"                
    };
                                        
    HttpClient cliente = new HttpClient();

    cliente.BaseAddress = new Uri(endpoint);

    cliente.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/json"));

    //Cria o Formatador em JSON
    MediaTypeFormatter formatadorJson = new JsonMediaTypeFormatter();

    //Serializa o dto em objeto JSON
    HttpContent conteudo = new ObjectContent<UsuarioDto>(dto, formatadorJson);

    //Envia a resposta ao cliente convertido em JSON
    //E retorna o código 200 se for sucesso ou outro configurado no microserviço anteriormente.
    HttpResponseMessage resposta = cliente.PostAsync("api/autenticacao/CadastrarUsuario", conteudo).Result;

    Assert.IsTrue(resposta.IsSuccessStatusCode);
}

Neste exemplo de consumo enviamos uma mensagem contendo um objeto complexo no corpo, que é a forma mais segura, pois de outra forma o conteúdo fica exposto a ataques externos.

Na linha 30 informamos ao microserviço que estamos enviando uma requisição via método POST e adicionamos ao endpoint o conteúdo do DTO, que será serializado na mensagem e enviado ao microserviço e processado.

Terminado o cadastro do usuário, vamos validar se sua senha é válida autenticando o mesmo, o que significa consumir a action AutenticarUsuario.

O correto para essa action é passar um DTO para encapsular o nome de usuário e a senha como forma de segurança, e isso ficará como um desafio ao leitor, agora que sabe como passar um objeto complexo para o microserviço.

Aqui enviaremos as informações pela URL, por se tratar de um método GET, mesmo não sendo a forma mais segura.

Na Listagem 4 temos a estrutura do nosso teste de consumo para autenticar o usuário cadastrado.

Listagem 4. Teste de consumo da Action AutenticarUsuario.

[TestMethod]
[Description("Autentica o Usuário")]
[TestCategory("Spike - Autenticacao - WebApi")]
public void AutenticarUsuario()
{
    String login = "liviahelena";
    String senha = "l1v1451m45,123";
    Boolean autenticado = false;

  String url = String.Format("api/autenticacao/AutenticarUsuario?usuario={0}&senha={1}", login, senha);

    HttpClient cliente = new HttpClient();

    cliente.BaseAddress = new Uri(endpoint);

    cliente.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/json"));

    HttpResponseMessage resposta = cliente.GetAsync(url).Result;

    if(resposta.IsSuccessStatusCode){
        autenticado = true;
    }

    Assert.IsTrue(autenticado);
}

Na linha 18 enviamos a informação através de um método GET e a URL junto com o endpoint. Com o novo usuário autenticado, pode ser que haja a necessidade da troca de senha, então na Listagem 5, temos a implementação do método de teste de consumo da action TrocarSenha.

Já nessa action utilizamos as boas práticas e enviamos a informação via POST no corpo da mensagem através de um DTO, por se tratar de uma ação de persistência.

Listagem 5. Teste de consumo da Action TrocarSenha.

[TestMethod]
[Description("Troca a senha do usuário")]
[TestCategory("Spike - Autenticacao - WebApi")]
public void TrocarSenha()
{
    String senhacriptografada = String.Empty;

    AutenticacaoDto dto = new AutenticacaoDto()
    {
        IdUsuario = 7,
        Login = "liviahelena",
        Senha = "l1v14h3l3n4",
        NovaSenha = "l1v1451m45,123"
    };           

    HttpClient cliente = new HttpClient();

    cliente.BaseAddress = new Uri(endpoint);

    cliente.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/json"));

    //Cria o Formatador em JSON
    MediaTypeFormatter formatadorJson = new JsonMediaTypeFormatter();

    HttpContent conteudo = new ObjectContent<AutenticacaoDto>(dto, formatadorJson);

    HttpResponseMessage resposta = cliente.PutAsync("api/autenticacao/TrocarSenha", conteudo).Result;

    Assert.IsTrue(resposta.IsSuccessStatusCode);
}

Na linha 27 enviamos o conteúdo da mensagem via POST, uma vez que essa action resulta em um método inserção no banco de dados.

Terminando nosso microserviço de autenticação, temos uma busca de usuário que pode auxiliar em outras ações dentro de um sistema. Como no nosso modelo de dados informamos que o Login e o E-mail são únicos, fazemos a busca pelo Login.

Na Listagem 6 temos o teste de consumo da action BuscarUsuarioPorLogin. Essa action também tem um funcionamento que devemos ressaltar: a mesma está retornando junto com o DTO a senha criptografada, isso é uma falha grave de segurança e a melhoria dessa action fica como mais um desafio ao leitor.

Listagem 6. Teste de Consumo de BuscarUsuarioPorLogin.

[TestMethod]
[Description("Busca um Usuário por seu Login")]
[TestCategory("Spike - Autenticacao - WebApi")]
public void BuscarUsuarioPorLogin()
{
    String login = "liviahelena";

    String uri = String.Format("api/autenticacao/BuscarUsuarioPorLogin?login={0}", login);

    UsuarioDto contato = new UsuarioDto();

    HttpClient cliente = new HttpClient();

    cliente.BaseAddress = new Uri(endpoint);

    cliente.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/json"));

    HttpResponseMessage resposta = cliente.GetAsync(uri).Result;

    if (resposta.IsSuccessStatusCode)
    {
        contato = resposta.Content.ReadAsAsync<UsuarioDto>().Result;
    }

    Assert.IsTrue(contato.Id.HasValue);
}

Agora que criamos os testes, vamos executá-los e verificar seus resultados. Caso todo o processo tenha corrido como esperado, devemos ter todos os métodos executados com sucesso, como vemos na Figura 14.

Arquitetura de Microserviços em .NET
Figura 14. Resultado dos Testes Unitários.

Nosso microserviço de autenticação está pronto para ser acoplado à nossa API. Podemos agora criar o nosso Gateway da API.

Criando as chamadas da API

Criaremos duas Camadas em nossa API, Autenticação e Contato, segregadas por Interfaces que vão funcionar como uma Camada de Proxy ou Serviço para encapsular as actions dos microserviços e forçar a passagem do DTO ao mesmo. O conceito de microserviço reza que precisamos de um DTO para passar e receber os valores entre o cliente e o microserviço.

Primeiramente vamos criar a Camada de Serviços para o microserviço de autenticação, e é nesse momento em que transformaremos os testes como base para criarmos nossos métodos de serviço.

É muito importante organizar o nosso código em responsabilidades e com isso, nos projetos de Serviço de Autenticação e Contato vamos utilizar interfaces que serão separadas das implementações com diretórios. Também criamos exceções separadas para cada serviço,

Também existe uma outra abordagem onde podemos unir todos os microserviços em apenas uma Camada de Serviço. No caso de uma aplicação interna, não tem problema algum, mas se ela for utilizada por muitos clientes, isso pode se tornar muito oneroso, porém o tipo de projeto é que irá nos informar qual a melhor abordagem.

Para esse artigo utilizaremos as Camadas de Serviço e colocaremos encapsuladas no Gateway de APIs, que nada mais é do que uma camada de proxy dessas duas primeiras.

Inicialmente, vamos criar a Camada de Serviço para encapsular o microserviço de autenticação.

Camada de Serviços para Autenticação

Utilizaremos a camada de DTO do projeto, Microservicos.Dto, como responsável pelos objetos de troca de informações.

Existe alguma codificação nessas camadas que se repetem, então, vamos utilizar essa camada em específico para mostrar e na camada de serviços de contatos mostraremos apenas o que é pertinente para evitar duplicação de conteúdo.

Também devemos instalar os pacotes presentes na Figura 13 para que não tenhamos quaisquer problemas com dependências, com exceção do pacote SHA3, que não é necessário neste ponto.

Começaremos pela exceção personalizada. Essa classe nada mais é do que uma extensão da Classe System.Extension. Crie uma pasta no projeto chamada Excecao e nela adicione uma nova classe com o nome de AutenticacaoServicoException, cujo código é apresentado na Listagem 7.

Listagem 7. Classe AutenticacaoServicoException.

public class AutenticacaoServicoException: Exception
{
      public AutenticacaoServicoException():base()
      {

      }

      public AutenticacaoServicoException(String mensagem):base(mensagem)
      {

      }

      public AutenticacaoServicoException(String mensagem,Exception excecaoProfunda):base(mensagem,excecaoProfunda)
      {

      }
  }

Após criar o tratamento para os erros, vamos criar a interface que nos permitirá segregar o código e forçar o cliente e utilizar o que for disponibilizado. Crie mais uma pasta com o nome de Interfaces e nela adicione uma nova interface chamada IAutenticacaoServico, adicionando a ela o código presente na Listagem 8.

Listagem 8. Interface IAutenticacaoServico.

public interface IAutenticacaoServico
{

    Boolean CadastrarUsuario(UsuarioDto dto);

    Boolean TrocarSenha(AutenticacaoDto dto);

    Boolean AutenticarUsuario(AutenticacaoDto dto);

    UsuarioDto BuscarUsuarioPorLogin(AutenticacaoDto dto);

}

Nessa listagem já podemos notar que a forma de implementação é bem diferente do que fora visto até então. Estamos utilizando os DTOs para passagem de todos os valores aos microserviços, retornado Boolean para as operações de persistência e retornando um DTO para as buscas.

Agora, vamos implementar as interfaces. Crie agora a pasta Implementacao e adiciona uma classe chamada AutenticacaoServico, com o código da Listagem 9.

Listagem 9. Estrutura da Classe AutenticacaoServico.

 public class AutenticacaoServico: IAutenticacaoServico
 {

     #region Atributos

     private String endpoint = ConfigurationManager.AppSettings["endpoint_autenticacao"];
     private HttpClient clienteAutenticacao;
     private MediaTypeFormatter formatadorJson;

     #endregion

     #region Construtores

     public AutenticacaoServico()
     {            
         this.InstanciaEndpoint();
     }

     #endregion

     #region Propriedades

     public HttpClient ClienteAutenticacao
     {
         get
         {
             return this.clienteAutenticacao;
         }

         set
         {
             this.clienteAutenticacao = value;
         }
     }

     #endregion

     #region Métodos do Serviço

     private HttpClient InstanciaEndpoint()
     {

         this.clienteAutenticacao = new HttpClient();
         this.clienteAutenticacao.BaseAddress = new Uri(this.endpoint);

         this.clienteAutenticacao.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/json"));

         return clienteAutenticacao;

     }

}

Essa classe foi criada com base no que fizemos nos testes unitários, dali identificamos os trechos de códigos que se repetiam e a partir disso criamos propriedades que visam facilitar o desenvolvimento.

Na linha 6 criamos um atributo que pega do arquivo de configuração o endpoint. Na linha 7 criamos um atributo que será acessado via propriedade de mesmo nome na qual já deixamos o objeto pronto com a configuração do objeto de saída, que é o JSON e com o endpoint configurado, isso já economiza algumas linhas de código repetitivas.

Na linha 23 temos a propriedade que acessa tal atributo. Na linha 8 configuramos o formato da mensagem que é o JSON.

Já vimos a estrutura da classe, agora veremos como essa estrutura é utilizada nos métodos subsequentes. Os métodos a seguir foram baseados nos testes que executamos anteriormente. Na Listagem 10 temos o método CadastrarUsuario com sua implementação bem enxuta.

Listagem 10. Método CadastrarUsuario.

public Boolean CadastrarUsuario(UsuarioDto dto)
{
    String uri = "api/autenticacao/CadastrarUsuario";

    HttpContent conteudo = new ObjectContent<UsuarioDto>(dto, formatadorJson);

    HttpResponseMessage resposta = clienteAutenticacao.PostAsync(uri, conteudo).Result;

    if (resposta.IsSuccessStatusCode)
    {
        return true;
    }
    else
    {
        return false;
    }

}

Com os atributos criados podemos economizar muito em linhas de código, essa listagem e as que virão a seguir comprovarão isso. Na Listagem 11 temos o método TrocarSenha.

Listagem 11. Método TrocarSenha.

public Boolean TrocarSenha(AutenticacaoDto dto)
{
    String uri = "api/autenticacao/TrocarSenha";
    HttpContent conteudo = new ObjectContent<AutenticacaoDto>(dto, formatadorJson);

    HttpResponseMessage resposta = clienteAutenticacao.PutAsync(uri, conteudo).Result;

    if (resposta.IsSuccessStatusCode)
    {
        return true;
    }
    else
    {
        return false;
    }
}

Nossos métodos carregam apenas o método HTTP correspondente, serializam ou desserializam os objetos e os retornam.

Na Listagem 12 temos o método AutenticarUsuario recebendo um DTO. Apesar de internamente estarmos enviando as informações via GET diretamente na URL, para o cliente externo isso não será visível, pois ele estará passando um DTO para o método de autenticação.

Listagem 12. Método AutenticarUsuario.

public Boolean AutenticarUsuario(AutenticacaoDto dto)
{
    String uri = String.Format("api/autenticacao/AutenticarUsuario?usuario={0}&senha={1}", dto.Login, dto.Senha);

    HttpResponseMessage resposta = clienteAutenticacao.GetAsync(uri).Result;

    if (resposta.IsSuccessStatusCode)
    {
        return true;
    }
    else
    {
        return false;
    }

}

Por fim, na Listagem 13 temos o método de BuscarUsuarioPorLogin, fechando o serviço de autenticação.

Listagem 13. Método BuscarUsuarioPorLogin.

public UsuarioDto BuscarUsuarioPorLogin(AutenticacaoDto dto)
{
    String uri = String.Format("api/autenticacao/BuscarUsuarioPorLogin?login={0}", dto.Login);

    HttpResponseMessage resposta = clienteAutenticacao.GetAsync(uri).Result;

    if (resposta.IsSuccessStatusCode)
    {
        UsuarioDto contato = new UsuarioDto();
        contato = resposta.Content.ReadAsAsync<UsuarioDto>().Result;

        return contato;
    }
    else
    {
        return null;
    }
}

Na linha 10 fazemos uma serialização entre o objeto JSON e o DTO correspondente para retornar o resultado ao cliente.

Camada de Serviços para Controle de Contatos

O microserviço de controle de contatos é o que reúne todas as formas de trabalho da Web API com o protocolo HTTP, pois tem um CRUD completo que nos permitirá utilizar os métodos HTTP POST, GET, PUT e DELETE, relacionando-os com as operações de Create, Read, Update e Delete, respectivamente.

O projeto deste serviço é o MicroserviceContato.Servico e nele devemos criar a mesma estrutura das pastas do projeto anterior (Interfaces, Excecao e Implementacao).

Teremos aqui as classes e interface responsáveis por expor o serviço de contatos para o cliente.

Adicione a interface IContatoServico na pasta Interfaces e deixe seu código igual ao da Listagem 14 onde temos as assinaturas dos métodos que permitirão a interação com as funcionalidades de CRUD dos contatos.

Listagem 14. Interface IContatoServico.

  public interface IContatoServico
  {
      Boolean CadastrarContato(ContatoDto dto);
  
      Boolean AlterarContato(ContatoDto dto);
  
      Boolean ApagarContato(ContatoDto dto);
  
      IList<ContatoDto> ListaContatosDoUsuario(ContatoDto dto);
  
      ContatoDto ListarContatoPorId(ContatoDto dto);
  }

Após ter criado a interface, adicione na pasta Implementacao a classe ContatoServico que deve implementar a interface IContatoServico, assim como foi feito no serviço de autenticação.

A seguir veremos a implementação de alguns métodos dessa classe, porém outros serão omitidos para fins de simplificação deste artigo, mas podem ser implementados seguindo a mesma lógica da classe AutenticacaoServico.

Na Listagem 15 temos o método AlterarContato, que utiliza o método PUT e serve para alterar dados no servidor, por isso utilizamos o método PutAssync do objeto HttpClient.

Listagem 15. Método AlterarContato.

public Boolean AlterarContato(ContatoDto dto)
{
    HttpContent conteudo = new ObjectContent<ContatoDto>(dto, this.formatadorJson);

    HttpResponseMessage resposta = this.cliente.PutAsync("api/Contato/AlterarContato", conteudo).Result;

    if(resposta.IsSuccessStatusCode){
        return true;
    } else {
        return false;
    }
}

Em seguida temos o método ApagarContato, que como o nome sugere faz exclusão de um contato e por isso utiliza o método HTTP GET. Na Listagem 16 vemos a implementação desse método, que na linha 4 usa o DeleteAssync do HttpClient e retorna um booleano indicando o sucesso da operação.

Listagem 16. Método ApagarContato.

public Boolean ApagarContato(ContatoDto dto)
{
    HttpResponseMessage resposta = cliente.DeleteAsync("api/Contato/ApagarContato/" + dto.Id).Result;

    if (resposta.IsSuccessStatusCode) {
        return true;
    } else {
        return false;
    }
}

O outro método que merece destaque é o ListaContatosDoUsuario, que deve retornar uma lista de objetos a partir do método GET equivalente na API. O código desse método é apresentado na Listagem 17.

Listagem 17. Método ListaContatosDoUsuario.
       
public IList<ContatoDto> ListaContatosDoUsuario(ContatoDto dto)
{
    IList<ContatoDto> contatos = new List<ContatoDto>();

    HttpResponseMessage resposta = cliente.GetAsync("api/Contato/ListarContatosDoUsuario/" + dto.IdUsuario).Result;

    if (resposta.IsSuccessStatusCode)
    {
        contatos = resposta.Content.ReadAsAsync<IList<ContatoDto>>().Result;

        return contatos;
    }
    else
    {
        return null;
    }
}

Caso haja alguma dúvida com relação a esses métodos, basta verificar no código fonte disponibilizado com este artigo, pois foram criados testes unitários específicos para essas camadas de serviço.

Em seguida, vamos mostrar como criar um Gateway e unir essas camadas de serviço em apenas uma API.

Criando o Gateway da API

De acordo com a Arquitetura de Microserviços, uma Gateway API é o ponto de junção de todos os endpoints, agindo como uma camada intermediária entre o microserviço e o cliente.

Esse conceito é muito abrangente, muitos projetos englobam uma infraestrutura como DMZ – Zona Desmilitarizada, balanços de carga, DNSs reversos, etc.

Aqui em nosso artigo, nosso gateway servirá para receber a camada de serviços e dispor de uma implementação com injeção de dependências feita de forma manual.

Não utilizaremos nenhum framework para tal, porém, é possível utilizar sem a necessidade de instâncias.

Com as interfaces criadas nas camadas de serviço, o cliente pode utilizar uma fachada que podemos disponibilizar, se possível, para deixar nossa estrutura de microserviços a mais personalizada possível e de acordo com as necessidades de nossos clientes.

O projeto de nosso Gateway se chama Microservice.GatewayApi, nele temos as abordagens informadas anteriormente, porém, vamos utilizar a mais simples e didática.

As outras abordagens deixaremos para que o leitor analise, porém é necessária cautela, pois as outras abordagens desfazem a necessidade de utilizar uma camada de serviços para acessar diretamente o gateway e então gerarmos toda a programação existente na camada de serviços, só que em apenas um local.

A abordagem que utilizaremos é apenas centralizar o uso das camadas de serviço.

Deixamos a cargo do Gateway a correta configuração do endpoint e com isso não há qualquer necessidade dessa configuração via código. Então, o que precisamos é apenas expôr os serviços através da instânciação do Gateway e para isso fizemos de duas formas: instanciação direta, classe GatewayApiProxy, ou instância via injeção de dependências, classe Gateway

Para fins de comparação, mostraremos as duas classes e em seguida mostraremos um teste unitário mostrando cada implementação.

As interfaces também nos permitem apontar assinaturas das propriedades e é apenas isso que implementamos na em GatewayApiProxy.

Na Listagem 18 temos a implementação da classe GatewayApiProxy.

Listagem 18. Classe GatewayApiProxy.

public class GatewayApiProxy
{
    #region Atributos

    private IContatoServico contatoServico;
    private IAutenticacaoServico autenticacaoServico;

    #endregion

    #region Construtores

    public GatewayApiProxy()
    {

    }

    #endregion

    #region Propriedades

    public IContatoServico ContatoServico
    {
        get
        {
            if (this.contatoServico == null)
            {
                this.contatoServico = new ContatoServico();
            }
            return this.contatoServico;
        }

        set
        {
            this.contatoServico = value;
        }
    }

    public IAutenticacaoServico AutenticacaoServico
    {
        get
        {
            if (this.autenticacaoServico == null){
                this.autenticacaoServico = new AutenticacaoServico();
            }
            return this.autenticacaoServico;
        }

        set
        {
            this.autenticacaoServico = value;
        }
    }
    
    #endregion
}

Nesta classe vamos precisar criar a injeção de dependências diretamente no cliente. Isso não é um problema, porém em alguns projetos pode ser que gere um trabalho a mais e mais algumas linhas de código.

Caso prefira, o leitor pode criar a injeção de dependências diretamente na classe, deixamos isso a cargo deste.

Vamos criar então um novo projeto de testes unitários para o Gateway, nomeando-o como TesteGatewayApi. Na Listagem 19 temos um trecho da classe de testes que deve ser adicionada nesse projeto, destacando o método que testará a autenticação do usuário.

Listagem 19. Uso do Gateway como um Proxy.

private GatewayApiProxy proxy;

public GatewayApiProxy Proxy
{
    get
    {
        if (this.proxy == null){
            this.proxy = new GatewayApiProxy();
        }
        return this.proxy;
    }

    set
    {
        this.proxy = value;
    }
}

[TestMethod]
[Description("Testa a implementação da Autenticacao")]
[TestCategory("Implementacao - GatewayApiProxy - Autenticação")]
public void AutenticarUsuarioComGatewayApiProxy()
{
    Boolean sucesso;

    AutenticacaoDto dto = new AutenticacaoDto()
    {
        Login = "gabrielsimas",
        Senha = "t3st3,123"
    };

    sucesso = Proxy.AutenticacaoServico.AutenticarUsuario(dto);

    Assert.IsTrue(sucesso);

}

Quando executarmos o teste, o mesmo passará sem problemas. Porém podemos também precisar em algum projeto de apenas utilizar o Gateway sem instanciá-lo, para esse propósito criamos a classe <GatewayApiInstancia, na qual temos métodos estáticos, como vemos na Listagem 20.

Listagem 20. Classe GatewayApiInstancia.

public class GatewayApiInstancia
{
    #region Atributos

    private static IContatoServico instanciaContato;
    private static IAutenticacaoServico instanciaAutenticacao;

    #endregion

    #region Construtores

    private GatewayApiInstancia()
    {

    }

    #endregion

    #region Propridades

    public static IContatoServico InstanciaContato
    {
        get
        {
            if (instanciaContato == null)
            {
                instanciaContato = new ContatoServico();
            }

            return instanciaContato;
        }
    }

    public static IAutenticacaoServico InstanciaAutenticacao
    {
        get
        {
            if (instanciaAutenticacao == null){
                instanciaAutenticacao = new AutenticacaoServico();
            }

            return instanciaAutenticacao;
        }
    }

    #endregion
}

Como podemos ver, essa é uma “implementação limpa”, ou seja, basta adicionar como referência e então utilizar o Gateway sem qualquer impedimento.

Criamos testes unitários que comprovam o funcionamento desta classe. Não há a necessidade de criação de propriedades para injeção de dependências, como podemos ver na Listagem 21.

Listagem 21. Teste do GatewayApiInstancia.

[TestMethod]
[Description("Testa a implementação da Autenticacao")]
[TestCategory("Implementacao - GatewayAPIInstancia - Autenticação")]
public void CadastrarUsuarioComGatewayApiInstancia()
{
    Boolean sucesso;
    UsuarioDto dto = new UsuarioDto()
    {

        Conta = "gabrielsimas",
        Email = "gabrielsimas@familiasimas.net",
        NomeCompleto = "Gabriel Simas",
        Senha = "t3st3,123"
    };

    sucesso = GatewayApiInstancia.InstanciaAutenticacao.CadastrarUsuario(dto);

    Assert.IsTrue(sucesso);
}

Como podemos notar, não precisamos fazer ou criar nada, apenas utilizar os métodos estáticos necessários, como mostrado na linha 16.

Já temos o nosso Gateway API criado, ou seja, já temos a última fronteira. Para utilizá-lo, basta fazer como nos testes unitários, abrir o controller e substituir o método de negócio pelo método do Gateway API.

Utilizaremos agora o OWIN através do Projeto Katana da Microsoft, que é uma implementação do OWIN pela Microsoft e que substitui o IIS por uma implementação direta e sem a necessidade de utilizar todas as bibliotecas padrão, utilizando apenas o suficiente para manter a aplicação no ar e com o mínimo de dependências.

Implementando o Microserviço com Katana

É sabido e notado que o ASP.NET é muito pesado, lento e completamente dependente de seus assemblies entre si. Tanto é verdade que quando “subimos” uma aplicação web, seja ela com MVC ou Web API, há uma grande demora no deploy, dado esse peso de assemblies e dependências.

Com o advento da computação em nuvem, foi necessário deixar tal modelo monolítico de lado e criar algum esforço que apenas utilizasse, como módulos, o que fosse necessário e o mínimo possível para manter a aplicação funcionando sem onerar os recursos e a disponibilidade de uma aplicação web.

Pensando nisso, foi criado o padrão aberto OWIN, que significa Open Web Interface for .NET, que foi inspirado em uma comunidade de outra linguagem de programação, o Ruby on Rails. Não vamos nos aprofundar muito na parte teórica do assunto, pois foge do escopo deste artigo, então vamos apenas criar um novo projeto.

Esse novo projeto irá hospedar o endpoint de nossa web api contendo os nossos microserviços localmente, porém não precisaremos alterar nada, a não ser o endpoint no arquivo de configuração no Gateway API.

O projeto que criaremos, chamado Microservicos.Owin e que é uma aplicação Console cuja estrutura final é mostrada na Figura 15, é uma mostra de como uma aplicação web pode funcionar com um novo padrão mais dinâmico e mais leve, sem a necessidade de todos os assemblies que não utilizamos e que sempre causam lentidão no deploy.

Arquitetura de Microserviços em .NET
Figura 15. Estrutura do Projeto OWIN.

Como podemos notar, o projeto é de console, mas existe uma estrutura bem semelhante à de uma aplicação MVC, sem os models e as views.

Precisamos criar essa estrutura e baixar os pacotes necessários para que possamos transformar uma aplicação console em uma aplicação ASP.NET MVC Web API e isso é feito pela classe Inicializacao, criada no diretório de infraestrutura, que garantirá que tudo isso funcione e seja montado dentro do método Main em Program.cs

Aqui também precisamos instalar alguns pacotes pertinentes, principalmente os pacotes do OWIN para a implementação do Web API, como mostra a Figura 16.

Arquitetura de Microserviços em .NET
Figura 16. Pacotes instalados.

O pacote Microsot.AspNet.WebApi.Owin nos permite, através do método UseWebApi(), e com um mínimo esforço, criar uma aplicação auto hospedada, ou seja, sem a necessidade de utilização de um servidor web externo.

Uma aplicação auto hospedada contém uma codificação para receber requisições HTTP, como se agisse como um servidor web embarcado, o que permite que possamos utilizar a Web API em uma aplicação sem a dependência de System.Web.

O pacote Microsoft.AspNet.WebApi.OwinSelfHost instala algumas referências novas em nosso projeto, dentre eles os pacotes Microsoft.Owin.Hosting e Microsoft.Owin.Host.HttpListener. Com essas duas bibliotecas, nossa aplicação pode funcionar como seu próprio host e receber requisições HTTP sobre uma porta específica que configuramos em nossa aplicação.

Então basta executar o projeto, que por ser um console gerará um executável e com ele nossa aplicação estará no ar sem a necessidade do IIS.

Tivemos de criar a mesma estrutura existente nos projetos Web API mostrados na primeira parte deste artigo, porém, ao invés de criarmos dois projetos criamos um projeto só com todos os métodos.

Copiamos e colamos os conteúdos dos controllers dos microserviços da primeira parte, quanto a isso, não há a necessidade de reproduzir os códigos pois são os mesmos, mas mostraremos toda a infraestrutura por trás desse projeto e destacaremos duas classes importantes: Inicializacao e Program, esta última onde está localizado o método Main.

A classe Inicializacao, como o nome já diz, é responsável pela configuração das rotas. Na Listagem 22 temos o conteúdo dessa classe, que é bem pequeno comparado ao que teríamos que fazer.

Listagem 22.Classe de Inicialização do Módulo Katana.

public class Inicializacao
{

    public void Configuration(IAppBuilder aplicacao)
    {
        HttpConfiguration microservicoStack = ConfigurarMicroservicoStack();

        aplicacao.UseWebApi(microservicoStack);            
    }

    private HttpConfiguration ConfigurarMicroservicoStack()
    {
        HttpConfiguration configuracao = new HttpConfiguration();

        configuracao.Routes.MapHttpRoute(
            "ApiComActions",
            "api/{controller}/{action}/{id}",
            new { id = RouteParameter.Optional });

        return configuracao;
    }

}

Com podemos ver, tudo o que estamos fazendo é configurar a nossa rota padrão aqui, muito parecido ao que nós fazemos no projeto original que é criado pelo Visual Studio quando selecionamos o projeto Web API.

Porém ao invés de adicionarmos rotas específicas para a coleção de rotas no ASP.NET, estamos passando HttpConfiguration como um argumento ao método aplicacao.UseWebApi(microservicoStack), que podemos ver na linha 08.

Para finalizar nosso microserviço auto hospedado, tudo o que vamos precisar é configurar tudo o que fizemos na Listagem 22 dentro do método Main() para iniciar a funcionalidade de servidor permitida por HttpListener. Vemos essa implementação e configuração final na Listagem 23.

Listagem 23. Método Main() do microserviço auto hospedado.

 using System;
 using Microservicos.Owin.Infraestrutura;
 Using Microsoft.Owin.Hosting;
 
  namespace Microservicos.Owin
 {
    class Program
    {
        static void Main(string[] args)
        {
 
            //Especifica a Uri padrão
            string uri = "http://localhost:8091";
 
            Console.WriteLine("Iniciando Servidor Web ...");
 
            //Inicializa a nossa aplicação via Classe Inicializar
            WebApp.Start<Inicializacao>(uri);
            Console.WriteLine("Servidor rodando em {0} 0 Pressione Enter para sair!", uri);
            Console.ReadLine();
 
        }
    }
 }

Agora, basta rodar o projeto e veremos uma aplicação de console como mostrado na Figura 17.

Arquitetura de Microserviços em .NET
Figura 17. Microserviço auto hospedado.

Caso queiramos utilizá-lo, basta modificar o enpoint no Gateway ou acessar via Fiddler que temos o mesmo comportamento que outrora, a diferença é que o tempo de resposta será muito menor se comparado ao hospedado via Web API real.

E com isso temos o nosso Projeto de Microserviços com consumo e testes unitários completo. Mas a arquitetura não para por aí, muitos itens podem ser adicionados a esse projeto.

Microserviços é uma arquitetura que não é nova, mas que agora está tendo um grande destaque por causa da arquitetura em nuvem e, com isso, quebrar camadas de acesso a dados e regras de negócio em microserviços tem se tornado muito comum, de forma que em um futuro próximo, só deveremos encontrar uma arquitetura monolítica apenas em projetos onde essa arquitetura seja realmente muito necessária.

Podemos melhorar nossos microserviços adicionando autenticação a eles através de OData ou OAuth 2, como fazem Facebook, Twitter, e outros serviços conhecidos. Podemos acoplar ainda o Log4Net em nossos microserviços e gravar os logs no banco de dados para depois minerar os dados para identificar erros de estrutura ou outros.

Outra possibilidade seria refazer a engenharia e acoplar nossas necessidades em nossos microserviços, pensando também em utilizar o NoSQL como alternativa para bases de dados que não necessitem de padrão relacional, como o nosso microserviço de autenticação, e quem sabe criar um microserviço para gerenciar as exceções e gravá-las em um Cassandra, CouchBase ou MongoDB.

Rapidamente surgem muitas ideias para evoluir nossos microserviços criados, e o principal conceito dessa arquitetura é justamente esse: arquitetura evolutiva e sem os problemas de engessamento de projeto que uma arquitetura monolítica dispõe.

Links
Fiddler

http://www.telerik.com/fiddler
Microsoft OWIN e Projeto Katana
http://www.asp.net/aspnet/overview/owin-and-katana/
an-overview-of-project-katana

Confira também