StrapiConnect 1.1.2
dotnet add package StrapiConnect --version 1.1.2
NuGet\Install-Package StrapiConnect -Version 1.1.2
<PackageReference Include="StrapiConnect" Version="1.1.2" />
<PackageVersion Include="StrapiConnect" Version="1.1.2" />
<PackageReference Include="StrapiConnect" />
paket add StrapiConnect --version 1.1.2
#r "nuget: StrapiConnect, 1.1.2"
#:package StrapiConnect@1.1.2
#addin nuget:?package=StrapiConnect&version=1.1.2
#tool nuget:?package=StrapiConnect&version=1.1.2
StrapiConnect
StrapiConnect is a lightweight .NET library that simplifies integration with a Strapi CMS backend using a resilient, pre-configured HttpClient. It includes built-in support for authentication and transient-fault handling using Polly.
Features
- 📦 Easily registers a typed
HttpClientforIStrapiConnect - 🔒 Supports Bearer Token authentication from config
- 🔁 Polly retry policy with exponential backoff
- 🚫 Validates
BaseUrlconfig at startup - 💡 Clean DI extension for
Program.csorStartup.cs
Installation
Install via NuGet:
dotnet add package StrapiConnect
Usage
- Add Configuration to appsettings.json
"StrapiConnect": {
"BaseUrl": "https://your-strapi-instance/api",
"ApiKey": "your-secret-api-token"
}
- Register the Service in Program.cs
builder.Services.AddStrapiService(builder.Configuration);
- Inject and Use IStrapiService
using StrapiConnect;
var strapiConnect = new StrapiConnect("http://localhost:1337/api");
var request = new FindManyRequest("articles");
var result = await strapiConnect.ExecuteAsync<ArticleResponse>(request);
Console.WriteLine(result);
Strapi 5 Support
StrapiConnect works with both Strapi v4 and Strapi 5, and defaults to the native Strapi 5 response shape.
Response format (v4 vs v5)
By default no Strapi-Response-Format header is sent, so a Strapi 5 server replies
in its native flattened shape: fields sit directly on data alongside documentId,
with no attributes wrapper. To ask for the legacy v4 shape instead, pass
StrapiResponseFormat.V4:
using StrapiConnect.Enums;
// Native Strapi 5 response — the default
var result = await strapiConnect.ExecuteAsync<ArticleResponse>(
"http://localhost:1337/api", request);
// Legacy v4 response (fields under data.attributes, relations wrapped in { data: ... })
var legacy = await strapiConnect.ExecuteAsync<ArticleResponse>(
"http://localhost:1337/api", request, StrapiResponseFormat.V4);
⚠️ Upgrading to 2.0.0 — breaking change
Versions 1.0.12 – 1.1.1 sent
Strapi-Response-Format: v4by default. From 2.0.0 the default is native v5. (Version 1.0.11 and earlier sent no header at all, so 2.0.0 restores that behaviour.)If your models are shaped like the v4 response — a
Data.Attributeswrapper, and relations as{ data: { id, attributes } }— addStrapiResponseFormat.V4to your calls to keep the old shape:await strapiConnect.ExecuteAsync<T>(baseUrl, request, StrapiResponseFormat.V4);If your models are flat (
documentId,title,cover.urlat the top level), you need no change — and if you were on 1.0.12–1.1.1, this fixes nested relations that were silently binding tonull.
Fetch a single document by documentId
Strapi 5 addresses single records by their string documentId:
var request = new FindByDocumentIdRequest("articles", "znrlzntu9ei5onjvwfaalu2v");
var article = await strapiConnect.ExecuteAsync<ArticleResponse>(
"http://localhost:1337/api", request, StrapiResponseFormat.V5);
FindByIdRequest(contentType, int id) remains available for v4-style numeric ids.
Create / update with relations
The client now supports POST / PUT / DELETE. Relations are managed with
RelationBuilder (Connect / Disconnect / Set, with optional positioning),
following the Strapi 5 relations API:
// Create an article and link categories
var create = new CreateRequest("articles")
.WithBody(new RequestBody()
.Field("title", "Hello world")
.Relation("categories", new RelationBuilder().Connect("cat1").Connect("cat2")));
await strapiConnect.ExecuteAsync<ArticleResponse>(
"http://localhost:1337/api", create, StrapiResponseFormat.V5);
// Update: connect with ordering, and disconnect another relation
var update = new UpdateRequest("articles", "znrlzntu9ei5onjvwfaalu2v")
.WithBody(new RequestBody()
.Relation("categories", new RelationBuilder()
.Connect("cat3", RelationPosition.Start())
.Disconnect("cat1")));
await strapiConnect.ExecuteAsync<ArticleResponse>(
"http://localhost:1337/api", update, StrapiResponseFormat.V5);
// Replace all relations (cannot be combined with connect/disconnect)
var replace = new UpdateRequest("articles", "znrlzntu9ei5onjvwfaalu2v")
.WithBody(new RequestBody()
.Relation("categories", new RelationBuilder().Set("cat2", "cat3")));
// Delete by documentId (204 No Content returns null)
var delete = new DeleteRequest("articles", "znrlzntu9ei5onjvwfaalu2v");
await strapiConnect.ExecuteAsync<object>("http://localhost:1337/api", delete);
Positioning options: RelationPosition.Before(id), RelationPosition.After(id),
RelationPosition.Start(), RelationPosition.End() (default).
Populate (and deep populate)
Strapi 5's native populate=* is only one level deep, so nested
components/relations/media are not returned by it. There are three ways to get
deeper data:
// 1. Everything, one level
request.PopulateAll(); // populate=*
// 2. Explicit nesting (relations / single components) — any depth
request.Populate("author").Populate("avatar").PopulateAll();
// => populate[author][populate][avatar][populate]=*
// 3. Dynamic zones (e.g. `blocks`) — MUST use the per-component `on` form.
// Strapi returns HTTP 400 for populate[blocks][populate][<field>]=...
var blocks = request.Populate("blocks");
blocks.OnComponent("blocks.hero").PopulateAll();
blocks.OnComponent("blocks.feature-block").Populate("features").PopulateAll();
// => populate[blocks][on][blocks.hero][populate]=*
// & populate[blocks][on][blocks.feature-block][populate][features][populate]=*
Or, if your Strapi has the
strapi-plugin-populate-deep
plugin installed, populate everything to a depth in one call:
request.SetPopulateLevel(5); // ?pLevel=5 (no-op if the plugin isn't installed)
Combining populate directives
Strapi has no "populate everything, plus this one relation narrowed" form: once you name relations, only those are populated. So these combinations resolve to one side, and it is worth knowing which:
| You wrote | You get | Why |
|---|---|---|
SetPopulateLevel(5) and Populate("cover")… |
pLevel=5 only — every populate[…] is discarded |
pLevel and explicit populate are mutually exclusive in the URL builder |
PopulateAll() and Populate("cover")… |
the named relations only | populate[*]=* reads as an attribute literally named *; ignored before Strapi 5.37, a 400 from 5.37 on |
node.PopulateAll() and node.Populate("child")… |
the named child only | same reason, one level down |
zone.OnComponent(…) and zone.Populate("child")… |
the on form only |
Strapi rejects named nested populate on a dynamic zone |
The first row is the easy one to trip over: adding Populate(...) to a request that
already calls SetPopulateLevel has no effect at all. Pick one strategy per request.
Draft & Publish (status)
Strapi 5 removed the v4 publicationState parameter and replaced it with status.
Use SetStatus to choose the draft or published version. With no call the Strapi
default (published) applies.
using StrapiConnect.Enums;
request.SetStatus(StrapiStatus.Draft); // ?status=draft
request.SetStatus(StrapiStatus.Published); // ?status=published
Localization (locale)
request.SetLocale("en"); // ?locale=en
Filtering, sorting & pagination
// Equality emits the canonical Strapi filter form (a bare `field=value` is ignored
// by Strapi), and values are URL-encoded automatically.
request.Equal("slug", "my-post"); // filters[slug][$eq]=my-post
request.Filter(FilterType.ContainsCaseInsensitive, "title", "hello"); // filters[title][$containsi]=hello
request.Filter(FilterType.Between, "price", "10,20"); // filters[price][$between][0]=10&[1]=20
request.Filter(FilterType.In, "category.slug", "tours"); // filters[category][slug][$in][0]=tours
// Multiple sorts produce distinct indices (sort[0], sort[1], …)
request.Sort("title", SortDirection.Ascending); // sort[0]=title:asc
request.Sort("createdAt", SortDirection.Descending); // sort[1]=createdAt:desc
request.SetPage(1);
request.SetPageSize(10);
Supported operators (via FilterType): $eq, $ne, $lt, $lte, $gt, $gte,
$in, $notIn, $contains, $notContains, $startsWith, $endsWith, $null,
$notNull, the case-insensitive variants $eqi/$nei/$containsi/$notContainsi/
$startsWithi/$endsWithi, and $between.
Error handling
By default a failed request (transport error, non-success status, or deserialization
failure) is logged and returns default (null). To make failures throw instead,
pass throwOnError: true; a StrapiRequestException (wrapping the original error) is
raised:
var result = await strapiConnect.ExecuteAsync<ArticleResponse>(
"http://localhost:1337/api", request, StrapiResponseFormat.V5, throwOnError: true);
Built-in Resilience with Polly
- Retries 3 times on transient errors
- Supports HttpStatusCode.TooManyRequests and SocketException
- Exponential backoff (2s, 4s, 8s)
Releasing to NuGet
scripts/publish-nuget.sh sets the package version, builds, tests, packs and pushes
the package in one step:
# Validate the whole pipeline without publishing (no API key needed)
./scripts/publish-nuget.sh 1.0.1 --dry-run
# Publish, then tag the release
NUGET_API_KEY=oy2... ./scripts/publish-nuget.sh 1.0.1 --tag
The version argument must be valid SemVer (1.0.1, 1.2.0-beta.1). It is written into
src/StrapiConnect/StrapiConnect.csproj as <Version>; on a dry run or any failure the
original value is restored, and after a successful push the bump is kept so you can
commit it.
| Option | Purpose |
|---|---|
--api-key <key> |
NuGet API key (default $NUGET_API_KEY) |
--source <url> |
Push target (default $NUGET_SOURCE or nuget.org) |
--output <dir> |
.nupkg output directory (default artifacts/nuget) |
--configuration <cfg> |
Build configuration (default Release) |
--skip-tests |
Skip dotnet test |
--keep-version |
Publish the version already in the .csproj |
--dry-run |
Build and pack only, no push |
--tag |
Create and push git tag v<version> after publishing |
Run ./scripts/publish-nuget.sh --help for the full reference. The
Release NuGet Package GitHub Actions workflow remains available for publishing from CI.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. 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. |
-
net9.0
- Microsoft.Extensions.Http.Polly (>= 9.0.8)
- Serilog.AspNetCore (>= 9.0.0)
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 |
|---|---|---|
| 1.1.2 | 65 | 8/25/2026 |
| 1.1.1 | 87 | 8/24/2026 |
| 1.0.12 | 203 | 5/14/2026 |
| 1.0.11 | 1,622 | 9/19/2025 |
| 1.0.10 | 240 | 8/13/2025 |
| 1.0.9 | 219 | 7/14/2025 |
| 1.0.8 | 259 | 6/2/2025 |
| 1.0.7 | 617 | 5/24/2025 |
| 1.0.6 | 181 | 5/23/2025 |
| 1.0.5 | 196 | 5/23/2025 |
| 1.0.4 | 238 | 5/22/2025 |
| 1.0.3 | 232 | 5/22/2025 |
| 1.0.2 | 237 | 5/22/2025 |
| 1.0.1 | 233 | 5/22/2025 |
| 1.0.0 | 232 | 5/22/2025 |