Skip to main content

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.json or in NEXT_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:

  1. SyncAsync chunks into batches of 500 when given 1,200 products.
  2. The Polly policy retries 3 times on 429 and 5xx.
  3. IssueWidgetTokenAsync parses { "token": "..." } correctly (the response does not include expiresAt).
  4. JsonNamingPolicy emits syncType, externalId, lineTotal (camelCase) — not SyncType etc.

Further reading