Saltar al contenido principal

Integración CDN

Vali-Blob permite mapear rutas de almacenamiento a URLs de CDN, de forma que los archivos públicos se sirvan desde la caché de la CDN en lugar del proveedor de almacenamiento directamente. Esto reduce la latencia y los costos de transferencia.

ICdnProvider

public interface ICdnProvider
{
/// <summary>Transforma una ruta de almacenamiento en una URL de CDN.</summary>
string GetCdnUrl(string storagePath);

/// <summary>Invalida la caché de la CDN para una ruta específica.</summary>
Task<bool> InvalidateCacheAsync(string storagePath, CancellationToken ct = default);

/// <summary>Invalida múltiples rutas en la CDN simultáneamente.</summary>
Task<bool> InvalidateCacheAsync(IEnumerable<string> paths, CancellationToken ct = default);
}

PrefixCdnProvider (incluido)

La implementación más simple: reemplaza el prefijo de almacenamiento con la URL base de la CDN.

public class PrefixCdnProvider(CdnOptions options) : ICdnProvider
{
public string GetCdnUrl(string storagePath)
{
var rutaNormalizada = storagePath.TrimStart('/');
return $"{options.BaseUrl.TrimEnd('/')}/{rutaNormalizada}";
}

public Task<bool> InvalidateCacheAsync(string storagePath, CancellationToken ct)
=> Task.FromResult(true);

public Task<bool> InvalidateCacheAsync(IEnumerable<string> paths, CancellationToken ct)
=> Task.FromResult(true);
}

CdnOptions

public class CdnOptions
{
/// <summary>URL base de la CDN. Ejemplo: "https://cdn.miempresa.com"</summary>
public required string BaseUrl { get; set; }

/// <summary>Mapeo de prefijos: rutas que inician con Key se sirven desde Value.</summary>
public Dictionary<string, string> PathMappings { get; set; } = new();
}

Configuración por proveedor CDN

Amazon CloudFront

builder.Services.Configure<CdnOptions>(opts =>
{
opts.BaseUrl = "https://d1234567890.cloudfront.net";
opts.PathMappings = new Dictionary<string, string>
{
["imagenes/"] = "https://img.miempresa.com",
["videos/"] = "https://media.miempresa.com"
};
});

builder.Services.AddSingleton<ICdnProvider, PrefixCdnProvider>();

Azure CDN con invalidación

public class AzureCdnProvider(CdnOptions opciones, ArmClient armClient) : ICdnProvider
{
public string GetCdnUrl(string storagePath) =>
$"{opciones.BaseUrl.TrimEnd('/')}/{storagePath.TrimStart('/')}";

public Task<bool> InvalidateCacheAsync(string storagePath, CancellationToken ct)
=> InvalidateCacheAsync([storagePath], ct);

public async Task<bool> InvalidateCacheAsync(IEnumerable<string> paths, CancellationToken ct)
{
var endpoint = armClient.GetCdnEndpointResource(
ResourceIdentifier.Parse(opciones.EndpointResourceId));

var contenidoPurge = new PurgeContent(
paths.Select(p => $"/{p.TrimStart('/')}").ToList());

await endpoint.PurgeContentAsync(WaitUntil.Completed, contenidoPurge, ct);
return true;
}
}

// Registro
builder.Services.AddSingleton<ICdnProvider, AzureCdnProvider>();

Google Cloud CDN / Cloudflare

builder.Services.Configure<CdnOptions>(opts =>
{
opts.BaseUrl = "https://cdn.miempresa.com"; // Dominio configurado con Cloud CDN
});

builder.Services.AddSingleton<ICdnProvider, PrefixCdnProvider>();

Uso del ICdnProvider

public class ServicioArchivosPublicos(
IStorageProvider storage,
ICdnProvider cdn)
{
public async Task<string?> SubirYObtenerUrlCdnAsync(
Stream contenido,
string nombre,
CancellationToken ct)
{
var ruta = StoragePath.From("publico", StoragePath.Sanitize(nombre));

var resultado = await storage.UploadAsync(new UploadRequest
{
Path = ruta,
Content = contenido
}, ct);

if (!resultado.IsSuccess) return null;

// Devolver URL de CDN en lugar de URL del proveedor
return cdn.GetCdnUrl(resultado.Value!.Path);
}
}

Mapeo de rutas múltiples

Para servir diferentes prefijos desde diferentes sub-dominios de CDN:

public class MultiPathCdnProvider(CdnOptions opciones) : ICdnProvider
{
public string GetCdnUrl(string storagePath)
{
// Buscar el mapeo más específico (mayor longitud del prefijo)
foreach (var (prefijo, urlBase) in opciones.PathMappings
.OrderByDescending(kv => kv.Key.Length))
{
if (storagePath.StartsWith(prefijo, StringComparison.OrdinalIgnoreCase))
{
var rutaRelativa = storagePath[prefijo.Length..];
return $"{urlBase.TrimEnd('/')}/{rutaRelativa}";
}
}

// Fallback a la URL base general
return $"{opciones.BaseUrl.TrimEnd('/')}/{storagePath.TrimStart('/')}";
}

public Task<bool> InvalidateCacheAsync(string storagePath, CancellationToken ct)
=> Task.FromResult(true);

public Task<bool> InvalidateCacheAsync(IEnumerable<string> paths, CancellationToken ct)
=> Task.FromResult(true);
}

// Configuración
builder.Services.Configure<CdnOptions>(opts =>
{
opts.BaseUrl = "https://cdn.miempresa.com";
opts.PathMappings = new Dictionary<string, string>
{
["imagenes/producto/"] = "https://img.mitienda.com",
["imagenes/"] = "https://img.miempresa.com",
["documentos/publicos/"] = "https://docs.miempresa.com"
};
});

builder.Services.AddSingleton<ICdnProvider, MultiPathCdnProvider>();

Invalidar caché al actualizar archivos

public class ServicioContenidoPublico(
IStorageProvider storage,
ICdnProvider cdn,
ILogger<ServicioContenidoPublico> logger)
{
public async Task ActualizarArchivoAsync(
string ruta,
Stream nuevoContenido,
CancellationToken ct)
{
// 1. Actualizar en el proveedor de almacenamiento
var resultado = await storage.UploadAsync(new UploadRequest
{
Path = ruta,
Content = nuevoContenido,
Overwrite = true
}, ct);

if (!resultado.IsSuccess)
{
logger.LogError("Error al actualizar {Ruta}: {Error}", ruta, resultado.ErrorMessage);
return;
}

// 2. Invalidar la caché de la CDN para que se sirva el nuevo archivo
var invalidado = await cdn.InvalidateCacheAsync(ruta, ct);

logger.LogInformation(
"Archivo actualizado: {Ruta}. Caché CDN invalidada: {Invalidado}",
ruta, invalidado);
}
}

Transformación de URL en endpoints de API

app.MapGet("/api/catalogo", async (
IStorageProvider storage,
ICdnProvider cdn,
CancellationToken ct) =>
{
var archivos = await storage.ListFilesAsync("catalogo/", ct);
if (!archivos.IsSuccess) return Results.StatusCode(500);

var items = archivos.Value!.Select(archivo => new
{
ruta = archivo.Path,
urlCdn = cdn.GetCdnUrl(archivo.Path), // URL optimizada desde CDN
tamanoBytes = archivo.SizeBytes,
ultimaModificacion = archivo.LastModified
});

return Results.Ok(items);
});
Consejo

Para archivos estáticos que cambian raramente (logos, assets CSS/JS), configura TTLs de CDN largos (86400–31536000 segundos) y versiona los archivos incluyendo un hash en el nombre (logo.abc123.svg). Para contenido dinámico, usa TTLs cortos o implementa invalidación automática al subir.

Información

La integración CDN de Vali-Blob solo maneja la generación de URLs y la invalidación programática. La configuración del origen, las reglas de caché, los certificados SSL y la distribución deben configurarse directamente en el panel del proveedor CDN (CloudFront, Azure CDN, Cloudflare, Cloud CDN).