https://a.storyblok.com/f/270183/91708/321a47ddd8/blog_asp-net_callauditing_1200x600.png

Audit chamadas telefônicas com Event Sourcing no .NET

Publicado em November 10, 2020

Tempo de leitura: 7 minutos

Os eventos estão em toda parte! O mundo do desenvolvimento de software percebeu os benefícios de modelar nossos processos de negócios e a lógica das Applications como um registro de eventos. O Event Sourcing vem ganhando popularidade como uma forma de construir sistemas que protegem contra a perda de dados, modelam cenários de negócios complexos com mais clareza e oferecem flexibilidade na forma como podem ser ampliados.

Quando conversamos com especialistas no assunto, naturalmente usamos eventos para descrever cenários de negócios. Por exemplo, eis uma breve discussão que define o contexto deste tutorial:

Especialista em negócios: “Quando um cliente faz uma ligação, queremos realizar um Audit para verificar se a ligação foi atendida e quando foi concluída.”

Desenvolvedor: “Ok. Então, precisamos monitorar quando as chamadas são iniciadas, atendidas e concluídas?”

Nesta breve discussão sobre a definição de requisitos, identificamos três eventos diferentes que precisaremos registrar: início da chamada, atendimento da chamada e término da chamada.

Felizmente para nós, a Vonage tem uma fantástica API para rastrear chamadas telefônicas!

Usaremos a API da Vonage e criaremos um aplicativo em .NET Core que armazena e exibe essas informações por meio do event sourcing.

Código

Se você quiser pular esta parte, pode acessar o código deste tutorial no GitHub.

Pré-requisitos

Para começar, você vai precisar de:

Noções básicas sobre Event Sourcing

O event sourcing é bem diferente do que estamos acostumados a fazer na indústria de software.

Normalmente, costumamos armazenar o estado atual do nosso sistema em tabelas de banco de dados ou documentos. Quaisquer dados históricos são armazenados separadamente.

O event sourcing, por outro lado, armazena todo o histórico de tudo o que acontece em nossas Applications. Exibimos o estado atual do nosso sistema “reproduzindo” e transformando todos esses eventos em modelos de visualização:

Event sourcingEvent sourcing

Todo o “registro” de eventos é chamado de fluxo. Os eventos estão associados a um fluxo principal que representa a entidade ou o processo ao qual pertencem — um cliente, um pedido, uma remessa etc.

Sem entrar em detalhes minuciosos e sem nos desviarmos do assunto, vamos modelar nosso fluxo para representar uma conversa telefônica individual, o que a API da Vonage torna extremamente fácil.

Configuração do banco de dados

Primeiro, precisaremos configurar nosso banco de dados PostgreSQL. A ferramenta mais fácil para fazer isso é pgAdmin.

Após baixar e instalar, execute o pgAdmin.

Crie um novo banco de dados chamado call_audit.

Create databaseCreate database

A seguir, vamos criar algumas credenciais para que nosso aplicativo .NET possa se comunicar com o novo banco de dados. Clique com o botão direito do mouse Funções de login/grupo > Criar > Login/Função de Grupo.

Create userCreate user

Digite seu nome de usuário call_audit.

Clique nas abas na parte superior da janela modal e clique em Definição. Digite call_audit como senha.

Por fim, clique na aba Privilégios e ative a opção “Pode fazer login?” e Superusuário.

Observação: Não habilite o “Superusuário” para o ambiente de produção! Estamos apenas criando um aplicativo de demonstração.

Criação de nosso aplicativo .NET

Vamos começar a criar o aplicativo web .NET que vai processar e exibir nossas conversas telefônicas!

Criar um novo aplicativo

Para criar um novo aplicativo web .NET Core, execute dotnet new mvc -n CallAudit --no-https no seu terminal.

Em seguida, execute o comando a seguir para instalar alguns pacotes de que você precisará:

dotnet add package Marten dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson dotnet add package Vonage

Sobre a Marten

Vamos usar a biblioteca Marten .NET para nos dar superpoderes em event sourcing.

O Marten nos permite usar facilmente o Postgres como banco de dados de documentos e armazenamento de eventos. Ele também se encarregará de atualizar nossos modelos de visualização do lado da leitura sempre que novos eventos forem adicionados aos nossos fluxos de eventos.

Configuração

No seu Startup.cs arquivo, substitua o ConfigureServices método pelo seguinte:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews().AddNewtonsoftJson();
    services.AddMarten(options =>
    {
        options.Connection("Server=127.0.0.1;Port=5432;Database=call_audit;Username=call_audit;Password=call_audit");
        options.AutoCreateSchemaObjects = AutoCreate.All;
    });
}

Criação dos nossos eventos

Precisaremos criar alguns objetos em C# para representar os eventos de domínio em nosso aplicativo. Embora esses eventos sejam muito semelhantes aos que receberemos da API da Vonage (mais adiante), queremos ser específicos quanto ao que salvamos em nosso repositório de eventos e ter controle total sobre isso.

Observação: Ao desenvolver sistemas baseados em eventos, não se deve armazenar “eventos” provenientes de sistemas externos. É sempre recomendável convertê-los em eventos específicos do seu domínio/sistema. Isso mantém o núcleo do seu sistema desacoplado e imune a alterações nesses eventos ou sistemas externos.

Em seguida, crie um novo diretório CallAudit/Events. Aqui estão os eventos a serem criados:

public class CallAnswered
{
    public Guid Id { get; set; }
    public Guid ConversationId { get; set; }
}

public class CallStarted
{
    public Guid Id { get; set; }
    public Guid ConversationId { get; set; }
    public string From { get; set; }
    public string To { get; set; }
}

public class CallCompleted
{
    public Guid Id { get; set; }
    public Guid ConversationId { get; set; }
    public DateTimeOffset? StartTime { get; set; }
    public DateTimeOffset? EndTime { get; set; }
    public int Duration { get; set; }
}

Criando nosso lado de leitura

Sempre que adicionarmos eventos aos nossos fluxos de eventos, o Marten criará ou atualizará automaticamente um documento no Postgres como uma versão em cache do estado atual do nosso fluxo. Em outra parte do nosso aplicativo, poderemos consultar o banco de dados de documentos usando os recursos de armazenamento de documentos do Marten e exibir nosso estado atual.

O Conversation tipo é o modelo de visualização que representará o estado atual armazenado em cache do nosso fluxo. Consulte a documentação do Marten para saber mais sobre isso.

Vamos criar a Conversation classe na pasta CallAudit/Projections:

public class Conversation
{
    private Conversation() { }
    
    public Guid Id { get; set; }
    public string From { get; set; }
    public string To { get; set; }
    public bool Answered { get; set; } = false;
    public DateTime? EndedAt { get; set; }
    public int? Duration { get; set; }

    public void Apply(CallStarted started)
    {
        this.From = started.From;
        this.To = started.To;
    }

    public void Apply(CallAnswered answered)
    {
        this.Answered = true;
    }

    public void Apply(CallCompleted completed)
    {
        this.EndedAt = completed.EndTime;
        this.Duration = completed.Duration;
    }
}

Por fim, precisaremos informar ao Marten que queremos que nosso stream crie/atualize a Conversation projeção automaticamente para nós.

No interior Startup.cs no ConfigureServices() método, adicione o seguinte:

services.AddMarten(options =>
{
    options.Connection("Server=127.0.0.1;Port=5432;Database=call_audit;Integrated Security=true;");
    options.AutoCreateSchemaObjects = AutoCreate.All;
    
    /***************
     * Add this one!
     ***************/
    options.Events.InlineProjections.AggregateStreamsWith<Conversation>();
});

Manipulador de comandos

No event sourcing, os eventos são normalmente criados por comandos. Vamos criar uma classe em C# que irá expor os três manipuladores diferentes que queremos acionar ao receber uma mensagem da API de rastreamento de chamadas da Vonage.

Vamos criar uma classe CallAudit/Handlers/CallAuditHandlers:

using System;
using System.Threading.Tasks;
using CallAudit.Events;
using Marten;
using Vonage.Voice.EventWebhooks;

namespace CallAudit.Handlers
{
    public class CallAuditHandlers
    {
        private IDocumentSession _session;

        public CallAuditHandlers(IDocumentSession session)
        {
            this._session = session;
        }

        public async Task Handle(CallStatusEvent @event)
        {
            switch (@event)
            {
                case Started started:
                    this.HandleCallStarted(started);
                    break;
                case Answered answered:
                    this.HandleCallAnswered(answered);
                    break;
                case Completed completed:
                    this.HandleCallCompleted(completed);
                    break;
            }

            await this._session.SaveChangesAsync();
        }

        private void HandleCallStarted(Started started)
        {
            var eventToStore = new CallStarted
            {
                ConversationId = Guid.Parse(FormatUuid(started.ConversationUuid)),
                From = started.From,
                To = started.To
            };
        
            // Create an individual stream per phone conversation.
            this._session.Events.Append(eventToStore.ConversationId, eventToStore);
        }
    
        private void HandleCallAnswered(Answered answered)
        {
            var eventToStore = new CallAnswered()
            {
                ConversationId = Guid.Parse(FormatUuid(answered.ConversationUuid))
            };
        
            this._session.Events.Append(eventToStore.ConversationId, eventToStore);
        }

        private void HandleCallCompleted(Completed completed)
        {
            var eventToStore = new CallCompleted()
            {
                ConversationId = Guid.Parse(FormatUuid(completed.ConversationUuid)),
                StartTime = completed.StartTime,
                EndTime = completed.EndTime,
                Duration = int.Parse(completed.Duration)
            };
        
            this._session.Events.Append(eventToStore.ConversationId, eventToStore);
        }

        private static string FormatUuid(string conversationUuid)
        {
            return conversationUuid.Replace("CON-", string.Empty);
        }
    }
}

Interface do usuário para exibir conversas telefônicas

Em algum momento, você vai querer visualizar os dados do seu sistema. Substitua o conteúdo de Views/Home/Index.cshtml pelo seguinte:

@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <table class="table table-bordered">
        <thead class="table-dark">
        <tr>
            <th>From</th>
            <th>To</th>
            <th>Answered</th>
        </tr>
        </thead>
        <tbody>
        @foreach (var convo in Model.Conversations)
        {
            <tr>
                <td>@convo.From</td>
                <td>@convo.To</td>
                <td>@(convo.Answered ? "yes" : "no")</td>
            </tr>
        }
        </tbody>
    </table>
</div>

Em seguida, crie uma classe em C# em Models/IndexModel:

using System.Collections.Generic;
using CallAudit.Projections;

namespace CallAudit.Models
{
    public class IndexModel
    {
        public IEnumerable<Conversation> Conversations { get; set; }
    }
}

Em seguida, substitua o controlador principal por Controllers/HomeController.cs pelo seguinte:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using CallAudit.Models;
using CallAudit.Projections;
using Marten;

namespace CallAudit.Controllers
{
    public class HomeController : Controller
    {
        private readonly IDocumentStore _store;

        public HomeController(IDocumentStore store)
        {
            this._store = store;
        }

        public async Task<IActionResult> Index()
        {
            using var session = this._store.OpenSession();
            
            var model = new IndexModel();
            model.Conversations = await session.Query<Conversation>()
                .ToListAsync();

            return this.View(model);
        }
    }
}

Esta página exibirá todas as conversas telefônicas Auditadas em nosso sistema.

Ponto de conexão dos webhooks

A última etapa do nosso aplicativo web consiste em criar os endpoints que serão utilizados pela Vonage para enviar eventos de rastreamento de chamadas.

Crie um controlador MVC na Controllers pasta chamada PhoneCallWebhooksController.

Aqui está o código que deve ser inserido:

using System.IO;
using System.Threading.Tasks;
using CallAudit.Handlers;
using Marten;
using Microsoft.AspNetCore.Mvc;
using Vonage.Voice.EventWebhooks;
using Vonage.Voice.Nccos;

namespace CallAudit.Controllers
{
    public class PhoneCallWebhooksController : Controller
    {
        private readonly IDocumentStore _store;

        public PhoneCallWebhooksController(IDocumentStore store)
        {
            this._store = store;
        }
        
        [HttpGet("/track-call")]
        public string TrackCall()
        {
            var talkAction = new TalkAction
            {
                Text = "This call will be tracked and stored using event sourcing."
            };
            var ncco = new Ncco(talkAction);
            return ncco.ToString();
        }
        
        [HttpPost("/event")]
        public async Task<IActionResult> Event()
        {
            // Read the incoming json and load it as the
            // proper C# type it represents ("Started", "Answered", etc.)
            var json = await new StreamReader(this.Request.Body).ReadToEndAsync();
            var @event = (CallStatusEvent) EventBase.ParseEvent(json);
            
            using var session = this._store.OpenSession();
            await new CallAuditHandlers(session).Handle(@event);
            return this.Ok();
        }
    }
}

Configurar nosso aplicativo de controle de horas da Vonage

Vamos começar a usar a API da Vonage e o rastreamento de chamadas em tempo real!

Lembre-se de ter sua Account da Vonage e CLI à mão.

Usando o ngrok

Para garantir que a API da Vonage consiga se conectar aos webhooks que criamos, precisamos de uma URL pública para hospedar nosso site.

Depois de instalar o ngrok (um pré-requisito), você precisará configurar seu token de autenticação.

Acesse este link e execute o comando para configurar seu token.

Em seguida, execute o seguinte comando em um terminal e mantenha-o aberto e em execução:

ngrok http http://localhost:5000

Criação de um aplicativo da Vonage

Usando a URL pública que o ngrok forneceu, abra um terminal e execute o seguinte comando (preenchendo as URLs):

nexmo app:create --keyfile private.key callaudit http://YOUR_URL.com/track-call http://YOUR_URL.com/event.

Você deverá ver “ID do aplicativo salvo: XXXXX”. Copie esse ID do aplicativo — você vai precisar dele.

Em seguida, acesse esta página para ver os Numbers da Vonage disponíveis. Se você tiver um período de teste gratuito, já deve ter um número disponível.

Pegue esse número de telefone e o ID do aplicativo e, em seguida, execute o seguinte:

nexmo link:app [number] [application ID]

Você deve receber a mensagem “número atualizado”.

Vamos lá!

Tudo bem, vamos testar isso!

No terminal, a partir do diretório raiz do seu aplicativo .NET Core, execute dotnet run.

Com o ngrok ainda em execução, tente ligar para o número de telefone da Vonage que você vinculou a este aplicativo.

Depois de encerrar a ligação, acesse http://localhost:5000/ no seu navegador e você deverá ver suas conversas listadas.

Conclusão

Agora você criou um sistema de auditoria de chamadas telefônicas que usa event sourcing! Experimente explorar outras formas que o Marten oferece para consultar os fluxos de eventos, como transformar um evento diretamente em um documento do banco de dados do lado da leitura para descobrir de que outras maneiras você pode visualizar suas conversas telefônicas geradas pela API da Vonage!

Compartilhar:

https://a.storyblok.com/f/270183/384x384/624c6e3593/james-hickey.png
James Hickey

James is a Microsoft MVP with a background in fintech & insurance industries building web and mobile applications. He's the author of Refactoring TypeScript and the creator of some open-source tools for .NET called Coravel. He lives in eastern Canada and is a 10-minute drive from the highest tides in the world!