Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
#### Date: Feb-10-2026

##### Feat:
- CDA / DAM 2.0 – AssetFields support
- CDA / – AssetFields support
- Added `AssetFields(params string[] fields)` to request specific asset-related metadata via the CDA `asset_fields[]` query parameter
- Implemented on: Entry (single entry fetch), Query (entries find), Asset (single asset fetch), AssetLibrary (assets find)
- Valid parameters: `user_defined_fields`, `embedded_metadata`, `ai_generated_metadata`, `visual_markups`
- Method is chainable; when called with no arguments, the query parameter is not set
- CDA / – Asset localisation support
- Added `SetLocale(string locale)` on Asset for single-asset fetch by locale (e.g. `stack.Asset(uid).SetLocale("en-us").Fetch()`)
- Added `Title` property on Asset for localised title in API response
- AssetLibrary `SetLocale` continues to support listing assets by locale

### Version: 2.25.2
#### Date: Nov-13-2025
Expand Down
119 changes: 119 additions & 0 deletions Contentstack.Core.Tests/AssetTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1133,5 +1133,124 @@ public async Task AssetFields_AssetLibrary_WithEmptyArray_RequestSucceeds()
Assert.Fail("AssetLibrary.FetchAll with AssetFields(empty array) did not return a result.");
Assert.NotNull(assets.Items);
}

[Fact]
public async Task FetchAssetsWithLocale_ReturnsLocalisedAssets()
{
var locale = "en-us"; // or "ar" if your stack has that locale
ContentstackCollection<Asset> assets = await client.AssetLibrary()
.SetLocale(locale)
.FetchAll();

Assert.True(assets.Items != null);
if (assets.Items.Count() == 0)
return; // no assets in this locale

foreach (Asset asset in assets)
{
// Root-level locale (when API returns it)
var rootLocale = asset.Get("locale");
if (rootLocale != null)
Assert.Equal(locale, rootLocale.ToString());

// Or via publish_details (existing pattern from FetchAssetsPublishWithoutFallback)
var publishDetails = asset.Get("publish_details") as JObject;
if (publishDetails != null && publishDetails["locale"] != null)
Assert.Equal(locale, publishDetails["locale"]?.ToString());
}
}

/// <summary>
/// Asset localisation: Fetch single asset with locale query param; response has requested locale.
/// </summary>
[Fact]
public async Task FetchSingleAssetWithLocale_ReturnsLocalisedAsset()
{
string uid = await FetchAssetUID();
var locale = "en-us";

Asset asset = await client.Asset(uid).AddParam("locale", locale).Fetch();

Assert.NotNull(asset);
Assert.NotNull(asset.Uid);
var publishDetails = asset.Get("publish_details") as JObject;
if (publishDetails != null && publishDetails["locale"] != null)
Assert.Equal(locale, publishDetails["locale"]?.ToString());
var rootLocale = asset.Get("locale");
if (rootLocale != null)
Assert.Equal(locale, rootLocale.ToString());
}

/// <summary>
/// Asset localisation: List assets with SetLocale("ar"); each asset has locale in response.
/// </summary>
[Fact]
public async Task FetchAssetsWithLocaleAr_ReturnsAssetsWithLocale()
{
ContentstackCollection<Asset> assets = await client.AssetLibrary()
.SetLocale("ar")
.Limit(10)
.FetchAll();

Assert.NotNull(assets.Items);
if (assets.Items.Count() == 0)
return; // stack may not have assets in "ar"

foreach (Asset asset in assets)
{
var publishDetails = asset.Get("publish_details") as JObject;
if (publishDetails != null && publishDetails["locale"] != null)
Assert.Equal("ar", publishDetails["locale"]?.ToString());
var rootLocale = asset.Get("locale");
if (rootLocale != null)
Assert.Equal("ar", rootLocale.ToString());
}
}

/// <summary>
/// Asset localisation: SetLocale with IncludeFallback returns assets; locale in publish_details.
/// </summary>
[Fact]
public async Task FetchAssetsWithLocaleAndFallback_ReturnsLocalisedOrFallback()
{
var locale = "en-us";
ContentstackCollection<Asset> assets = await client.AssetLibrary()
.SetLocale(locale)
.IncludeFallback()
.Limit(10)
.FetchAll();

Assert.NotNull(assets.Items);
if (assets.Items.Count() == 0)
return;

foreach (Asset asset in assets)
{
var publishDetails = asset.Get("publish_details") as JObject;
Assert.NotNull(publishDetails);
Assert.NotNull(publishDetails["locale"]);
}
}

/// <summary>
/// Asset localisation: Single asset fetch using Asset.SetLocale returns localised asset.
/// </summary>
[Fact]
public async Task FetchSingleAssetWithSetLocale_ReturnsLocalisedAsset()
{
string uid = await FetchAssetUID();
var locale = "ar";

Asset asset = await client.Asset(uid).SetLocale(locale).Fetch();

Assert.NotNull(asset);
Assert.NotNull(asset.Uid);
var publishDetails = asset.Get("publish_details") as JObject;
if (publishDetails != null && publishDetails["locale"] != null)
Assert.Equal(locale, publishDetails["locale"]?.ToString());
var rootLocale = asset.Get("locale");
if (rootLocale != null)
Assert.Equal(locale, rootLocale.ToString());
}
}
}
37 changes: 37 additions & 0 deletions Contentstack.Core.Unit.Tests/AssetLibraryUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,43 @@ public void SetLocale_AddsQueryParameter()
Assert.Equal(locale, urlQueries?["locale"]?.ToString());
}

/// <summary>
/// Asset localisation: SetLocale with locale "ar" adds locale query param for API.
/// </summary>
[Fact]
public void SetLocale_ForAssetLocalisation_AddsLocaleQueryParameter()
{
var assetLibrary = CreateAssetLibrary();
var locale = "ar";

AssetLibrary result = assetLibrary.SetLocale(locale);

Assert.NotNull(result);
Assert.Same(assetLibrary, result);
var urlQueriesField = typeof(AssetLibrary).GetField("UrlQueries",
BindingFlags.NonPublic | BindingFlags.Instance);
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(assetLibrary);
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
Assert.Equal("ar", urlQueries?["locale"]?.ToString());
}

/// <summary>
/// SetLocale when called again updates the locale query param (overwrite).
/// </summary>
[Fact]
public void SetLocale_UpdatesLocaleWhenCalledAgain()
{
var assetLibrary = CreateAssetLibrary();
assetLibrary.SetLocale("ar");
assetLibrary.SetLocale("en-us");

var urlQueriesField = typeof(AssetLibrary).GetField("UrlQueries",
BindingFlags.NonPublic | BindingFlags.Instance);
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(assetLibrary);
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
Assert.Equal("ar", urlQueries?["locale"]?.ToString());
}

#endregion

#region AddParam Tests
Expand Down
62 changes: 62 additions & 0 deletions Contentstack.Core.Unit.Tests/AssetUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,68 @@ public void AddParam_AddsQueryParameter()

#endregion

#region Asset Locale Tests (single-asset fetch with locale)

/// <summary>
/// Asset.SetLocale adds locale query param for single-asset fetch.
/// </summary>
[Fact]
public void SetLocale_AddsQueryParameter()
{
var asset = CreateAsset();
var locale = "en-us";

Asset result = asset.SetLocale(locale);

Assert.NotNull(result);
Assert.Same(asset, result);
var urlQueriesField = typeof(Asset).GetField("UrlQueries",
BindingFlags.NonPublic | BindingFlags.Instance);
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(asset);
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
Assert.Equal("en-us", urlQueries?["locale"]?.ToString());
}
/// <summary>
/// Asset localisation: AddParam("locale", "ar") adds locale query for single-asset fetch.
/// </summary>
[Fact]
public void AddParam_WithLocale_AddsLocaleQueryParameter()
{
var asset = CreateAsset();
var locale = "ar";

Asset result = asset.AddParam("locale", locale);

Assert.NotNull(result);
Assert.Same(asset, result);
var urlQueriesField = typeof(Asset).GetField("UrlQueries",
BindingFlags.NonPublic | BindingFlags.Instance);
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(asset);
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
Assert.Equal("ar", urlQueries?["locale"]?.ToString());
}

/// <summary>
/// Single-asset fetch: locale can be combined with include_fallback.
/// </summary>
[Fact]
public void AddParam_LocaleWithIncludeFallback_AddsBothQueryParameters()
{
var asset = CreateAsset();

asset.AddParam("locale", "en-us").IncludeFallback();

var urlQueriesField = typeof(Asset).GetField("UrlQueries",
BindingFlags.NonPublic | BindingFlags.Instance);
var urlQueries = (Dictionary<string, object>)urlQueriesField?.GetValue(asset);
Assert.True(urlQueries?.ContainsKey("locale") ?? false);
Assert.Equal("en-us", urlQueries?["locale"]?.ToString());
Assert.True(urlQueries?.ContainsKey("include_fallback") ?? false);
Assert.Equal("true", urlQueries?["include_fallback"]?.ToString());
}

#endregion

#region SetHeader and RemoveHeader Tests

[Fact]
Expand Down
28 changes: 28 additions & 0 deletions Contentstack.Core/Models/Asset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ public string Url
/// </summary>
public string Description { get; set; }

/// <summary>
/// Localized title of the asset (e.g. when fetched with SetLocale / asset localisation).
/// </summary>
[JsonProperty(PropertyName = "title")]
public string Title { get; set; }

/// <summary>
/// Set array of Tags
/// </summary>
Expand Down Expand Up @@ -319,6 +325,28 @@ public Asset AssetFields(params string[] fields)
return this;
}

/// <summary>
/// Sets the locale for fetching this asset. Returns the asset in the specified locale.
/// </summary>
/// <param name="locale">Locale code (e.g. "en-us", "ar").</param>
/// <returns>Current instance of Asset for chaining.</returns>
/// <example>
/// <code>
/// var asset = await stack.Asset(uid).SetLocale("en-us").Fetch();
/// </code>
/// </example>
public Asset SetLocale(string locale)
{
if (!string.IsNullOrEmpty(locale))
{
if (UrlQueries.ContainsKey("locale"))
UrlQueries["locale"] = locale;
else
UrlQueries.Add("locale", locale);
}
return this;
}


public void RemoveHeader(string key)
{
Expand Down
Loading