Resiliência de conexão do Web Forms do ASP.NET e interceptação de comando
por Erik Reitan
Neste tutorial, você modificará o aplicativo de exemplo Wingtip Toys para dar suporte à resiliência de conexão e à interceptação de comandos. Ao habilitar a resiliência de conexão, o aplicativo de exemplo Wingtip Toys repetirá automaticamente as chamadas de dados quando ocorrerem erros transitórios típicos de um ambiente de nuvem. Além disso, ao implementar a interceptação de comandos, o aplicativo de exemplo Wingtip Toys capturará todas as consultas SQL enviadas ao banco de dados para registrá-las ou alterá-las.
Observação
Este tutorial do Web Forms foi baseado no seguinte tutorial do MVC de Tom Dykstra:
Resiliência de conexão e interceptação de comando com o Entity Framework em um aplicativo MVC ASP.NET
O que você aprenderá:
- Como fornecer resiliência de conexão.
- Como implementar a interceptação de comandos.
Pré-requisitos
Antes de começar, certifique-se de ter o seguinte software instalado em seu computador:
Microsoft Visual Studio 2013 ou Microsoft Visual Studio Express 2013 para Web. O .NET Framework é instalado automaticamente.
O projeto de exemplo Wingtip Toys, para que você possa implementar a funcionalidade mencionada neste tutorial no projeto Wingtip Toys. O link a seguir fornece detalhes de download:
Antes de concluir este tutorial, considere examinar a série de tutoriais relacionados, Introdução ao ASP.NET 4.5 Web Forms e ao Visual Studio 2013. A série de tutoriais ajudará você a se familiarizar com o projeto e o código WingtipToys .
Resiliência de conexão
Quando você considera a implantação de um aplicativo no Windows Azure, uma opção a ser considerada é implantar o banco de dados no Banco de dados SQL do Windows Azure, um serviço de banco de dados em nuvem. Erros de conexão transitórios geralmente são mais frequentes quando você se conecta a um serviço de banco de dados em nuvem do que quando o servidor da Web e o servidor de banco de dados estão conectados diretamente no mesmo data center. Mesmo que um servidor Web em nuvem e um serviço de banco de dados em nuvem estejam hospedados no mesmo data center, há mais conexões de rede entre eles que podem ter problemas, como balanceadores de carga.
Além disso, um serviço de nuvem é normalmente compartilhado por outros usuários, o que significa que sua capacidade de resposta pode ser afetada por eles. E seu acesso ao banco de dados pode estar sujeito a limitação. Limitação significa que o serviço de banco de dados gera exceções quando você tenta acessá-lo com mais frequência do que o permitido em seu SLA (Contrato de Nível de Serviço).
Muitos ou a maioria dos problemas de conexão que ocorrem quando você está acessando um serviço de nuvem são transitórios, ou seja, eles se resolvem em um curto período de tempo. Portanto, quando você tenta uma operação de banco de dados e obtém um tipo de erro que normalmente é transitório, você pode tentar a operação novamente após uma curta espera e a operação pode ser bem-sucedida. Você pode fornecer uma experiência muito melhor para seus usuários se lidar com erros transitórios tentando novamente automaticamente, tornando a maioria deles invisível para o cliente. O recurso de resiliência de conexão no Entity Framework 6 automatiza esse processo de repetição de consultas SQL com falha.
O recurso de resiliência de conexão deve ser configurado adequadamente para um serviço de banco de dados específico:
- Ele precisa saber quais exceções provavelmente serão transitórias. Você deseja repetir os erros causados por uma perda temporária na conectividade de rede, não os erros causados por bugs do programa, por exemplo.
- Ele precisa aguardar um período de tempo apropriado entre as tentativas de uma operação com falha. Você pode esperar mais tempo entre as tentativas por um processo em lote do que por uma página da Web online em que um usuário está aguardando uma resposta.
- Ele tem que tentar novamente um número apropriado de vezes antes de desistir. Talvez você queira repetir mais vezes em um processo em lote do que faria em um aplicativo online.
Você pode definir essas configurações manualmente para qualquer ambiente de banco de dados compatível com um provedor do Entity Framework.
Tudo o que você precisa fazer para habilitar a resiliência de conexão é criar uma classe em seu assembly que derive da DbConfiguration
classe e, nessa classe, definir a estratégia de execução do Banco de Dados SQL, que no Entity Framework é outro termo para política de repetição.
Implementando a resiliência de conexão
Baixe e abra o aplicativo Web Forms de exemplo WingtipToys no Visual Studio.
Na pasta Logic do aplicativo WingtipToys, adicione um arquivo de classe chamado WingtipToysConfiguration.cs.
Substitua o código existente pelo seguinte código:
using System.Data.Entity; using System.Data.Entity.SqlServer; namespace WingtipToys.Logic { public class WingtipToysConfiguration : DbConfiguration { public WingtipToysConfiguration() { SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy()); } } }
O Entity Framework executa automaticamente o código encontrado em uma classe derivada de DbConfiguration
. Você pode usar a DbConfiguration
classe para executar tarefas de configuração no código que, de outra forma, faria no arquivo Web.config . Para obter mais informações, consulte Configuração baseada em código do EntityFramework.
Na pasta Lógica, abra o arquivo AddProducts.cs.
Adicione uma
using
instrução paraSystem.Data.Entity.Infrastructure
conforme mostrado destacado em amarelo:using System; using System.Collections.Generic; using System.Linq; using System.Web; using WingtipToys.Models; using System.Data.Entity.Infrastructure;
Adicione um
catch
bloco ao método para que o seja registrado conforme destacadoAddProduct
RetryLimitExceededException
em amarelo:public bool AddProduct(string ProductName, string ProductDesc, string ProductPrice, string ProductCategory, string ProductImagePath) { var myProduct = new Product(); myProduct.ProductName = ProductName; myProduct.Description = ProductDesc; myProduct.UnitPrice = Convert.ToDouble(ProductPrice); myProduct.ImagePath = ProductImagePath; myProduct.CategoryID = Convert.ToInt32(ProductCategory); using (ProductContext _db = new ProductContext()) { // Add product to DB. _db.Products.Add(myProduct); try { _db.SaveChanges(); } catch (RetryLimitExceededException ex) { // Log the RetryLimitExceededException. WingtipToys.Logic.ExceptionUtility.LogException(ex, "Error: RetryLimitExceededException -> RemoveProductButton_Click in AdminPage.aspx.cs"); } } // Success. return true; }
Ao adicionar a RetryLimitExceededException
exceção, você pode fornecer um log melhor ou exibir uma mensagem de erro para o usuário, onde ele pode optar por tentar o processo novamente. Ao capturar a RetryLimitExceededException
exceção, os únicos erros que provavelmente serão transitórios já terão sido tentados e falhados várias vezes. A exceção real retornada será encapsulada RetryLimitExceededException
na exceção. Além disso, você também adicionou um bloco catch geral. Para obter mais informações sobre a RetryLimitExceededException
exceção, consulte Resiliência de Conexão/Lógica de Repetição do Entity Framework.
Interceptação de comandos
Agora que você ativou uma política de repetição, como você testa para verificar se ela está funcionando conforme o esperado? Não é tão fácil forçar a ocorrência de um erro transitório, especialmente quando você está executando localmente, e seria especialmente difícil integrar erros transitórios reais em um teste de unidade automatizado. Para testar o recurso de resiliência de conexão, você precisa de uma maneira de interceptar consultas que o Entity Framework envia ao SQL Server e substituir a resposta do SQL Server por um tipo de exceção que normalmente é transitório.
Você também pode usar a interceptação de consulta para implementar uma prática recomendada para aplicativos em nuvem: registrar a latência e o sucesso ou a falha de todas as chamadas para serviços externos, como serviços de banco de dados.
Nesta seção do tutorial, você usará o recurso de interceptação do Entity Framework para registrar em log e simular erros transitórios.
Criar uma interface e uma classe de log
Uma prática recomendada para o registro em log é fazê-lo usando uma interface
classe de registro em System.Diagnostics.Trace
vez de codificá-la. Isso torna mais fácil alterar seu mecanismo de registro posteriormente, se você precisar fazer isso. Portanto, nesta seção, você criará a interface de log e uma classe para implementá-la.
Com base no procedimento acima, você baixou e abriu o aplicativo de exemplo WingtipToys no Visual Studio.
Crie uma pasta no projeto WingtipToys e nomeie-a como Logging.
Na pasta Log, crie um arquivo de classe chamado ILogger.cs e substitua o código padrão pelo seguinte código:
using System; namespace WingtipToys.Logging { public interface ILogger { void Information(string message); void Information(string fmt, params object[] vars); void Information(Exception exception, string fmt, params object[] vars); void Warning(string message); void Warning(string fmt, params object[] vars); void Warning(Exception exception, string fmt, params object[] vars); void Error(string message); void Error(string fmt, params object[] vars); void Error(Exception exception, string fmt, params object[] vars); void TraceApi(string componentName, string method, TimeSpan timespan); void TraceApi(string componentName, string method, TimeSpan timespan, string properties); void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars); } }
A interface fornece três níveis de rastreamento para indicar a importância relativa dos logs e um projetado para fornecer informações de latência para chamadas de serviço externas, como consultas de banco de dados. Os métodos de log têm sobrecargas que permitem que você passe uma exceção. Isso ocorre para que as informações de exceção, incluindo rastreamento de pilha e exceções internas, sejam registradas de forma confiável pela classe que implementa a interface, em vez de depender disso sendo feito em cada chamada de método de log em todo o aplicativo.
Os
TraceApi
métodos permitem que você acompanhe a latência de cada chamada para um serviço externo, como o Banco de Dados SQL.Na pasta Log, crie um arquivo de classe chamado Logger.cs e substitua o código padrão pelo seguinte código:
using System; using System.Diagnostics; using System.Text; namespace WingtipToys.Logging { public class Logger : ILogger { public void Information(string message) { Trace.TraceInformation(message); } public void Information(string fmt, params object[] vars) { Trace.TraceInformation(fmt, vars); } public void Information(Exception exception, string fmt, params object[] vars) { Trace.TraceInformation(FormatExceptionMessage(exception, fmt, vars)); } public void Warning(string message) { Trace.TraceWarning(message); } public void Warning(string fmt, params object[] vars) { Trace.TraceWarning(fmt, vars); } public void Warning(Exception exception, string fmt, params object[] vars) { Trace.TraceWarning(FormatExceptionMessage(exception, fmt, vars)); } public void Error(string message) { Trace.TraceError(message); } public void Error(string fmt, params object[] vars) { Trace.TraceError(fmt, vars); } public void Error(Exception exception, string fmt, params object[] vars) { Trace.TraceError(FormatExceptionMessage(exception, fmt, vars)); } public void TraceApi(string componentName, string method, TimeSpan timespan) { TraceApi(componentName, method, timespan, ""); } public void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars) { TraceApi(componentName, method, timespan, string.Format(fmt, vars)); } public void TraceApi(string componentName, string method, TimeSpan timespan, string properties) { string message = String.Concat("Component:", componentName, ";Method:", method, ";Timespan:", timespan.ToString(), ";Properties:", properties); Trace.TraceInformation(message); } private static string FormatExceptionMessage(Exception exception, string fmt, object[] vars) { var sb = new StringBuilder(); sb.Append(string.Format(fmt, vars)); sb.Append(" Exception: "); sb.Append(exception.ToString()); return sb.ToString(); } } }
A implementação usa System.Diagnostics
para fazer o rastreamento. Esse é um recurso interno do .NET que facilita a geração e o uso de informações de rastreamento. Há muitos "ouvintes" que você pode usar com System.Diagnostics
rastreamento, para gravar logs em arquivos, por exemplo, ou para gravá-los no armazenamento de blobs no Windows Azure. Consulte algumas das opções e links para outros recursos para obter mais informações em Solução de problemas de sites do Windows Azure no Visual Studio. Para este tutorial, você examinará apenas os logs na janela Saída do Visual Studio.
Em um aplicativo de produção, talvez você queira considerar o uso de estruturas de rastreamento diferentes do System.Diagnostics
, e a ILogger
interface torna relativamente fácil alternar para um mecanismo de rastreamento diferente se você decidir fazer isso.
Criar classes de interceptor
Em seguida, você criará as classes que o Entity Framework chamará sempre que enviar uma consulta ao banco de dados, uma para simular erros transitórios e outra para fazer o registro em log. Essas classes de interceptores devem derivar da DbCommandInterceptor
classe. Neles, você escreve substituições de método que são chamadas automaticamente quando a consulta está prestes a ser executada. Nesses métodos, você pode examinar ou registrar a consulta que está sendo enviada ao banco de dados e pode alterar a consulta antes que ela seja enviada ao banco de dados ou retornar algo ao Entity Framework por conta própria, mesmo sem passar a consulta para o banco de dados.
Para criar a classe interceptor que registrará todas as consultas SQL antes de serem enviadas ao banco de dados, crie um arquivo de classe chamado InterceptorLogging.cs na pasta Logic e substitua o código padrão pelo seguinte código:
using System; using System.Data.Common; using System.Data.Entity; using System.Data.Entity.Infrastructure.Interception; using System.Data.Entity.SqlServer; using System.Data.SqlClient; using System.Diagnostics; using System.Reflection; using System.Linq; using WingtipToys.Logging; namespace WingtipToys.Logic { public class InterceptorLogging : DbCommandInterceptor { private ILogger _logger = new Logger(); private readonly Stopwatch _stopwatch = new Stopwatch(); public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { base.ScalarExecuting(command, interceptionContext); _stopwatch.Restart(); } public override void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { _stopwatch.Stop(); if (interceptionContext.Exception != null) { _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText); } else { _logger.TraceApi("SQL Database", "Interceptor.ScalarExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText); } base.ScalarExecuted(command, interceptionContext); } public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { base.NonQueryExecuting(command, interceptionContext); _stopwatch.Restart(); } public override void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { _stopwatch.Stop(); if (interceptionContext.Exception != null) { _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText); } else { _logger.TraceApi("SQL Database", "Interceptor.NonQueryExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText); } base.NonQueryExecuted(command, interceptionContext); } public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { base.ReaderExecuting(command, interceptionContext); _stopwatch.Restart(); } public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { _stopwatch.Stop(); if (interceptionContext.Exception != null) { _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText); } else { _logger.TraceApi("SQL Database", "Interceptor.ReaderExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText); } base.ReaderExecuted(command, interceptionContext); } } }
Para consultas ou comandos bem-sucedidos, esse código grava um log de informações com informações de latência. Para exceções, ele cria um log de erros.
Para criar a classe interceptor que gerará erros transitórios fictícios quando você inserir "Throw" na caixa de texto Nome na página chamada AdminPage.aspx, crie um arquivo de classe chamado InterceptorTransientErrors.cs na pasta Logic e substitua o código padrão pelo seguinte código:
using System; using System.Data.Common; using System.Data.Entity; using System.Data.Entity.Infrastructure.Interception; using System.Data.Entity.SqlServer; using System.Data.SqlClient; using System.Diagnostics; using System.Reflection; using System.Linq; using WingtipToys.Logging; namespace WingtipToys.Logic { public class InterceptorTransientErrors : DbCommandInterceptor { private int _counter = 0; private ILogger _logger = new Logger(); public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { bool throwTransientErrors = false; if (command.Parameters.Count > 0 && command.Parameters[0].Value.ToString() == "Throw") { throwTransientErrors = true; command.Parameters[0].Value = "TransientErrorExample"; command.Parameters[1].Value = "TransientErrorExample"; } if (throwTransientErrors && _counter < 4) { _logger.Information("Returning transient error for command: {0}", command.CommandText); _counter++; interceptionContext.Exception = CreateDummySqlException(); } } private SqlException CreateDummySqlException() { // The instance of SQL Server you attempted to connect to does not support encryption var sqlErrorNumber = 20; var sqlErrorCtor = typeof(SqlError).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 7).Single(); var sqlError = sqlErrorCtor.Invoke(new object[] { sqlErrorNumber, (byte)0, (byte)0, "", "", "", 1 }); var errorCollection = Activator.CreateInstance(typeof(SqlErrorCollection), true); var addMethod = typeof(SqlErrorCollection).GetMethod("Add", BindingFlags.Instance | BindingFlags.NonPublic); addMethod.Invoke(errorCollection, new[] { sqlError }); var sqlExceptionCtor = typeof(SqlException).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 4).Single(); var sqlException = (SqlException)sqlExceptionCtor.Invoke(new object[] { "Dummy", errorCollection, null, Guid.NewGuid() }); return sqlException; } } }
Esse código substitui apenas o
ReaderExecuting
método, que é chamado para consultas que podem retornar várias linhas de dados. Se você quisesse verificar a resiliência da conexão para outros tipos de consultas, também poderia substituir osNonQueryExecuting
métodos andScalarExecuting
, como faz o interceptor de log.Mais tarde, você fará login como "Admin" e selecionará o link Admin na barra de navegação superior. Em seguida, na página AdminPage.aspx , você adicionará um produto chamado "Throw". O código cria uma exceção fictícia do Banco de Dados SQL para o erro número 20, um tipo conhecido por ser tipicamente transitório. Outros números de erro atualmente reconhecidos como transitórios são 64, 233, 10053, 10054, 10060, 10928, 10929, 40197, 40501 e 40613, mas eles estão sujeitos a alterações em novas versões do Banco de Dados SQL. O produto será renomeado para "TransientErrorExample", que você pode seguir no código do arquivo InterceptorTransientErrors.cs .
O código retorna a exceção para o Entity Framework em vez de executar a consulta e retornar os resultados. A exceção transitória é retornada quatro vezes e, em seguida, o código é revertido para o procedimento normal de passar a consulta para o banco de dados.
Como tudo está registrado, você poderá ver que o Entity Framework tenta executar a consulta quatro vezes antes de finalmente ter êxito, e a única diferença no aplicativo é que leva mais tempo para renderizar uma página com resultados de consulta.
O número de vezes que o Entity Framework tentará novamente é configurável; o código especifica quatro vezes porque esse é o valor padrão para a política de execução do Banco de Dados SQL. Se você alterar a política de execução, também alterará o código aqui que especifica quantas vezes os erros transitórios são gerados. Você também pode alterar o código para gerar mais exceções para que o Entity Framework gere a
RetryLimitExceededException
exceção.Em Global.asax, adicione as seguintes instruções using:
using System.Data.Entity.Infrastructure.Interception;
Em seguida, adicione as linhas destacadas ao
Application_Start
método:void Application_Start(object sender, EventArgs e) { // Code that runs on application startup RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); // Initialize the product database. Database.SetInitializer(new ProductDatabaseInitializer()); // Create administrator role and user. RoleActions roleActions = new RoleActions(); roleActions.createAdmin(); // Add Routes. RegisterRoutes(RouteTable.Routes); // Logging. DbInterception.Add(new InterceptorTransientErrors()); DbInterception.Add(new InterceptorLogging()); }
Essas linhas de código são o que faz com que o código do interceptor seja executado quando o Entity Framework envia consultas ao banco de dados. Observe que, como você criou classes de interceptor separadas para simulação e registro de erros transitórios, é possível habilitá-las e desabilitá-las de forma independente.
Você pode adicionar interceptores usando o DbInterception.Add
método em qualquer lugar do seu código; ele não precisa estar no Application_Start
método. Outra opção, se você não adicionou interceptores no Application_Start
método, seria atualizar ou adicionar a classe chamada WingtipToysConfiguration.cs e colocar o código acima no final do construtor da WingtipToysConfiguration
classe.
Onde quer que você coloque esse código, tome cuidado para não executar DbInterception.Add
o mesmo interceptor mais de uma vez, ou você obterá instâncias adicionais do interceptor. Por exemplo, se você adicionar o interceptor de log duas vezes, verá dois logs para cada consulta SQL.
Os interceptores são executados na ordem de registro (a ordem em que o DbInterception.Add
método é chamado). A ordem pode ser importante dependendo do que você está fazendo no interceptor. Por exemplo, um interceptor pode alterar o comando SQL que ele obtém na CommandText
propriedade. Se ele alterar o comando SQL, o próximo interceptor obterá o comando SQL alterado, não o comando SQL original.
Você escreveu o código de simulação de erro transitório de uma forma que permite causar erros transitórios inserindo um valor diferente na interface do usuário. Como alternativa, você pode escrever o código do interceptor para sempre gerar a sequência de exceções transitórias sem verificar um valor de parâmetro específico. Você pode então adicionar o interceptor somente quando quiser gerar erros transitórios. Se você fizer isso, no entanto, não adicione o interceptor até que a inicialização do banco de dados seja concluída. Em outras palavras, faça pelo menos uma operação de banco de dados, como uma consulta em um de seus conjuntos de entidades, antes de começar a gerar erros transitórios. O Entity Framework executa várias consultas durante a inicialização do banco de dados e elas não são executadas em uma transação, portanto, erros durante a inicialização podem fazer com que o contexto entre em um estado inconsistente.
Log de teste e resiliência de conexão
No Visual Studio, pressione F5 para executar o aplicativo no modo de depuração e, em seguida, faça logon como "Admin" usando "Pa$$word" como senha.
Selecione Administrador na barra de navegação na parte superior.
Insira um novo produto chamado "Throw" com descrição, preço e arquivo de imagem apropriados.
Pressione o botão Adicionar produto .
Você observará que o navegador parece travar por vários segundos enquanto o Entity Framework está repetindo a consulta várias vezes. A primeira repetição acontece muito rapidamente, depois a espera aumenta antes de cada repetição adicional. Esse processo de esperar mais tempo antes de cada nova tentativa é chamado de retirada exponencial .Aguarde até que a página não esteja mais tentando carregar.
Pare o projeto e examine a janela Saída do Visual Studio para ver a saída de rastreamento. Você pode encontrar a janela Saída selecionando Depurar ->Windows ->Saída. Talvez seja necessário rolar por vários outros logs gravados pelo seu registrador.
Observe que você pode ver as consultas SQL reais enviadas ao banco de dados. Você vê algumas consultas e comandos iniciais que o Entity Framework faz para começar, verificando a versão do banco de dados e a tabela de histórico de migração.
Observe que você não pode repetir esse teste, a menos que pare o aplicativo e reinicie-o. Se você quiser testar a resiliência da conexão várias vezes em uma única execução do aplicativo, poderá escrever código para redefinir o contador de erros noInterceptorTransientErrors
.Para ver a diferença que a estratégia de execução (política de repetição) faz, comente a
SetExecutionStrategy
linha em WingtipToysConfiguration.cs arquivo na pasta Logic , execute a página Admin no modo de depuração novamente e adicione o produto chamado "Throw" novamente.Desta vez, o depurador para na primeira exceção gerada imediatamente quando tenta executar a consulta pela primeira vez.
Remova o comentário da
SetExecutionStrategy
linha no arquivo WingtipToysConfiguration.cs .
Resumo
Neste tutorial, você viu como modificar um aplicativo de exemplo Web Forms para dar suporte à resiliência de conexão e interceptação de comando.
Próximas etapas
Depois de revisar a resiliência de conexão e a interceptação de comandos em ASP.NET Web Forms, revise o tópico ASP.NET Web Forms Métodos assíncronos no ASP.NET 4.5. O tópico ensinará as noções básicas da criação de um aplicativo assíncrono ASP.NET Web Forms usando o Visual Studio.