# Tutorial API filme C# pentru .NET 8

> Acest tutorial construiește o aplicație consolă .NET 8 care primește titlul unui film, caută în amodb, încarcă cea mai bună potrivire cu evaluările ei și afișează un card scurt. Folosește doar HttpClient și System.Text.Json, citește cheia din AMODB_KEY, reîncearcă la 429 și ține răspunsurile în cache în fișiere. Configurarea durează cam 15 minute.

## Ce construiești

Construiești `MovieLookup`, o aplicație consolă care primește un titlu în linia de comandă, caută în amodb, încarcă cea mai bună potrivire cu evaluările ei și afișează un card scurt cu un link de poster. Ai nevoie de SDK-ul .NET 8, o cheie amodb și cam 15 minute. Programul folosește doar `HttpClient` și `System.Text.Json`, deci nu ai de instalat pachete NuGet.

## Pasul 1: obține o cheie API

Alege un plan la [prețuri](https://amodb.dev/ro/pricing/). Planurile diferă doar prin ritm, la 20, 40 sau 120 de cereri pe minut. Creează o cheie în panoul contului tău, un șir de 32 de caractere hexazecimale minuscule. Exportă-o și testează-o cu un apel:

```bash
export AMODB_KEY=YOUR_API_KEY
curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Authorization: Bearer $AMODB_KEY" \
  "https://api.amodb.dev/3/movie/550"
```

Un `200` înseamnă că cheia funcționează. Un `401` înseamnă că cheia este greșită sau lipsește. În PowerShell setează variabila cu `$env:AMODB_KEY = "YOUR_API_KEY"`. Pagina [autentificare](https://amodb.dev/ro/authentication/) tratează parametrul de interogare `api_key` ca alternativă la antet.

## Pasul 2: creează proiectul

```bash
dotnet new console -n MovieLookup -f net8.0
cd MovieLookup
```

Șablonul .NET 8 activează using-urile implicite și tipurile referință nullable. Asta importă deja `System`, `System.IO`, `System.Linq` și `System.Net.Http`. Celelalte linii `using` le adaugi în fișier.

## Pasul 3: trimite prima cerere

Creează un singur `HttpClient` cu o adresă de bază, un timeout de 10 secunde și antetul Bearer. Încheie adresa de bază cu o bară oblică și scrie căile relative fără bară oblică la început. O bară la început ar înlocui partea `/3` din calea de bază.

```csharp
using System.Net.Http.Headers;

using var http = new HttpClient
{
    BaseAddress = new Uri("https://api.amodb.dev/3/"),
    Timeout = TimeSpan.FromSeconds(10),
};
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AMODB_KEY"));

var response = await http.GetAsync("search/movie?query=fight%20club&language=en-US");
Console.WriteLine((int)response.StatusCode);
```

## Pasul 4: parsează JSON-ul în recorduri

Răspunsul folosește nume în snake case, precum `release_date`. Setează `JsonNamingPolicy.SnakeCaseLower`, nou în .NET 8, și scrie membri PascalCase. Câmpurile pe care nu le declari sunt ignorate.

```csharp
record SearchResponse(List<SearchHit> Results);
record SearchHit(int Id, string Title, string? ReleaseDate);
record Movie(int Id, string Title, string? Tagline, string? ReleaseDate, int? Runtime,
    string? PosterPath, double VoteAverage, RatingSet? Ratings);
record RatingSet(Score? Imdb, Score? Kinopoisk, Score? Metacritic, Score? RottenTomatoes, Score? Tmdb);
record Score(double Value, int? Votes);
```

Obiectul `ratings` apare pentru că apelul de detalii adaugă `append_to_response=ratings`. Fiecare dintre cele cinci surse ale lui este un obiect cu `value` și `votes`, sau `null` când scorul lipsește. Și `votes` poate fi `null`: răspunsul real pentru Fight Club nu are număr de voturi pentru Kinopoisk, Metacritic sau Rotten Tomatoes. Pagina [filme](https://amodb.dev/ro/movies/) listează toate celelalte câmpuri.

## Pasul 5: tratează erorile și limitele de cereri

Învelește apelul într-o buclă care reacționează la codul de stare.

| Stare       | Semnificație                        | Ce face aplicația                                                                         |
| ----------- | ----------------------------------- | ----------------------------------------------------------------------------------------- |
| 401         | Cheie lipsă, greșită sau suspendată | Afișează un mesaj și se oprește, o reîncercare nu ajută.                                  |
| 402         | Perioada planului s-a încheiat      | Afișează un mesaj și se oprește. Reînnoiește la [prețuri](https://amodb.dev/ro/pricing/). |
| 404         | Nicio înregistrare pentru acel ID   | O raportează și iese.                                                                     |
| 429         | Ai depășit cererile pe minut        | Așteaptă `Retry-After` secunde, cu plafon de 30, și reîncearcă de cel mult două ori.      |
| 500 sau 504 | Eroare pe partea serverului         | Raportează starea, iar comanda se rulează din nou mai târziu.                             |

```csharp
for (var attempt = 0; ; attempt++)
{
    using var response = await http.GetAsync(path);
    if (response.StatusCode == HttpStatusCode.TooManyRequests && attempt < 2)
    {
        var seconds = Math.Min(response.Headers.RetryAfter?.Delta?.TotalSeconds ?? 5, 30);
        await Task.Delay(TimeSpan.FromSeconds(seconds));
        continue;
    }
    if (response.StatusCode == HttpStatusCode.Unauthorized)
        throw new AmodbException("The API key was rejected (401). Check AMODB_KEY.");
    // ...handle other statuses, then read the body
}
```

Eșecurile de rețea apar ca `HttpRequestException`, iar un timeout apare ca `TaskCanceledException`. Prinde-le pe ambele la nivelul de sus. Fiecare răspuns cu cheie poartă și `X-RateLimit-Remaining`, deci un job lung poate încetini înainte să ajungă la un 429. Vezi [limite de cereri](https://amodb.dev/ro/rate-limits/) și [erori](https://amodb.dev/ro/errors/) pentru listele complete.

## Pasul 6: păstrează răspunsurile în cache pe disc

Verificarea limitei de cereri rulează înaintea cache-ului la margine, deci un răspuns servit de la margine se numără tot în cererile tale pe minut. Un cache în propriul program este singura cale de a economisi cereri. Marginea ține detaliile 6 ore și rezultatele căutării 10 minute, așa că aplicația folosește aceleași durate. Un fișier din folderul temporar este proaspăt când timpul lui de scriere se află în TTL.

```csharp
class FileCache
{
    readonly string dir = Path.Combine(Path.GetTempPath(), "amodb-cache");

    string PathFor(string key) =>
        Path.Combine(dir, Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(key))) + ".json");

    public string? Get(string key, TimeSpan ttl)
    {
        var file = PathFor(key);
        return File.Exists(file) && DateTime.UtcNow - File.GetLastWriteTimeUtc(file) < ttl
            ? File.ReadAllText(file)
            : null;
    }
}
```

## Pasul 7: pune totul cap la cap

Acesta este `Program.cs` complet. Adaugă metoda `Set` la cache și clasa `AmodbClient` care combină cache-ul, bucla de reîncercare și opțiunile JSON.

```csharp
using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; // same number format on every machine

var key = Environment.GetEnvironmentVariable("AMODB_KEY");
if (string.IsNullOrWhiteSpace(key))
{
    Console.Error.WriteLine("Set AMODB_KEY first.");
    return 1;
}

var title = string.Join(' ', args).Trim();
if (title.Length == 0)
{
    Console.Error.WriteLine("Usage: dotnet run -- \"movie title\"");
    return 1;
}

using var http = new HttpClient
{
    BaseAddress = new Uri("https://api.amodb.dev/3/"),
    Timeout = TimeSpan.FromSeconds(10),
};
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key);
var client = new AmodbClient(http, new FileCache());

try
{
    var search = await client.GetAsync<SearchResponse>(
        $"search/movie?query={Uri.EscapeDataString(title)}&language=en-US", TimeSpan.FromMinutes(10));
    var hit = search.Results.FirstOrDefault();
    if (hit is null)
    {
        Console.WriteLine($"No movie found for \"{title}\".");
        return 0;
    }

    var movie = await client.GetAsync<Movie>(
        $"movie/{hit.Id}?language=en-US&append_to_response=ratings", TimeSpan.FromHours(6));
    Print(movie);
    return 0;
}
catch (AmodbException ex)
{
    Console.Error.WriteLine(ex.Message);
    return 1;
}
catch (HttpRequestException ex)
{
    Console.Error.WriteLine($"Network error: {ex.Message}");
    return 1;
}
catch (TaskCanceledException)
{
    Console.Error.WriteLine("The request timed out after 10 seconds.");
    return 1;
}

static void Print(Movie m)
{
    var year = m.ReleaseDate is { Length: >= 4 } d ? d[..4] : "n/a";
    Console.WriteLine($"{m.Title} ({year}), {m.Runtime?.ToString() ?? "?"} min");
    if (!string.IsNullOrEmpty(m.Tagline)) Console.WriteLine(m.Tagline);
    Console.WriteLine($"IMDb: {Describe(m.Ratings?.Imdb)}");
    Console.WriteLine($"TMDB: {Describe(m.Ratings?.Tmdb)}");
    if (m.PosterPath is not null)
        Console.WriteLine($"Poster: https://img.amodb.dev/t/p/w342{m.PosterPath}");
}

static string Describe(Score? s) =>
    s is null ? "not known" : $"{s.Value:0.0}" + (s.Votes is { } v ? $" ({v:N0} votes)" : "");

record SearchResponse(List<SearchHit> Results);
record SearchHit(int Id, string Title, string? ReleaseDate);
record Movie(int Id, string Title, string? Tagline, string? ReleaseDate, int? Runtime,
    string? PosterPath, double VoteAverage, RatingSet? Ratings);
record RatingSet(Score? Imdb, Score? Kinopoisk, Score? Metacritic, Score? RottenTomatoes, Score? Tmdb);
record Score(double Value, int? Votes);

class AmodbException : Exception
{
    public AmodbException(string message) : base(message) { }
}

class FileCache
{
    readonly string dir = Path.Combine(Path.GetTempPath(), "amodb-cache");

    string PathFor(string key) =>
        Path.Combine(dir, Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(key))) + ".json");

    public string? Get(string key, TimeSpan ttl)
    {
        var file = PathFor(key);
        return File.Exists(file) && DateTime.UtcNow - File.GetLastWriteTimeUtc(file) < ttl
            ? File.ReadAllText(file)
            : null;
    }

    public void Set(string key, string json)
    {
        Directory.CreateDirectory(dir);
        File.WriteAllText(PathFor(key), json);
    }
}

class AmodbClient(HttpClient http, FileCache cache)
{
    static readonly JsonSerializerOptions Options = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
    };

    public async Task<T> GetAsync<T>(string path, TimeSpan ttl)
    {
        var cached = cache.Get(path, ttl);
        if (cached is not null) return JsonSerializer.Deserialize<T>(cached, Options)!;

        for (var attempt = 0; ; attempt++)
        {
            using var response = await http.GetAsync(path);
            if (response.StatusCode == HttpStatusCode.TooManyRequests && attempt < 2)
            {
                var seconds = Math.Min(response.Headers.RetryAfter?.Delta?.TotalSeconds ?? 5, 30);
                await Task.Delay(TimeSpan.FromSeconds(seconds));
                continue;
            }
            if (response.StatusCode == HttpStatusCode.Unauthorized)
                throw new AmodbException("The API key was rejected (401). Check AMODB_KEY.");
            if (!response.IsSuccessStatusCode)
                throw new AmodbException($"amodb returned status {(int)response.StatusCode}.");

            var body = await response.Content.ReadAsStringAsync();
            cache.Set(path, body);
            return JsonSerializer.Deserialize<T>(body, Options)!;
        }
    }
}
```

Rulează-l cu un titlu. Aceasta este ieșirea pentru prima potrivire din căutare, filmul 550. Numărul de voturi crește în timp, deci al tău va diferi puțin.

```bash
dotnet run -- "fight club"
# Fight Club (1999), 139 min
# Mischief. Mayhem. Soap.
# IMDb: 8.8 (2,669,989 votes)
# TMDB: 8.4 (33,039 votes)
# Poster: https://img.amodb.dev/t/p/w342/jSziioSwPVrOy9Yow3XhWIBDjq1.jpg
```

Linia TMDB arată 8.4 pentru că codul afișează o zecimală din 8.437 din `ratings.tmdb.value`. Gazda de postere `img.amodb.dev` nu cere cheie, deci poți da URL-ul posterului direct unui browser sau unui control de imagine. Vezi [imagini](https://amodb.dev/ro/images-api/) pentru lista de dimensiuni.

## Pașii următori

- Caută cu un an sau cu paginare în [căutare](https://amodb.dev/ro/search/) și filtrează listele cu [descoperire](https://amodb.dev/ro/discover/).
- Arată fiecare scor și regulile lui null cu [evaluări](https://amodb.dev/ro/ratings/).
- Mută codul consolei într-un serviciu ASP.NET Core cu `AddHttpClient` și `IMemoryCache`.
- Compară aceeași sarcină în [Java](https://amodb.dev/ro/java-movie-api/) sau [Go](https://amodb.dev/ro/go-movie-api/).

## Întrebări și răspunsuri

### Am nevoie de un pachet NuGet ca să apelez API-ul din C#?

Nu. HttpClient și System.Text.Json vin cu .NET 8, deci acest tutorial nu adaugă pachete. Trimite cheia într-un antet Authorization Bearer sau, dacă preferi, ca parametru de interogare api_key.

### Cum citesc antetul Retry-After în .NET?

Folosește response.Headers.RetryAfter. Când serverul trimite un număr de secunde, proprietatea Delta conține un TimeSpan. Așteaptă atât, cu plafon de 30 de secunde, și reîncearcă de cel mult două ori înainte să raportezi 429.

### Trebuie să creez un HttpClient pentru fiecare cerere?

Nu. Creează o singură instanță și reutilizează-o, pentru că fiecare instanță nouă deschide conexiuni noi. În ASP.NET Core înregistrează un client tipizat cu AddHttpClient și injectează-l. O aplicație consolă poate păstra o singură instanță pe toată durata rulării.

### Pot reutiliza modelele C# scrise pentru TMDB v3?

Da. Pentru endpoint-urile pe care le oferă amodb, căile, parametrii și câmpurile JSON se potrivesc cu TMDB v3, deci modelele pe care le-ai scris deja se deserializează neschimbate. Setează adresa de bază la https://api.amodb.dev/3/ și folosește cheia ta amodb.

### De ce afișează cardul „not known” la o evaluare?

O sursă de evaluare este null când amodb nu are scor pentru acel titlu. Tratează null ca necunoscut, niciodată ca zero, și afișează doar sursele prezente. Pot apărea câmpuri noi în timp, așa că ignoră câmpurile pe care nu le cunoști.

## Pagini conexe

- [Tutorial API filme Java](https://amodb.dev/ro/java-movie-api/): Ghid de episoade Java cu HttpClient din JDK și recorduri Jackson: preia un sezon, respectă Retry-After la 429 și salvează răspunsurile în fișiere. Java 17+.
- [API căutare](https://amodb.dev/ro/search/): API căutare cu căile și JSON-ul TMDB v3. Găsește filme, seriale, persoane, colecții, companii și cuvinte-cheie după text, cu filtre de an și paginare.
- [API detalii film](https://amodb.dev/ro/movies/): API detalii film cu aceleași căi și același JSON ca TMDB v3. Preia un film după ID, adaugă credite, evaluări și atmosferă cu append_to_response, în 14 limbi.
- [Evaluări pentru filme și TV](https://amodb.dev/ro/ratings/): API evaluări filme ce întoarce scorurile IMDb, Kinopoisk, Metacritic, Rotten Tomatoes și TMDB pentru un titlu într-un apel, cu null pentru sursele necunoscute.
- [Autentificare](https://amodb.dev/ro/authentication/): Cum trimiți cheia amodb: api_key în interogare, antetul Authorization Bearer sau apikey pentru apelurile OMDb. Format, stocare, rotire și erorile 401.
- [Limite de cereri](https://amodb.dev/ro/rate-limits/): amodb limitează fiecare cheie în cereri pe minut, în fereastră glisantă de 60 de secunde: 20, 40 sau 120. Citește antetele și tratează 429 cu Retry-After.

Actualizat: 2026-10-18. Source: https://amodb.dev/ro/csharp-movie-api/
