Custom · .NET
Esta guía es para integradores .NET: ASP.NET Core, BackgroundService, Worker Service, MAUI, Azure Functions, etc.
El patrón canónico es: HttpClient reusable (vía IHttpClientFactory) + Polly para resilience + JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }.
Cliente HTTP
// 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);
}
Registro DI con 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"
}
}
Nunca pongas la API Key en
wwwroot/appsettings.jsonni en variablesNEXT_PUBLIC_*. Usa Azure Key Vault, AWS Secrets Manager, o Configuration secrets.
BackgroundService para sync nocturno
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");
}
// Próxima ejecución a las 03:00 local
var next = DateTime.Now.Date.AddDays(1).AddHours(3);
await Task.Delay(next - DateTime.Now, stoppingToken).ConfigureAwait(false);
}
}
}
Registro:
builder.Services.AddHostedService<NeuroonNightlySync>();
Servir el token al frontend
// Endpoint privado: el frontend lo llama y recibe el Widget Token cacheado server-side.
app.MapGet("/internal/widget-token", async (NeuroonTokenProvider provider, CancellationToken ct) =>
Results.Ok(new { token = await provider.GetAsync(ct) }));
Donde NeuroonTokenProvider es la clase del recipe Server-to-server token (con cache issuedAt y rotación a las 23 h).
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();
}
Llámalo desde tu hook de payment success (OnOrderCompleted, webhook de Stripe procesado, etc.). Es idempotente por orderId.
Testing
Mantén una suite mínima con WireMock.Net que valide:
SyncAsynctrocea en lotes de 500 cuando le pasas 1.200 productos.- La política Polly reintenta 3 veces sobre
429y5xx. IssueWidgetTokenAsyncparsea correctamente{ "token": "..." }(el response no traeexpiresAt).JsonNamingPolicyemitesyncType,externalId,lineTotal(camelCase) — noSyncTypeetc.
Próximas lecturas
- Recipe · Server-to-server token — cache + Polly + Redis distribuido.
- Custom · Server-to-server — patrón cross-stack incluyendo verificación de dominio.
- Reference · CORS y orígenes — qué endpoints validan Origin.
- Plugins · DNN (DotNetNuke) — si estás dentro del runtime DNN.