Israel Aece

Software Developer

Integração do IIS com o DNX.exe

Como sabemos, o ASP.NET 5 é desenvolvido para ser independente de hosting, ou seja, podemos hospedar as aplicações no IIS, via self-hosting (WebListener) e com o, novo hosting, Kestrel, que é multi-plataforma. Em uma nova atualização que a Microsoft liberou do ASP.NET 5 (beta8), foi realizada uma mudança na forma como as aplicações ASP.NET 5 rodam no IIS.

Até então, a Microsoft utilizava um componente chamado "Helios" que servia como ponte entre a estrutura existente do .NET dentro IIS com este novo modelo do ASP.NET 5. Esse componente tinha uma série de responsabilidades, entre elas, fazer a configuração e gestão do runtime (DNX).

A partir da beta8, a Microsoft passou a utilizar o HttpPlataformHandler, que é um módulo nativo (código não gerenciado) que quando instalado no IIS, permite que as requisições HTTP que chegam para ele sejam encaminhadas para um outro processo. E, no caso do ASP.NET 5, esse processo é o dnx.exe (ambiente de execução do ASP.NET 5). Isso garantirá à Microsoft e ao time do projeto, o controle do hosting e a possibilidade de integrá-lo facilmente em ambientes Windows que já utilizam o IIS, tirando o proveito das funcionalidades que ele já disponibiliza. Depois de instalado, já é possível visualizarmos este módulo no IIS:



Quando criamos um novo projeto no Visual Studio, podemos perceber que dentro da pasta wwwroot haverá um arquivo web.config. Vale lembrar que o ASP.NET não utiliza mais este tipo de arquivo para centralizar as configurações; web.config é também o arquivo de configuração do IIS. Dentro deste arquivo temos a configuração do HttpPlataformHandler, que é onde informamos o processo para qual a requisição será encaminhada. Note que o arquivo referencia variáveis de ambiente, mas quando o deployment é feito, ele subistitui pelo endereço até o respectivo local.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="httpPlatformHandler"
           path="*"
           verb="*"
           modules="httpPlatformHandler"
           resourceType="Unspecified"/>
    </handlers>
    <httpPlatform processPath="%DNX_PATH%"
                  arguments="%DNX_ARGS%"
                  stdoutLogEnabled="false"
                  startupTimeLimit="3600"/>
  </system.webServer>
</configuration>

É importante dizer que o comando web continua apontando para o hosting Kestrel. Por padrão, existe um pacote já adicionado chamado Microsoft.AspNet.IISPlatformHandler. Ele é responsável por fornecer um middleware que captura os headers customizados que o IIS passa para o DNX e inclui na coleção de headers da requisição que foi criada dentro deste ambiente.

{
  "dependencies": {
    "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8"
  },

  "commands": {
    "web": "Microsoft.AspNet.Server.Kestrel"
  }
}

Esse middleware é adicionado através do método UseIISPlatformHandler, e faz a cópia de alguns cabeçalhos (X-Forwarded-Proto, X-Original-Proto, etc.) utilizados para acompanha-la durante o repasse entre os ambientes. Ele também já faz a mescla da identidade (ClaimsPrincipal) repassada do IIS com a identidade da aplicação, caso o usuário já esteja autenticado deste lado.

Quando a aplicação é instalada no IIS, como falando acima, o web.config sobre algumas mudanças e ajusta os caminhos para o endereço de onde está o executável do DNX:

<httpPlatform processPath="..\approot\web.cmd" ... />

Finalmente, ao rodar a aplicação, é possível percebermos que o dnx.exe está abaixo do w3wp.exe, que é o processo que o IIS utiliza para rodar cada pool de aplicação. As funcionalidades fornecidas pelo IIS continuam disponíveis, como reciclagem do processo, inatividade, etc. O interessante também é que o pool não precisa mais rodar sobre código gerenciado, e ele também não será mais o responsável por fazer o carregando do runtime (CLR). Isso passa a ser responsabilidade do processo que está abaixo dele. Nem mesmo as funcionalidades do ASP.NET fornecidas pelo Windows precisam ser mais instaladas; a aplicação já é toda "auto-contida" (código, pacotes e runtime).


Request Features no ASP.NET

O ASP.NET 5 está sendo construído para ser completamente independente do hosting em que ele está sendo executado, permitindo assim, que este tipo de aplicação seja também levado para outras plataformas (Linux e Mac). Atualmente o ASP.NET 5 pode ser hospedado no IIS (incluindo a versão Express), via self-host (WebListener) ou através do Kestrel (que é um servidor multi-plataforma baseado no libuv.

Mesmo que seja independente do hosting, o ASP.NET fornece um conjunto de tipos que permite interagir, de forma genérica e abstrata, com as funcionalidades que são expostas pelo HTTP, e que na maioria das vezes, são criadas e abastecidas pelo receptor da requisição, ou seja, o próprio hosting. Esses tipos são chamados de request features, e estão incluídas no pacote chamado Microsoft.AspNet.Http.Features. Entre os tipos (funcionalidades) disponíveis temos as interfaces IHttpRequestFeatureIHttpResponseFeature, que representam o básico para o funcionamento de qualquer servidor web, que é receber a requisição, processar e devolver o resultado. Um outro exemplo é a interface ITlsConnectionFeature, que pode ser utilizada para interagir com os certificados enviados pelo cliente. Para uma relação completa, consulte a documentação.

Cada um dos servidores disponíveis já implementam as interfaces definidas (desde que eles suportem), e nos permite acessar essas funcionalidades durante a execução da aplicação, incluindo a possibilidade de acesso através de qualquer middleware. A classe HttpContext expõe uma propriedade chamada Features, que nada mais é que uma coleção das funcionalidades que são incluídas (e, novamente, suportadas) pelo hosting onde a aplicação está rodando. Essa coleção fornece um método genérico chamado Get<TFeature>, onde podemos definir a funcionalidade que queremos acessar. Abaixo um trecho do código de exemplo:

public Task Invoke(HttpContext httpContext)
{
    var clientCertificate =
        httpContext.Features.Get<ITlsConnectionFeature>()?.ClientCertificate;

    if (clientCertificate != null)
    {
        //...
    }

    return _next(httpContext);
}

Além da possibilidade de extrair alguma funcionalidade, é possível também incluir funcionalidade durante a execução, e como já era de se esperar, basta utilizar o método, também genérico, Set<TFeature>. E o mais interessante é que não estamos restritos a utilizar uma das interfaces expostas pelo ASP.NET; podemos criar novas funcionalidades e incorporá-las na execução.

Considere o exemplo abaixo, onde criamos uma funcionalidade para simular a proteção da requisição por uma transação. Basta utilizarmos uma interface para descrever a funcionalidade e criar uma classe que a implemente.

public interface ITransactionFeature
{
    string TransactionId { get; }
    void Commit();
    void Abort();
}

public class TransactionFeatureContext : ITransactionFeature
{
    public string TransactionId { get; } = Guid.NewGuid().ToString();

    public void Commit()
    {
        //...
    }

    public void Abort()
    {
       //...
    }
}

A classe TransactionFeatureContext faz toda a gestão da transação, desde a sua criação (identificando-a) e expõe métodos para conclusão com sucesso (Commit) ou falha (Abort). Repare que tanto a interface quanto a classe não referenciam nenhum tipo do ASP.NET; estamos livres para determinarmos toda a estrutura da nossa funcionalidade. Só que estas classes precisam ser incorporadas na execução, e para isso, vamos criar um middelware que incluirá esta funcionalidade na coleção de Features do contexto da requisição.

public class TransactionMiddleware
{
    private readonly RequestDelegate _next;

    public TransactionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        var transactionContext = new TransactionFeatureContext();
        httpContext.Features.Set<ITransactionFeature>(transactionContext);

        try
        {
            await _next(httpContext);
            transactionContext.Commit();
        }
        catch
        {
            transactionContext.Abort();
            throw;
        }
    }
}

O middleware é criado é os middlewares internos que serão invocados a partir dele estarão sendo gerenciados pela transação. Qualquer exceção não tratada que ocorra, a transação será automaticamente abortada. Por fim, depois do middleware criado, incluímos ele na aplicação através do método UseMiddleware.

Vale lembrar que a ordem em que se adiciona os middlewares são importantes. Como a classe TransactionMiddleware adiciona a feature TransactionFeature que criamos, os middlewares que vem a seguir podem acessar a funcionalidade através do método Get<TFeature>, conforme falado acima. Opcionalmente podemos criar na interface métodos para que os middlewares da sequência possam votar no sucesso ou falha e este, por sua vez, avalia os votos antes de chamar o método Commit.

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();
    app.UseTransactionMiddleware();
    app.UseClientCertificateLogMiddleware();
    app.UseMvc();
}

Convenções no ASP.NET

Durante a construção e evolução do ASP.NET MVC e Web API a Microsoft foi assumindo algumas regras, que durante a execução, os frameworks alteram e geram informações de acordo com o código escrito por nós. Isso é conhecido como convention-over-configuration, e tem como foco diminuir algumas configurações explícitas por parte do desenvolvedor, focando em detalhes mais importantes para atender o negócio e o framework se responsabiliza por realizar as configurações necessárias para o funcionamento daquele código respeitando as tais convenções.

Apesar de as vezes não notarmos, isso está bastante presente em nosso dia a dia. Quando criamos um controller, dentro dele uma ação e esta, por sua vez, retornarmos uma view, o ASP.NET MVC assume a estrutura física de Views/NomeDoController/NomeDaAcao.cshtml para armazenar o arquivo e localiza-lo para exibir para o cliente. Outro exemplo é quando utilizamos o Entity Framework e queremos que a entidade Cliente seja persistida na base de dados; o ORM irá criar, automaticamente, a tabela com o mesmo nome daquela entidade. Já no ASP.NET Web API, se criarmos uma ação com o método chamado Get, ele estará automaticamente acessível através do verbo GET do HTTP, sem precisar decorar o mesmo com o atributo HttpGetAttribute.

Note que nos dois exemplos a Microsoft assumiu essas regras e as asseguram durante a execução, ficando, de certa forma, transparente para nós. Se não houvesse isso, teríamos que configurar as mesmas coisas em 100% dos casos; com essas convenções predefinidas, precisamos apenas nos preocupar quando queremos algo diferente do padrão.

O ASP.NET MVC e Web API expõem tipos para que possamos customizar o framework de acordo com nossas necessidades. Até então, isso poderia ser realizado customizando alguns serviços de baixo nível (controller factory, por exemplo), mas o problema desta técnica é que estaremos tocando em outras atividades e que qualquer descuido poderemos ter problemas ao executar e, consequentemente, não atingirmos o objetivo esperado.

O ASP.NET 5 simplifica bastante essa customização expondo uma interface chamada IApplicationModelConvention. Ao implementar ela e acopla-la a execução, o ASP.NET identificará todos os controllers, ações, rotas e filtros candidatos a serem expostos (de acordo com a convenção predefinida) e entregará para analisarmos (através da classe ApplicationModel), e com isso, podemos customizar da forma que precisamos. Tudo isso através de uma API de mais fácil utilização, sem a necessidade de conhecer detalhes dos bastidores para implementar convenções que estão em um nível mais superficial.

Para exemplificar o uso, considere o controller abaixo, que possui duas ações e que retornam uma string. Como podemos perceber, o código foi escrito utilizando o português. Por convenção, o ASP.NET Web API assumirá que o nome dos membros (classe e métodos) serão os mesmos que serão expostos e utilizados pelo sistema de roteamento para montar os links de acesso à API e suas ações.

public class UtilitariosController : Controller
{
    [HttpGet]
    public string ExecutarTarefa()
    {
        return "..." ;
    }

    [HttpGet]
    public string EnviarEmail()
    {
        return "...";
    }
}

A ideia é deixar o desenvolvedor livre para codificar no idioma desejado, e através do recurso de convenção, vamos estipular que o framework irá traduzir de acordo com a escolha realizada durante a sua configuração. Na ausência do idioma, a nomenclatura utilizada no código será exposta.

O que fazemos abaixo é no método Apply (único, exposto pela interface IApplicationModelConvention) percorremos os controllers encontrados pelo ASP.NET, procuramos pela chave no dicionário (que em um ambiente real, poderíamos estar recorrendo à algum outro repositório ao invés de hard-code), e por fim substituímos o nome do controller e da ação pelo encontrado no dicionário, baseado na cultura escolhida.

public class LocalizedApiNamingModelConvention : IApplicationModelConvention
{
    private string culture;

    private IDictionary<string, Tuple<string, string>> mapping =
        new Dictionary<string, Tuple<string, string>>();

    public LocalizedApiNamingModelConvention(string culture)
    {
        this.culture = culture;

        mapping.Add(
            "en-US.Utilitarios.ExecutarTarefa",
            Tuple.Create("Utilities", "RunTask"));

        mapping.Add(
            "en-US.Utilitarios.EnviarEmail",
            Tuple.Create("Utilities", "SendEmail"));
    }

    public void Apply(ApplicationModel application)
    {
        foreach (var c in application.Controllers)
        {
            var controllerName = c.ControllerName;

            foreach (var a in c.Actions)
            {
                var key = BuildKey(controllerName, a.ActionName);

                if (this.mapping.ContainsKey(key))
                {
                    var info = this.mapping[key];

                    c.ControllerName = info.Item1;
                    a.ActionName = info.Item2;
                }
            }
        }
    }

    private string BuildKey(string controllerName, string actionName)
    {
        return $"{culture}.{controllerName}.{actionName}";
    }
}

Depois da classe criada, basta incluirmos na coleção chamada Conventions, e fazemos isso através do método AddMvcCore, conforme é mostrado no código abaixo:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvcCore(o =>
        o.Conventions.Add(new LocalizedApiNamingModelConvention("en-US")));
}

Repare que criamos a classe e no construtor passamos a cultura en-US e ao rodar a aplicação, os links acessíveis serão /Utilities/RunTask e /Utilities/SendEmail. Os links em português retornarão o erro 404 do HTTP (Not Found).

Testes com a classe TelemetryClient

No artigo anterior eu mostrei o uso da classe TelemetryClient para catalogar informações customizadas. O fato de utilizar a classe em sua configuração padrão, fará com que as informações sejam enviadas para o serviço (na nuvem) mesmo que você ainda esteja em ambiente de testes. Talvez isso não seja o que estamos querendo, pois espalhamos pelo código os pontos que são cruciais para enviar as informações, mas talvez só faça sentido quando estive em ambiente de produção.

A classe TelemetryClient confia nas configurações que são disponibilizadas pela classe TelemetryConfiguration. Ela, por sua vez, possui uma propriedade estática chamada Active que retorna as configurações globais (para a aplicação) do Application Insights. A partir daí podemos recorrer a propriedade DisableTelemetry que quando definida como true, não enviará as informações ao serviço. Para utiliza-la podemos também condicionar à diretiva de DEBUG para somente desabilitar enquanto estivermos em desenvolvimento:

#if DEBUG
            TelemetryConfiguration.Active.DisableTelemetry = true;
#endif

Ainda com o exemplo utilizado no artigo anterior, não estávamos nos preocupando com os testes unitários. É uma preocupação que também devemos ter para não reportar os eventos quando estamos rodando os testes. Apesar da opção acima resolver, as vezes podemos querer validar se o nosso código está ou não reportando quando necessário e se as informações que foram levadas estão de acordo com a nossa expectativa.

A classe de configuração também nos permite configurar o canal de comunicação, e com isso customizar (sobrescrever) onde e como queremos armazenar os logs. Só que antes de visualizarmos o código necessário para alcançar isso, vamos entender a dependência entre a classe TelemetryClient e o restante da configuração.



A classe TelemetryClient possui dois overloads, onde um deles espera a instância da classe TelemetryConfiguration, que se for omitida, o Application Insights utiliza o valor fornecido pela propriedade Active. A classe TelemetryConfiguration possui uma propriedade chamada TelemetryChannel, onde podemos customizar o canal de comunicação através da implementação da interface ITelemetryChannel. Para o exemplo, vou optar por armazenar os logs em uma coleção interna, conforme é possível ver no código abaixo:

public class TestChannel : ITelemetryChannel
{
    private readonly IList<ITelemetry> itens;

    public TestChannel()
    {
        this.itens = new List<ITelemetry>();
    }

    public void Send(ITelemetry item)
    {
        this.itens.Add(item);
    }

    public IList<ITelemetry> Itens
    {
        get
        {
            return this.itens;
        }
    }

    //outros membros, omitidos por questão de espaço
}

Depois da classe implementada, precisamos criar a instância e associa-la à propriedade da configuração para que ela passe a ser utilizada quando chamarmos os métodos para catalogar as informações (Track*). O código que foi criado para calcular o frete não foi alterado em nada. Mesmo que a configuração tenha sido realizada externamente, a classe TelemetryClient irá utilizar esta classe via TelemetryConfiguration (via propriedade Active). O teste ficaria da seguinte forma:

[TestClass]
public class CalculadoraDeFrete
{
    private TestChannel channel;

    [TestInitialize]
    public void Inicializar()
    {
        this.channel = new TestChannel();
        TelemetryConfiguration.Active.TelemetryChannel = channel;
    }

    [TestMethod]
    public void DeveCalcularFreteParaValinhos()
    {
        var cep = "13273-047";
        var valorEsperado = 12.38M;

        var calculadora = new CalculadoraDeFrete();
        var resultado = calculadora.Calcular(cep);

        Assert.AreEqual(valorEsperado, resultado);

        var evento = this.channel.Itens.Single() as EventTelemetry;

        Assert.AreEqual("CalculoDeFrete", evento.Name);
        Assert.AreEqual(cep, evento.Properties["Cep"]);
        Assert.AreEqual(resultado.ToString(), evento.Properties["Resultado"]);
    }
}

ApplicationInsights - Utilizando a classe TelemetryClient

A Microsoft está disponibilizando um serviço de telemetria de aplicações chamado de Application Insights. É um serviço que roda no Azure (nuvem) e que permite integrar nas aplicações que desenvolvemos para que seja possível extrair informações, armazenar e, consequentemente, analisar os dados gerados para tentar identificar algum problema de performance ou de qualquer outra natureza.

Dependendo do tipo de aplicações onde este serviço é instalado, ele utiliza o recurso exposto pela aplicação para interceptar a requisição e catalogar as informações. Por exemplo, se este serviço é instalado em uma aplicação ASP.NET, então o módulo (IHttpModule) ApplicationInsightsHttpModule é incluído para interceptar as requisições que chegam à aplicação; já para aplicações ASP.NET 5, acopla-se através do método UseApplicationInsightsRequestTelemetry. Em ambos os casos, automaticamente o Application Insights começa a receber informações sobre as requisições que chegam para a aplicação, duração da requisição, cabeçalho, etc.

A identificação da aplicação para o serviço é uma chave chamada instrumentation key (GUID) que é gerada pelo Azure, e deve ser configurada na aplicação para que o Application Insights possa enviar ao serviço e ele, por sua vez, saiba qual aplicação está gerando os dados. E, novamente, dependendo da tecnologia, o arquivo utilizado para armazenar a chave é o ApplicationInsights.config para os projetos ASP.NET tradicionais ou aplicações Windows, e o config.json para o ASP.NET 5.

[ApplicationInsights.config]
<?xml version="1.0" encoding="utf-8"?>
<ApplicationInsights>
  ...
  <InstrumentationKey>bab34a9e-f1d9-4cf9-8a09-5f33b8f4ebed</InstrumentationKey>
</ApplicationInsights>

[config.json]
{
  "ApplicationInsights": {
    "InstrumentationKey": "bab34a9e-f1d9-4cf9-8a09-5f33b8f4ebed"
  },
  ...
  }
}

Mesmo que ocorra essa mágica, o Application Insights fornece uma API que podemos recorrer para customizar os dados que são enviados para o serviço. Isso pode ser feito através da classe TelemetryClient, que expõe alguns métodos específicos ou de mais baixo nível para customizar diversas informações que podem ser levadas para o serviço. Para fazer uso desta classe, primeiramente precisamos utilizar o Nuget para adicionar o seguinte pacote:

Install-Package Microsoft.ApplicationInsights

É importante dizer que é possível fazer uso desta classe em projetos que não sejam do tipo ASP.NET. Como exemplo, vou utilizar uma aplicação Console para catalogar os mais diversos tipos de informações no Application Insights. No caso de projetos ASP.NET, já há uma opção durante a criação do projeto para habilita-lo, assim como é possível vermos na imagem abaixo, e a instalação explícita do pacote não é necessária.

>

Independente da forma como você adiciona o suporte para o Application Insights na aplicação, a classe TelemetryClient pode ser utilizada. Como já era de se esperar, esta classe fornece uma propriedade chamada InstrumentationKey, que é onde devemos configurar a chave gerado pelo Azure, mas em casos de projetos do tipo Windows, podemos criar o arquivo ApplicationInsights.config e lá ter o elemento InstrumentationKey, o que facilita a alteração sem precisar recompilar a aplicação.

var tc = new TelemetryClient()
{
    InstrumentationKey = "bab34a9e-f1d9-4cf9-8a09-5f33b8f4ebed"
};

A partir da instância da classe TelemetryClient criada, temos alguns métodos para enviar informações para o serviço. O Application Insights possui diversos tipos de informações ("categorias") que possamos enviar os dados: Trace, Request, Page View, Expcetion, Dependency e Custom Event. Para cada uma destas informações, há um método correspondente: TrackTrace, TrackRequest, TrackPageView, TrackException, TrackDependency e TrackEvent.

Para exemplificar, considere o código abaixo. Estamos utilizando o método TrackEvent para indicar ao serviço que trata-se de um evento que vamos gerar diversas entradas e que corresponde à uma atividade de rotina da nossa aplicação. Além do nome, também podemos, opcionalmente, informar um dicionário de dados contendo propriedades que queremos anexar aquele evento. No exemplo, estou optando por incluir o parâmetro, o resultado e o tempo que levou para realizar o cálculo.

public class CalculadoraDeFrete
{
    private static TelemetryClient tc = new TelemetryClient()
    {
        InstrumentationKey = "bab34a9e-f1d9-4cf9-8a09-5f33b8f4ebed"
    };

    public static decimal Calcular(string cep)
    {
        var sw = Stopwatch.StartNew();

        //cálculo do frete
        var resultado = 12.38M;

        tc.TrackEvent(
            "CalculoDeFrete",
            new Dictionary<string, string>()
            {
                { "Cep", cep },
                { "Resultado", resultado.ToString() },
                { "Tempo", sw.Elapsed.ToString() }
            });
        tc.Flush();

        return resultado;
    }
}

Ainda em relação as propriedades que podemos anexar ao log no momento em que o evento ocorre, podemos também definir valores globais que serão enviados em todas as requisições, independentemente se está catalogando um evento, uma exceção, ou qualquer outra informação. E para isso, basta recorrer ao dicionário que está exposto através do contexto da classe TelemetryClient:

static CalculadoraDeFrete()
{
    tc.Context.Properties.Add("Usuario", "Israel Aece");
}

Internamente a classe mantém um buffer com as entradas e periodicamente envia as requisições para o serviço. Utilizamos o método Flush para adiantar esse processo. Se consultarmos o portal, já podemos visualizar o evento adicionado e as respectivas propriedades que enviamos (incluindo a propriedade global Usuario). A partir deste nome do evento, eles sempre serão agrupados para uma melhor visualização e análise.



Como foi comentado acima, também é possível realizar o log de uma exceção. Para isso, podemos envolver o código inseguro em um bloco try/catch e utilizar o método TrackException. Estou optando por utilizar a versão mais simples do método, mas há parâmetros opcionais que permitem incluir um dicionário com valores customizados, assim como fizemos no código acima.

public static decimal Calcular(string cep)
{
    var resultado = 0M;

    try
    {
        throw new ArgumentException("O CEP informado está fora da área de cobertura da transportadora.");
        //cálculo do frete
    }
    catch (Exception e)
    {
        tc.TrackException(e);
        tc.Flush();
        throw;
    }

    return resultado;
}

Agora se atualizarmos o portal do Azure, irmos até a seção de Failures, o contador foi incrementado e é possível visualizar o erro que ocorreu, incluindo além da mensagem, toda a stack trace, útil para nós desenvolvedores, identificar onde o problema exatamente aconteceu.



Como é um exemplo simplista, estou fazendo o tratando localizado da exceção. Dependendo da estratégia de tratamento de erros da aplicação, é possível centralizar o envio das informações do erro em um ponto global, e dependendo da tecnologia utilizada, você pode recorrer ao suporte que ela dá para isso. Em aplicações ASP.NET tradicionais, você pode concentrar isso no evento Application_Error no arquivo Global.asax, no ASP.NET MVC em um filtro, no ASP.NET Web API no ExceptionLogger ou, no WCF, através da interface IErrorHandler.

Como podemos notar, o Application Insights eleva o "simples log de aplicações" para um outro nível. Podemos gerar diversas informações e temos a certeza que por trás existe uma grande infraestrutura para armazenamento e processamento delas. Terceirizando isso nos permite ainda mais focar no que de fato importa: o desenvolvimento da regra de negócio para qual a aplicação está sendo construída.

Web Sockets com ASP.NET Web API

Há algum tempo eu comentei sobre a possibilidade do WCF expor serviços para serem consumidos utilizando a tecnologia de Web Sockets. Neste mesmo artigo eu falei sobre os detalhes do funcionamento deste protocolo, e que se quiser saber o seu funcionamento e implementação em um nível mais baixo, aconselho a leitura.

Como sabemos, o WCF é uma tecnologia que roda do lado do servidor. Da mesma forma que temos o ASP.NET Web API, que é uma tecnologia que utilizamos para criarmos APIs e expor para os mais diversos tipos de clientes. Assim como a Microsoft adicionou o suporte à web sockets no WCF, ela também fez o mesmo com o ASP.NET Web API e MVC, ou seja, incorporou no próprio framework o suporte para criação de recursos que são expostos utilizando web sockets.

É importante dizer que a Microsoft também criou uma outra tecnologia chamada de SignalR, que fornece diversos recursos para a criação de aplicações que precisam gerar e consumir informações que são consideradas de tempo real. Este framework já possui classes que abstraem a complexidade de exposição destes tipos de serviços, fornecendo classes de mais alto nível para trabalho.

Entre os novos tipos que foram adicionados, temos a propriedade IsWebSocketRequest (exposta pela classe HttpContext), que retorna um valor boleano indicando se a requisição está solicitando a migração do protocolo para web sockets. Caso seja verdadeiro, então recorremos ao método AcceptWebSocketRequest da mesma classe para especificarmos a função que irá periodicamente ser executada.

Para o exemplo, vamos criar um publicador de notícias, que quando o cliente optar por assiná-lo, ele irá receber as notícias de forma randômica em um banner a cada 2 segundos. Como podemos ver no código abaixo, o método Assinar da API é exposto para que os clientes cheguem até ele através do método GET do HTTP. Como se trata de uma solicitação de migração, retornamos o código 101 do HTTP indicando a troca do protocolo para web sockets.

public class PublicadorController : ApiController
{
    private static IList<string> noticiais;
    private static Random random;

    static PublicadorController()
    {
        noticiais = new List<string>()
        {
            "You can buy a fingerprint reader keyboard for your Surface Pro 3",
            "Microsoft's new activity tracker is the $249 Microsoft Band",
            "Microsoft's Lumia 950 is the new flagship Windows phone",
            "Windows 10 will start rolling out to phones in December",
            "Microsoft's Panos Panay is pumped about everything (2012–present)"
        };

        random = new Random();
    }

    [HttpGet]
    public HttpResponseMessage Assinar()
    {
        var httpContext = Request.Properties["MS_HttpContext"] as HttpContextBase;

        if (httpContext.IsWebSocketRequest)
            httpContext.AcceptWebSocketRequest(EnviarNoticias);

        return new HttpResponseMessage(HttpStatusCode.SwitchingProtocols);
    }

    private async Task EnviarNoticias(AspNetWebSocketContext context)
    {
        var socket = context.WebSocket;

        while (true)
        {
            await Task.Delay(2000);

            if (socket.State == WebSocketState.Open)
            {
                var noticia = noticiais[random.Next(0, noticiais.Count - 1)];
                var buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(noticia));

                await socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
            }
            else
            {
                break;
            }
        }
    }
}

Já dentro do método EnviarNoticias criamos um laço infinito que fica selecionando randomicamente uma das notícias do repositório e enviando para o(s) cliente(s) conectado(s). É claro que também podemos receber informações dos clientes conectados. Para o exemplo estou me limitando a apenas gerar o conteúdo de retorno, sem receber nada. Se quiser, pode utilizar o método ReceiveAsync da classe WebSocket para ter acesso à informação enviada por ele. E, por fim, para não ficar eternamente rodando o código, avaliamos a cada iteração se o estado da conexão ainda continua aberto, caso contrário, encerramos o mesmo.

E do lado do cliente, se for uma aplicação web, vamos recorrer ao código Javascript para consumir este serviço. Temos também a disposição deste lado a classe WebSocket, que quando instanciada, devemos informar o endereço para o método que assina e migra o protocolo para web sockets. Repare que a instância é criada dentro do evento click do botão Conectar e também já nos associamos aos eventos - autoexplicativos - onopen, onmessage e onclose (e ainda tem o onerror).

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
    <script type="text/javascript">
        var ws;

        $().ready(function ()
        {
            $("#Conectar").click(function () {
                ws = new WebSocket("ws://localhost:2548/api/Publicador/Assinar");

                ws.onopen = function () {
                    $("#Status").text("Conectado. Recuperando Notícias...");
                };

                ws.onmessage = function (evt) {
                    $("#Status").text("");
                    $("#Banner").text(evt.data);
                };

                ws.onclose = function () {
                    $("#Banner").text("");
                    $("#Status").text("Desconectado");
                };
            });

            $("#Desconectar").click(function () {
                ws.close();
            });
        });
    </script>
</head>
<body>
    <input type="button" value="Conectar" id="Conectar" />
    <input type="button" value="Desconectar" id="Desconectar" />
    <br /><br />
    <span id="Status"></span>
    <span id="Banner"></span>
</body>
</html>

Quando acessar a página HTML e clicar no botão Conectar, o resultado é apresentado abaixo. A qualquer momento podemos pressionar no botão Desconectar e indicar ao servidor que não estamos mais interessados no retorno das informações. A cada envio de mensagem do servidor para o cliente, o evento onmessage é disparado, e estamos exibindo a mensagem em um campo na tela.



Apenas para informação, o Fiddler é capaz de identificar quando a conexão é migrada para web sockets, e a partir daí consegue capturar as mensagens que são enviados para o cliente. A imagem abaixo é possível visualizar o log capturado depois da assinatura realizada:

Protegendo Configurações no ASP.NET

Quando construímos uma aplicação, temos diversos itens para realizar a configuração. Entre estes itens, estão configurações que guiam a execução da aplicação, o gerenciamento de algum recurso que foi incorporado a ela ou simplesmente valores que são necessários para o negócio para qual a mesma está sendo construída.

Entre estas configurações, temos strings de conexões com base de dados, endereços de servidor SMTP para envio de e-mails, chave de identificação para serviços de autenticação (Google, Facebook, etc.), etc. Não é comum manter estes valores em hard-code, pois pode mudar a qualquer momento e variar de ambiente para ambiente, e precisamos ter a flexibilidade de alterar sem a necessidade de recompilar a aplicação.

Isso nos obriga a manter a chave em um arquivo de configuração. Até então trabalhamos com o arquivo chamado web.config, que serve justamente para incluir diversas configurações da aplicação. A nova versão do ASP.NET (5) muda isso, disponibilizando um novo sistema de configuração mais simples e não menos poderoso. Apesar de continuar dando suporte ao formato XML, a versão mais recente adotou como padrão o formato JSON.

O problema desse modelo é a exposição das configurações. Quando trabalhamos com projetos e que podemos expor o repositório do código publicamente (através do GitHub ou qualquer outro gerenciador de código fonte), deixar armazenado neste arquivo informações sigilosas e, consequentemente, torna-las públicas, é algo que nem sempre desejamos. O ASP.NET 5 resolve isso através de um recurso chamado de User Secrets.

Esta funcionalidade permite externalizar certas configurações em um arquivo que fica de fora do controle de versão do código fonte. Sendo assim, para as chaves que são colocadas no arquivo de configuração da aplicação (config.json) e que precisam ser protegidas, podemos recorrer à este recurso, que basicamente consiste em criar um novo arquivo (chamado de secrets.json) e que será armazenado, no Windows, no seguinte endereço: %APPDATA%\microsoft\UserSecrets\<userSecretsId>\secrets.json.

É importante dizer que o local deste arquivo varia de acordo com o sistema operacional onde a aplicação está sendo desenvolvida, e não é aconselhável tornar a aplicação dependente dele; futuramente a Microsoft se dá o direito de mudar (como, por exemplo, criptografar o seu conteúdo), e a aplicação corre o risco de parar de funcionar. O <userSecretsId> é substituído pela chave que é colocada no arquivo project.json, e dentro deste diretório teremos o arquivo secrets.json, conforme comentado acima. É importante notar também que precisamos adicionar a dependência para usar este recurso, e para isso, devemos incluir o pacote chamado Microsoft.Framework.Configuration.UserSecrets:

{
  "commands": {
    "web": "Microsoft.AspNet.Hosting --config hosting.ini"
  },
  "dependencies": {
    "Microsoft.AspNet.Server.IIS": "1.0.0-beta5",
    "Microsoft.AspNet.Server.WebListener": "1.0.0-beta5",
    "Microsoft.Framework.Configuration.UserSecrets": "1.0.0-beta5"
  },
  "userSecretsId": "WebApplication1-12345",
  "version": "1.0.0-*",
  "webroot": "wwwroot"
}

Com essa configuração realizada, podemos adicionar no arquivo config.json todas as configurações necessárias para o funcionamento da aplicação e incluir no arquivo secrets.json somente as configurações que queremos proteger.

[config.json]
{
  "Data": {
    "DefaultConnection": {
      "ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=Teste;Trusted_Connection=True;"
    }
  },
  "Certificado":  "123"
}

[secrets.json]
{
  "Certificado":  "128372478278473248378"
}

Opcionalmente você pode recorrer à uma opção que existe se clicar com o botão direito sobre o projeto e, em seguida, em "Manage User Secrets", e assim ter acesso ao arquivo secrets.json sem a necessidade de saber onde o Visual Studio está armazenando-o. Mas isso não basta para o ASP.NET entender que ele deve, também, considerar a busca no arquivo secrets.json. Para isso precisamos indicar que à ele que vamos utilizar o recurso de User Secrets, conforme podemos ver abaixo:

public class Startup
{
    public void Configure(IApplicationBuilder app, IApplicationEnvironment appEnv)
    {
        this.Configuration =
            new ConfigurationBuilder(appEnv.ApplicationBasePath)
                .AddJsonFile("config.json")
                .AddUserSecrets()
                .Build();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(this.Configuration["Certificado"]);
        });
    }

    public IConfiguration Configuration { get; set; }
}

A ordem em que invocamos o método é importante, ou seja, neste caso se ele encontrar a chave "Certificado" no arquivo secrets.json o seu valor será apresentado. Caso o valor não exista, então ele extrairá o valor do arquivo config.json.

Composição de Tokens para Cancelamento

Ao contrário do que temos na classe Thread, a classe Task não fornece um método para abortar a execução da mesma. Se desejamos "monitorar" a execução e ter a chance de cancelar, temos que passar uma espécie de token ao criar a tarefa, e externamente, quando quisermos, podemos cancelar a execução. Compete aquele que programa a tarefa a ser executada, avaliar se o cancelamento foi ou não solicitado.

No .NET Framework a classe que nos permite controlar o cancelamento é a CancellationTokenSource. Ela expõe uma propriedade chamada Token que por sua vez, é representada pela classe CancellationToken, e é este valor que temos que passar para a tarefa a ser executada, e através do método Cancel podemos solicitar o cancelamento. Um detalhe importante aqui é que a classe CancellationTokenSource também disponibiliza um método estático chamado CreateLinkedTokenSource, que nos permite agrupar vários tokens, e quando qualquer um deles for cancelado, a tarefa é cancelada.

var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();

using (var cts3 = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token))
{
    var task = Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < int.MaxValue; i++)
        {
            cts3.Token.ThrowIfCancellationRequested();
            Console.WriteLine(i);
        }
    }, cts3.Token);
               
    Console.ReadLine();
    cts2.Cancel();
    Console.ReadLine();
}

Repare que criamos dois tokens (cts1 e cts2) e agrupamos em um terceiro chamado cts3. Se invocarmos o método Cancel de qualquer um dos três, a tarefa será interrompida. Vale lembrar que somente o fato de passar o token na criação da tarefa não é suficiente para interromper a execução; como foi dito acima, é de responsabilidade do desenvolver monitorar a solicitação de cancelamento, e para isso, neste caso estamos utilizando o método ThrowIfCancellationRequested, que dispara uma exceção se a solicitação foi feita.

Por fim, note que apenas o terceiro CancellationTokenSource está sendo envolvido em um bloco using. Isso é porque quando criamos este objeto a partir do link entre outros, o método Dispose irá executar algumas atividades que irão impactar a memória, descartando objetos que são criados exclusivamente para isso. O método Dispose nesta classe também pode ser útil quando estamos utilizando a funcionalidade CancelAfter, que utiliza internamente um Timer para monitorar o tempo e, automaticamente, interromper a tarefa que está - ainda - sendo executada depois que o tempo expira.