Skip to main content

DNN · Product sync

DNN runs scheduled tasks via the native Scheduler (Host → Schedule). The recommended pattern is:

  1. A NeuroonSyncScheduler : SchedulerClient class that wraps the sync logic.
  2. A NeuroonClient with a reused HttpClient and Polly retries.
  3. A mapping from your DB (DNN Commerce, NB_Store, Hotcakes or a custom module) to NeuroonProduct.
  4. Frequency 1 day for INCREMENTAL sync; an optional extra job every hour for deltas after edits.

Endpoint we are going to feed

POST/api/plugin/shops/{shopId}/products/sync
{
"syncType": "INCREMENTAL",
"products": [/* up to 500 per request */]
}
LayerMaximum size
DNN client (recommended chunk)500 products per request
Backend (hard limit)500 products per request

The end-to-end recipe uses Chunk(products, 500) precisely for this reason. See docs/docs/recipes/dnn-end-to-end.md (Step 2).

Mapping from your DB

The DTO aligns with the backend validation:

public sealed record NeuroonProduct
{
public string ExternalId { get; init; } // your PK / SKU
public string Sku { get; init; }
public string Name { get; init; } // max 512
public string Description { get; init; } // max 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; }
}

Backend validations:

  • externalId, name, price, url are required.
  • name max 512 chars, description max 5000.
  • price >= 0, currency ISO 4217, rating between 0 and 5.

Auto-batching and retries

The client automatically chunks into batches of 500 and applies exponential retry (3 attempts, 2^n s backoff) on 429 and 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;
}

Full implementation: see examples/full-csharp or the DNN end-to-end recipe (Step 2).

FULL vs INCREMENTAL modes

ModeUse
FULLInitial bootstrap or after a massive catalog change. Marks absent products as inactive in Neuroon.
INCREMENTALDay-to-day. Only creates / updates the products sent.

The base scheduler uses INCREMENTAL; expose a parameter on the ScheduleHistoryItem or a toggle in ModuleSettings.ascx to force FULL when needed.

Base scheduler

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()
{
// Adapt to your module (NB_Store, DNN Commerce, custom module).
throw new NotImplementedException("Implement the mapping from your e-commerce module.");
}
}

Register the task from Host → Schedule → Add Item with frequency 1 day and Catch-up Enabled. See details in examples/scheduled-task.

Delta sync (every hour)

To react quickly to catalog changes without waiting for the nightly job, add a second task with frequency 1 hour that filters by LastUpdatedDate > UtcNow.AddHours(-1). The sync stays INCREMENTAL; since it is idempotent by externalId, it does not cause duplicates.

Latency (eventual consistency)

After the 200 OK, products go through Neuroon's internal pipeline . Indexing takes 2 to 5 seconds. If your immediate test does not return a freshly uploaded product, wait 5 s and retry.

Aggregate response

{
"totalReceived": 500,
"newProducts": 142,
"updatedProducts": 358,
"skipped": 0,
"failed": 0,
"errors": [],
"productsCount": 3210,
"remainingProducts": 6790
}

RemainingProducts is the remaining quota counter for your plan. The scheduler should stop and emit a warning in ScheduleHistoryItem.AddLogNote if it reaches 0.

Common errors

SymptomCauseFix
401 UnauthorizedEmpty API key or decryption key changedHostController.GetEncryptedString with the right key.
400 with name: Size must be betweenname exceeds 512 charsTruncate / normalize in the mapper.
429 Too Many RequestsSync window 100/min exceededPolly already backs off; reduce concurrency or negotiate more quota.
Task stuck at RunningUncaught exception in DoWorkMake sure to call Errored(ref ex) in the catch.

Next steps