TcgDex.CSharpSdk 0.2.1

dotnet add package TcgDex.CSharpSdk --version 0.2.1
                    
NuGet\Install-Package TcgDex.CSharpSdk -Version 0.2.1
                    
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="TcgDex.CSharpSdk" Version="0.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TcgDex.CSharpSdk" Version="0.2.1" />
                    
Directory.Packages.props
<PackageReference Include="TcgDex.CSharpSdk" />
                    
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 TcgDex.CSharpSdk --version 0.2.1
                    
#r "nuget: TcgDex.CSharpSdk, 0.2.1"
                    
#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 TcgDex.CSharpSdk@0.2.1
                    
#: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=TcgDex.CSharpSdk&version=0.2.1
                    
Install as a Cake Addin
#tool nuget:?package=TcgDex.CSharpSdk&version=0.2.1
                    
Install as a Cake Tool

TcgDex.CSharpSdk

A .NET SDK for the TCGdex Pokémon TCG API — strongly typed models, a fluent query builder over the full REST filter syntax, and first-class support for dependency injection, trimming and Native AOT.

CI

Targets .NET 8, .NET 10 and netstandard2.0 — so it runs on modern .NET and on .NET Framework 4.6.1+. No API key required — TCGdex is a free, public, read-only API.

Unity is a supported target by construction rather than by test: the netstandard2.0 assembly contains no runtime code generation, no Expression.Compile() and no reflection-based serialization, and a Native AOT publish exercises the one reflective path under full trimming. Nobody has yet run it inside a Unity project, so see docs/unity.md for what is verified, what is not, and the packaging and stripping caveats Unity adds.

The API surface is identical on every target, async included. One difference is worth knowing before you rely on it: connection recycling, which keeps a long-lived client from pinning stale DNS, uses SocketsHttpHandler on modern .NET and ServicePoint.ConnectionLeaseTimeout on .NET Framework. Same guarantee, different mechanism. Response cancellation mid-body is best-effort on netstandard2.0, because the cancellable HttpContent read overloads do not exist there.


Install

dotnet add package TcgDex.CSharpSdk

Quick start

With dependency injection, which wires the client through IHttpClientFactory:

builder.Services.AddTcgDex();
public sealed class CardLookup(ITcgDexClient tcgdex)
{
    public async Task<string?> DescribeAsync(string id, CancellationToken ct)
    {
        Card? card = await tcgdex.Cards.GetAsync(id, ct);

        return card is null ? null : $"{card.Name} ({card.Category}) — {card.Rarity}";
    }
}

Without a container:

using HttpClient http = new();
TcgDexClient tcgdex = new(http, new TcgDexOptions());

Card? card = await tcgdex.Cards.GetAsync("swsh3-136", cancellationToken);
Console.WriteLine(card?.Name);   // Furret

Querying

Predicates are written in C# and translated to the API's filter syntax:

CardQuery query = new CardQuery()
    .Where(c => c.Name.Contains("Pikachu"))
    .Where(c => c.Hp > 100)
    .OrderByDescending(c => c.Name)
    .Page(1, 50);

IReadOnlyList<CardBrief> cards = await tcgdex.Cards.ListAsync(query, cancellationToken);

which becomes:

cards?name=Pikachu&hp=gt:100&sort:field=name&sort:order=DESC&pagination:page=1&pagination:itemsPerPage=50

Supported translations, covering every operator the API has:

C# Query syntax
c.Name == "Furret" name=eq:Furret
c.Name != "Furret" name=neq:Furret
c.Hp > 100 / >= / < / <= hp=gt:100 / gte: / lt: / lte:
c.Name.Contains("pika") name=pika
c.Name.StartsWith("fu") name=fu*
c.Name.EndsWith("chu") name=*chu
!c.Name.Contains("pika") name=not:pika
c.Effect == null effect=null:
c.Effect != null effect=notnull:
a && b separate parameters
c.Name == "a" \|\| c.Name == "b" name=eq:a\|b

Two limits are inherited from the API rather than chosen here:

  • || works only within a single field. An OR across two fields has no encoding, so it throws rather than silently dropping half your predicate.
  • No total count exists. The API sends no count and no pagination headers, so page until you get back fewer items than you asked for.

Anything the API cannot express is rejected with a message naming the offending expression — never approximated into a filter that would quietly return the wrong cards.

Full card detail in one request

ListAsync returns briefs, so getting full detail for a result set costs one call per card. When you need the detail, SearchDetailedAsync fetches it in a single request over GraphQL:

IReadOnlyList<Card> cards = await tcgdex.Cards.SearchDetailedAsync(
    new CardFilter { Name = "Furret" },
    cancellationToken: ct);

// 12 fully populated cards — hp, types, attacks, weaknesses, set — in one hop.
// The REST equivalent is 13 round trips.

Three limits come with it, all imposed by the TCGdex GraphQL endpoint rather than by this SDK:

REST SearchDetailedAsync
Languages all 18 English only
Filters all ten operators equality only
Pricing populated never populated

So reach for it when you want breadth cheaply, and stay on REST when you need a language, a range filter, or prices.

Caching

The API sends no-store, but it honours If-None-Match. Enabling the cache means fresh reads skip the network entirely, and stale reads are revalidated — an unchanged 22 KB set response costs a 304 and 0 bytes instead of a re-download.

builder.Services.AddTcgDexWithCaching();

Errors are never cached, non-GET requests bypass it, and concurrent reads of the same URL are collapsed into a single request. See Caching.

What you can read

Card?  card  = await tcgdex.Cards.GetAsync("swsh3-136", ct);
Set?   set   = await tcgdex.Sets.GetAsync("swsh3", ct);      // includes its cards
Serie? serie = await tcgdex.Series.GetAsync("swsh", ct);     // includes its sets
Card   lucky = await tcgdex.Random.CardAsync(ct);

// Distinct values, useful for building filters and pickers
IReadOnlyList<string> rarities = await tcgdex.Catalog.RaritiesAsync(ct);
IReadOnlyList<int>    hpValues = await tcgdex.Catalog.HitPointsAsync(ct);

Catalog covers all thirteen enumeration endpoints: categories, rarities, types, illustrators, stages, suffixes, variants, energy types, regulation marks, trainer types, HP, retreat costs and dex ids.

Languages

18 are supported. Set one at registration:

builder.Services.AddTcgDex(options => options.Language = TcgDexLanguages.French);

An unsupported code fails at registration with a message listing the valid set, rather than surfacing later as a 404 that looks like a missing card.

Error handling

One rule, applied everywhere:

  • A missing resource returns null. Asking for a card that does not exist is a normal outcome, not an exception.
  • Everything else throws TcgDexApiException — server errors, unsupported languages, timeouts and unparseable bodies alike, so you catch one type rather than four.
try
{
    Card? card = await tcgdex.Cards.GetAsync(id, ct);
    if (card is null) { /* no such card */ }
}
catch (TcgDexApiException ex)
{
    logger.LogError(ex, "TCGdex failed with {Status}", ex.StatusCode);
}

Worth knowing: the API returns 404 for an unsupported language too, so the status code alone cannot distinguish that from a missing card. The SDK discriminates on the error body and exposes ex.IsLanguageError.

Trimming and Native AOT

The SDK is trim- and AOT-safe: serialization is source-generated, and the query builder walks expression trees rather than calling Expression.Compile(), which would emit IL at runtime.

This is verified, not asserted — CI publishes a Native AOT binary and runs it on every push. See TcgDex.CSharpSdk.AotSmokeTest.

Cards are modelled as the API returns them

Fields are populated by category: Pokémon carry Hp, Types, Attacks and Weaknesses; Trainers carry TrainerType and Effect; Energy cards carry EnergyType. Anything category-specific is nullable because the API omits it entirely rather than sending null.

Collections are never null — an absent array arrives as empty, so iterating a Trainer's Attacks is safe.

A few shapes are irregular and the SDK smooths them over:

  • Attack.Damage is text, because the API sends 60 on one card and "50+" on another. Attack.BaseDamage gives you the numeric part.
  • WeaknessOrResistance.Value is text — values include "×2" and "-20".
  • TCGplayer prices are keyed by printing name, and the names vary per card, so they are exposed as a dictionary: card.Pricing?.Tcgplayer?["holofoil"].

Images

Image, Logo and Symbol are base URLs without a file extension. The helpers build the right form for each:

string? art  = card.GetImageUrl(ImageQuality.High, ImageFormat.Png);
string? logo = card.Set.GetLogoUrl();
string? sym  = card.Set.GetSymbolUrl(ImageFormat.Webp);

They are separate methods because card artwork takes a quality segment and set assets do not:

https://assets.tcgdex.net/en/swsh/swsh3/136/high.png   card    200
https://assets.tcgdex.net/en/swsh/swsh3/logo.png       logo    200
https://assets.tcgdex.net/en/swsh/swsh3/logo/high.png  logo    404

The three fields look alike on the model, so applying the card pattern to a logo is an easy mistake — and returns 404.

Each helper returns null rather than a broken URL when the asset is absent; some cards genuinely have no artwork.

Logging and tracing

Structured logs via ILogger and OpenTelemetry spans via ActivitySource, both free when nothing is listening. With DI it is automatic; everything is under the TcgDex category.

builder.Services.AddOpenTelemetry()
    .WithTracing(t => t.AddSource(TcgDexActivity.SourceName));

The SDK takes no dependency on any telemetry vendor — it writes to .NET's standard abstractions, so Serilog, Application Insights, Datadog, Sentry or anything else picks it up by configuring that tool in your application. See Logging and tracing.

Documentation

Full documentation and API reference

In this repository:

  • docs/api-info.md — the API reference this SDK is built against, verified field by field against live responses.
  • docs/learnings.md — non-obvious behaviour discovered while building it.

Security

Found a vulnerability? Report it privately through GitHub Security Advisories rather than opening an issue — see SECURITY.md, which also sets out what this library actually exposes and what is not in scope.

Contributing

dotnet build -warnaserror          # zero warnings is enforced
dotnet test TcgDex.CSharpSdk.Tests # unit tests, offline
dotnet test TcgDex.CSharpSdk.IntegrationTests --filter "TestCategory=Integration"

Coverage is gated in CI and the same check runs locally. Run this before pushing — it is the gate most easily forgotten, because everything else is green by the time you get here:

dotnet test TcgDex.CSharpSdk.Tests/TcgDex.CSharpSdk.Tests.csproj   --collect:"XPlat Code Coverage" --settings coverlet.runsettings   --results-directory ./TestResults
pwsh ./scripts/Check-Coverage.ps1

--settings coverlet.runsettings is not optional. Without it the source-generated files swamp the hand-written ones and the number means nothing — 1,455 lines counted against the real 986.

Two further gates that are not part of a normal push:

  • The public API is pinned. Adding, removing or changing anything public fails PublicApiTests until the diff is accepted into TcgDex.CSharpSdk.Tests/PublicApi.approved.cs. That commit is the record of what consumers are promised.
  • Mutation testing and fuzzing run periodically rather than per push. See docs/measuring.md for both, including how to run the fuzzer locally under WSL.

Unit tests run against recorded API responses in TcgDex.CSharpSdk.Tests/Fixtures, so they need no network. Integration tests hit the live API and run weekly in CI rather than per push, so a TCGdex outage does not redden a pull request.

License

MIT — see LICENSE.txt.

This is an unofficial, community-maintained SDK. It is not affiliated with TCGdex, Nintendo, Creatures Inc., GAME FREAK inc. or The Pokémon Company. Pokémon and all related names are trademarks of their respective owners.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 is compatible.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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.2.1 44 8/22/2026
0.2.0 70 8/19/2026
0.1.1 95 8/8/2026
0.1.0 102 8/8/2026