Prdb.Sdk 0.14.0

dotnet add package Prdb.Sdk --version 0.14.0
                    
NuGet\Install-Package Prdb.Sdk -Version 0.14.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Prdb.Sdk" Version="0.14.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Prdb.Sdk" Version="0.14.0" />
                    
Directory.Packages.props
<PackageReference Include="Prdb.Sdk" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Prdb.Sdk --version 0.14.0
                    
#r "nuget: Prdb.Sdk, 0.14.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Prdb.Sdk@0.14.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Prdb.Sdk&version=0.14.0
                    
Install as a Cake Addin
#tool nuget:?package=Prdb.Sdk&version=0.14.0
                    
Install as a Cake Tool

Prdb.Sdk (C#)

C# client for the prdb Public API.

Install

dotnet add package Prdb.Sdk

Targets .NET 8.0, so it runs on .NET 8 and later.

Usage

using Prdb.Sdk;

var client = PrdbClientFactory.Create("...");

// GET /videos
var page = await client.Videos.GetAsync();
foreach (var video in page?.Items ?? [])
{
    Console.WriteLine(video.Title);
}

// GET /videos/{id}
var single = await client.Videos[videoId].GetAsync();

// Query parameters are typed, including the closed-set ones.
var pageTwo = await client.Videos.GetAsync(config =>
{
    config.QueryParameters.Page = 2;
    config.QueryParameters.PageSize = 50;
    config.QueryParameters.Search = "...";
});

The request builders mirror the API's URL structure, so GET /videos/{id}/filehashes is client.Videos[videoId].Filehashes.GetAsync().

Authentication

PrdbClientFactory.Create sends the key in the X-Api-Key header, and keeps it on the API host: a redirect to a different origin throws CrossOriginRedirectException rather than handing your credential to whoever answers there. Redirects that stay on the same origin are followed normally.

baseUrl must use https, so the key is never sent in cleartext. A loopback address (localhost, 127.0.0.1 or [::1]) is the exception: the request never leaves the machine, so plain http is accepted there and a local stand-in for the API needs no certificate.

GET /health is the only endpoint that works without a key; use PrdbClientFactory.CreateAnonymous() for health probes. That one has no credential to protect, so it accepts a plain http base URL.

Dependency injection

In an application with a service container, register the client instead of building one by hand:

using Prdb.Sdk;

services.AddPrdbClient(options =>
{
    options.ApiKey = configuration["Prdb:ApiKey"];
});

PrdbClient can then be injected anywhere. Its connections are managed by IHttpClientFactory, so handler lifetime and pooling work the way the rest of an ASP.NET application expects — a client built by hand and held as a singleton never picks up a DNS change, and one built per call exhausts sockets.

AddPrdbClient returns the IHttpClientBuilder for the underlying named client, which is where an application attaches its own pipeline:

services.AddPrdbClient(options =>
{
    options.ApiKey = configuration["Prdb:ApiKey"];
    options.Retry = PrdbRetryOptions.Disabled;   // see below
})
.AddStandardResilienceHandler();

Anything added to that builder runs inside the SDK's middleware, so a resilience handler there sees the individual HTTP attempts.

Leaving ApiKey unset registers an anonymous client. An empty one is rejected, because that is a configuration value that failed to resolve rather than a deliberate choice. Every other setting is checked at registration too, so a bad base URL stops startup instead of the first request.

Settings that change while the application runs

The overload above reads the options once, at registration. If your API key or base URL lives somewhere a user can edit — a database row, a reloading configuration source — take the overload that also gets the IServiceProvider. It runs on every resolution, and the client is transient, so each injected client uses the current values:

services.AddPrdbClient((serviceProvider, options) =>
{
    var settings = serviceProvider.GetRequiredService<ISettingsSnapshot>();
    options.ApiKey = settings.PrdbApiKey;
    options.BaseUrl = settings.PrdbApiUrl;
    options.Retry = PrdbRetryOptions.Disabled;
})
.AddStandardResilienceHandler();

Settings that are not known at registration cannot be validated there, so a bad base URL or an empty key throws when a client is resolved rather than at startup. Resolve one while starting up if you want the failure there.

Options

var client = PrdbClientFactory.Create(
    apiKey: "...",
    baseUrl: "https://api.prdb.net",     // override for a staging deployment
    transport: myHandler,                // proxies, pooling, your own pipeline
    retry: PrdbRetryOptions.Disabled,    // see below
    timeout: TimeSpan.FromSeconds(30));  // per request, default 100 seconds

transport is the innermost HttpMessageHandler. The SDK's middleware is layered on top of it, so the redirect rule above applies to it too.

A transport must not follow redirects itself. One that does would follow a redirect off the API host before the SDK's rule could refuse it, and nothing below strips X-Api-Key. So the SDK checks, and refuses to build a client on a transport whose primary handler has AllowAutoRedirect set:

var transport = new SocketsHttpHandler { AllowAutoRedirect = false };
var client = PrdbClientFactory.Create("...", transport: transport);

KiotaClientFactory.GetDefaultHttpMessageHandler() produces a suitable handler too. For a handler that comes from IHttpClientFactory, configure it where it is registered:

services.AddHttpClient("prdb")
    .ConfigurePrimaryHttpMessageHandler(
        () => new SocketsHttpHandler { AllowAutoRedirect = false });

The SDK neither disposes nor modifies a transport you supply. Both matter when it comes from IHttpMessageHandlerFactory.CreateHandler, where handlers are pooled and shared across the process — and where a SocketsHttpHandler refuses to be reconfigured at all once it has served its first request.

Retrying

By default the SDK retries a 429, 503 or 504 up to three times, honouring Retry-After.

Turn that off if your application already retries prdb calls:

var client = PrdbClientFactory.Create("...", retry: PrdbRetryOptions.Disabled);

Otherwise the two policies multiply — one logical call becomes up to n×m requests against an API that rate limits, and an outer circuit breaker never sees a stable failure to open on. The built-in policy also retries writes, so an application that must not repeat one should own the retry itself.

To keep it but change it:

var client = PrdbClientFactory.Create("...", retry: new PrdbRetryOptions
{
    MaxRetries = 5,
    Delay = TimeSpan.FromSeconds(1),
});

Retrying costs you the error body. Kiota's retry handler throws its own AggregateException of bare ApiExceptions once the attempts are spent, instead of handing the last response on, so the error mapping never runs:

503, retrying enabled   AggregateException of ApiException, no body
503, retrying disabled  ProblemDetails, detail: "fail-closed"

That applies to a refusal that persists — one the API repeats until the attempts run out, which is exactly the case where 403 explains that there is no API plan, or 503 that rate limiting is unavailable and the API is fail-closed. A retry that succeeds is unaffected.

So an application that wants to log why prdb refused should disable the SDK's retry and own the retrying itself, with a policy that returns the final response rather than throwing — AddStandardResilienceHandler does. This is Kiota's behaviour in .NET only; the Python, TypeScript and Go SDKs return the last response and keep the typed error.

Reading the response status

A typed call returns the deserialised body but not the response status. Pass a ResponseStatusOption when the status itself matters; the conditional-request example below uses it to distinguish a 304 Not Modified response from other responses with no body.

Pass a ResponseStatusOption to read it:

using System.Diagnostics;
using System.Net;

var status = new ResponseStatusOption();

var health = await client.Health.GetAsync(
    config => config.Options.Add(status));

Debug.Assert(health?.Status == "healthy");
Debug.Assert(status.StatusCode == HttpStatusCode.OK);

Kiota's own NativeResponseHandler surfaces the raw HttpResponseMessage but suppresses deserialisation while doing so. This option keeps the typed result and records the status alongside it.

Use one instance per call. It is written when the response arrives, so sharing one across concurrent calls means whichever finishes last wins.

The status recorded is the one the result was built from: after a redirect the SDK followed, and after the last retry, whether that retrying is the SDK's own or your resilience handler inside the pipeline. A call that throws records too, so a ProblemDetails caught from a 403 still has its status alongside. It stays null when no response was reached at all — a failed connection, a timeout, or a refused cross-origin redirect.

Uploading an image

POST /video-user-images takes a MultipartBody:

using Microsoft.Kiota.Abstractions;

using var file = File.OpenRead("preview.jpg");

var body = new MultipartBody();
body.AddOrReplacePart("File", "image/jpeg", file, "preview.jpg");
body.AddOrReplacePart("PreviewImageType", "text/plain", "Single");
body.AddOrReplacePart("VideoId", "text/plain", videoId.ToString());

var result = await client.VideoUserImages.PostAsync(body);

Do not set RequestAdapter on the body. The property is public and its documentation says serialisation needs it, which makes the endpoint look uncallable from outside the SDK — the adapter behind PrdbClient is protected, so there is no way to reach it. There is no need to: the request adapter fills the property in while sending. A test in this repository pins that down.

Reading the rate limit

Every metered response carries the rate limit it was counted against, so you can pace off the answers you are already getting instead of spending a request on GET /rate-limit to ask.

var limits = new RateLimitOption();

var sites = await client.Sites.GetAsync(config => config.Options.Add(limits));

if (limits.Hour is { Remaining: < 50 } hour)
{
    // Slow down; hour.ResetInSeconds until a slot frees up.
}

Hour and Month are each a RateLimitWindow with Limit, Remaining and ResetInSeconds, or null.

ResetInSeconds is the wait until the oldest request leaves the sliding window and frees one slot — not a timestamp, and not the time until the whole window resets. It is the same quantity resetsInSeconds carries on GET /rate-limit.

Null is an answer rather than a gap. A response the API did not meter — 401, 403, 503, and GET /rate-limit itself — carries no headers at all, and a 429 carries only the window that refused the request, so exactly one of the two being set is normal. A call that throws records too, so the reading is there for a caller that catches the error.

Reading response headers

The rate limit above is the typed reading of six of them. For anything else — ETag, Retry-After on a 429 — the raw headers are reachable per request through HeadersInspectionHandlerOption:

using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;

var inspection = new HeadersInspectionHandlerOption
{
    InspectResponseHeaders = true,
};

var page = await client.WantedVideos.Changes.GetAsync(config =>
{
    config.QueryParameters.Since = DateTimeOffset.UtcNow.AddDays(-1);
    config.Options.Add(inspection);
});

var date = inspection.ResponseHeaders["Date"];

It populates on a 304 too, so the ETag from a conditional GET /sites is readable on both legs of the round trip.

Conditional requests

GET /sites returns a weak ETag covering the matched rows and the paging, sorting and search parameters. Send it back as If-None-Match and the endpoint answers 304 Not Modified with no body while nothing has changed — the whole site list fits in one request at PageSize = 1000, so this is worth doing.

using System.Net;
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;

// First call: read the validator off the response.
var inspection = new HeadersInspectionHandlerOption { InspectResponseHeaders = true };
await client.Sites.GetAsync(config => config.Options.Add(inspection));
var etag = inspection.ResponseHeaders["ETag"].First();

// Later: ask only for what changed.
var status = new ResponseStatusOption();

var sites = await client.Sites.GetAsync(config =>
{
    config.Headers.Add("If-None-Match", etag);
    config.Options.Add(status);
});

if (status.StatusCode == HttpStatusCode.NotModified)
{
    // Nothing changed; sites is null, keep the copy you already have.
}

A 304 returns null from the typed call rather than throwing. Null alone does not distinguish "not modified" from "no rows", so pass a ResponseStatusOption when you need to tell them apart.

This is the one place where the SDK reshapes a response. Kiota generates no handling for a 3xx in any language, and the C# request adapter alone treats an unmapped non-2xx as a failure — Python, TypeScript and Go all return null from the same call. So the SDK presents the 304 to the adapter as a 204, after the real status has been recorded and with the headers left intact. The only place the substitution shows is Kiota's NativeResponseHandler, which sits above it and sees the 204.

One wrinkle from the API side: the shared read-only cache does not vary by If-None-Match, so a request that hits it is answered 200 with a body even when your validator still matches. That is expected rather than an error.

Computing the file hashes

Several endpoints identify a file by its osHash and pHash rather than by its name — POST /videos/filehashes/lookup, POST /videos/identify, POST /videos/filehash-submissions. This package sends those values; it does not compute them.

Prdb.Hashing does, matching what Stash produces bit for bit:

dotnet add package Prdb.Hashing
using Prdb.Hashing;

string? osHash = OsHash.Compute(path);
var pHash = await new VideoPerceptualHasher().ComputeAsync(path);

It is a separate package because it starts processes and needs ffmpeg, which an HTTP client has no business doing. The method is specified in docs/video-hashing.md.

Generated code

Everything under src/Prdb.Sdk/Generated/ is produced by Kiota from spec/openapi.json in the repository root and is overwritten on every regeneration. Do not edit it — see the root README.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.14.0 96 9/5/2026
0.13.0 688 8/30/2026
0.12.0 107 8/28/2026
0.11.0 531 8/23/2026
0.10.0 112 8/23/2026
0.9.0 131 8/20/2026
0.8.0 106 8/19/2026
0.7.0 115 8/19/2026
0.6.2 145 8/19/2026
0.6.1 188 8/9/2026
0.6.0 98 8/9/2026
0.5.0 156 8/8/2026
0.4.0 128 8/8/2026
0.3.1 99 8/8/2026
0.3.0 108 8/8/2026
0.2.0 100 8/8/2026
0.1.1 102 8/7/2026
0.1.0 98 8/7/2026