Escolhendo o gerenciamento de estado no ASP.NET

by Israel Aece 29. July 2010 16:24

Uma das principais necessidades em uma aplicação Web é a manutenção de estado. O protocolo HTTP determina que todas as aplicações que rodam sobre ele, não mantém nenhuma espécie de informação do lado do servidor, ou seja, ao requisitar qualquer recurso, uma vez que o conteúdo é enviado ao cliente solicitante, tudo o que foi gerado do lado do servidor para atende-lo, será descartado. Caso uma nova requisição ocorrer, mesmo que seja para o mesmo recurso, tudo será reconstruído.

Mas o que é o "estado"? Trata-se de dados que representam certas informações para o usuário ou para a aplicação, e que precisam ser armazenadas em algum local para conseguirmos restaurá-las no momento em que precisarmos delas. Depois que a requisição foi atendida pelo servidor, se ele precisar armazenar informações para aquele usuário, precisamos fazer uso de alguma das alternativas que são disponibilizadas pela tecnologia que estamos utilizando, e com isso, sermos capazes de manter esses dados mesmo que o usuário esteja totalmente "desconectado" da aplicação, para que quando ele voltar ao servidor, os dados continuarão disponíveis, refazendo o estado da requisição antes dela ser enviada ao navegador.

As alternativas que temos depende da tecnologia que estamos utilizando. No ambiente Microsoft, a tecnologia para desenvolvimento de aplicações Web é o ASP.NET. Ele, por sua vez, traz uma porção de opções para utilizarmos, e atualmente temos: Application, Caching, Controles Hidden, ViewState, Cookie, Session, Profile, QueryString e Context.Items. Para você escolher o que melhor se encaixa a sua necessidade, é necessário avaliar detalhes como segurança e performance. São essas duas categorias que mais influenciam na escolha de qual opção utilizar para manter as informações.

Para tentar auxiliar na escolha de cada um, criei um fluxograma que tenta exibir a melhor opção de acordo com a necessidade, balanceando entre escopo, performance e segurança. Abaixo temos a imagem que representa esse fluxograma, e na sequência, uma descrição mais detalhada de cada condicional que vemos nele.

  • Compartilhar: O compartilhamento consiste em ter uma informação que pode estar acessível entre vários usuários que acessam a mesma aplicação. Isso quer dizer que qualquer alteração que um usuário faça, já refletirá para todos aqueles que a acessam.
  • Dependência ou Expiração: Algumas vezes, a informação que é compartilhada entre os usuários poderá ter critérios de invalidação. Se você tem algo na memória e quer determinar uma forma de removê-la de lá, você pode optar por dependência ou expiração. A primeira delas consiste em ser reativo, ou seja, seremos notificado quando alguma mudança ocorrer na origem da informação, enquanto a segunda opção, devemos especificar um valor para determinar quando ela deverá ser removida.
  • Acessível entre requisições (escopo de usuário): A ideia desta condição é determina se os dados precisam ou não sobreviver entre a navegação das páginas da aplicação.
  • Dados Complexos ou Sigilosos (escopo de usuário): Para as informações que devem sobreviver entre todas as páginas da aplicação, podemos dividir em dados complexos ou sigilosos. Informações simples, como por exemplo, o idioma selecionado pelo usuário, podemos recorrer aos Cookies para armazená-las. Já aqueles dados que vão muito além do que simples strings, como é o caso de um carrinho de compras, podemos recorrer a recursos mais poderosos, como Session ou Profile.
  • Persistência: Utilizar Session ou Profile dependerá, principalmente, se você quer ou não ter a capacidade de persistir as informações. A principal diferença entre armazenar o carrinho de compras na Session ou não Profile, é que na segunda opção, as informações sobreviverão entre reinicializações do navegador e do servidor, ao contrário do que acontece com a Session [1], que é descartada ao fechar o navegador.
  • Transferência: A transferência consiste em levar dados de uma página para a outra, que geralmente parametrizam as tarefas que esta segunda página irá desempenhar.
  • Transferência no Servidor: Caso você já esteja executando algum código no servidor, então você pode optar pelo Context.Items para enviar os dados para uma outra página, sem a necessidade de voltar ao navegador, e a partir dali, ir para a página solicitada. Utilizar o Context.Items garante que as informações sejam passadas de forma transparente, e o usuário ou até mesmo os interceptadores de requisições HTTP não conseguirão capturar esses dados. Agora, se tivermos a necessidade de parametrizar publicamente a chamada de uma página, então a QueryString é a melhor saída, mas apenas lembre-se de que ela possue limitação de tamanho e você não deve passar dados sensíveis ali.
  • Manter dados na própria página: Para casos onde precisamos manter dados para uso excluso da página que estamos utilizando, podemos recorrer aos controles Hidden [2]. Esse tipo de controle é capaz de guardar informações simples, e o seu uso serve apenas para manter informações que não sejam sensíveis, estando tão vulneráveis quanto as QueryStrings.

[1] - A configuração padrão da Session no ASP.NET faz com que as informações sejam armazenadas InProc, ou seja, na memória do próprio servidor Web. Você pode alterar esse comportamento, e eleger um servidor SQL Server para persistir essas informações. Mas para essa situação, o Profile pode ser melhor, já que possui várias outras características interessantes, como a possibilidade de tipar as informações que serão armazenadas, migração de usuários anônimos, etc.

[2] - No caso do ASP.NET Web Forms, o ViewState também pode ser utilizado para manter as informações em nível de página, mas que irá recorrer ao uso de controles Hidden para o armazenamento delas. É importante dizer que o ViewState é armazenado nestes controles de forma codificada, ou seja, é possível extrair o que armazenamos ali. Seguindo a mesma linha de alguns itens anteriores, você não deve colocar informações sigilosas dentro dele.

Todos as opções para armazenamento do estado são basicamente dicionários, ou seja, utilizam a chave como sendo uma string. Já o valor dependerá do que está utilizando, por exemplo, no caso de Caching, Application, Session, Profile ou Context.Items, você poderá armazenar qualquer objeto, desde que ele esteja decorado com o atributo SerializableAttribute. As outras opções apenas trabalham com simples strings como valor.

Tags:

ASP.NET

Novas funcionalidades do Process Explorer

by Israel Aece 26. March 2010 16:27

A nova versão do Process Explorer, ferramenta que utilizamos para gerenciar e monitorar processos dentro do sistema operacional, passa a ter novas funcionalidades para processos gerenciados, ou melhor, para conseguirmos visualizar algumas informações relacionados exclusivamente ao .NET (CLR). A partir de agora ele é capaz de listar dos AppDomains que existem dentro da aplicação, os assemblies que foram carregados para dentro de cada um deles, contadores de performance relacionados à exceções, threading, etc. Essas informações já estavam acessíveis a partir de contadores de performance do Windows (perfmon.exe), mas agora fica muito mais fácil e intuitivo de chegar até elas. A imagem abaixo ilustra as suas duas novas abas:

Tags: ,

.NET Framework

Compressão em Serviços WCF

by Israel Aece 18. March 2010 00:16

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.

Mas infelizmente o WCF não traz a possibilidade de compactar e/ou descompactar as mensagens que são enviadas. Sendo assim, para conseguirmos efetuar a compactação, temos que recorrer aos pontos de estensibilidade de WCF, para conseguir interceptar o envio e recebimento das mensagens, para assim conseguir diminuir o conteúdo a ser trafegado. A Microsoft se preocupou em deixar a disposição de todos, um exemplo que estende o WCF, e utiliza a classe GZipStream (System.IO.Compression) para compactar as mensagens; além deste exemplo, há um projeto chamado WCF Extensions, que possui essa funcionalidade de compactação. É importante dizer que em ambos os lados (cliente e serviço) precisam acoplar o código para efetuar a compactação e descompactação, caso contrário, a mensagem não poderá ser lida.

Além disso, para aqueles que hospedam o serviço no IIS, podem tirar proveito da compactação que ele faz. O IIS fornece um serviço que permite efetuar a compactação de conteúdo dinâmico, que é o caso de serviços WCF. Quando habilitado, este serviço irá compactar a resposta que será enviada ao cliente. O IIS compacta a resposta dependendo de um header que vem na requisição, indicando se o cliente consegue ou não interpretar o conteúdo compactado. Como os bindings do WCF não fornecem isso, você tem que explicitamente adicionar um header para encaminhar ao servidor (IIS) que você consegue compreender o algoritmo GZIP ou o Deflate. Esse header é o Accept-Encoding, e para fazer isso podemos recorrer ao código abaixo:

using (ServiceClient p = new ServiceClient())
{
    using (new OperationContextScope(p.InnerChannel))
    {
        var props = new HttpRequestMessageProperty();
        props.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate");
        OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = props;

        Console.WriteLine(p.RetornaUmTextoMuitoGrande());
    }
}

Ao monitorar a requisição através do Fiddler, você poderá perceber que essas informações são encaminhadas ao IIS, incluindo o header necessário:

POST /MeuServico/Service.svc HTTP/1.1
Content-Type: text/xml; charset=utf-8
Accept-Encoding: gzip,deflate
SOAPAction: "http://tempuri.org/IService/RetornaUmTextoMuitoGrande"
Host: israelaecenb1
Content-Length: 151
Expect: 100-continue
Connection: Keep-Alive

Com isso, a mensagem de retorno que antes tinha um total de 2.000.176 bytes, passa a ter apenas 17.494 bytes usando o GZIP. Mas apesar de conseguirmos visualizar o conteúdo compactado, isso não quer dizer o proxy, ou melhor, o binding que está do lado do cliente, irá conseguir interpretar o mesmo. Como comentado acima, pelo fato do WCF não suportar nativamente a compactação, uma exceção do tipo ProtocolException será disparada, indicando exatamente isso.

Na versão 4.0 do WCF, uma nova propriedade foi adicionada na classe HttpTransportElement, chamada de DecompressionEnabled. Essa propriedade recebe um valor boleano indicando se o WCF pode ou não compactar as mensagens de resposta que são enviadas ao cliente. Por padrão, essa propriedade é definida como True, mas para conseguir acessá-la, é necessário a criação de um binding customizado. Um outro ponto importante é com relação à mensagem de requisição, que ao contrário das versões anteriores, a versão 4.0 do WCF já embuti o header necessário (Accept-Encoding) para que o serviço saiba que o cliente consegue descompactar a resposta.

Tags: , , , ,

WCF

Mensagens Assíncronas com ChannelFactory

by Israel Aece 18. November 2009 10:41

Uma das formas que temos para consumir serviços WCF em um cliente qualquer, é utilizando a classe ChannelFactory<TChannel>, assim como eu já mostrei um exemplo neste post. Geralmente utilizamos esta técnica quando optamos por compartilhar os tipos (incluindo a interface que representa o contrato) entre o serviço e o cliente, evitando a publicação do documento WSDL, a reconstrução destes tipos do lado do cliente e, principalmente, a necessidade de efetuar a referência do serviço através da IDE do Visual Studio .NET.

Atenção: Antes de prosseguir a leitura deste post, aconselho que leia os seguintes artigos:

Quando fazemos a referência ao serviço através do Visual Studio, há uma opção chamada "Generate asynchronous operations", que para cada operação encontrada no serviço, um par de métodos BeginNomeDaOperacao e EndNomeDaOperacao serão criados, e que irão trabalhar em conjunto, para que assim, o cliente consiga invocar a respectiva operação de forma assíncrona. O problema disso é que muitas vezes o contrato não oferece suporte à chamadas assíncronas, o que nos obrigará a criar toda a infraestrutura do lado do cliente para suportar isso. Imagine que temos o seguinte contrato:

[ServiceContract]
public interface IContrato
{
    [OperationContract]
    string FazAlgo(string value);
}

Como não temos a versão assíncrona do método FazAlgo, temos que recorrer à delegates para conseguir efetuar a chamada. Sendo assim, o código do lado do cliente, poderia ficar da seguinte forma:

private ChannelFactory<IContrato> _factory;
private IContrato _proxy;

public Form1()
{
    this._factory =
        new ChannelFactory<IContrato>(
            new NetTcpBinding(),
            "net.tcp://localhost:8722/srv");

    this._proxy = this._factory.CreateChannel();
}

private void button1_Click(object sender, EventArgs e)
{
    string parametro = "algum valor";

    Func<string, string> executor = p => this._proxy.FazAlgo(p);

    executor.BeginInvoke(
        parametro,
        result =>
        {
            this.textBox1.Invoke(
                new Action<string>(valor => this.textBox1.Text = valor),
                ((Func<string, string>)result.AsyncState).EndInvoke(result));

        },
        executor);
}

A diferença que vemos no código acima, é que invocamos o método FazAlgo através de um delegate. Ao invés de criarmos delegates a todo momento para uma necessidade específica, podemos recorrer aos delegates expostos pelo .NET Framework Action<> e Func<>. No caso acima, estamos fazendo uso do Func<string, string>, que coincide com a assinatura do método FazAlgo, ou seja, recebe e devolve uma string. No exemplo acima, estamos fazendo o uso de lambda ao invés de explicitamente criar a instância do delegate.

Todos os delegates dão suporte à chamada assíncrono para método que ele mantém a referência, e sendo assim, via BeginInvoke disparamos a execução da operação do serviço, utilizando uma segunda thread, e mantendo a aplicação disponível para outros trabalhos. Neste caso, o método BeginInvoke recebe três parâmetros: o primeiro é o parâmetro de entrada que o serviço recebe; o segundo é um delegate de callback, que será invocado quando o resultado voltar; e finalmente, o terceiro parâmetro é um System.Object que será passado para o callback, e que estará acessível através da propriedade AsyncState, e que no caso, passamos a instância do delegate criado para invocar o método.

Para recuperar o resultado, dentro do callback invocamos o método EndInvoke, que retornará o resultado do serviço, e como na maioria dos casos, precisamos exibir isso na tela. Como o callback sempre é disparado na thread que está executando o método assíncrono, você não pode tocar nos controles, já que eles tem afinidade com a thread de criação deles. Aqui entra em cena o método Invoke, exposto pelo do controle que deseja atualizar, e através dele, determinamos um método para ser executado na mesma thread que o criou, e aqui também utilizando lambda.

Se você tiver acesso ao contrato e poder alterá-lo, então você pode dar suporte ao processamento assíncrono ao mesmo, e grande parte do trabalho que vimos acima, do lado do cliente, será descartado. Para suportar o processamento assíncrono no contrato, temos que criar as versões assíncronas do método, e com isso, o nosso contrato de exemplo ficará da seguinte forma:

[ServiceContract]
public interface IContrato
{
    [OperationContract]
    string FazAlgo(string value);

    [OperationContract(AsyncPattern = true)]
    IAsyncResult BeginFazAlgo(string value, AsyncCallback callback, object state);

    string EndFazAlgo(IAsyncResult result);
}

Ao contrário do que vimos anteriormente, ao utilizar essa técnica, você terá o processamento assíncrono tanto do lado do cliente, quanto do lado do serviço. Eu já discuti bastante sobre isso nestes outros artigos. Sendo assim, toda a complexidade da chamada assíncrona será movida para o serviço, que deveremos criar a versão assíncrona do método FazAlgo. O código do lado do cliente ficará relativamente mais simples:

private void button2_Click(object sender, EventArgs e)
{
    string parametro = "algum valor";

    this._proxy.BeginFazAlgo(
        parametro,
        result =>
        {
            this.textBox1.Invoke(
                new Action<string>(valor => this.textBox1.Text = valor),
                this._proxy.EndFazAlgo(result));
        }, null);
}

Para ajudar no entendimento, você pode baixar o código de exemplo clicando aqui.

Tags: , , ,

Async | WCF

Caching via Providers

by Israel Aece 16. November 2009 13:08

Assim como o membership, roles, profile, webparts ou health monitoring, a versão 4.0 do ASP.NET também permitirá trabalhar com o caching seguindo essa mesma arquitetura, ou seja, através de provider models. Com isso, teremos uma maior flexibilidade para elegermos o nosso repositório de caching, que nem sempre será a memória do próprio servidor onde a aplicação está hospedada. Provavelmente será através deste recurso que a Microsoft irá acoplar o "Velocity" ao pipeline do ASP.NET.

Para customizar, tudo o que precisamos fazer é criar uma classe que herde da classe abstrata OutputCacheProvider, e sobrescrever os seus métodos autoexplicativos: Add, Get, Remove e Set, assim como é mostrado abaixo.

public class TextCacheProvider : OutputCacheProvider
{
    private static readonly string PATH;

    static TextCacheProvider()
    {
        PATH = HttpContext.Current.Server.MapPath("~/CachedPages/");
    }

    public override object Add(string key, object entry, DateTime utcExpiry) { } 

    public override object Get(string key) { }

    public override void Remove(string key) { }

    public override void Set(string key, object entry, DateTime utcExpiry) { }
}

Depois desta classe criada, que seguirá as regras de caching customizadas, precisamos configurá-la para poder ser utilizada, e para isso recorremos ao arquivo Web.config. Tudo o que precisamos fazer para que essa nova classe funcione, é acoplá-la a execução, definindo-a como sendo o provider padrão de caching. O trecho de código abaixo ilustra essa configuração:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0"/>
    <caching>
      <outputCache defaultProvider="TextCacheProvider" enableOutputCache="true">
        <providers>
          <clear/>
          <add name="TextCacheProvider" type="TextCacheProvider"/>
        </providers>
      </outputCache>
    </caching>
</configuration>

E se desejar escolher, em tempo de execução, um dos providers de acordo com uma condição específica, você poderá fazer isso através do método GetOutputCacheProviderName, que é exposto pela classe HttpApplication (Global.asax), e retorna uma string que representá o provider a ser utilizado.

Tags: , ,

ASP.NET

Ativação do host baseando-se na memória

by Israel Aece 20. October 2009 09:03

O WCF fornece uma opção que nos permite configurar a porcentagem mínima de memória disponível na máquina antes de ativar o serviço. Para customizar isso, recorremos a propriedade (atributo) MinFreeMemoryPercentageToActivateService da classe (elemento) ServiceHostingEnvironmentSection. Essa propriedade recebe um número inteiro, que corresponde a quantidade de memória disponível que a máquina onde o serviço está rodando deverá ter disponível.

Como essa configuração recorre a métodos não gerenciados para fazer essa verificação, a aplicação (serviço) deverá estar rodando em full-trust. Caso contrário, uma exceção do tipo SecurityException será disparada e, consequentemente, o serviço não estará disponível para receber requisições.

Tags: , ,

WCF

Métodos Sincronizados

by Israel Aece 25. September 2009 12:00

Quando criamos uma classe e dentro dela colocamos métodos, estes podem ser acessados por múltiplas threads ao mesmo tempo. Caso você queira prevenir isso, ou seja, permitir que somente uma única thread por vez o acesse, você pode fazer uso de métodos sincronizados. Tudo o que você precisa fazer para isso funcionar, é decorar o método com o atributo MethodImplAttribute (namespace System.Runtime.CompilerServices), definindo em seu construtor a opção Synchronized, fornecida pelo enumerador MethodImplOptions, assim como é mostrado abaixo:

public class Processo
{
    [MethodImpl(MethodImplOptions.Synchronized)]
    public void Metodo()
    {
        //...
    }
}

Com isso, você terá a garantia de que somente uma thread por vez o executará, enquanto as outras que, eventualmente, cheguem a este mesmo método, ficarão em uma fila, esperando com que a thread atual finalize o seu trabalho.

Apesar de “proteger” as informações, você limitará o acesso à sua classe/tipo. Quando este atributo é aplicado à um método de instância, ele efetuará o bloqueio da instância como um todo (lock(this) { }); já se aplicá-lo em um método estático, ele bloqueará o tipo (lock(typeof(Processo)) { }). A implementação é fácil, mas você será prejudicado com os efeitos colaterais, pois terá maior contenção. Para melhorar o throughput, o ideal é você utilizar alguma técnica de locking que o próprio .NET Framework fornece, para assim refinar o bloqueio da sua classe/tipo.

Tags: ,

.NET Framework

Host de Serviços em Aplicações Windows

by Israel Aece 24. September 2009 21:42

Uma das grandes vantagens do WCF é que qualquer aplicação pode servir como hosting para ele. Isso quer dizer que além do tradicional IIS, podemos recorrer à outros tipos, como Windows Services, Console, Windows Forms ou WPF. Cada uma tem as suas vantagens e desvantagens, mas isso já foi abordado neste artigo.

Cada uma dessas opções de hosting, possuem implementações diferentes, e aplicações Windows Forms e WPF tem um sofrimento maior em relação às outras. Isso se deve ao fato de que aplicações Windows não gerenciam apenas o serviço, mas também há uma interface (formulários) para controlar. Ao utilizar um host próprio, temos que recorrer à classe ServiceHost que gerenciará a vida e a execução do serviço. O problema já começa aqui, ou seja, onde criar e manter a instância desta classe?

Antes de prosseguirmos, precisamos analisar o que são os Synchronization Contexts. Synchronization Contexts permite executarmos uma determinada tarefa em uma outra thread, diferente da qual estamos atualmente, representando uma espécie de “canal” entre as duas threads envolvidas, fazendo tudo o que for necessário para isso. Dentro do namespace System.Threading existe uma classe chamada SynchronizationContext, que representa esse “canal”, e o WCF interage com ele através da propriedade boleana UseSynchronizationContext do atributo ServiceBehaviorAttribute, que por padrão é True.

Na configuração padrão, quando criamos o ServiceHost e invocamos o método Open, o WCF irá verificar se há um synchronization context definido e o utilizará. Quando não existir ou a propriedade UseSynchronizationContext é definida explicitamente como False, as operações serão executadas em uma outra thread, que são extraídas do ThreadPool.

Como o Windows Forms cria automaticamente o SynchronizationContext no construtor de um formulário, quando criamos a instância da classe ServiceHost dentro dele, o WCF irá executar todas as operações nesta mesma thread (que também atende aos comandos do usuário (message loop)), pois o contexto já está criado. O exemplo abaixo ilustra isso:

public partial class CadastroDeClientes : Form
{
    private ServiceHost _host;

    public CadastroDeClientes()
    {
        //Neste momento, o context já está criado
        this._host = new ServiceHost(typeof(ServicoDeClientes), new Uri[] { });
    }
}

O problema desta técnica é que você sobrecarregará a thread de UI, já que ela terá que se preocupar com as operações do serviço e com os eventos tradicionais do formulário. Se você optar por abrir o host antes da chamada de um formulário, como por exemplo, dentro do método Main da aplicação, você ainda não terá o contexto estabelecido. Neste caso, qualquer manipulação que você, eventualmente, faça nos controles do formulário dentro das operações do serviço, precisarão de um tratamento especial, pois você não poderá manipular os controles em uma thread diferente da qual eles foram criados.

Finalmente, se você estiver com a propriedade UseSynchronizationContext definida como True (que é o padrão), e estiver consumindo o serviço no mesmo formulário que possui o ServiceHost criado, você terá problemas. Isso se deve ao fato de que a chamada para o serviço bloqueia a thread de UI, enquanto o WCF posta a mensagem para essa mesma thread para invocar o serviço (que está ocupada). Sendo assim, o deadlock será garantido.

Tags: , ,

WCF

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

Host