Saltar al contenido principal

DNN · Sincronización de productos

DNN ejecuta tareas planificadas mediante el Scheduler nativo (Host → Schedule). El patrón recomendado es:

  1. Una clase NeuroonSyncScheduler : SchedulerClient que envuelve la lógica de sync.
  2. Un NeuroonClient con HttpClient reutilizado y reintentos Polly.
  3. Un mapeo desde tu BD (DNN Commerce, NB_Store, Hotcakes o módulo propio) hacia NeuroonProduct.
  4. Frecuencia 1 día para sync INCREMENTAL; un job adicional opcional cada hora para deltas tras edición.

Endpoint que vamos a alimentar

POST/api/plugin/shops/{shopId}/products/sync
{
"syncType": "INCREMENTAL",
"products": [/* hasta 500 por petición */]
}
CapaTamañ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. Ver docs/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, url son obligatorios.
  • name máx 512 caracteres, description máx 5000.
  • price >= 0, currency ISO 4217, rating entre 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-csharp o el recipe DNN end-to-end (Paso 2).

Modos FULL vs INCREMENTAL

ModoUso
FULLBootstrap inicial o tras un cambio masivo de catálogo. Marca productos ausentes como inactivos en Neuroon.
INCREMENTALDí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íntomaCausaSolución
401 UnauthorizedAPI key vacía o decryption key cambiadaHostController.GetEncryptedString con la key correcta.
400 con name: Size must be betweenname excede 512 charsTrunca / normaliza en el mapper.
429 Too Many RequestsSync window 100/min superadaPolly ya hace backoff; reduce concurrencia o pacta más cuota.
Tarea queda Stuck en RunningExcepción no capturada en DoWorkAsegúrate de llamar a Errored(ref ex) en el catch.

Próximos pasos