Persistência de Estado com ASP.NET Profile
A funcionalidade de carrinho de compras, lista de desejos e informações cadastrais na aplicação PetShop utiliza o sistema ASP.NET Profile como mecanismo central de armazenamento persistente. Diferente das sessões HTTP, que possuem tempo de vida limitado e são gerenciadas automaticamente pelo runtime, o Profile permite a retenção prolongada de dados personalizados por usuário, sendo armazenado em fonte externa configurável. O fluxo inicial de cadastro emprega o controle CreateUserWizard integrado ao framework Membership, criando registros na base MSPetShop4Services. Já a atualização de dados persistentes é responsabilidade do módulo Profile, que grava nas bases dedicadas MSPetShop4Profile.
Configuração de Provedores e Propriedades
A definição do comportamento do Profile ocorre exclusivamente no arquivo web.config. É necessário mapear provdeores customizados e declarar propriedades tipadas que serão serializadas para o banco de dados. A configuração abaixo demonstra a estrutura adaptada com nomes padronizados e providers especializados:
<profile enabled="true" defaultProvider="CustomSQLProvider">
<providers>
<add name="CustomSQLProvider"
type="PetShop.Providers.SqlProfileProvider"
connectionStringName="PerfilConnString"
applicationName=".NETPetShop4" />
</providers>
<properties>
<group name="CarrinhoECompras">
<add name="ItensSelecionados" type="PetShop.Model.CarrinhoInfo" allowAnonymous="true" />
<add name="ListaDesejos" type="PetShop.Model.CarrinhoInfo" allowAnonymous="true" />
</group>
<group name="DadosPessoais">
<add name="Endereço" type="PetShop.Model.EnderecoInfo" allowAnonymous="false" />
</group>
</properties>
</profile>
No código-behind, a manipulação torna-se intuitiva através do contexto estático HttpContext.Current.Profile. Exemplo de uso direto:
var perfilAtual = HttpContext.Current.Profile;
perfilAtual.DadosPessoais.IdiomaPreferido = ddlIdiomas.SelectedItem.Value;
Fluxo de Inserção no Carrinho
A adição de um novo produto dispara um evento na fase inicial da página (PreInit). Isso garante que o estado seja alterado antes do carregamento dos controles postback. O método intercepta parâmetros via query string, invoca o serviço de persistência e redireciona o cliente para evitar duplicidades em recarregamentos:
protected void Page_PreInit(object sender, EventArgs e)
{
if (!IsPostBack)
{
var parametroProduto = Request.QueryString["id"];
if (string.IsNullOrWhiteSpace(parametroProduto)) return;
var gerenciadorCarrinho = new GerenciadorProdutos();
gerenciadorCarrinho.IncluirItemAoContexto(parametroProduto);
PerfilSalvador.SincronizarAlteracoes();
Response.Redirect(Request.RawUrl, endResponse: true);
}
}
A classe responsável por manter os itens segue uma estrutura baseada em dicionário interno. A lógica verifica existência prévia ou incrementa a quantidade existente:
public class GerenciadorProdutos
{
private readonly Dictionary<string, ItemCarrinho> _registroLocal = new Dictionary<string, ItemCarrinho>();
public void IncluirItemAoContexto(string codigoProduto)
{
if (_registroLocal.TryGetValue(codigoProduto, out var itemExistente))
{
itemExistente.Quantidade++;
return;
}
var catalogo = new CatalogoBase();
var detalhes = catalogo.ObterDetalhesPorCodigo(codigoProduto);
if (detalhes != null)
{
var novoRegistro = new ItemCarrinho(
codigo: codigoProduto,
nome: detalhes.Nome,
qtde: 1,
valor: Detalhes.PrecoUnitario,
categoria: Detalhes.Categoria
);
_registroLocal[codigoProduto] = novoRegistro;
}
}
}
Provedor Customizado e Manutenção de Estado SQL
O salvamento aciona o mecanismo de provimento de perfil, que sobrepõe métodos padrão para controlar a gravação no banco. A tabela estrutural armazena identificadores únicos, nomes de usuário, status de autenticação e carimbos de tempo:
CREATE TABLE Tbl_Perfis_Usuarios (
IdentificadorUnico INT IDENTITY PRIMARY KEY,
NomeUsuario NVARCHAR(255) NOT NULL,
AplicacaoReferencia NVARCHAR(255) NOT NULL,
EAnonimo BIT DEFAULT 0,
DataUltimaAtividade DATETIME,
DataUltimaModificacao DATETIME,
CONSTRAINT UQ_Perfil_UsuarioAplicacao UNIQUE(NomeUsuario, AplicacaoReferencia)
);
A implementação do provedor recebe uma coleção de valores modificados e roteia a gravação conforme o tipo de dado configurado:
public override void SetPropertyValues(SettingsContext ctx, SettingsPropertyValueCollection collection)
{
var usuarioNome = ctx["UserName"]?.ToString() ?? string.Empty;
ValidarIdentificadorUsuario(usuarioNome);
var autenticado = Convert.ToBoolean(ctx["IsAuthenticated"]);
var uid = AcessoBanco.PerfilObterIdentificador(usuarioNome, autenticado, false, NomeAplicacao);
if (uid == default(int))
uid = AcessoBanco.PerfilCriarNovo(usuarioNome, autenticado, NomeAplicacao);
foreach (var valorPropriedade in collection.Values)
{
if (valorPropriedade.PropertyValue == null) continue;
switch (valorPropriedade.Property.Name)
{
case "ItensSelecionados":
PersistirItensCarrinho(uid, (CarrinhoInfo)valorPropriedade.PropertyValue, ehCompraReal: true);
break;
case "ListaDesejos":
PersistirItensCarrinho(uid, (CarrinhoInfo)valorPropriedade.PropertyValue, ehCompraReal: false);
break;
case "Endereço":
if (autenticado)
GravarInformacoesCadastro(uid, (EnderecoInfo)valorPropriedade.PropertyValue);
break;
}
}
AtualizarCarimbosTempo(usuarioNome, atualizarAtividade: false);
}
A atualização efetiva dos itens utiliza transações explícitas para garantir atomicidade entre exclusão e inserção:
private static void PersistirItensCarrinho(int identificadorUsuario, CarrinhoInfo dadosCarrinho, bool ehCompraReal)
{
if (dadosCarrinho.Itens.Count == 0)
{
AcessoBanco.ExecutarDelete("DELETE FROM Tbl_Carrinho WHERE UsuarioId = @Uid AND EhCompra = @Flag;",
new { @Uid = identificadorUsuario, @Flag = ehCompraReal });
return;
}
var conexao = new SqlConnection(Configurar.ConnectionStringPerfil);
conexao.Open();
using (var transacao = conexao.BeginTransaction(System.Data.IsolationLevel.ReadCommitted))
{
try
{
var stmtDelete = "DELETE FROM Tbl_Carrinho WHERE UsuarioId = @Uid AND EhCompra = @Flag;";
AcessoBanco.ExecutarNonQuery(transacao, stmtDelete, new { @Uid = identificadorUsuario, @Flag = ehCompraReal });
var stmtInsercao = @"INSERT INTO Tbl_Carrinho (UsuarioId, ProdutoId, Nome, Tipo, Preco, CategoriaId, ProdutoReference, EhCompra, Quantidade)
VALUES (@Uid, @Pid, @Nome, @Tipo, @Valor, @CatId, @ProdRef, @Flag, @Qtde);";
foreach (var registro in dadosCarrinho.Itens)
{
AcessoBanco.ExecutarNonQuery(transacao, stmtInsercao, new {
@Uid = identificadorUsuario,
@Pid = registro.Codigo,
@Nome = registro.Nome,
@Tipo = registro.Tipo,
@Valor = registro.ValorUnitario,
@CatId = registro.CategoriaId,
@ProdRef = registro.ProdutoId,
@Flag = ehCompraReal,
@Qtde = registro.Quantidade
});
}
transacao.Commit();
}
catch
{
transacao.Rollback();
throw;
}
finally
{
conexao.Close();
}
}
}
Estratégias de Processamento de Pedidos: Síncrono vs Assíncrono
O módulo de checkout possui duas rotas arquitetônicas distintas para confirmação de compra:
- Síncrono: Executa a inserção no banco de vendas e o decremento de estoque dentro do mesmo escopo transacional HTTP. Oferece baixa latência, mas pode bloquear a thread de resposta caso haja gargalos de IO.
- Assíncrono: Utiliza filas de mensagens internas (MSMQ) para desacoplar a validação fiannceira do compromisso fiscal. O servidor web apenas publica a mensagem e responde imediatamente ao cliente, enquanto um consumidor dedicado processa a fila.
Integração com Message Queue e Serialização
O acesso às filas ocorre mediante caminhos exclusivos seguindo o padrão servidor\$\private\$nome-da-fila. Para ambientes locais, o ponto . representa a máquina atual. O exemplo abaixo demonstra a criação e referência direta:
var caminhoFila = @".\\private$\\pedidos-negociacao";
var filaDestino = new MessageQueue(caminhoFila);
A troca de objetos requer formatação compatível. O XmlMessageFormatter é preferencial pela interoperabilidade e legibilidade:
var tiposRecebidos = new[] { typeof(PedidoInfo) };
filaDestino.Formatter = new XmlMessageFormatter(tiposRecebidos);
Operações de publicação e consumo mantêm separação clara entre remoção definitiva e leitura preventiva:
// Publicação
filaDestino.Send(novaMensagemPedido, MessageQueueTransactionType.Single);
// Consumo com remoção da fila
var mensagemEntrante = filaDestino.Receive(new TimeSpan(0, 0, 5));
var objetoPedido = (PedidoInfo)mensagemEntrante.Body;
// Leitura sem remoção
var mensagemObservavel = filaDestino.Peek();
Abstração de Estratégia e Fábrica de Mensagens
O PetShop aplica o padrão Strategy para alternar entre comportamentos de gravação de pedido sem alterar a camada de apresentação. A interface IOrderStrategy define um único contrato:
public interface IOrdemEstrategia
{
void RegistrarPedido(Model.PedidoInfo referenciaPedido);
}
No controller ou service layer, a instanciação delega à fábrica que lê a configuração para injetar a implementação correta:
public class GestorPedidos
{
private static readonly IOrdemEstrategia _estrategiaGravacao = CarregarEstrategia();
private static readonly IMensagemOrdenacao _canalMsmq = FabricaFilas.ConectarCanal();
public void Confirmar(Model.PedidoInfo dados)
{
ValidarAutenticacaoPagamento(dados);
_estrategiaGravacao.RegistrarPedido(dados);
}
private static IOrdemEstrategia CarregarEstrategia()
{
// Retorna instância sincronizada ou assíncrona baseada em appSetting
return Configuracao.Global.AmbienteProcessamento == "Async"
? new PedidoEstrategiaAssincrona(_canalMsmq)
: new PedidoEstrategiaSincrona(BancoDadosCore);
}
}
A classe assíncrona encapsula a publicação para a fila:
public class PedidoEstrategiaAssincrona : IOrdemEstrategia
{
private readonly IMensagemOrdenacao _pipe;
public PedidoEstrategiaAssincrona(IMensagemOrdenacao canal) => _pipe = canal;
public void RegistrarPedido(Model.PedidoInfo refPedido)
{
_pipe.Publicar(refPedido);
}
}
Adaptação do Wrapper MSMQ
A implementação concreta herda uma base genérica que gerencia transações por mensagem e roteia operações de envio/recepção:
public class CanalMensageiroPedidos : BaseGerenciadorFilas
{
public Modelo.PedidoInfo Receber()
{
base.ConfigurarTransacao(MessageQueueTransactionType.Automatic);
var msg = base.ExecutarReceber();
return (Modelo.PedidoInfo)msg.Body;
}
public void Publicar(Modelo.PedidoInfo payload)
{
base.ConfigurarTransacao(MessageQueueTransactionType.Single);
base.ExecutarEnviar(payload);
}
}
Consumidor Assíncrono e Commit Bancário
O processo responsável por esvaziar a fila roda isoladamente, podendo ser implementado como aplicação console ou serviço Windows. Seu ciclo vital envolve monitoramento contínuo, agrupamento de lotes para otimização de IO e execução dentro de um escopo transacional seguro antes de confirmar a baixa de estoque e geração de nota fiscal:
class ExecutorPedidosOffline
{
public async Task ExecutarLacoProcessamento(CancellationToken token)
{
var filaOrigem = new MessageQueue(Configurar.CaminhoPedidos);
filaOrigem.Formatter = new XmlMessageFormatter(new[] { typeof(Modelo.PedidoInfo) });
while (!token.IsCancellationRequested)
{
try
{
var entrada = filaOrigem.Receive(TimeSpan.FromSeconds(2));
var dadoProcessar = (Modelo.PedidoInfo)entrada.Body;
await ExecutarEscopoTransacional(async (ctx) =>
{
await RepositorioVendas.InserirPedidoAsync(ctx, dadoProcessar);
await RepositorioEstoque.AplicarBaixaAsync(ctx, dadoProcessar.Linhas);
});
}
catch (MessageQueueException ex) when (ex.MessageQueueErrorCode == MessageQueueIOErrorType.Timeout)
{
await Task.Delay(TimeSpan.FromSeconds(1), token); // Aguarda próxima mensagem
}
}
}
}