Custom · .NET
This guide is for .NET integrators: ASP.NET Core, BackgroundService, Worker Service, MAUI, Azure Functions, etc.
The canonical pattern is: reusable HttpClient (via IHttpClientFactory) + Polly for resilience + JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }.
HTTP client
// NeuroonClient.cs
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
public sealed class NeuroonOptions
{
public string ApiUrl { get; init; } = "https://api.neuroon.ai";
public string ShopId { get; init; } = "";
public string ApiKey { get; init; } = "";
}
public sealed class NeuroonClient
{
private readonly HttpClient _http;
private readonly NeuroonOptions _opts;
private static readonly JsonSerializerOptions Json = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
};
public NeuroonClient(HttpClient http, NeuroonOptions opts)
{
_http = http;
_opts = opts;
_http.BaseAddress = new Uri(_opts.ApiUrl);
_http.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-neuroon/1.0");
}
public async Task<SyncResult> SyncAsync(SyncMode mode, IEnumerable<NeuroonProduct> products, CancellationToken ct = default)
{
var aggregate = new SyncResult();
foreach (var batch in products.Chunk(500))
{
using var req = new HttpRequestMessage(HttpMethod.Post,
$"/api/plugin/shops/{_opts.ShopId}/products/sync");
req.Headers.Add("X-Shop-API-Key", _opts.ApiKey);
req.Content = JsonContent.Create(new { syncType = mode.ToString(), products = batch }, options: Json);
using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
res.EnsureSuccessStatusCode();
var partial = await res.Content.ReadFromJsonAsync<SyncResult>(Json, ct).ConfigureAwait(false);
aggregate.Merge(partial!);
}
return aggregate;
}
public async Task<string> IssueWidgetTokenAsync(CancellationToken ct = default)
{
using var req = new HttpRequestMessage(HttpMethod.Post,
$"/api/shops/{_opts.ShopId}/widget-token");
req.Headers.Add("X-Shop-API-Key", _opts.ApiKey);
using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
res.EnsureSuccessStatusCode();
var body = await res.Content.ReadFromJsonAsync<TokenResponse>(Json, ct).ConfigureAwait(false);
return body!.Token;
}
public enum SyncMode { FULL, INCREMENTAL }
public sealed record TokenResponse(string Token);
}
DI registration with Polly
// Program.cs (ASP.NET Core 8+ / minimal API)
using Polly;
using Polly.Extensions.Http;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<NeuroonOptions>(builder.Configuration.GetSection("Neuroon"));
builder.Services.AddHttpClient<NeuroonClient>()
.AddPolicyHandler(HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(r => (int)r.StatusCode == 429)
.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))));
var app = builder.Build();
app.MapGet("/internal/widget-token", async (NeuroonClient client) =>
Results.Ok(new { token = await client.IssueWidgetTokenAsync() }));
app.Run();
appsettings.json:
{
"Neuroon": {
"ApiUrl": "https://api.neuroon.ai",
"ShopId": "shop_xxx",
"ApiKey": "sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
Never put the API Key in
wwwroot/appsettings.jsonor inNEXT_PUBLIC_*variables. Use Azure Key Vault, AWS Secrets Manager, or Configuration secrets.
BackgroundService for nightly sync
public sealed class NeuroonNightlySync : BackgroundService
{
private readonly NeuroonClient _client;
private readonly ILogger<NeuroonNightlySync> _log;
public NeuroonNightlySync(NeuroonClient client, ILogger<NeuroonNightlySync> log)
{
_client = client;
_log = log;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var products = await LoadProductsFromDbAsync(stoppingToken);
var result = await _client.SyncAsync(NeuroonClient.SyncMode.INCREMENTAL, products, stoppingToken);
_log.LogInformation("Neuroon sync OK: +{New} ~{Updated} ✗{Failed}",
result.NewProducts, result.UpdatedProducts, result.Failed);
}
catch (Exception ex)
{
_log.LogError(ex, "Neuroon sync failed");
}
// Next run at 03:00 local
var next = DateTime.Now.Date.AddDays(1).AddHours(3);
await Task.Delay(next - DateTime.Now, stoppingToken).ConfigureAwait(false);
}
}
}
Registration:
builder.Services.AddHostedService<NeuroonNightlySync>();
Serving the token to the frontend
// Private endpoint: the frontend calls it and gets the server-side cached Widget Token.
app.MapGet("/internal/widget-token", async (NeuroonTokenProvider provider, CancellationToken ct) =>
Results.Ok(new { token = await provider.GetAsync(ct) }));
Where NeuroonTokenProvider is the class from the Server-to-server token recipe (with issuedAt cache and 23 h rotation).
Conversion tracking
public async Task TrackConversionAsync(string orderId, decimal total, string currency, IEnumerable<ConversionLine> lines, CancellationToken ct = default)
{
using var req = new HttpRequestMessage(HttpMethod.Post,
$"/api/plugin/shops/{_opts.ShopId}/track/conversion");
req.Headers.Add("X-Shop-API-Key", _opts.ApiKey);
req.Content = JsonContent.Create(new { orderId, total, currency, lines }, options: Json);
using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
res.EnsureSuccessStatusCode();
}
Call it from your payment success hook (OnOrderCompleted, processed Stripe webhook, etc.). Idempotent by orderId.
Testing
Keep a minimal suite with WireMock.Net that asserts:
SyncAsyncchunks into batches of 500 when given 1,200 products.- The Polly policy retries 3 times on
429and5xx. IssueWidgetTokenAsyncparses{ "token": "..." }correctly (the response does not includeexpiresAt).JsonNamingPolicyemitssyncType,externalId,lineTotal(camelCase) — notSyncTypeetc.
Further reading
- Recipe · Server-to-server token — cache + Polly + distributed Redis.
- Custom · Server-to-server — cross-stack pattern including domain verification.
- Reference · CORS & origins — which endpoints validate Origin.