Máy chủ

C# and .NET

A typed HttpClient, registered once, injected where it is needed.

No package to install. HttpClient and System.Text.Json are in the framework.

A typed client

PulseClient.cs
using System.Net.Http.Json;

public sealed record Signal(string Type, string Value);

public sealed class PulseClient(HttpClient http, string organizationId)
{
    public Task<JsonElement> TrackAsync(string sessionId, IEnumerable<Signal> signals) =>
        PostAsync("track", new
        {
            sessionId,
            organizationId,
            signals = signals.Select(s => new { type = s.Type, value = s.Value }),
        });

    public Task<JsonElement> RankedCatalogAsync(string sessionId, int limit = 24) =>
        PostAsync("catalog/ranked", new { sessionId, organizationId, limit });

    public Task<JsonElement> SessionAsync(string sessionId) =>
        PostAsync("session", new { sessionId });

    private async Task<JsonElement> PostAsync(string path, object body)
    {
        var response = await http.PostAsJsonAsync(path, body);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<JsonElement>();
    }
}

Registration

Program.cs
builder.Services.AddHttpClient<PulseClient>(client =>
{
    // The trailing slash matters. Without it, a relative path replaces the last segment
    // and every request goes to https://api.thepulseapi.com/api/track instead.
    client.BaseAddress = new Uri("https://api.thepulseapi.com/api/v1/");
    client.DefaultRequestHeaders.Add("x-api-key", builder.Configuration["Pulse:ApiKey"]);
    client.Timeout = TimeSpan.FromSeconds(5);
});

AddHttpClient is what you want rather than new HttpClient(): it pools handlers and rotates them, which is the difference between reusing connections and exhausting sockets under load.

public class CatalogController(PulseClient pulse) : Controller
{
    public async Task<IActionResult> Category(string slug)
    {
        var sessionId = Request.Cookies["pulse_session"];
        if (sessionId is null) return View(DefaultOrder(slug));

        try
        {
            var ranked = await pulse.RankedCatalogAsync(sessionId);
            return View(ranked);
        }
        catch (HttpRequestException)
        {
            // Ranking is an enhancement, never a reason to fail the page.
            return View(DefaultOrder(slug));
        }
    }
}

Property names

System.Text.Json serialises anonymous object members verbatim, so sessionId stays sessionId. If you swap in a named class with PascalCase properties, set JsonNamingPolicy.CamelCase or the API will reject the body as missing its required fields.

Cập nhật lần cuối Xem lịch sử thay đổi

Trong trang này