DNN · Product sync
DNN runs scheduled tasks via the native Scheduler (Host → Schedule). The recommended pattern is:
- A
NeuroonSyncScheduler : SchedulerClientclass that wraps the sync logic. - A
NeuroonClientwith a reusedHttpClientandPollyretries. - A mapping from your DB (DNN Commerce, NB_Store, Hotcakes or a custom module) to
NeuroonProduct. - Frequency 1 day for
INCREMENTALsync; an optional extra job every hour for deltas after edits.
Endpoint we are going to feed
/api/plugin/shops/{shopId}/products/sync{
"syncType": "INCREMENTAL",
"products": [/* up to 500 per request */]
}
| Layer | Maximum 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. Seedocs/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,urlare required.namemax 512 chars,descriptionmax 5000.price >= 0,currencyISO 4217,ratingbetween 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-csharpor the DNN end-to-end recipe (Step 2).
FULL vs INCREMENTAL modes
| Mode | Use |
|---|---|
FULL | Initial bootstrap or after a massive catalog change. Marks absent products as inactive in Neuroon. |
INCREMENTAL | Day-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
| Symptom | Cause | Fix |
|---|---|---|
401 Unauthorized | Empty API key or decryption key changed | HostController.GetEncryptedString with the right key. |
400 with name: Size must be between | name exceeds 512 chars | Truncate / normalize in the mapper. |
429 Too Many Requests | Sync window 100/min exceeded | Polly already backs off; reduce concurrency or negotiate more quota. |
| Task stuck at Running | Uncaught exception in DoWork | Make sure to call Errored(ref ex) in the catch. |
Next steps
- Examples ·
IScheduledTask— Scheduler registration and configuration. - Examples · Full
NeuroonClient. - API ·
products/sync— reference with playground.