En el desarrollo web moderno, es crucial llevar un registro preciso de las visitas a nuestras páginas. Este artículo te guiará paso a paso en la creación de un contador de visitas utilizando ASP.NET Core 8, Minimal API, caché y Quartz Scheduler. Aprenderás a implementar una solución eficiente y escalable que sincroniza el contador de visitas desde la caché a la base de datos cada cinco minutos.

¿Qué es Quartz Scheduler?

Quartz es una librería open-source para programar y ejecutar trabajos en .NET. Proporciona una manera flexible y potente de manejar tareas programadas, como la sincronización de datos entre caché y base de datos en nuestro caso.

Estrategia Paso a Paso

Carga de la Página y Solicitud POST desde el Frontend

Cuando un usuario carga la página web, el frontend espera 5 segundos antes de enviar una solicitud POST al backend. Este retraso asegura que la visita es válida y no simplemente una carga rápida o un clic accidental, mejorando la precisión del contador de visitas.

Incremento del Contador en Caché

Al recibir la solicitud POST, el backend incrementa un contador almacenado en la caché en memoria (IMemoryCache). Utilizar caché permite manejar un alto volumen de solicitudes sin sobrecargar directamente la base de datos en cada incremento, optimizando el rendimiento del sistema.

Sincronización Periódica con la Base de Datos usando Quartz

Para mantener la persistencia y asegurar la integridad de los datos, se utiliza Quartz.NET para programar una tarea que sincroniza el contador de la caché con la base de datos cada 5 minutos. Este proceso implica leer el valor actual del contador de la caché y almacenarlo en la base de datos, garantizando que los datos se mantengan actualizados y precisos.

Ventajas de esta Estrategia

  1. Reducción de la Carga en la Base de Datos: Al utilizar caché, reducimos la cantidad de operaciones directas en la base de datos, lo que disminuye la carga y mejora el rendimiento general del sistema.
  2. Eficiencia y Escalabilidad: La combinación de caché y tareas programadas permite manejar grandes volúmenes de tráfico de manera eficiente sin comprometer la integridad de los datos, asegurando que la aplicación pueda escalar con el aumento del tráfico.
  3. Flexibilidad: Esta arquitectura es fácilmente escalable y se puede adaptar para diferentes tipos de aplicaciones web, ofreciendo una solución robusta y adaptable a diversas necesidades de desarrollo.

Implementación Paso a Paso

1. Configuración del Proyecto

Comienza creando un nuevo proyecto ASP.NET Core 8:

dotnet new webapi -n VisitCounter
cd VisitCounter

2. Instalación de Dependencias

Instala las librerías necesarias:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Quartz.AspNetCore

3. Configuración de Entity Framework

Configura Entity Framework para conectar con la base de datos y crear el modelo Page:

// Page.cs
public class Page
{
    public Page(string title)
    {
        Title = title;
        ViewCount = 0;
    }

    public int Id { get; set; }
    public string Title { get; set; }
    public int ViewCount { get; set; }
}

Vamos a crear el DBContext para poder utilizar EntityFramework.

using Microsoft.EntityFrameworkCore;


public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<Page> Pages => Set<Page>();

}

Como en todos los proyectos de .NET con EntityFramework debemos realizar la configuración en el Program.cs.

// Program.cs

builder.Services.AddDbContext<ApplicationDbContext>(o => o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

Agregar nuestra ConnectionString en el AppSettings.json.

// AppSettings.json

{
  "ConnectionStrings": {
    "Default": "[La Connection String de SQL Server]"
  },

  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

Crea la migración y actualiza la base de datos con el Package Manager Console:

Add-Migration InitialCreate
Update-Database

4. Creación del Endpoint de Minimal API

Vamos a crear 2 endpoints, uno para obtener la información de la página y el otro para incrementar el contador de visitas.

// Program.cs

builder.Services.AddCors();
builder.Services.AddMemoryCache();
app.UseCors(builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());

app.MapGet("Pages/{title}", async (ApplicationDbContext dbContext, string title) =>
{
    // obtener la informacion de la pagina por titulo y si la pagina no existe, crearla
    var page = await dbContext.Pages.FirstOrDefaultAsync(x => x.Title == title);
    if (page == null)
    {
        page = new Page(title);
        dbContext.Pages.Add(page);
        await dbContext.SaveChangesAsync();
    }

    return Results.Ok(page);

});

app.MapPost("/Pages/Views", (IMemoryCache cache, IncrementViewRequest request) =>
{
    string cacheKey = $"page-visitcount";

    var viewCounts = cache.GetOrCreate(cacheKey, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
        return new Dictionary<string, int>();
    });

    if (viewCounts!.ContainsKey(request.Title))
    {
        viewCounts[request.Title]++;
    }
    else
    {
        viewCounts[request.Title] = 1;
    }

    cache.Set(cacheKey, viewCounts, TimeSpan.FromMinutes(10));
    return Results.Ok();
});


app.Run();

public record IncrementViewRequest(string Title);

5. Configuración de Quartz Scheduler

Configura Quartz para sincronizar el contador de visitas cada 5 minutos:

// Program.cs

builder.Services.AddQuartz(q =>
{
    // Just use the name of your job that you created in the Jobs folder.
    var jobKey = new JobKey(nameof(SyncPageViewsJob));
    q.AddJob<SyncPageViewsJob>(opts => opts.WithIdentity(jobKey));

    q.AddTrigger(opts => opts
   .ForJob(jobKey)
   .WithIdentity(nameof(SyncPageViewsJob) + "-trigger")

   // Corre cada 5 minutos
   //.WithCronSchedule("0 0/5 * * * ?"));

    // Corre cada 10 segundos
    .WithCronSchedule("0/10 * * * * ?"));
});

// ASP.NET Core hosting
builder.Services.AddQuartzServer(options =>
{
    options.WaitForJobsToComplete = true;
});

Crea una nueva clase llamada SyncPageViewsJob.cs que va a contener el código que se ejecutara cada 5 minutos y se encargara de realizar la sincronización entre el cache y la base de datos.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;

using Quartz;


public class SyncPageViewsJob : IJob
{
    private readonly IMemoryCache _cache;
    private readonly ApplicationDbContext _dbContext;

    public SyncPageViewsJob(IMemoryCache cache, ApplicationDbContext dbContext)
    {
        _cache = cache;
        _dbContext = dbContext;
    }

    public async Task Execute(IJobExecutionContext context)
    {
        string cacheKey = $"page-visitcount";

        if (_cache.TryGetValue(cacheKey, out Dictionary<string, int>? viewCounts) && viewCounts!.Count > 0)
        {

            var titles = viewCounts!.Keys.ToList();

            var pages = await _dbContext.Pages.Where(x => titles.Contains(x.Title)).ToListAsync();


            foreach (var page in pages)
            {
                if (page != null)
                {
                    var viewCount = viewCounts[page.Title];

                    page.ViewCount += viewCount;
                }
            }
            _dbContext.UpdateRange(pages);

            await _dbContext.SaveChangesAsync();

            _cache.Remove(cacheKey);
        }
    }
}

7. Probar desde una página web

Por último, vamos a crear una simple página web para probar nuestro contador de visitas con Asp :NET Core. Para esto vamos a crear un archivo index.html y vamos a poner le siguiente HTML.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Page View Counter</title>
    <script>
        document.addEventListener('DOMContentLoaded', function () {
           const apiBase= "http://localhost:5111"  
          
          const title = 'your-page-title'; // Replace 'your-page-title' with the actual title

            // Fetch the view count
            fetch(`${apiBase}/pages/${title}`)
                .then(response => response.json())
                .then(data => {
                    document.getElementById('page-title').innerText = data.title;
                    document.getElementById('view-count').innerText = `View Count: ${data.viewCount}`;
                })
                .catch(error => console.error('Error fetching view count:', error));

            // Set a timer to send a POST request after 5 seconds
            setTimeout(() => {
                fetch(`${apiBase}/pages/views`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ title: title })
                })
                .then(_ => console.log('View count incremented'))
                .catch(error => console.error('Error incrementing view count:', error));
            }, 5000);
        });
    </script>
</head>
<body>
    <h1 id="page-title"></h1>
    <h2 id="view-count"></h2>
</body>
</html>

Conclusión

Implementar un contador de visitas utilizando ASP.NET Core 8, Minimal API, caché y Quartz Scheduler es una solución efectiva y escalable para el seguimiento de visitas a páginas web. Siguiendo este tutorial, puedes crear una infraestructura robusta que garantiza la precisión y consistencia de los datos, además de optimizar el rendimiento del sistema.

Este tutorial te proporciona las bases para adaptar y ampliar la funcionalidad según las necesidades específicas de tu proyecto. ¡Empieza a implementar esta estrategia en tu próxima aplicación web y lleva el seguimiento de visitas al siguiente nivel!

Si encuentras útil este trabajo y deseas mostrar tu apoyo, siempre puedes invitarme a un buen café ☕.

Buy Me A Coffee