DNN · Sincronización de productos
DNN ejecuta tareas planificadas mediante el Scheduler nativo (Host → Schedule). El patrón recomendado es:
- Una clase
NeuroonSyncScheduler : SchedulerClientque envuelve la lógica de sync. - Un
NeuroonClientconHttpClientreutilizado y reintentosPolly. - Un mapeo desde tu BD (DNN Commerce, NB_Store, Hotcakes o módulo propio) hacia
NeuroonProduct. - Frecuencia 1 día para sync
INCREMENTAL; un job adicional opcional cada hora para deltas tras edición.
Endpoint que vamos a alimentar
/api/plugin/shops/{shopId}/products/sync{
"syncType": "INCREMENTAL",
"products": [/* hasta 500 por petición */]
}
| Capa | Tamaño máximo |
|---|---|
| Cliente DNN (chunk recomendado) | 500 productos por petición |
| Backend (límite duro) | 500 productos por petición |
El recipe end-to-end usa
Chunk(products, 500)exactamente por esta razón. Verdocs/docs/recipes/dnn-end-to-end.md(Paso 2).
Mapeo desde tu BD
El DTO se alinea con la validación del backend:
public sealed record NeuroonProduct
{
public string ExternalId { get; init; } // tu PK / SKU
public string Sku { get; init; }
public string Name { get; init; } // máx 512
public string Description { get; init; } // máx 5000
public decimal Price { get; init; }
public string Currency { get; init; } = "EUR"; // ISO 4217
public string Url { get; init; }
public string ImageUrl { get; init; }
public IReadOnlyList<string> Categories { get; init; }
public IReadOnlyList<string> Brands { get; init; }
public bool? InStock { get; init; }
public int? StockQuantity { get; init; }
public decimal? Rating { get; init; }
public int? ReviewCount { get; init; }
public bool? IsActive { get; init; } = true;
public IReadOnlyList<string> Tags { get; init; }
public decimal? SalePrice { get; init; }
public string ParentExternalId { get; init; }
public IReadOnlyDictionary<string, string> Attributes { get; init; }
}
Validaciones del backend:
externalId,name,price,urlson obligatorios.namemáx 512 caracteres,descriptionmáx 5000.price >= 0,currencyISO 4217,ratingentre 0 y 5.
Auto-batching y reintentos
El cliente trocea automáticamente en lotes de 500 y aplica retry exponencial (3 intentos, backoff 2^n s) sobre 429 y 5xx:
public async Task<SyncResult> SyncProductsAsync(SyncMode mode, IEnumerable<NeuroonProduct> products)
{
var aggregate = new SyncResult();
foreach (var batch in Chunk(products, 500))
{
var resp = await _retry.ExecuteAsync(() => _http.PostAsJsonAsync(
$"/api/plugin/shops/{_shopId}/products/sync",
new { syncType = mode.ToString(), products = batch },
JsonOpts));
resp.EnsureSuccessStatusCode();
aggregate.Merge(await resp.Content.ReadFromJsonAsync<SyncResult>(JsonOpts));
}
return aggregate;
}
Implementación completa: ver
examples/full-csharpo el recipe DNN end-to-end (Paso 2).
Modos FULL vs INCREMENTAL
| Modo | Uso |
|---|---|
FULL | Bootstrap inicial o tras un cambio masivo de catálogo. Marca productos ausentes como inactivos en Neuroon. |
INCREMENTAL | Día a día. Solo crea / actualiza los productos enviados. |
El scheduler base usa INCREMENTAL; expón un parámetro en el ScheduleHistoryItem o un toggle en ModuleSettings.ascx para forzar FULL cuando sea necesario.
Scheduler base
using DotNetNuke.Services.Scheduling;
public class NeuroonSyncScheduler : SchedulerClient
{
public NeuroonSyncScheduler(ScheduleHistoryItem oItem) : base() { ScheduleHistoryItem = oItem; }
public override void DoWork()
{
try
{
Progressing();
var client = new NeuroonClient();
var products = LoadProductsFromCommerceModule();
var result = client
.SyncProductsAsync(NeuroonClient.SyncMode.INCREMENTAL, products)
.GetAwaiter().GetResult();
ScheduleHistoryItem.AddLogNote(
$"Synced {result.NewProducts} new, {result.UpdatedProducts} updated, " +
$"{result.Failed} failed, remaining quota {result.RemainingProducts}");
ScheduleHistoryItem.Succeeded = true;
}
catch (Exception ex)
{
ScheduleHistoryItem.Succeeded = false;
ScheduleHistoryItem.AddLogNote("Neuroon sync failed: " + ex.Message);
Errored(ref ex);
}
}
private static IEnumerable<NeuroonProduct> LoadProductsFromCommerceModule()
{
// Adapta a tu módulo (NB_Store, DNN Commerce, módulo propio).
throw new NotImplementedException("Implementa el mapeo desde tu módulo de e-commerce.");
}
}
Registra la tarea desde Host → Schedule → Add Item con frecuencia 1 day y Catch-up Enabled. Ver detalles en examples/scheduled-task.
Sync delta (cada hora)
Para reaccionar rápido a cambios de catálogo sin esperar al job nocturno, añade una segunda tarea con frecuencia 1 hour que filtre por LastUpdatedDate > UtcNow.AddHours(-1). La sync sigue siendo INCREMENTAL; al ser idempotente por externalId, no causa duplicados.
Latencia (eventual consistency)
Tras el 200 OK, los productos pasan por el pipeline interno de Neuroon . La indexación tarda 2 a 5 segundos. Si tu test inmediato no devuelve un producto recién subido, espera 5 s y reintenta.
Respuesta agregada
{
"totalReceived": 500,
"newProducts": 142,
"updatedProducts": 358,
"skipped": 0,
"failed": 0,
"errors": [],
"productsCount": 3210,
"remainingProducts": 6790
}
RemainingProducts es el contador de cuota restante de tu plan. El scheduler debería detenerse y emitir un warning en ScheduleHistoryItem.AddLogNote si llega a 0.
Errores frecuentes
| Síntoma | Causa | Solución |
|---|---|---|
401 Unauthorized | API key vacía o decryption key cambiada | HostController.GetEncryptedString con la key correcta. |
400 con name: Size must be between | name excede 512 chars | Trunca / normaliza en el mapper. |
429 Too Many Requests | Sync window 100/min superada | Polly ya hace backoff; reduce concurrencia o pacta más cuota. |
Tarea queda Stuck en Running | Excepción no capturada en DoWork | Asegúrate de llamar a Errored(ref ex) en el catch. |
Próximos pasos
- Ejemplos ·
IScheduledTask— registro y configuración del Scheduler. - Ejemplos ·
NeuroonClientcompleto. - API ·
products/sync— referencia con playground.