Controllers no ASP.NET vNext

by Israel Aece 7. July 2014 21:51

Nos dias de hoje, ao criar um controller para o ASP.NET MVC, precisamos herdar da classe abstrata Controller; se precisamos criar um controller para o ASP.NET Web API, precisamos herdar da classe, também abstrata, ApiController. Apesar de ambos os frameworks terem uma API bastante semelhante, como eles foram desenvolvidos em épocas distintas e com objetivos diferentes, temos essa separação física.

Felizmente a próxima versão do ASP.NET (ainda chamada de vNext) unifica estes frameworks passam a fornecer uma API única e centralizada para a construção de qualquer recurso que desejamos expor através do protocolo HTTP. Sendo assim, não teremos mais duas classes, onde uma delas representa um controller para HTML e outra para Web API. Apesar disso, neste novo modelo passamos a ter os chamados Controllers POCO, ou seja, não há mais a necessidade nem de herdar da classe base Controller. O padrão para que o ASP.NET "descubra" o controller é que a classe seja pública, não abstrata e o nome seja sufixado com a palavra "Controller".

public class UsuariosController
{
    public string Get()
    {
        return "Israel Aece";
    }
}

Só que a classe Controller tem outras utilidades, e uma das principais é fornecer acesso à recursos inerentes (ActionContext, Url e ViewData) à requisição que está sendo executada. Só que nem sempre precisamos de todas essas informações dentro do controller. Esta versão do ASP.NET nos permite incluir apenas as propriedades que queremos utilizar no controller e o DefaultControllerFactory injeta a instância nestas propriedades durante a criação o controller.

public class UsuariosController
{
    public string Get()
    {
        return "Israel Aece";
    }

    public ActionContext ActionContext { get; set; }

    public IUrlHelper Url { get; set; }

    public ViewDataDictionary ViewData { get; set; }
}

Tags: , ,

ASP.NET

Compressão no ASP.NET Web API

by Israel Aece 19. June 2014 16:45

Em certas situações, quando um cliente executa uma operação de um serviço, o resultado pode ser uma grande massa de dados. Essa massa de dados pode ser qualquer coisa, desde os bytes de um arquivo até mesmo objetos que foram extraídos de um banco de dados.

Como todos sabem, o problema disso é a quantidade de informações que irá trafegar na rede, podendo causar uma grande lentidão, caso isso seja uma operação comum. A principal solução que geralmente utilizamos quando queremos diminuir o tráfego, é a compactação das informações. Isso fará com que a quantidade total de dados seja bem menor, aliviando consideravelmente a quantidade de informações que são trafegadas. Obviamente que a compactação tem um overhead quando você compacta ou descompacta, e isso deve ser levado em consideração para decidir se deve ou não optar por ela, mas na maioria das vezes, acaba compensando.

Quando estamos utilizando o ASP.NET Web API para construir e expor recursos, podemos implementar uma funcionalidade que permite a compactação das mensagens que são devolvidas para os clientes. O ASP.NET Web API não possui nativamente um mecanismo que permite a compactação das mensagens produzidas pelo serviço, mas graças ao modelo de estensibilidade que ele fornece, é possível recorrer as classes GZipStream e DeflateStream (do próprio .NET Framework) para gerar esse tipo de conteúdo.

O primeiro passo é criar um MessageHandler, que nos permite interceptar a requisição, e no interior do método SendAsync, chegamos até a mensagem de resposta (HttpResponseMessage) gerado pelo ASP.NET Web API (lembrando que neste momento a ação do controller já foi executada), e depois disso, analisamos o cabeçalho da requisição para se certificar se o cliente suporta ou não este tipo de codificação. Se sim, o cabeçalho AcceptEncoding deve ter como valor uma string com "gzip" ou "deflate", indicando qual dos formatos ele consegue interpretar.

public class CompressionHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>(t =>
        {
            var response = t.Result;

            if (request.Headers.AcceptEncoding != null)
            {
                var encoding = request.Headers.AcceptEncoding.FirstOrDefault();

                if (encoding != null && !string.IsNullOrWhiteSpace(encoding.Value))
                    response.Content = new CompressedHttpContent(t.Result.Content, encoding.Value);
            }

            return response;
        });
    }
}

Uma vez que é possível determinar o suporte por parte do cliente, então criamos e sobrescrevemos o resultado da requisição com a instância da classe CompressedHttpContent, que também herda de HttpContent. É dentro desta classe que existe todo o trabalho para copiar o resultado gerado pelo controller, passar pelo compactador e gerar o mesmo conteúdo mais enxuto. Repare que o construtor recebe o conteúdo da mensagem atual e copia os headers existentes para a classe atual; note também que é incluído o header ContentEncoding com o mesmo tipo indicado pelo cliente, justamente para informar ao cliente que o conteúdo está compactado.

public class CompressedHttpContent : HttpContent
{
    private readonly HttpContent content;
    private readonly string compressorType;

    public CompressedHttpContent(HttpContent content, string compressorType)
    {
        this.content = content;
        this.compressorType = compressorType;

        ConfigHeaders();
    }

    private void ConfigHeaders()
    {
        foreach (var item in this.content.Headers)
            this.Headers.TryAddWithoutValidation(item.Key, item.Value);

        this.Headers.ContentEncoding.Add(this.compressorType);
    }

    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
    {
        var compressorStream = CreateCompressorStream(stream);

        return this.content.CopyToAsync(compressorStream).ContinueWith(t => compressorStream.Dispose());
    }

    private Stream CreateCompressorStream(Stream output)
    {
        return 
            this.compressorType == "gzip" ?
            (Stream)new GZipStream(output, CompressionMode.Compress, true) : 
            new DeflateStream(output, CompressionMode.Compress, true);
    }

    protected override bool TryComputeLength(out long length)
    {
        length = -1;
        return false;
    }
}

O método SerializeToStreamAsync é invocado pelo próprio runtime, que passará o stream de saída e o envolveremos no stream de compactação, para que ao copiar o conteúdo atual para o stream de saída, ele passe pelo algoritmo que efetivamente realiza a compactação. Aqui seria interessante melhorar o código, para que seja possível acoplar novas implementações de novos compactadores. Para isso poderíamos criar uma abstração (IHttpCompressor) e implementá-lo para cada um dos compactadores que queremos que sejam suportados pela API.

Este código por si só não funciona. É necessário incluir o message handler na coleção exposta pelo ASP.NET Web API, e para isso, recorreremos as configurações da API, conforme é mostrado no código abaixo:

config.MessageHandlers.Add(new CompressionHandler());

Para mensurar o ganho gerado por este código, ao acessar um método que até então retornava um resultado de 562 Bytes de tamanho, passa a devolver o mesmo conteúdo com apenas 28 Bytes.

GET http://127.0.0.1:9393/api/Teste HTTP/1.1
Host: 127.0.0.1:9393
Accept-Encoding: gzip, deflate
Connection: Keep-Alive

HTTP/1.1 200 OK
Content-Length: 28
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Date: Thu, 19 Jun 2014 19:08:08 GMT

RESULTADO COMPACTADO, OMITIDO POR QUESÕES DE ESPAÇO

Vale lembrar que o cliente que consome este serviço precisa saber lidar com este tipo de resultado. Caso o consumo do serviço esteja sendo feito através do próprio ASP.NET Web API (HttpClient), então ele já possui o suporte interno para descompactar o resultado, sem que seja necessário qualquer customização. Ao criar o HttpClient, podemos recorrer à um segundo construtor, que recebe a instância da classe HttpClientHandler, que por sua vez, expõe uma propriedade chamada AutomaticDecompression, que também é utilizada para indicar ao serviço que possui suporte para descompactação (Accept-Encoding) daquele formato.

using (var client = new HttpClient(new HttpClientHandler()
{
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
}))
{
    //...
}

Tags: , , ,

ASP.NET | CSD

Expondo vídeo através do ASP.NET Web API

by Israel Aece 27. May 2014 09:13

Além de possibilitar o retorno de tipos mais simples, tais como inteiros, strings e também de objetos que representam o nosso negócio (Produto, Pedido, Cliente, etc.), o ASP.NET Web API também é capaz de retornar outros tipos de recursos, tais como vídeos ou músicas, permitindo ao cliente ir consumindo o mesmo sob demanda, ou seja, na medida em que ele vai tocando, o conteúdo vai sendo baixado, sem a necessidade de trazer todo o conteúdo antes de utilizar. Isso garantirá ao cliente ouvir/assistir e decidir abortar sem a necessidade de realizar o download completo do arquivo, que muitas vezes é grande.

Para este tipo de cenário temos a classe PushStreamContent. Essa classe herda de HttpContent e, consequentemente, pode ser definida como o conteúdo da mensagem de resposta. Essa classe permite estabelecer a comunicação entre o serviço e o cliente através de um stream, que ao serem escritas informações nele, viajará até o cliente para que ele decida o que fazer com o mesmo. O construtor desta classe permite a iniciliazação através de um delegate do tipo Func<>, que entre os parâmetros que são fornecidos, um deles é justamente o stream que liga o cliente ao serviço.

Ao utilizar este stream, o conteúdo será encaminhado ao cliente diretamente. Para o exemplo, vamos permitir ao cliente informar o nome do vídeo que ele deseja assistir, e o serviço será responsável por extrair este arquivo do disco, particionar os bytes do arquivo (através de um buffer de 5 MB), e dentro do laço while, ele vai enviando estes "pedaços" ao cliente de forma assíncrona. Abaixo está o controller completo com esta funcionalidade.

public class EntretenimentoController : ApiController
{
    private const int TamanhoDoBuffer = 1024 * 32;
    private readonly string Diretorio;

    public EntretenimentoController()
    {
        this.Diretorio = HttpContext.Current.Server.MapPath("~/App_Data");
    }

    [HttpGet]
    public HttpResponseMessage BuscarVideo(string nomeDoVideo)
    {
        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new PushStreamContent(
                async (stream, content, context) =>
                    await ExtrairRecurso(Path.Combine(Diretorio, nomeDoVideo), stream), "video/mp4")
        };
    }

    private static async Task ExtrairRecurso(string fileName, Stream output)
    {
        var buffer = new byte[TamanhoDoBuffer];
        var read = 0;

        using (var input = File.OpenRead(fileName))
            while ((read = input.Read(buffer, 0, TamanhoDoBuffer)) > 0)
                await output.WriteAsync(buffer, 0, read);
    }
}

O consumo do vídeo pode ser realizado através dos novos recursos do HTML 5, que através da tag <video /> permite associar uma URL que produz o conteúdo, fazendo o trabalho de acumular e ir exibindo o vídeo que está sendo retornado. O código abaixo ilustra como embutir o player no HTML:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<body>
    <video width="480" height="320" controls="controls" autoplay="autoplay">
        <source src="/api/Entretenimento/BuscarVideo?nomeDoVideo=TranneNoi.mp4"
                type="video/mp4">
    </video>
</body>
</html>

Por fim, para conseguir perceber o resultado, a imagem abaixo (clique para ampliar) ilustra de um lado o Visual Studio com o breakpoint parado no interior do while, e apesar de não perceber isso na imagem (porque ela é estática), o vídeo também está parado esperando que se escreva mais bytes no stream para que ele possa exibir, e na medida em que se passa pelo loop, logo o vídeo volta a ser executado até que, obviamente, pare de receber novamente os bytes ou quando o mesmo já estiver encerrado.

Clique para Ampliar

Tags: ,

ASP.NET

Buffer e Streaming no ASP.NET Web API

by Israel Aece 26. May 2014 21:02

Como sabemos, o ASP.NET Web API permite o upload e download de arquivos para uma determinada ação dentro do controller, e apesar de podermos receber e retornar sem maiores problemas estes arquivos, precisamos nos atentar à algumas configurações que precisamos realizar dependendo do tamanho dos arquivos que desejamos receber.

Devido a isso, precisamos explicitamente realizar essas configurações, já que o ASP.NET vem com algumas definições padrões que talvez não irão satisfazer as nossas necessidades. Como sabemos, uma das formas de hospedarmos a nossa API é utilizando a própria infraestrutura do ASP.NET, que por sua vez, deve ser colocado dentro do IIS. Tanto o IIS quanto o ASP.NET possuem limitadores que abortam a requisição caso a mesma seja maior que um determinado valor.

Um velho conhecido de quem trabalha com o ASP.NET é o arquivo Web.config. Dentro dele há um elemento chamado httpRuntime, que possui entre inúmeras configurações que interferem na execução daquela aplicação ASP.NET, uma que é justamente o tamanho máximo (em bytes) que a requisição deve ter. Abaixo estou definindo que a mesma não possa exceder 5 MB, caso contrário, o cliente receberá um erro de código 500 (Internal Server Error), indicando o excesso de informações que foi submetida.

<system.web>
  <httpRuntime maxRequestLength="5120" />
</system.web>

É importante que se mantenha este valor no limite viável para sua aplicação, para que não abra demais e, consequentemente, comece a receber requisições mal intencionadas e que poderão comprometer o bom desempenho do servidor onde a API está sendo hospedada e executada.

Muitas vezes a necessidade de alterar essas configurações tem a ver com o recebimento de arquivos (uploads). Apesar de alterarmos essa configuração, ainda há algo que possamos fazer para que seja possível ter uma melhor performance durante o processamento da requisição. Por padrão, o ASP.NET Web API faz o buffer da mensagem completa em memória, para depois disso, processá-la. Por mais que o valor informado na atributo acima seja enorme, se você não tiver memória suficiente, a requisição não será processada.

Felizmente o ASP.NET Web API permite alterar esse comportamento, fazendo com que a requisição seja convertida em um modelo de streaming, repassando os bytes para o destino diretamente, sem a necessidade de alocar memória para isso. Tudo o que precisamos fazer é implementar a interface IHostBufferPolicySelector e definir qual o modelo de buffer a ser utilizado, podendo definir um padrão para a entrada (upload) e outro para a saída (download).

Já existe uma implementação padrão desta interface que é a classe WebHostBufferPolicySelector (já utilizada pelo ASP.NET Web API). Essa classe permite sobrescrever os mesmos métodos expostos pela interface (UseBufferedInputStream e UseBufferedOutputStream), onde ambos os métodos, retornam um valor boleano indicando se deve ou não efetuar o buffer da requisição/resposta. Para o exemplo, vamos apenas nos preocupar com as requisições, ou seja, os uploads enviados à API, não devem ser colocados na memória. Apesar de poder condicionar ao controller aqui, optei por não realizar o buffer se o tipo de conteúdo da requisição se tratar de um formulário defina o multipart no enctype, ou seja, trata-se de um upload. Note que não colocamos o controller/ação em hard-code aqui.

public class NoBufferPolicySelector : WebHostBufferPolicySelector
{
    public override bool UseBufferedInputStream(object hostContext)
    {
        var context = hostContext as HttpContextBase;

        if (context != null)
            if (context.Request.ContentType != null && context.Request.ContentType.Contains("multipart"))
                return false;

        return true;
    }
}

Depois da classe acima criada, é necessário acoplarmos a mesma a execução do ASP.NET Web API, e para isso, basta recorrermos à coleção de serviços exposta pela classe HttpConfiguration, e lá substituirmos o seletor padrão por este que acabamos de criar. O código abaixo ilustra essa configuração.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Services.Replace(typeof(IHostBufferPolicySelector), 
            new NoBufferPolicySelector());

        //Outras configurações
    }
}

Vale lembrar que essa configuração é válida somente quando estamos optando por hospedar as APIs no IIS. Quando optamos pelo modelo de self-host, então devemos continuar recorrendo ao enumerador TransferMode, criado e exposto pelo API do WCF.

Tags: , , ,

ASP.NET

Upload de Arquivos no ASP.NET Web API

by Israel Aece 26. May 2014 19:45

O ASP.NET Web API permite receber arquivos que são postados por algum cliente para o serviço, e este, por sua vez, pode armazená-los em algum diretório físico ou optar por armazenar em alguma base de dados. Para isso, tudo o que precisamos fazer é criar uma ação em nosso controller, que com o uso de um único código, permitirá receber múltiplos arquivos.

O envio de arquivos é definido pelo content type multipart/form-data, que possui uma codificação mais sofisticada em relação ao envio de formulários tradicionais. Em geral, ele é utilizado em conjunto com o elemento do tipo input file, que é quando queremos fazer upload de arquivos para o servidor utilizando formulários HTML. Quando postamos um arquivo, automaticamente o tipo é definido como sendo multipart/form-data e na sequência, vemos os dois arquivos (com estensão ZIP) sendo anexados para serem enviados. Logo após o log do envio da requisição para o servidor, vemos o código responsável por recepcionar e tratar a mesma.

POST http://localhost:6764/api/Teste/Upload HTTP/1.1
Content-Type: multipart/form-data; boundary=-------------------------acebdf13572468
User-Agent: Fiddler
Host: localhost:6764
Content-Length: 148996

---------------------------acebdf13572468
Content-Disposition: form-data; name="fieldNameHere"; filename="Arquivo1.zip"
Content-Type: application/octet-stream

--- Omitidos por questões de espaço ---
---------------------------acebdf13572468
Content-Disposition: form-data; name="fieldNameHere"; filename="Arquivo2.zip"
Content-Type: application/octet-stream


[HttpPost]
public async Task<HttpResponseMessage> Upload()
{
    var provider = 
        new MultipartFormDataStreamProvider(HttpContext.Current.Server.MapPath("~/Uploads"));

    return await Request.Content.ReadAsMultipartAsync(provider).ContinueWith<HttpResponseMessage>(t =>
    {
        if (t.IsFaulted || t.IsCanceled) 
            return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception);

        return Request.CreateResponse(HttpStatusCode.OK);
    });
}

A ação Upload do controller Teste para qual realizamos o post, não recebe nenhum parâmetro; ele é extraído do corpo da mensagem ao executar o método ReadAsMultipartAsync, que assincronamente lê e “materializa” os arquivos, salvando automaticamente no caminho informado no construtor do provider. Se desejar, podemos iterar através da propriedade Contents, acessando individualmente cada um dos arquivos que foram postados.

Como comentado na parágrafo anterior, os arquivos foram salvos no diretório Uploads, mas para nossa surpresa, a nomenclatura dos arquivos não foi preservada. Isso se deve ao fato de que o ASP.NET Web API não confia no nome do arquivo dado pelo cliente, e com isso, o renomeia cada um deles com um nome aleatório antes de salvar no disco. Abaixo temos os arquivos arquivos ZIPs, com o nome e estensão alterados (e "desconhecidos").

Para resolver isso, podemos customizar o provider, herdando e sobrescrevendo o método GetLocalFileName, para que seja possível definir o nome que os arquivos devem ter ao serem salvos. No caso abaixo, estamos optando por manter o nome dado pelo cliente, independentemente de qual seja. Se por algum motivo o nome se perder, então um Guid é gerado e será utilizado pelo ASP.NET Web API para nomear aquele arquivo. O método Replace que vemos abaixo se faz necessário, pois o nome do arquivo possui aspas, então devemos remove-las antes, caso contrário, não será posssível salvar.

public class CustomMultipartFormDataStreamProvider : MultipartFormDataStreamProvider
{
    public CustomMultipartFormDataStreamProvider(string rootPath)
        : base(rootPath) { }

    public override string GetLocalFileName(HttpContentHeaders headers)
    {
        var filename = headers.ContentDisposition.FileName;

        return !string.IsNullOrWhiteSpace(filename) ?
                    filename.Replace("\"", string.Empty) :
                    Guid.NewGuid().ToString();
    }
}

Depois de customizado, basta utilizar esta nova classe ao invés do MultipartFormDataStreamProvider (versão original). Ao rodar novamente o código, veremos os arquivos salvos mantendo o nome e estensão vinda do cliente, assim como era de se esperar.

Tags: , ,

ASP.NET

OutputCache com Cookies

by Israel Aece 20. May 2014 21:39

Presente desde as primeiras versões do ASP.NET, o OutputCache é um recurso que permite armazenar todo o conteúdo HTML gerado por uma requisição para que em futuras requisições não seja necessário executar novamente todas as regras, gerar o HTML e devolver para o cliente o que está sendo solicitado.

O OutputCache intercepta a requisição logo nos estágios iniciais em que ela chega ao ASP.NET. Se nada for encontrado na memória, a requisição é encaminhada para os níveis mais baixos, gera o HTML e antes de retornar ao cliente, é colocado em memória. Futuras requisições farão com que o HTML seja devolvido prontamente, sem que seja necessário passar por tudo isso novamente. Vale lembrar que existem políticas de expiração (que são configuráveis), para que seja possível a renovação depois de um determinado período que o conteúdo se encontra no cache.

Existe um detalhe que se não se atentar, o OutputCache não funcionará como esperado, e para exemplificar, vamos analisar o código abaixo. Note que a ação está decorada com o atributo OutputCacheAttribute, e em seu construtor estamos definindo que queremos que o conteúdo gerado seja armazenado por 10 segundos. Uma vez retornado, as requisições subsequentes que ocorrerão antes desse tempo (10 segundos) expirar, não serão executadas, e como disse acima, o HTML gerado pela primeira vez será retornado à qualquer cliente que solicitar.

public class TesteController : Controller
{
    [OutputCache(Duration = 10)]
    public ActionResult Index()
    {
        this.Response.AppendCookie(
            new HttpCookie("Cultura", "pt-BR"));

        return View();
    }
}

<html>
<head>
    <title>Index</title>
</head>
<body>
    <div>
        <h2>Index</h2>
        <br /><br />
        @DateTime.Now.ToLongTimeString()
    </div>
</body>
</html>

Só que para nossa surpresa, a cada requisição a data exibida na tela sempre é a mais atual, ou seja, o conteúdo não seja sendo colocado em cache. Isso está ocorrendo pelo fato de estar criado e anexando um cookie à resposta. Por padrão, o ASP.NET ignora o OutputCache, caso contrário o cookie sempre seria retornado, e muitas vezes ele não está associado diretamente a requisição e sim a algum usuário.

Para burlar isso, você pode utilizar a propriedade booleana chamada Shareable, exposta pela própria classe HttpCookie. Isso indicará ao ASP.NET que o resultado pode ser colocado em cache mesmo que a resposta contenha cookies definidos. O cookie só será enviado ao navegados (Set-Cookie) quando for a primeira requisição ou quando o cache expirar. O exemplo abaixo ilustra a sua utilização:

this.Response.AppendCookie(
    new HttpCookie("Cultura", "pt-BR") { Shareable = true });

Tags:

ASP.NET

Tarefas de Background no ASP.NET

by Israel Aece 18. May 2014 20:21

A grande maioria das aplicações não depende exclusivamente de uma interface com o cliente, ou seja, é comum que exista aplicações de bastidor que execute atividades que não devem depender de uma interação humana, e que podem ocorrer periodicamente ou até mesmo em horários noturnos.

Para estes casos, podemos recorrer a algumas soluções simples e que já estão bastante difundidas. Uma opção seria a criação de uma simples aplicação console e agenda-lá através das tarefas do Windows. Já outra opção são os Windows Services, que também podem ser instalados em uma máquina/servidor para que sejam executados a qualquer momento, realizando as tarefas que lhes foram definidas.

Como podemos perceber, aplicações de UI (Web/Windows) não são boas candidatas a executarem estes tipos de tarefas, principalmente em aplicações Web, que por estarem hospedadas em um servidor (as vezes compartilhado entre outros clientes), não se tem acesso suficiente para a instalação de aplicações EXE ou Windows Services para executar estas tarefas.

A criação e manutenção de tarefas dentro de uma aplicação ASP.NET nunca foi uma tarefa fácil de se fazer. Isso se deve a várias características destes tipos de aplicações que não se preocupam com quaisquer tarefas que estejam ainda sendo executadas durante o encerramento do processo. A reciclagem do processo do IIS (por inatividade, por alteração no Web.config, etc.) acabam finalizando todo o processo w3wp.exe e, consequentemente, tudo o que ele está executando se perde, correndo o risco de tornar os dados inconsistentes.

O ASP.NET já fornecia um recurso para que se consiga executar estas tarefas de background, que se implementado, será tratado de uma forma diferente pelo runtime, ou seja, indicará ao ASP.NET que ele se atente ao código antes de abortar o mesmo. Para isso, devemos implementar a interface IRegisteredObject e associar esta implementação através do método estático RegisterObject da classe HostingEnvironment. Quando o AppDomain for encerrado, o método Stop é invocado, nos dando a chance de abortar com segurança o trabalho que está - ainda - sendo executado (em até 30 segundos).

Com o recém lançado .NET Framework 4.5.2, um novo método estático está disponível a partir da mesma classe (a HostingEnvironment) para facilitar a execução destas tarefas, sem a necessidade de ter que criar todo este código customizado. Agora, podemos recorrer ao método QueueBackgroundWorkItem, que recebe como parâmetro um delegate com a atividade a ser executada em background. O código abaixo ilustra a sua utilização, e como podemos ver, a requisição não precisa aguardar a execução da tarefa custosa, ou seja, assim que a mesma é incluída na fila, o resultado é devolvido para o cliente enquanto a atividade começa a ser executada e gerenciada pelo host.

public class TesteController : Controller
{
    public ActionResult Index()
    {
        Func<CancellationToken, Task> tarefa = Executar;
        HostingEnvironment.QueueBackgroundWorkItem(tarefa);
        
        return View();
    }

    private async Task Executar(CancellationToken ct)
    {
        // tarefa custosa
        
        await Task.Delay(5000);
    }
}

Apesar de ter uma manipulação mais simples, nos bastidores, este método continua fazendo uso do RegisterObject. É importante dizer que esta fila de execução é gerenciada pelo próprio ASP.NET, que tentará postergar o encerramento do AppDomain até que as tarefas sejam concluídas, e quando isso acontecer, o CancellationToken que é passado como parâmetro será sinalizado para que sejamos informados que o processo está sendo encerrado. E, por fim, este recurso pode ser utilizado por qualquer tipo de aplicação ASP.NET.

Tags: ,

ASP.NET | Async

Versionamento de APIs

by Israel Aece 28. April 2014 22:48

Sempre que precisamos desenvolver um serviço para que ele seja consumido (interna ou externamente), a maior dificildade é sempre decidir o que e como expor. A tecnologia em pouco tempo é possível que se tenha conhecimento suficiente para extrair o seu potencial, mas o maior desafio é saber o que expor, e isso muitas vezes está diretamente ligado ao conhecimento que se tem do negócio.

E como se não fosse suficiente, os problemas não param por aí. Depois que o serviço (ou API) esteja no ar, o desafio é outro, seja, a manutenção do mesmo, no que diz respeito a segurança, performance e evolução. A partir do momento em que a API está sendo consumida por, no mínimo, um cliente, uma preocupação passa a ser necessária ao fazer qualquer alteração em sua interface pública, pois dependendo do que é alterado, podemos deixar alguns clientes inoperantes, problema que não tínhamos quando colocamos pela primeira vez a API no ar.

O versionamento da API é importante para caracterizar a evolução da mesma, mas é útil também para que o cliente saiba o que e como está consumindo, e quando uma nova versão entrar no ar, é desejável que se mantenha compatibilidade com os clientes que já fazem uso das versões anteriores, e os novos clientes, já podem usufruir da nova versão sem qualquer restrição.

Quando falamos de API REST, podemos fazer uso de uma das três opções abaixo para identificar a versão, a saber:

  • Headers: inclusão de uma chave no cabeçalho da mensagem.
  • URI: embutir como parte da URI a versão: http://localhost/Documentos/v2/Listar
  • QueryStrings: sufixar a URI com uma informação extra, exemplo: http://localhost/Documentos/Listar?v=2

A primeira opção, que é a utilização da coleção de headers, acaba sendo uma opção bastante interessante, já que não altera a URI e permite manter separado qualquer detalhe de versionamento; já a segunda opção, é bem mais problemática, pois se o cliente salvar localmente o endereço e mais tarde quiser acessá-lo novamente, o servidor ainda terá que responder à esta solicitação, ou seja, sabe-se lá por quanto tempo ainda será necessário manter os dois endereços e, consequentemente, as duas APIs rodando. E por fim, a terceira opção, apesar de menos elegante que a primeira, permite facilmente expressar qual versão da API deseja acessar, sem a manipulação de headers (que pode complicar para alguns clientes) e sem agregar à URI alguma informação que possa prejudicar futuramente.

O ASP.NET Web API permite que você customize a seleção do controller através de um ponto de estensibilidade, sem misturar infraestrutura com regra de negócio. Para isso, podemos recorrer à requisição extraindo as informações (headers, querystrings, etc.) que são necessárias para tomar a decisão de qual controller acessar. Para nosso exemplo, suponhamos que temos um controller que retorna documentos (versão 1.0) e mais tarde, criamos uma nova versão que retorna os mesmos documentos, só que agora incluindo a assinatura de quem o assinou (versão 2.0). A imagem abaixo ilustra os tipos que foram utilizados.

Para que seja possível influenciar na escolha do controller, o primeiro passo para é implementar a interface IHttpControllerSelector, e dentro desta classe escrever a regra necessária para tomar esta decisão. No exemplo abaixo tentamos extrair o header com o nome "Versao"; se encontrado a versão 1.0 ou se nada for encontrado, então retornamos o controller DocumentosController (que é a versão 1.0). Se a versão solicitada pelo cliente for a 2.0, então retornamos a classe DocumentosAssinadosController.

public class SeletorDeControllerDeDocumento : IHttpControllerSelector
{
    private readonly Dictionary<string, HttpControllerDescriptor> controllersConhecidos;
    private const string HeaderDeVersao = "Versao";
    private const string VersaoPadrao = "1.0";

    public SeletorDeControllerDeDocumento(HttpConfiguration config)
    {
        this.controllersConhecidos = new Dictionary<string, HttpControllerDescriptor>()
        {
            { "1.0", new HttpControllerDescriptor(config, "DocumentosController", 
                typeof(DocumentosController)) },
            { "2.0", new HttpControllerDescriptor(config, "DocumentosAssinadosController", 
                typeof(DocumentosAssinadosController)) }
        };
    }

    public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
    {
        return this.controllersConhecidos;
    }

    public HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {
        IEnumerable<string> valores = null;

        if (request.Headers.TryGetValues(HeaderDeVersao, out valores))
            foreach (var item in valores)
                if (controllersConhecidos.ContainsKey(item))
                    return controllersConhecidos[item];

        return controllersConhecidos[VersaoPadrao];
    }
}

Só que esta classe por si só não funciona, ou seja, precisamos acoplá-la à execução, substituindo a implementação padrão que vem com o ASP.NET Web API. Para isso, basta ir até o arquivo Global.asax e fazer o seguinte ajuste:

config.Services.Replace(typeof(IHttpControllerSelector), 
    new SeletorDeControllerDeDocumento(config));

Depois da implementação e da configuração da API, basta executarmos e através de algum cliente (vamos utilizar o Fiddler para os testes), iremos notar a diferença na requisição e, principalmente, na resposta. Como vamos notar, competirá ao cliente expressar qual a versão que ele deseja, e se omitir (pois isso deve ser a configuração padrão dos clientes iniciais), então a versão 1.0 será retornada.

[ Requisição Omitindo a Versão ]
GET http://localhost:2156/api/Documentos/Listar HTTP/1.1
User-Agent: Fiddler
Host: localhost:2156

[ Resposta na Versão 1.0 ]
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 247

[{"Descricao":"Documento1","DataDaAssinatura":"2014-04-23"},{"Descricao":"Documento2","DataDaAssinatura":"2014-04-25"},{"Descricao":"Documento3","DataDaAssinatura":"2014-04-26"}]

[ Requisição na Versão 2.0 ]
GET http://localhost:2156/api/Documentos/Listar HTTP/1.1
User-Agent: Fiddler
Host: localhost:2156
Versao: 2.0

[ Resposta na Versão 2.0 ]
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 307

[{"Assinatura":"AQID","Descricao":"Documento1","DataDaAssinatura":"2014-04-23"},{"Assinatura":"BAUG","Descricao":"Documento2","DataDaAssinatura":"2014-04-25"},{"Assinatura":"BwgJ","Descricao":"Documento3","DataDaAssinatura":"2014-04-26"}]

Tags: , ,

ASP.NET | CSD

Powered by BlogEngine.NET 1.5.0.0
Theme by Mads Kristensen

Sobre

Meu nome é Israel Aece e sou especialista em tecnologias de desenvolvimento Microsoft, atuando como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. [ Mais ]

Twitter

Indicações

Introdução ao ASP.NET Web API - e-Book