Arquitetura e Implementação de Carrinho de Compras e Processamento Assíncrono de Pedidos no .NET PetShop

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
            }
        }
    }
}

Tags: asp-net-petshop c-sharp msmq xmlmessageformatter transações-sql

Publicado em 9-1 12:55