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. 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:
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_. Pagina autentificare tratează parametrul de interogare api_key ca alternativă la antet.
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"Pasul 2: creează proiectul
Ș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.
dotnet new console -n MovieLookup -f net8.0
cd MovieLookupPasul 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ă.
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.
Obiectul ratings apare pentru că apelul de detalii adaugă append_. 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 listează toate celelalte câmpuri.
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);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. |
| 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. |
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 și erori pentru listele complete.
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
}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.
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.
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.
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 pentru lista de dimensiuni.
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.jpgPașii următori
- Caută cu un an sau cu paginare în căutare și filtrează listele cu descoperire.
- Arată fiecare scor și regulile lui null cu evaluări.
- Mută codul consolei într-un serviciu ASP.NET Core cu
AddHttpClientșiIMemoryCache. - Compară aceeași sarcină în Java sau Go.