Desenvolvimento - ASP. NET

Performance em aplicações ASP.NET

Este artigo abordará boas práticas de escrita de código, tratamento de Exceptions, validações e principalmente performance em aplicações ASP.NET.

por Israel Aéce



Um dos pontos mais críticos de um software, principalmente se o mesmo é utilizado em Web é a questão de performance do mesmo. Performance é algo que não podemos deixar de analisar e sempre analisar um processo se o mesmo será custoso ou não, para que possamos diminuir ao máximo o tempo de execução das tarefas e assim devolver o que o cliente solicitou mais rapidamente.

Este artigo abordará boas práticas de escrita de código, tratamento de Exceptions, validações e principalmente performance em aplicações ASP.NET. Entre estes, vamos ver: validação de dados que o usuário insere no sistema, Data Binding, Cache, ViewState, Session, Server Controls, acesso à dados entre outras características do ASP.NET, que se, mal usadas podem causar um impacto negativo na aplicação desenvolvida.

Sempre quando pensamos em aplicações ASP.NET, nos vem a cabeça uma aplicação rápida e robusta, pois como sabemos a partir do momento que a aplicação estiver disponível para acesso, não conseguimos mais ter controle de quantos ou quais usuários estão acessando a aplicação. Vale lembrar que entre estes usuários sempre existem aqueles que querem de alguma forma destruir e/ou danificar o sistema. Além de termos que garantir uma boa performance, temos que sempre tomarmos cuidado com a segurança, pois é inadimissível aumentar o desempenho com baixa segurança.

Características de Linguagens

  • Option Strict - Se estiver utilizando Visual Basic .NET, uma boa técnica é ativar a opção Option Stricit. Isso fará com que o Visual Basic .NET não permita conversões de objetos implícitamente. Para ativar isso isso você vai até as propriedades do projeto, opção Builder e marque como "On" a opção Option Strict.

  • With ... End With - Ainda em Visual Basic .NET, voce pode utilizar a estrutura With ... End With, onde com ela você não precisa mais referenciar o objeto até que se passe pelo End With.

  • Strings - Quando estiver realizando concatenações de strings é sempre bom avaliar a quantidade e tamanho das strings a serem concatenadas. Com essa análise você poderá fazer o uso ou não do objeto StringBuilder que garante uma concatenação extremamente rápida. Um dica é utilizar o objeto StringBuilder quando não se sabe o número de concatenações que serão realizadas.

  • IDisposable - Implemente esta Interface em seus objetos que utilizam recursos não gerenciados pelo .NET Framework.

Restringindo acesso na aplicação

Apesar de não ter conhecido à fundo o ASP Classic 3.0, via que a forma de restringir uma área do site, onde somente usuário cadastrados em algum lugar tivesse acesso era através de variáveis de sessão, onde quando o usuário fizesse um Login válido, uma váriavel de sessão era criada com algum valor lá dentro.

Com isso, para não ficar fazendo a verificação da variável de sessão em todas as páginas, um arquivo "include" era criado e dentro dele verificado se a variável de sessão é ou não válida e não sendo, redireciona o usuário à página de Login do sistema. Este arquivo era incluído no topo de todas as páginas da aplicação que são restritas.

No ASP.NET isso muda completamente. Continuar fazendo o que era feito no ASP Classic é programar em ASP.NET pensando no passado. Dentro do ASP.NET você tem classes e infra-estrutura para criar uma área restrita facilmente e segura através do FormsAuthentication. Para saber como criar esta área restrita em ASP.NET utilizando Forms Authentication, pode consultar este artigo. Além disso, o ASP.NET fornece também intrinsicamente formas de autenticação via Windows e Passport.

Passagem de Parâmetros

Este é um ponto bastante interessante no ASP.NET, ou melhor, em aplicações Web no geral. A passagem de parâmetros entre as páginas é sempre algo que necessitamos, onde é com ela que mandamos uma referência à algo que precisamos fazer ou consultar em uma outra página. Esse é um dos pontos que a segurança também deve ser bastante reforçada, pois como sabemos, usuários mal intencionados podem causam um estrago considerável em nossa aplicação. Analisaremos abaixo cada um dos métodos que são possíveis utilizá-los:

  • Variáveis de Sessão - É interessante para guardar valores que serão "carregados" por toda a aplicação. Temos a vantagem do cliente não saber onde esta sendo "guardado" este objeto e assim não consegue alterá-lo. Já utilizá-lo para apenas passar um simples parâmetro entre páginas não é viável pois degrada a performance, visto que a Session depois de criada demora 20 minutos (padrão) para ser eliminada da memória do servidor, caso ela não seja removida. Para casos como este ela não deve ser utilizada.

  • QueryString - A QueryString é um objeto bastante usado nas aplicações atuais. É um objeto "leve" e de fácil manipulação. Mas há o inconveniente da mesma ficar exposta ao cliente, podendo ele alterar, colocando ali dado ou script malicioso e assim comprometer a aplicação. É sempre uma boa prática verificar antes de utilizá-la se trata-se de um dado seguro onde podemos passá-lo para o procedimento que o utiliza sem problemas.

    • Aqui um ponto bastante importante para o tratamento "server-side" de QueryStrings. Vemos bastante casos onde os desenvolvedores utilizam o bloco de tratamento de erros estruturado Try...Catch...Finally para este analisá-la. Além de não ser muito interessante, prejudica a peformance levemente, principalmente se o desenvolvedor ainda apanhar a Exception e não faz nada com ela. Um exemplo desse mal uso é mostrado abaixo:


    1
    2
    3
    4
    5
    6
    7
    8
    Dim clienteID As Integer
    Try
    clienteID = Convert.ToInt32(Request.QueryString("ClienteID"))
    Catch ex As Exception
    clienteID = 0
    End Try
    "Processa o Código








    Como podemos ver no código acima, tentamos converter o valor que é passado através da QueryString "ClienteID" para um inteiro. Se o método falhar uma Expcetion é atirada no Cliente e o pior é que não fazemos nada com ela. Apenas no bloco Catch definimos novamente para 0 o valor da variável e competirá ao programador testá-la se é ou não zero.

    Este processo funciona sem problemas, porém não faz se necessário utilizar um tratamento de erros estruturado para isso. Existe uma forma mais elegante de tratar esses valores, que é utilizando Regular Expressions. Através dele não há a necessidade de gerenciarmos, pois através de um método que nos retornará True ou False saberemos se é ou não um número valido. O exemplo abaixo ilustra como fazer isso:












    1
    2
    3
    4
    5
    6
    Imports System.Text.RegularExpressions
    "...
    Dim clienteID As String = Request.QueryString("ClienteID")
    If Regex.IsMatch(clienteID, "^[0-9]+$") Then
    "Processa o Código
    End If






    Nota: Não utilize a propriedade Request.Params quando você sabe de onde o parâmetro está vindo. Esta propriedade percorre as coleções de QueryStrings, Forms, ServerVariables e Cookies, gerando assim um overhead desnecessário.





  • HttpContext.Items - Considero a melhor forma de passar parâmetros entre páginas. É um objeto "leve" e o cliente não sabe por onde os dados estão sendo enviados. Ele une a segurança das variáveis de sessão e a performance da QueryString. Mas há também um pequeno problema, ou seja, ele existe apenas durante a requisição de HTTP e depois disso ele já desaparece.

    • Há uma técnica bastante interessante para utilizá-lo. O segredo está na criação de um método compartilhado (Shared em VB.NET e static em C#) na página que receberá o parâmetro. Este método compartilhado deverá ter os parâmetros que você precisará na página de destino. O código abaixo mostra como criá-lo e chamá-lo, onde o cenário é passar parâmetros da página WebForm1 para a página WebForm2:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      Public Class WebForm2
      Inherits System.Web.UI.Page
      "...
      Public Sub Page_Load(...) Handles MyBase.Load
      Response.Write(HttpContext.Current.Items.Item("NomeCliente"))
      End Sub
      "...
      Public Shared Sub PassaParametros(ByVal nome As String)
      HttpContext.Current.Items.Add("NomeCliente", nome)
      HttpContext.Current..Server.Transfer("WebForm2.aspx")
      End Sub
      End Class












      1
      2
      3
      4
      5
      6
      7
      8
      Public Class WebForm1
      Inherits System.Web.UI.Page
      "...
      Public Sub Page_Load(...) Handles MyBase.Load
      WebForm2.PassaParametros("Microsoft Corporation")
      End Sub
      "...
      End Class








      Analisando o código acima, vemos que dentro do método PassaParametros adicionamos o valor que lhe é passado na coleção de Items do HttpContext. Notem que esta é uma coleção do tipo "chave-valor". Além disso fazemos o redirecionamento "server-side" do usuário através do método Server.Transfer. No evento Load do WebForm2, exibimos o valor passado para o usuário.

      No WebForm1 ou em qualquer outra parte da aplicação, temos acesso a este método justamente porque ele é compartilhado e não necessita da instância da classe para isso.









Pages - Páginas

Temos várias considereções a serem feitas a nível de página, mas também devem ser estendidas a nível de Web User Controls (arquivos ASCX). Veremos abaixos algumas técnicas para garantir um melhor desempenho da aplicação:

  • Page.IsPostBack - Utilize sempre a propriedade Page.IsPostBack da classe Page para evitar o processamento redundante. Como o ViewState mantém o estado dos objetos durante o PostBack não é necessário recarregar e/ou fazer as manipulações a nível de página sempre que ela é requisitada.

  • Page.EnableSessionState - Defina para False quando você não precisa criar/ler variáveis de sessão dentro de uma página específica. Caso precise apenas ler estas variáveis, defina esta propriedade como Readonly.

  • Response.BufferOutput - Como ele é habilitado por padrão no ASP.NET, convém você analisar o cenário para desabilitá-lo ou não. A sua função é se estiver definido como True, o ASP.NET processará toda a página e depois de concluído, mandará o output para o cliente. Caso contrário, ou seja, se estiver definido como False, o ASP.NET já irá enviando o output para o cliente na medida em que a página vai sendo processada. Um cenário em que isso é usado é quando temos uma página com um processamento custoso e temos clientes que não possuem banda-larga. Você pode utilizar o método Response.Flush para enviar o conteúdo corrente armazenado em buffer até o momento para o cliente, lembrando que pode ser invocado múltiplas vezes durante a requisição. Existem três formas de habilitarmos o Buffer, as quais veremos abaixo:

1
2
3
4
5
6
7
8
" via programação
Response.BufferOutput = True
" via diretiva @Page
<@ Page Buffer = "True" %>
" via web.config
<pages buffer="true">








  • Diminuir o tamanho da página - É algo que muitos desenvolvedores não levam em conta. Em design-time, por questões até de pressa ou mesmo de querermos fazer algo mais rápido, acabamos deixando nossa página maior do que ela poderia ser com espaços em brancos e "TABs" desnecessários que aumentarão consideravelmente o tamanho da página a ser enviada para o cliente. O que se tem a fazer neste caso é remover todos os caracteres que não são necessários para apresentação da página. Outro cuidado que devemos ter, para diminuirmos o tamanho, é separar o código Javascript e estilos em outros arquivos, *.js e *.css respectivamente, e depois anexados a página, como é mostrado abaixo:

1
2
3
4
<script language=jscript src="Funcoes.js">
<link href="Estilos.css" rel="stylesheet" type="text/css">




  • Redirecionamento de usuários - Pelo fato de que o método Response.Redirect força o cliente à emitir um novo pedido, torna o método menos eficiente que o Server.Transfer, que evita esta ação. Quando o Server.Tranfer é executado, percebe-se que a URL no browser não é alterada. Ambos os métodos invocam o método Response.End, que dispara uma Exception do tipo ThreadAbortException, tem a finalidade de abortar a requisição corrente.

    • Um erro bastante comum (Thread was being aborted.), é quando chamamos o método Response.Redirect dentro de um bloco Try. O problema é que ele invoca o método Response.End, que este por sua vez atira uma Exception, provavelmente não executará o código de acordo como queremos, pois o Catch irá ser executado.

      Para contornar este problema, devemos utilizar a sobrecarga do método Redirect, onde passamos um valor booleano para indicar se o método Reponse.End deve ou não ser invocado. Definindo este parâmetro como False, a execução do código continua sem problemas, não interferindo assim na lógica do código. Abaixo o código que exemplifica a chamada deste método:


    1
    2
    3
    4
    5
    6
    Try
    "Código...
    Response.Redirect("Pagina.aspx", False)
    Catch ex As Exception
    "Tratamento de erro...
    End Try






  • Validação Client-Side - utilize os controles Validators que o ASP.NET fornece para o tratamento de dados que os usuários inserem do lado do cliente. Com eles você também pode customizar a validação através do JavaScript. É importante deixar claro que isso não dispensa a validação do lado do servidor.

ViewState

ViewState é o mecanismo qual o ASP.NET utiliza para manter o estado de controles e objetos quando um Postback ocorre na página. As informações são armazenadas em um controle do tipo hidden chamado _VIEWSTATE. Apesar de ser um recurso interessante, compromete a performance quando é mal utilizado, pois há ocasiões onde ele não é necessário, e vale lembrar que por padrão ele é habilitado.

Leve em consideração os cenários abaixo para a não utilização do ViewState:

  • Quando a página não dispara Postback.

  • Quando os eventos dos server-controls não são disparados.

  • Quando você popula os controle a cada Refresh da página.

Abaixo as formas de você habilitar/desabilitar o ViewState:

1
2
3
4
5
6
7
8
" via programação
TeuControle.EnableViewState = False
" via diretiva @Page
<@ Page EnableViewState="false" %>
" via web.config
<pages enableViewState="false">

Exceptions

É bastante complicado, mas temos que escrever códigos para evitar Exceptions. Uma boa dica para gerenciar as Exceptions que são geradas pela aplicação é através do evento Application_Error do arquivo Global.asax, que é sempre disparado quando uma Expcetion na aplicação ocorre. Um exemplo simples seria algo como o código mostrado abaixo:

1
2
3
4
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
Dim e As Exception = Server.GetLastError()
" Efetue o Log da Exception para controle.
End Sub

Através do método Server.GetLastError() você recupera a última Exception que ocorreu na aplicação e faz o log da mesma no local que você desejar.

O que também devemos nos atentar é com relação a quando disparar (Throw) Exceptions entre camadas ou mesmo quando chamamos uma determinada função. Isso gera uma perda de performance e obriga também o cliente a tratá-la. Uma técnica comumente utilizada é a criação de um Enumerador e as funções, quando possível, retorna o status da execução do código, baseado neste enumerador. Exemplo:

1
2
3
4
5
Public Enum DBStatus
Falha
JaExistente
Sucesso
End Enum

Depois do Enumerador DBStatus criado, um método, como por exemplo que adicione dados dentro de uma Base de Dados que passará a retornar o status da execução e assim você analisa no cliente e baseado nisso, você informa sucesso ou falha.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Public Function AdicionaRegistro(...) As DBStatus
"...
End Function
" Chamada no Cliente
Dim retorno As DBStatus = AdicionaRegistro(...)
Select Case retorno
Case DBStatus.Falha
"...
Case DBStatus.JaExistente
"...
Case DBStatus.Sucesso
"...
End Select

Acesso à Dados

Sempre que possível, é interessante utilizar o objeto DataReader para a leitura de dados de uma base de dados qualquer. Isso porque é o objeto mais rápido para se resgatar os dados e exibí-los ao cliente. Mas vale levar em consideração que ele é somente-leitura (readonly) e somente avança (foward-only) e com isso necessita de uma conexão ativa até que se percorra todos os dados retornados pela query, requerendo uma atenção especial do desenvolvedor para atentar-se ao fechamento do mesmo.

Uma boa prática é paginarmos os dados retornados pela query, retornando da base de dados somente os registros correspondentes a página qual o usuário requerer. Além de diminuir o tráfego de dados na rede, isso nos proporcionará um grande ganho de performance. O que também deve ser considerado é a possibilidade de armazenar os dados que são retornados em cache de aplicação, onde assim, o custo de resgatar os dados é apenas uma única vez, já que o cache é compartilhado entre todos os usuários da aplicação. Para um exemplo completo de como proceder neste tipo de paginação, consulte este artigo.

Data Binding

Algo que perdemos performance é quando utilizamos o método DataBinder.Eval em DataBindings, geralmente para resgatarmos os dados das colunas da nossa fonte de dados em controles como Repeater e DataList. Isso porque este método utiliza Reflection para avaliar os argumentos e retornar o resultado. O exemplo abaixo mostra o exemplo do uso deste método:

1
2
3
4
5
6
<ItemTemplate>
<tr>
<td><%# DataBinder.Eval(Container.DataItem, "Campo1") %></td>
<td><%# DataBinder.Eval(Container.DataItem, "Campo2") %></td>
</tr>
</ItemTemplate>

Uma alternativa a este método é utilizar a conversão explícita do DataItem para o objeto, que varia de acordo com a sua fonte de dados. Se tua fonte de dados for do tipo DataReader, então o teu DataItem será do tipo DbDataRecord (que encontra-se dentro de System.Data.Common). O código acima, com a conversão explícita, fica da seguinte forma:

1
2
3
4
5
6
<ItemTemplate>
<tr>
<td><%# DirectCast(Container.DataItem, DbDataRecord).Item("Campo1") %></td>
<td><%# DirectCast(Container.DataItem, DbDataRecord).Item("Campo2") %></td>
</tr>
</ItemTemplate>

Nota: Caso a sua fonte de dados seja do tipo Dataset/DataTable, o seu DataItem deve ser convertido para DataRowView. Se quiser saber um pouco mais sobre isto, pode consultar este post.

Caso você tenha vários campos que são exibidos ao usuário, o que você deverá fazer para ganhar ainda mais performance, é utilizar o evento ItemDataBound do seu controle, pois assim, fará a conversão apenas uma única vez. O código abaixo ilustra este processo:

1
2
3
4
5
Private Sub DataGrid1_ItemDataBound(...) Handles DataGrid1.ItemDataBound
Dim registro As DbDataRecord = DirectCast(e.Item.DataItem, DbDataRecord)
Response.Write(registro.Item("Campo1"))
Response.Write(registro.Item("Campo2"))
End Sub

Caching

Como Cache do ASP.NET é algo bastante extenso e muito útil, e também não quero prolongar o artigo para não desgastar o leitor, opto por apenas indicar um ótimo artigo de Miguel Ferreira que encontra-se publicado no site do MSDN Brasil neste endereço.

CONCLUSÃO: Como vimos neste artigo, há uma série de recursos que o ASP.NET nos fornece, porém devemos saber como utilizá-los, caso contrário ao invés do recurso nos ajudar, pode prejudicar. Temos que nos atentarmos aos detalhes, que como vimos, por serem simples muitas vezes passam despercebidos e compromete a qualidade de nossa aplicação.

Artigo desenvolvido utilizando:

* Visual Studio .NET 2003
* .NET Framework 1.1
* Windows XP Professional

Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.