Web API

Verificando a integridade da aplicação ASP.NET Core com Health Checks

Alguns artigos passados falei sobre as ferramentas de linha de comando que permitem diagnosticar uma aplicação no .NET Core 3.0. Por serem ferramentas de linha de comando, elas requerem acesso a máquina onde a aplicação está instalada. Infelizmente em muitos cenários algo assim não é possível, mas o monitoramento do status da aplicação não deixa de ser importante nesses casos.

Se tratando de aplicações ASP.NET, antigamente o comum era criar um endpoint que retornasse o estado da aplicação ou menos nada era definido. Mas com a adoção cada vez maior de microsserviços e a criação de aplicações para um ambiente distribuído, implementar a verificação da integridade da aplicação se tornou algo essencial.

Felizmente, a partir da versão 2.2 do ASP.NET Core, foi introduzido os Health Checks que facilitam este trabalho.

Conhecendo os Health Checks

Health check é um middleware que fornece um endpoint que retorna o status da aplicação. Na sua versão básica, a aplicação é considerada saudável caso retorne o código 200 (OK) para uma solicitação web. Mas também são fornecidas bibliotecas que nos permite verificar o status de serviços utilizados pela aplicação, como: banco de dados, sistema de mensageria, cache, logging, serviços externos ou mesmo a criação de um health check customizado.

Aplicação que terá a integridade verificada

Para exemplificar o uso do health check, vou utilizar uma aplicação ASP.NET Core Web API simples, que já foi mostrada no artigo sobre a documentação de uma ASP.NET Core Web API com o Swagger, que você pode ver no meu Github.

Adicionando o Health Check na aplicação

Agora que já temos a aplicação, já podemos ativar o health check nela. Para isso, basta adicionar o service no método ConfigureServices da classe Startup e indicar a URL dele no método Configure:

public void ConfigureServices(IServiceCollection services)
{
    //....
    services.AddHealthChecks();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //...

    //Ativa o HealthChecks
    app.UseHealthChecks("/status");
    //...
}

Agora, ao executar a aplicação e acessar a URL /status será exibido se ela está saudável ou não:

Como está sendo testado apenas se ela responde a uma requisição, a aplicação é considerada saudável.

Integridade do banco de dados

Existem várias formas de testar a integridade do banco de dados. É fornecido uma gama de pacotes que nos permite testar a integridade do banco de acordo com o SGBD:

  • SQL Server: AspNetCore.HealthChecks.SqlServer;
  • MySQL: AspNetCore.HealthChecks.MySql;
  • PostgreSQL: AspNetCore.HealthChecks.Npgsql;
  • SQLite: AspNetCore.HealthChecks.SqLite;
  • Oracle: AspNetCore.HealthChecks.Oracle;
  • MongoDb: AspNetCore.HealthChecks.MongoDb.

Caso esteja trabalhando com o Entity Framework, também há o pacote Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore, que será o que utilizaremos na aplicação:

dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore

Com isso, basta adicionar a classe DbContext, que o estado da conexão com o banco será verificada:

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.AddHealthChecks()
            .AddDbContextCheck<TodoContext>();
}

Se o banco estiver saudável, o resultado não será alterado:

Caso haja algum problema com a conexão com o banco, a aplicação não será considerada saudável:

Alterando as informações exibidas

No momento, somos informados apenas se a aplicação está ou não saudável. Para uma aplicação pequena, isso pode ser útil, mas imagine uma aplicação que verifica vários serviços? Apenas com esta informação não conseguiremos saber qual serviço não está funcionando corretamente.

Para obter esses dados, podemos customizar as informações de saída do health check, definindo um delegate na opção ResponseWriter:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //...

    //Ativa o HealthChecks
    app.UseHealthChecks("/status", new HealthCheckOptions()
    {
        // WriteResponse é um delegate que permite alterar a saída.
        ResponseWriter = (httpContext, result) => {
            httpContext.Response.ContentType = "application/json";

            var json = new JObject(
                new JProperty("status", result.Status.ToString()),
                new JProperty("results", new JObject(result.Entries.Select(pair =>
                    new JProperty(pair.Key, new JObject(
                        new JProperty("status", pair.Value.Status.ToString()),
                        new JProperty("description", pair.Value.Description),
                        new JProperty("data", new JObject(pair.Value.Data.Select(
                            p => new JProperty(p.Key, p.Value))))))))));
            return httpContext.Response.WriteAsync(json.ToString(Formatting.Indented));
        }
    });
    //...
}

Com isso, obteremos a causa do problema da aplicação:

Criando health checks customizados

Existe uma grande gama de health checks definidos, mas em algumas situações, pode ser necessário definir um customizado. Para isso, é necessário definir uma classe que implemente a interface IHealthCheck e definir no método CheckHealthAsync qual tipo de verificação ele fará:

public class SelfHealthCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        return Task.FromResult(new HealthCheckResult(
            HealthStatus.Healthy,
            description: "API up!"));
    }
}

Acima é definido um health check simples, que apenas indica que a API está funcionando.

Agora é necessário adicioná-lo ao serviço com o método AddCheck:

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.AddHealthChecks()
            .AddDbContextCheck<TodoContext>()
            .AddCheck<SelfHealthCheck>("Self");
}

Também pode ser definido um método de extensão:

public static class HealthCheckBuilderExtensions
{
    public static IHealthChecksBuilder AddSelfCheck(this IHealthChecksBuilder builder, string name, HealthStatus? failureStatus = null, IEnumerable<string> tags = null)
    {
        // Register a check of type SelfHealthCheck
        builder.AddCheck<SelfHealthCheck>(name, failureStatus ?? HealthStatus.Degraded, tags);

        return builder;
    }
}

E utilizá-lo para registrar o health check:

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.AddHealthChecks()
            .AddDbContextCheck<TodoContext>()
            .AddSelfCheck("Self");
}

Ao acessar a aplicação, ele também será mostrado:

Adicionando uma interface

No momento o resultado do status da aplicação é um JSON, mas existe um pacote que nos permite visualizar as informações em uma interface gráfica. Para isso, adicione na aplicação os pacotes AspNetCore.HealthChecks.UI e AspnetCore.HealthChecks.UI.Client:

dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspnetCore.HealthChecks.UI.Client

Em seguida é necessário habilitar o Health Checks UI nos métodos ConfigureServices e Configure da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    //....
    services.AddHealthChecks()
            .AddDbContextCheck<TodoContext>()
            .AddSelfCheck("Self");

    services.AddHealthChecksUI();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //...

    //Ativa o HealthChecks
    app.UseHealthChecks("/status", new HealthCheckOptions()
    {
        // WriteResponse is a delegate used to write the response.
        ResponseWriter = (httpContext, result) => {
            httpContext.Response.ContentType = "application/json";

            var json = new JObject(
                new JProperty("status", result.Status.ToString()),
                new JProperty("results", new JObject(result.Entries.Select(pair =>
                    new JProperty(pair.Key, new JObject(
                        new JProperty("status", pair.Value.Status.ToString()),
                        new JProperty("description", pair.Value.Description),
                        new JProperty("data", new JObject(pair.Value.Data.Select(
                            p => new JProperty(p.Key, p.Value))))))))));
            return httpContext.Response.WriteAsync(json.ToString(Formatting.Indented));
        }
    });

    //Ativa o HealthChecks utilizado pelo HealthCheckUI
    app.UseHealthChecks("/status-api", new HealthCheckOptions()
    {
        Predicate = _ => true,
        ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
    });

    app.UseHealthChecksUI(opt => {
        opt.UIPath = "/status-dashboard";
    });

    //...
}

Além de habilitar o Health Checks UI note que também é definido um novo endpoint para o health check, configurado para o delegate UIResponseWriter.WriteHealthCheckUIResponse. Este é um delegate criado pelo Health Checks UI, e configura um arquivo JSON que será lido pela biblioteca.

Para que ele seja lido, é necessário especificá-lo no arquivo appsettings.json:

{
  "HealthChecks-UI": {
    "HealthChecks": [
      {
        "Name": "Status-API",
        "Uri": "https://localhost:5001/status-api"
      }
    ]
  }
}

Com isso, ao executar a aplicação e acessar a URL /status-dashboard a interface será exibida:

Por hoje é isso 🙂

Lembrando que você pode ver o código da aplicação no Github.

Documentando uma ASP.NET Core Web API com o Swagger

Com cada vez mais aplicações multiplataformas, é comum definir uma API REST que será consumida pelas várias versões da aplicação. Mas com vários times tendo acesso a API, não basta mais apenas desenvolvê-la e esperar que todos consigam utilizá-la institivamente.

Na época do web service, existia o WSDL que funcionava como uma documentação, já que facilitava a criação dos clientes que iria consumí-lo. Com as APIs REST não temos esta facilidade, assim, torna-se imprescindível que ela seja bem documentada.

Mas como realizar esta documentação? Existem algumas ferramentas que podem nos auxiliar neste processo, como: API Blueprint, RAML, Swagger, entre outras.

Como o nome deste artigo sugere, aqui abordaremos o Swagger.

Swagger

O Swagger é uma aplicação open source que auxilia os desenvolvedores a definir, criar, documentar e consumir APIs REST. Sendo uma das ferramentas mais utilizadas para esta função, a empresa por trás dela (a SmartBear Software), decidiu criar o Open API Iniciative e renomearam as especificações do Swagger para OpenAPI Specification.

Ela visa padronizar as APIs REST, desta forma descreve os recursos que uma API deve possuir, como endpoints, dados recebidos, dados retornados, códigos HTTP, métodos de autenticação, entre outros.

Para facilitar o processo de especificação/documentação de uma API, o Swagger fornece algumas ferramentas, como:

  • Swagger Editor: Editor que permite especificar uma API. Há uma versão online grátis e aplicativos para desktop;
  • Swagger UI: Interface interativa para a documentação da API;
  • Swagger Codegen: Gera templates de código (mais de 20 linguagens estão disponíveis) de acordo com uma especificação de API.

Neste artigo vamos focar na documentação de uma API existente, criada em ASP.NET Core Web API.

Especificação do Swagger

O ponto mais importante do Swagger é a sua especificação, que era chamada de Swagger specification e agora é OpenAPI Specification. Esta especificação trata-se de um documento, JSON ou YAML, que define a estrutura da API. Indicando os endpoints, formatos de entrada, de saída, exemplos de requisições, forma de autenticação, etc.

Abaixo você pode ver um exemplo desta especificação definida em JSON:

{
   "swagger": "2.0",
   "info": {
       "version": "v1",
       "title": "API V1"
   },
   "basePath": "/",
   "paths": {
       "/api/Example": {
           "get": {
               "tags": [
                   "Example"
               ],
               "operationId": "ApiExampleGet",
               "consumes": [],
               "produces": [
                   "text/plain",
                   "application/json",
                   "text/json"
               ],
               "responses": {
                   "200": {
                       "description": "Success",
                       "schema": {
                           "type": "array",
                           "items": {
                               "$ref": "#/definitions/Item"
                           }
                       }
                   }
                }
           },
           "post": {
               ...
           }
       },
       "/api/Example/{id}": {
           "get": {
               ...
           },
           "put": {
               ...
           },
           "delete": {
               ...
   },
   "definitions": {
       "Item": {
           "type": "object",
            "properties": {
                "id": {
                    "format": "int64",
                    "type": "integer"
                },
                "name": {
                    "type": "string"
                },
                "isValid": {
                    "default": false,
                    "type": "boolean"
                }
            }
       }
   },
   "securityDefinitions": {}
}

É possível definir esta especificação na mão, olhando as opções na documentação, através do Swagger Editor ou utilizando outra ferramenta para gerá-la.

No ASP.NET Core podemos utilizar duas bibliotecas para gerar este documento:

Por ser mais completa, faremos uso da Swashbuckle.

Adicionando o Swashbuckle na aplicação

Para este exemplo, estou utilizando uma aplicação ASP.NET Core Web API, baseada no exemplo fornecido pela documentação da Microsoft, que pode ser vista aqui. Você pode ver a aplicação que criei no meu Github.

Com a aplicação Web API criada, inicialmente é necessário adicionar o pacote do Swashbuckle na aplicação. Ele possui três componentes:

Mas é necessário instalar apenas o pacote Swashbuckle.AspNetCore. Por se tratar de uma aplicação .NET Core, este pacote pode ser adicionado com o comando abaixo:

dotnet add package Swashbuckle.AspNetCore

Após isso, para que o arquivo de especificação da API do Swagger seja criado, é necessário adicionar o gerador dele nos serviços da aplicação, no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    //... código omitido

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new Info { Title = "TodoAPI", Version = "v1" });
    });
}

Para a classe Info, defina o using do namespace abaixo:

using Swashbuckle.AspNetCore.Swagger;

Agora, no método Configure é necessário ativar o Swagger e indicar o local onde o seu arquivo de especificação será criado:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //Ativa o Swagger
    app.UseSwagger();

    // Ativa o Swagger UI
    app.UseSwaggerUI(opt =>
    {
        opt.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoAPI V1");
    });

    //... código omitido
}

Com isso, através de reflection a biblioteca consegue especificar os endpoints da aplicação.

Caso seja executada, podemos acessar o Swagger UI da aplicação no caminho /swagger:

Caso queira que o Swagger UI seja acessado a partir da raiz da aplicação, na ativação dela, no método Configure, atribua vazio para a propriedade RoutePrefix:

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoAPI V1");
    c.RoutePrefix = string.Empty;
});

Customizando o Swagger

Como o Swashbuckle trabalha com reflection, podemos fazê-lo gerar mais informações das nossas APIs com base em configurações definidas nela.

Comentários de documentação

Sabemos que no C# podemos adicionar comentários de documentação nos nossos códigos, como:

/// <summary>
/// Lista os itens da To-do list.
/// </summary>
/// <returns>Os itens da To-do list</returns>
/// <response code="200">Returna os itens da To-do list cadastrados</response>
[HttpGet]
public ActionResult<List<Item>> Get()
{
    return _repository.GetAll();
}

É possível fazer o Swashbuckle ler esses comentários e assim tornar a especificação da API mais detalhada.

Para isso, na definição de geração do arquivo de especificação, no método ConfigureServices deve ser adicionado a informação abaixo:

services.AddSwaggerGen(opt =>
{
    opt.SwaggerDoc("v1", new Info { Title = "TodoAPI", Version = "v1" });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    opt.IncludeXmlComments(xmlPath);
});

Note que estamos indicando que ele deve ler os comentários do código a partir de um arquivo XML. Para que eles sejam gerados neste arquivo indicado, no arquivo de configuração do projeto (o .csproj), adicione o trecho abaixo:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

O <NoWarn> é definido para que a IDE não exiba um alerta sobre os códigos que não possuírem comentários de documentação.

Agora ao acessar o Swagger API, o que foi definido em <summary> será mostrado:

Também podemos o <remarks> para indicar um exemplo de requisição:

// POST api/todo
/// <summary>
/// Cria um item na To-do list.
/// </summary>
/// <remarks>
/// Exemplo:
///
///     POST /Todo
///     {
///        "id": 1,
///        "name": "Item1",
///        "iscomplete": true
///     }
///
/// </remarks>
/// <param name="value"></param>
/// <returns>Um novo item criado</returns>
/// <response code="201">Retorna o novo item criado</response>
/// <response code="400">Se o item não for criado</response>        
[HttpPost]
public ActionResult<Item> Post([FromBody] Item value)
{
    Console.WriteLine(value?.Name);

    var item = _repository.Save(value);
    if(item != null)
        return item;

    return BadRequest();
}

O resultado será:

Com as anotações ProducesResponseType e Produces podemos indicar os códigos HTTP e o tipo de dado que os endpoints irão retornar:

[Authorize]
[Produces("application/json")]
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
    //..código omitido

    // GET api/todo
    /// <summary>
    /// Lista os itens da To-do list.
    /// </summary>
    /// <returns>Os itens da To-do list</returns>
    /// <response code="200">Returna os itens da To-do list cadastrados</response>
    [HttpGet]
    [ProducesResponseType(200)]
    public ActionResult<List<Item>> Get()
    {
        return _repository.GetAll();
    }

    //... código omitido
}

Especificando a autenticação

Uma das principais vantagens de adicionar o Swagger na aplicação, é poder testar os endpoints pela Swagger UI. Quando a aplicação define autenticação, é necessário indicar isso para o Swashbuckle, para que ele também gere uma especificação desde detalhe.

Com isso, será possível testar os endpoints pela Swagger UI utilizando a autenticação definida.

Para fazer isso, devemos adicionar a informação abaixo, na definição de geração do arquivo de especificação, no método ConfigureServices:

services.AddSwaggerGen(opt =>
{
    opt.SwaggerDoc("v1", new Info { Title = "TodoAPI", Version = "v1" });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    opt.IncludeXmlComments(xmlPath);

    var security = new Dictionary<string, IEnumerable<string>>
    {
        {"Bearer", new string[] { }},
    };

    opt.AddSecurityDefinition(
        "Bearer",
        new ApiKeyScheme
        {
            In = "header",
            Description = "Copie 'Bearer ' + token'",
            Name = "Authorization",
            Type = "apiKey"
        });

    opt.AddSecurityRequirement(security);
});

Acima estamos definindo que a aplicação faz uso de autenticação do tipo JWT Token (“Bearer Token”). Na documentação do Swagger, você pode ver os outros tipos de autenticação suportados.

Ao acessar o Swagger UI, ele irá mostrar o botão “Autorize”:

Exibe um botão com o texto "Autorize"

Ao clicar nele, é possível informar o tipo de autenticação especificado:

Com isso, mesmo que necessite de autenticação, será possível realizar as requisições dos endpoints da aplicação pelo Swagger UI.

Descrição

Por fim, é possível descrever um pouco da aplicação através das propriedades da classe Info da especificação do Swagger:

services.AddSwaggerGen(opt =>
{
    opt.SwaggerDoc("v1", new Info
    {
        Version = "v1",
        Title = "Todo API",
        Description = "Um exemplo de aplicação ASP.NET Core Web API",
        TermsOfService = "Não aplicável",
        Contact = new Contact
        {
            Name = "Wladimilson",
            Email = "contato@treinaweb.com.br",
            Url = "https://treinaweb.com.br"
        },
        License = new License
        {
            Name = "CC BY",
            Url = "https://creativecommons.org/licenses/by/4.0"
        }
    });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    opt.IncludeXmlComments(xmlPath);

    var security = new Dictionary<string, IEnumerable<string>>
    {
        {"Bearer", new string[] { }},
    };

    opt.AddSecurityDefinition(
        "Bearer",
        new ApiKeyScheme
        {
            In = "header",
            Description = "Copie 'Bearer ' + token'",
            Name = "Authorization",
            Type = "apiKey"
        });

    opt.AddSecurityRequirement(security);
});

O resultado será:

Swagger UI mostrando a descrição da API

Conclusão

O Swagger é uma ótima forma de documentar APIs REST e com a biblioteca Swashbuckle, podemos gerar o arquivo de especificação do Swagger com facilidade. Assim, não há mais desculpa para se definir documentações interativas e detalhadas para as suas APIs REST.

Gerando sons com a Web Audio API do JavaScript

Olá, Web Developers!

Sabiam que podemos gerar sons com JavaScript? Isso graças à Web Audio API, que nos fornece poderosas funcionalidades para se trabalhar com áudio. Podemos fazer coisas como adicionar efeitos a um arquivo de áudio ou criar músicas a partir do JavaScript.

Começando com um beep

Para começar, vamos gerar um simples beep.

let context = new AudioContext(),
   oscillator = context.createOscillator();

oscillator.type = 'sine';
oscillator.connect(context.destination);
oscillator.start();

Primeiro nós instanciamos o AudioContext, que é o principal objeto para podermos gerar um som. A partir dele nós criamos um oscilador, que é responsável por emitir um sinal contínuo, que será o nosso som.

Precisamos indicar o tipo de onde que nosso oscilador irá gerar. Indicamos que queremos uma onda senoidal (sine).

Depois conectamos o nosso oscilador ao context.destination, que representa o destino final do áudio. Se você está no computador, provavelmente isso indica as suas caixas de som.
Por fim, executamos a função start() para iniciar o som.

Para parar o som, basta executar oscillator.stop().

Ao parar o som você pode notar um pequeno clique. Isso acontece basicamente quando paramos o som em um lugar qualquer que não seja o ponto zero da onda, como mostrado na imagem abaixo.

Onda Senoidal

Encerrando o som

Um modo de acabar com esse som de clique ao pararmos o som (e que pode ser bem desconfortável) é alterar a nossa onda, como se estivéssemos diminuindo ela.

Para isso, usamos a função exponentialRampToValueAtTime(), que vai alterando de forma exponencial o valor do nosso som.

Vamos então fazer uma pequena alteração em nosso código:

let context = new AudioContext(),
   oscillator = context.createOscillator(),
   contextGain = context.createGain();

oscillator.connect(contextGain);
contextGain.connect(context.destination);
oscillator.start(0);

Agora criamos um ganho, um objeto que nos permitirá trabalhar com a função exponentialRampToValueAtTime(). Ligamos ele ao oscilador e depois ligamos ao destino de saída do som.

Para parar o som, ao invés de simplesmente executarmos a função stop(), vamos executar o seguinte código:

contextGain.gain.exponentialRampToValueAtTime(
    0.00001, context.currentTime + 0.04
    )

O que fizemos aqui foi alterar o valor do ganho, que basicamente seria como se estivéssemos abaixando o volume aos poucos.

Veja que o primeiro parâmetro que passamos é bem próximo de zero. Isso porque essa função não pode receber um valor zerado ou negativo.

O segundo parâmetro é o tempo em que queremos que a alteração seja feita.

Abaixo você pode conferir que não importa mais em que momento pare o som, ele não terá mais aquele clique.

Efeitos ao parar o som

Quando paramos o som, passamos um tempo bem pequeno para que a alteração no ganho fosse realizada (0.4).

Dependendo do tempo que passarmos, podemos ter efeitos interessantes.

Isso acaba nos dando sensações diferentes. E essa sensação fica ainda mais evidente quando iniciamos e já encerramos o som logo em seguida.

Sons bens mais agradáveis do que aquele som contínuo, não é mesmo?

Diferentes ondas, diferentes osciladores

O que criamos até agora foi um som a partir de um oscilador usando uma onda senoidal. Há outros tipos de ondas, como podemos ver na seguinte imagem:

Para alterar o tipo de onda, basta passarmos um dos seguintes códigos:

oscillator.type = 'sine';
oscillator.type = 'square';
oscillator.type = 'triangle';
oscillator.type = 'sawtooth';

Tocando notas

Até o momento nós só tocamos um simples beep. Mas se quisermos tocar algo, precisamos tocar notas musicais diferentes.

Cada nota é um som emitido a uma determinada frequência. Abaixo temos uma tabela que indica cada nota e sua respectiva frequência:

Para alterar a frequência, basta indicar um valor em oscillator.frequency.value. Para tocar um “Dó” (C4):

const C4 = 261.6,
    D4 = 293.7,
  E4 = 329.6,
  F4 = 349.2,
  G4 = 392.0,
  A4 = 440.0,
  B4 = 493.9;

oscillator.frequency.value = C4;

No exemplo, passei a frequência para uma constante apenas para o nosso código ficar mais legível. É bem mais simples e legível deixar o nome das notas do que deixar o número da frequência.

Evoluindo

Caso queira se aprofundar, procure saber mais sobre bibliotecas e frameworks como o Tone.js, uma ferramenta completa com várias funcionalidades para se trabalhar com música interativa no navegador.

Criando uma aplicação distribuída com ASP.NET Core e o Microsoft Orleans

Este é mais um artigo de uma série sobre o Microsoft Orleans. Caso não tenha visto o primeiro, recomendo que o leia: Conhecendo o Microsoft Orleans.

Como dito no artigo passado, o Orleans está na segunda versão e uma das novidades dela é o suporte ao .NET Standard 2.0, desta forma, como o título indica, a aplicação demonstrada aqui utilizará este framework.

Estrutura da solução

Antes de colocarmos a mão na massa, é importante entender como funciona a solução do Orleans. Esta biblioteca recomenda que um projeto que a implemente seja estruturado com no mínimo quatro projetos:

  • OrleansHost: Este projeto irá criar um executável que iniciará os silos, os hosts do Orleans que conterá as instâncias dos grãos. É possível iniciar vários silos, que irão trabalhar em conjunto e compartilhar a carga.

  • GrainsInterfaces: Este projeto contém as interfaces que definem todos os grãos contido nos silos.

  • Grains: Este projeto contém a implementação de todos os grãos definidos em GrainsInterfaces. Por isso, ele é implementado em conjunto com o OrleansHost.

  • Inteface: Este projeto pode ser qualquer projeto de interface, mas geralmente trata-se de API. Ele irá se conectar ao OrleansHost, por TCP, para ter acesso aos grãos.

Durante o desenvolvimento, o projeto interface e o OrleansHost podem ser executados em conjunto na mesma máquina. Mas em produção, geralmente eles são executados separadamente. O OrleansHost é implementado em um cluster e a interface em um servidor web.

Neste artigo, a solução será feita em uma máquina Mac OSX, utilizando a versão 2.1.4, em conjunto com o Visual Studio Code, pois amo este ambiente. Mas o Orleans fornece um plugin de templates para o Visual Studio, que facilita a criação de projetos com esta biblioteca. Desta forma, caso esteja utilizando esta IDE recomendo que instale este plugin.

Criando o projeto GrainsInterfaces

Como o projeto Grains necessita do GrainsInterfaces e o OrleansHost fará uso do Grains, o primeiro projeto que precisa ser criado é o GrainsInterfaces.

Caso esteja utilizando o Visual Studio com o plugin do Orleans instalado, crie um projeto com base no template “Orleans Grain Interface Collection. No meu ambiente, incialmente irei criar uma pasta chamada OrleansDotNet, e dentro dela uma solução:

dotnet new sln -n OrleansDotNet

Em seguida, um projeto Class Library:

dotnet new classlib -n GraosInterfaces

Neste projeto adicione a biblioteca Microsoft.Orleans.OrleansCodeGenerator.Build:

dotnet add package Microsoft.Orleans.OrleansCodeGenerator.Build

E adicione nele a interface abaixo:

using System;
using Orleans;
using System.Threading.Tasks;

namespace OrleansDotNet.GraosInterfaces
{
    public interface IGraoContador: IGrainWithStringKey
    {
        Task Incremento(int incremento);
        Task<int> GetContador();
    }
}

As interfaces grãos devem definir a implementação de uma das interfaces “GrainWith*” do Orleans e definir métodos que retornem uma Task, para métodos void, ou Task, caso o método retorne algum valor.

A interface implementada acima, IGrainWithStringKey, é utilizada para indicar que o código de identificação do grão será definido com uma string. Infelizmente a documentação do Orleans ainda não documenta bem todas as interfaces disponíveis, mas em um artigo futuro abordo todas.

Criando o projeto Grains

Com a interface definida podemos implementá-la um um projeto Grains. Caso esteja utilizando o Visual Studio, crie um projeto com o template Orleans Grain Class Collection. No meu ambiente, irei criar um projeto Class Library chamado Graos:

dotnet new classlib -n Graos

E este projeto também precisa da referência abaixo:

dotnet add package Microsoft.Orleans.OrleansCodeGenerator.Build

Em seguida, referencie o projeto de interface:

dotnet add Graos.csproj reference ../GraosInterfaces/GraosInterfaces.csproj

Agora podemos implementar o nosso grão:

using System;
using Orleans;
using System.Threading.Tasks;
using OrleansDotNet.GraosInterfaces;

namespace OrleansDotNet.Graos
{
    public class GraoContador: Grain, IGraoContador
    {
        private int _contador;

        public Task Incremento(int incremento)
        {
            _contador += incremento;
            return Task.CompletedTask;
        }

        public Task<int> GetContador()
        {
            return Task.FromResult(_contador);
        }
    }
}

Esta classe não tem segredo, o importante é implementar a nossa interface grão e herdar a classe Grain. Dentro dela é definido um contador simples.

Criando o projeto OrleansHost

Com o grão definido, podemos definir o nosso silo (host). Caso esteja utilizando o Visual Studio, crie um projeto com base no template “Orleans Dev/Test Host“.

No meu ambiente irei criar uma aplicação console chamada Silo:

dotnet new console -n Silo

Nele adicione adicione uma referência para a biblioteca Microsoft.Orleans.Server:

dotnet add package Microsoft.Orleans.Server

Como iremos registrar o log do silo do console, adicione a referencia abaixo:

dotnet add package Microsoft.Extensions.Logging.Console

Por fim, adicione a referencia do projeto Graos:

dotnet add Silo.csproj reference ../Graos/Graos.csproj

E na classe Program adicione o código abaixo:

using System;
using Orleans;
using Orleans.Runtime.Configuration;
using Orleans.Hosting;
using Orleans.Configuration;
using System.Threading.Tasks;
using OrleansDotNet.Graos;
using Microsoft.Extensions.Logging;
using System.Runtime.Loader;
using System.Threading;
using System.Net;

namespace OrleansDotNet.Silo
{
    class Program
    {
        private static ISiloHost silo;
        private static readonly ManualResetEvent siloStopped = new ManualResetEvent(false);

        static void Main(string[] args)
        {

            silo = new SiloHostBuilder()
                .UseLocalhostClustering()
                .Configure<ClusterOptions>(options =>
                {
                    options.ClusterId = "OrleansDotNet-cluster";
                    options.ServiceId = "OrleansDotNet";
                })
                .Configure<EndpointOptions>(options => options.AdvertisedIPAddress = IPAddress.Loopback)
                .ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(GraoContador).Assembly).WithReferences())
                .ConfigureLogging(logging => logging.AddConsole())
                .Build();

            Task.Run(StartSilo);

            AssemblyLoadContext.Default.Unloading += context =>
            {
                Task.Run(StopSilo);
                siloStopped.WaitOne();
            };

            siloStopped.WaitOne();

        }

        private static async Task StartSilo()
        {
            await silo.StartAsync();
            Console.WriteLine("Silo iniciado");
        }

        private static async Task StopSilo()
        {
            await silo.StopAsync();
            Console.WriteLine("Silo parado");
            siloStopped.Set();
        }
    }
}

Nesta classe, estamos definindo que o Silo irá utilizar as configurações de um cluster local (UseLocalhostClustering()), também é definido as identificações do cluster, bem com o seu IP (Configure()). Por fim, se define o grão que será adicionado ao silo e onde o seu log deve ser exibido.

Com o silo definido, ele é iniciado. Ele só irá parar em caso de algum erro ou quando o usuário forçar a sua parada.

Criando o projeto Interface

Com o Silo definido podemos criar a nossa aplicação interface. Neste projeto vou criar uma aplicação WebAPI chamada InterfaceAPI:

dotnet new webapi -n InterfaceApi

Aproveite e já vincule os projetos a solução:

dotnet sln OrleansDotNet.sln add **/*.csproj

Na api é necessário adicionar a referência Microsoft.Orleans.Client:

dotnet add package Microsoft.Orleans.Client

E referenciar o projeto GraosInterfaces:

dotnet add InterfaceApi.csproj reference ../GraosInterfaces/GraosInterfaces.csproj

Para trabalhar com “dependency injection“, vamos configurar o cliente do Orleans na classe Startup conforme o código abaixo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Orleans;
using Orleans.Runtime.Configuration;
using Orleans.Hosting;
using System.Net;
using Orleans.Configuration;
using OrleansDotNet.GraosInterfaces;

namespace InterfaceApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddSingleton<IClusterClient>(provider =>
            {
                var client = new ClientBuilder()
                            .UseLocalhostClustering()
                            .Configure<ClusterOptions>(options =>
                            {
                                options.ClusterId = "OrleansDotNet-cluster";
                                options.ServiceId = "OrleansDotNet";
                            })
                            .ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(IGraoContador).Assembly).WithReferences())
                            .ConfigureLogging(logging => logging.AddConsole())
                            .Build();

                StartClientWithRetries(client).Wait();

                return client;
            });
        }

        private static async Task StartClientWithRetries(IClusterClient client)
        {
            for (var i=0; i<5; i++)
            {
                try
                {
                    await client.Connect();
                    return;
                }
                catch(Exception)
                { }
                await Task.Delay(TimeSpan.FromSeconds(5));
            }
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
            app.UseStaticFiles();
        }
    }
}

Note que a configuração do cliente é parecida com o do host:

var client = new ClientBuilder()
            .UseLocalhostClustering()
            .Configure<ClusterOptions>(options =>
            {
                options.ClusterId = "OrleansDotNet-cluster";
                options.ServiceId = "OrleansDotNet";
            })
            .ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(IGraoContador).Assembly).WithReferences())
            .ConfigureLogging(logging => logging.AddConsole())
            .Build();

As diferenças é que agora estamos utilizando a classe ClientBuilder e se adiciona a interface IGraoContador (em detrimento a GraoContador definida no host.

Nesta classe também foi definido o método StartClientWithRetries que tenta se conectar ao host 5 vezes antes de desistir. Quando a conexão for obtida, o objeto client é retornado.

Este cliente será obtido no controller, conforme o código abaixo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Orleans;
using OrleansDotNet.GraosInterfaces;

namespace InterfaceApi.Controllers
{
    [Route("api/[controller]")]
    public class ContadorController : Controller
    {
        private IClusterClient client;

        public ContadorController(IClusterClient client){
            this.client = client;
        }

        [HttpGet]
        public async Task<int> Get()
        {
            var contador = client.GetGrain<IGraoContador>("TW-1");

            return await contador.GetContador();
        }


        [HttpPost]
        public async Task Post()
        {
            var contador = client.GetGrain<IGraoContador>("TW-1");
            await contador.Incremento(1);
        }
    }
}

Para obter o grão, utilizamos o método GetGrain. A este método deve ser passado a chave do grão:

var contador = client.GetGrain<IGraoContador>("TW-1");

Como definimos que a chave dele será uma string, acima estou passando uma string arbitrária. A partir da instância obtida os métodos do grão são chamados, como o GetContador():

return await contador.GetContador();

Com isso, nós estamos obtendo informações do nosso grão contido no silo.

Para finalizar, criei dentro da pasta wwwroot um arquivo HTML, onde os métodos da api são chamados:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">


    <title>Exemplo de Orleans, no ASP.NET Core *.*</title>
</head>
<body>
    <div class="container">
        <div class="card">
            <div class="card-body">
                Contador: <spam id="countValor">0</spam>

                <button id="btnIncrement" class="btn-primary">Incrementar</button>
            </div>
        </div>


    </div>


    <!-- JavaScript -->
    <script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>

    <script type="text/javascript">
        $(document).ready(function () {
            $.get("api/contador/", function (value) {
                console.log('GET contador=' + value);

                $('#countValor').html(value);
            });

            $('#btnIncrement').click(function () {
                $.post("api/contador/", function () {
                    $.get("api/contador/", function (value) {
                        console.log('GET contador=' + value);

                        $('#countValor').html(value);
                    });
                });
                return false;
            });
        });
    </script>
</body>
</html>

Pronto, a nossa aplicação está finalizada. Para testá-la, primeiro é necessário iniciar o Silo:

Em seguida a aplicação web:

No navegador teremos o resultado abaixo:

Ao clicar em “Incrementar“, veremos o valor do contador ser incrementado:

Conclusão

Este é um exemplo simples, porém funcional, que nos dá uma noção do poder do Orleans. Em artigos futuros mostrarei mais detalhes dele.

Caso queria executar a aplicação demostrada aqui no curso, você pode vê-la aqui no GitHub.

JUNTE-SE A MAIS DE 150.000 PROGRAMADORES