Saltar al contenido principal

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.json ni en variables NEXT_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:

  1. SyncAsync trocea en lotes de 500 cuando le pasas 1.200 productos.
  2. La política Polly reintenta 3 veces sobre 429 y 5xx.
  3. IssueWidgetTokenAsync parsea correctamente { "token": "..." } (el response no trae expiresAt).
  4. JsonNamingPolicy emite syncType, externalId, lineTotal (camelCase) — no SyncType etc.

Próximas lecturas