https://d226lax1qjow5r.cloudfront.net/blog/blogposts/audit-phone-calls-with-event-sourcing-in-net/blog_asp-net_callauditing_1200x600.png

Audit de llamadas telefónicas con Event Sourcing en .NET

Publicado el November 10, 2020

Tiempo de lectura: 7 minutos

Los eventos están en todas partes El mundo del desarrollo de software se ha dado cuenta de las ventajas de modelar nuestros procesos empresariales y la lógica de las aplicaciones como un registro de eventos. La externalización de eventos ha ido ganando popularidad como forma de construir sistemas que protegen contra la pérdida de datos, modelan escenarios empresariales complejos con mayor claridad y ofrecen flexibilidad en cuanto a la forma de ampliarlos.

Cuando hablamos con expertos en la materia, utilizamos naturalmente eventos para describir escenarios empresariales. Por ejemplo, he aquí una breve discusión que enmarca el contexto de este tutorial:

Experto empresarial: "Cuando un cliente realiza una llamada telefónica, queremos auditar si se ha contestado y cuándo se ha completado la llamada."

Desarrollador: "Vale. Entonces, ¿tenemos que hacer un seguimiento de cuándo se inician, contestan y finalizan las llamadas telefónicas?".

En esta breve discusión sobre la recopilación de requisitos, hemos descubierto tres eventos diferentes que necesitaremos capturar: llamada iniciada, llamada contestada y llamada completada.

Por suerte para nosotros, Vonage tiene una fantástica API para rastrear llamadas telefónicas¡!

Utilizaremos la API de Vonage y crearemos una aplicación .NET Core que almacene y muestre esta información mediante el uso de fuentes de eventos.

Código

Si desea saltar más adelante, puede ver el código de este tutorial en GitHub.

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Requisitos previos

Para empezar, necesitarás:

Aspectos básicos de la contratación de eventos

El aprovisionamiento de eventos es bastante diferente de lo que estamos acostumbrados a hacer en la industria del software.

Normalmente, tendemos a almacenar el estado actual de nuestro sistema en tablas o documentos de la base de datos. Los datos históricos se almacenan por separado.

Por otro lado, la fuente de eventos almacena el historial completo de todo lo que ocurre en nuestras aplicaciones. Mostramos el estado actual de nuestro sistema "reproduciendo" y transformando todos esos eventos en modelos de vista:

Event sourcingEvent sourcing

El "registro" completo de eventos se denomina flujo. Los eventos se asocian a un flujo primario que representa la entidad o el proceso al que pertenecen: un cliente, un pedido, un envío, etc.

Sin entrar en detalles ni desviarnos del tema, modelaremos nuestro flujo para representar una conversación telefónica individual, algo que la API de Vonage hace muy fácil.

Configuración de la base de datos

Primero tendremos que configurar nuestra base de datos PostgreSQL. La herramienta más sencilla para hacerlo es pgAdmin.

Después de descargar e instalar, ejecute pgAdmin.

Cree una nueva base de datos llamada call_audit.

Create databaseCreate database

A continuación, crearemos algunas credenciales para que nuestra aplicación .NET pueda comunicarse con la nueva base de datos. Clic derecho Login/Grupo Roles > Crear > Función de inicio de sesión/grupo.

Create userCreate user

Nombre de usuario call_audit.

Haga clic en las pestañas de la parte superior de la ventana modal y haga clic en Definición. Escriba call_audit como contraseña.

Por último, haga clic en la pestaña Privilegios y active ¿Puede iniciar sesión? y Superusuario.

Nota: ¡No habilites "Superusuario" para producción! Sólo estamos construyendo una aplicación de demostración.

Creación de nuestra aplicación .NET

Empecemos a crear la aplicación web .NET que gestionará y mostrará nuestras conversaciones telefónicas.

Crear una nueva aplicación

Para crear una nueva aplicación web .NET Core, ejecute dotnet new mvc -n CallAudit --no-https en su terminal.

A continuación, ejecuta lo siguiente para instalar un par de paquetes que necesitarás:

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

Acerca de Marten

Vamos a utilizar la biblioteca biblioteca Marten .NET para darnos superpoderes de aprovisionamiento de eventos.

Marten nos da la capacidad de utilizar fácilmente Postgres como un documento DB y almacén de eventos. También se encargará de actualizar nuestros modelos de vista de lectura cada vez que se añadan nuevos eventos a nuestros flujos de eventos.

Configuración

En su Startup.cs sustituya el método ConfigureServices por lo siguiente:

// 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;
    });
}

Crear nuestros eventos

Necesitaremos crear algunos objetos C# para representar los eventos de dominio en nuestra aplicación. Aunque estos eventos tendrán un aspecto muy similar a los eventos que recibimos de la API de Vonage (más adelante), queremos ser específicos sobre lo que guardamos en nuestro almacén de eventos y tener un control total.

Nota: Cuando construyas sistemas basados en eventos, no querrás almacenar "eventos" que provengan de sistemas externos. Siempre querrás convertirlos en eventos específicos de tu dominio/sistema. Esto mantiene el núcleo de tu sistema desacoplado y no afectado por cambios en esos eventos o sistemas externos.

A continuación, cree un nuevo directorio CallAudit/Events. Estos son los eventos a crear:

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

Crear nuestro lado de lectura

Cada vez que añadimos eventos a nuestros flujos de eventos, Marten creará/actualizará automáticamente un documento en Postgres como una versión en caché del estado actual de nuestro flujo. En algún lugar de nuestra aplicación, seremos capaces de consultar el documento DB utilizando las capacidades de almacenamiento de documentos de Marten y mostrar nuestro estado actual.

El tipo Conversation es el modelo de vista que representará el estado actual almacenado en caché de nuestro flujo. Véase la documentación de Marten para más información.

Vamos a crear la clase Conversation en la carpeta 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 último, tendremos que decirle a Marten que queremos que nuestro flujo para crear / actualizar la Conversation automáticamente por nosotros.

En Startup.cs en el método ConfigureServices() añada lo siguiente:

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

En event sourcing, los eventos son típicamente creados por comandos. Crearemos una clase C# que expondrá los tres controladores diferentes que deseamos activar al recibir un mensaje de la API de seguimiento de llamadas de Vonage.

Crearemos una clase 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);
        }
    }
}

Interfaz de usuario para mostrar conversaciones telefónicas

En algún momento, querrás ver los datos de tu sistema. Sustituya el contenido de Views/Home/Index.cshtml por el siguiente:

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

A continuación, cree una clase C Models/IndexModel:

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

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

A continuación, sustituya el controlador de inicio en Controllers/HomeController.cs por el siguiente:

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 mostrará todas las conversaciones telefónicas auditadas en nuestro sistema.

Punto final de Webhooks

La última parte de nuestra aplicación web es crear los puntos finales que utilizará Vonage para enviar eventos de rastreo telefónico.

Cree un controlador MVC en la carpeta Controllers con el nombre PhoneCallWebhooksController.

Aquí está el código que debe ir en él:

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();
        }
    }
}

Configura nuestra aplicación de control del tiempo de Vonage

¡Comencemos con la API de Vonage y el seguimiento de llamadas en tiempo real!

Sólo recuerda tener tu cuenta de Vonage y CLI listos.

Uso de ngrok

Para asegurarnos de que la API de Vonage pueda conectarse a los webhooks que hemos creado, necesitamos una URL pública para alojar nuestro sitio.

Después de haber instalado ngrok (un requisito previo), tendrá que configurar su auth token.

Visite este enlace y ejecute el comando para configurar su token.

A continuación, ejecute lo siguiente en un terminal y manténgalo abierto y en funcionamiento:

ngrok http http://localhost:5000

Creación de una aplicación de Vonage

Usando la URL pública que ngrok te dio, abre un terminal y ejecuta el siguiente comando (rellenando las URLs):

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

Deberías ver "Saved application id: XXXXX". Copie ese identificador de aplicación, lo necesitará.

A continuación, visita esta página para ver tus números de Vonage disponibles. Si tienes una prueba gratuita, ya deberías tener un número disponible.

Toma ese número de teléfono y el id de la aplicación y ejecuta lo siguiente:

nexmo link:app [number] [application ID]

Debería aparecer el mensaje "número actualizado".

¡A correr!

¡Muy bien, probemos esto!

Desde el directorio raíz de su aplicación .NET Core en un terminal, ejecute dotnet run.

Con ngrok aún en funcionamiento, intenta llamar al número de teléfono de Vonage que vinculaste a esta aplicación.

Una vez finalizada la llamada, ve a http://localhost:5000/ en tu navegador web y deberías ver tu(s) conversación(es) en la lista.

Conclusión

Ahora ha construido un sistema de auditoría de llamadas telefónicas que utiliza fuentes de eventos. Intente jugar con otras formas de Marten para consultar los flujos de eventos como transformar un evento directamente en un documento de base de datos de lectura para ver de qué otras maneras puedes ver tus conversaciones telefónicas producidas por la API de Vonage.

Compartir:

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!