diff --git a/Contentstack.Core.Tests/Helpers/TestDataHelper.cs b/Contentstack.Core.Tests/Helpers/TestDataHelper.cs
index 9499df5..b1ea3b1 100644
--- a/Contentstack.Core.Tests/Helpers/TestDataHelper.cs
+++ b/Contentstack.Core.Tests/Helpers/TestDataHelper.cs
@@ -117,19 +117,49 @@ static TestDataHelper()
#endregion
#region Taxonomy
-
+
///
/// Gets the taxonomy term for USA state (e.g., "california")
///
- public static string TaxUsaState =>
+ public static string TaxUsaState =>
GetRequiredConfig("TAX_USA_STATE");
-
+
///
/// Gets the taxonomy term for India state (e.g., "maharashtra")
///
- public static string TaxIndiaState =>
+ public static string TaxIndiaState =>
GetRequiredConfig("TAX_INDIA_STATE");
-
+
+ ///
+ /// API key for the taxonomy-publish test stack (gadgets).
+ ///
+ public static string TaxPublishApiKey =>
+ GetRequiredConfig("TAX_PUBLISH_API_KEY");
+
+ ///
+ /// Delivery token for the taxonomy-publish test stack.
+ ///
+ public static string TaxPublishDeliveryToken =>
+ GetRequiredConfig("TAX_PUBLISH_DELIVERY_TOKEN");
+
+ ///
+ /// Environment for the taxonomy-publish test stack.
+ ///
+ public static string TaxPublishEnvironment =>
+ GetRequiredConfig("TAX_PUBLISH_ENVIRONMENT");
+
+ ///
+ /// UID of the published taxonomy to use in localization tests (e.g. "gadgets").
+ ///
+ public static string TaxPublishTaxonomyUid =>
+ GetRequiredConfig("TAX_PUBLISH_TAXONOMY_UID");
+
+ ///
+ /// Locale code used for localized taxonomy/term delivery tests (e.g. "hi-in").
+ ///
+ public static string TaxPublishLocale =>
+ GetRequiredConfig("TAX_PUBLISH_LOCALE");
+
#endregion
#region Live Preview
diff --git a/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationRunner.cs b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationRunner.cs
new file mode 100644
index 0000000..2d09a79
--- /dev/null
+++ b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationRunner.cs
@@ -0,0 +1,177 @@
+using System;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+using Contentstack.Core.Configuration;
+using Contentstack.Core.Models;
+using Contentstack.Core.Tests.Helpers;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Contentstack.Core.Tests.Integration.Taxonomy
+{
+ ///
+ /// End-to-end runner for all taxonomy localisation CDA calls.
+ /// Mirrors the TypeScript smoke-test script — runs every endpoint in sequence,
+ /// prints the SDK call, real HTTP request (URL + headers), and full JSON response.
+ /// Run with:
+ /// dotnet test --filter "FullyQualifiedName~TaxonomyLocalisationRunner" --logger "console;verbosity=detailed"
+ ///
+ [Trait("Category", "TaxonomyLocalisationRunner")]
+ public class TaxonomyLocalisationRunner
+ {
+ private readonly ITestOutputHelper _out;
+
+ private const string ApiKey = "blt168147f34138ebac";
+ private const string DeliveryToken = "cs91c6d782d3c9ca953c0a2685";
+ private const string Environment = "dev";
+ private const string TaxonomyUid = "gadgets";
+ private const string Locale = "hi-in";
+
+ public TaxonomyLocalisationRunner(ITestOutputHelper output)
+ {
+ _out = output;
+ }
+
+ private ContentstackClient CreateClient()
+ {
+ var options = new ContentstackOptions
+ {
+ ApiKey = ApiKey,
+ DeliveryToken = DeliveryToken,
+ Environment = Environment
+ };
+ return new ContentstackClient(options);
+ }
+
+ private void Section(int num, string title)
+ {
+ var line = new string('─', 60);
+ _out.WriteLine("");
+ _out.WriteLine(line);
+ _out.WriteLine($" {num}. {title}");
+ _out.WriteLine(line);
+ }
+
+ private void PrintResponse(string label, object data)
+ {
+ _out.WriteLine($"✅ {label}");
+ _out.WriteLine(JsonConvert.SerializeObject(data, Formatting.Indented));
+ }
+
+ private void PrintError(string label, Exception ex)
+ {
+ _out.WriteLine($"❌ {label}");
+ _out.WriteLine($" {ex.Message}");
+ }
+
+ [Fact(DisplayName = "TaxonomyLocalisationRunner - Full end-to-end run of all CDA localisation calls")]
+ public async Task Run_All_TaxonomyLocalisation_Calls()
+ {
+ var client = CreateClient();
+ string firstTermUid = null;
+
+ // ── 1. GET /v3/taxonomies/gadgets?environment=dev ─────────────────
+ Section(1, $"Fetch taxonomy uid={TaxonomyUid} (master locale)");
+ _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Fetch()");
+ try
+ {
+ var result = await client.Taxonomies(TaxonomyUid).Fetch();
+ PrintResponse($"GET /taxonomies/{TaxonomyUid}", result);
+ }
+ catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}", ex); }
+
+ // ── 2. GET /v3/taxonomies/gadgets?environment=dev&locale=hi-in ────
+ Section(2, $"Fetch taxonomy uid={TaxonomyUid} locale={Locale}");
+ _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Fetch(\"{Locale}\")");
+ try
+ {
+ var result = await client.Taxonomies(TaxonomyUid).Fetch(Locale);
+ PrintResponse($"GET /taxonomies/{TaxonomyUid}?locale={Locale}", result);
+ }
+ catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}?locale={Locale}", ex); }
+
+ // ── 3. GET /v3/taxonomies/gadgets/terms?environment=dev&locale=hi-in
+ Section(3, $"Find terms taxonomy={TaxonomyUid} locale={Locale}");
+ _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Terms().SetLocale(\"{Locale}\").Find()");
+ try
+ {
+ var result = await client.Taxonomies(TaxonomyUid).Terms()
+ .SetLocale(Locale)
+ .Find();
+ PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms?locale={Locale}", result);
+ }
+ catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms?locale={Locale}", ex); }
+
+ // ── 4. GET /v3/taxonomies/gadgets/terms?locale=hi-in&include_fallback=true
+ Section(4, $"Find terms taxonomy={TaxonomyUid} locale={Locale} include_fallback=true");
+ _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Terms().SetLocale(\"{Locale}\").IncludeFallback().Find()");
+ try
+ {
+ var result = await client.Taxonomies(TaxonomyUid).Terms()
+ .SetLocale(Locale)
+ .IncludeFallback()
+ .Find();
+ PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms?locale={Locale}&include_fallback=true", result);
+
+ // Grab first term uid for subsequent calls
+ var terms = result?.Items;
+ foreach (var t in terms ?? System.Array.Empty())
+ {
+ firstTermUid = t?["uid"]?.ToString();
+ if (!string.IsNullOrEmpty(firstTermUid)) break;
+ }
+ }
+ catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms with fallback", ex); }
+
+ if (string.IsNullOrEmpty(firstTermUid))
+ {
+ _out.WriteLine("");
+ _out.WriteLine("⚠️ No term UID found — skipping single-term calls.");
+ return;
+ }
+
+ _out.WriteLine($"\n📌 Using term uid = \"{firstTermUid}\" for single-term calls.");
+
+ // ── 5. GET /v3/taxonomies/gadgets/terms/:uid?locale=hi-in ─────────
+ Section(5, $"Fetch single term uid={firstTermUid} locale={Locale}");
+ _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Term(\"{firstTermUid}\").Fetch(\"{Locale}\")");
+ try
+ {
+ var result = await client.Taxonomies(TaxonomyUid).Term(firstTermUid).Fetch(Locale);
+ PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}?locale={Locale}", result);
+ }
+ catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}?locale={Locale}", ex); }
+
+ // ── 6. GET /v3/taxonomies/gadgets/terms/:uid/locales ──────────────
+ Section(6, $"Term locales uid={firstTermUid}");
+ _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Term(\"{firstTermUid}\").Locales()");
+ try
+ {
+ var result = await client.Taxonomies(TaxonomyUid).Term(firstTermUid).Locales();
+ PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/locales", result);
+ }
+ catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/locales", ex); }
+
+ // ── 7. GET /v3/taxonomies/gadgets/terms/:uid/ancestors ────────────
+ Section(7, $"Term ancestors uid={firstTermUid}");
+ _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Term(\"{firstTermUid}\").Ancestors()");
+ try
+ {
+ var result = await client.Taxonomies(TaxonomyUid).Term(firstTermUid).Ancestors();
+ PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/ancestors", result);
+ }
+ catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/ancestors", ex); }
+
+ // ── 8. GET /v3/taxonomies/gadgets/terms/:uid/descendants ──────────
+ Section(8, $"Term descendants uid={firstTermUid}");
+ _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Term(\"{firstTermUid}\").Descendants()");
+ try
+ {
+ var result = await client.Taxonomies(TaxonomyUid).Term(firstTermUid).Descendants();
+ PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/descendants", result);
+ }
+ catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/descendants", ex); }
+ }
+ }
+}
diff --git a/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationTest.cs b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationTest.cs
new file mode 100644
index 0000000..1b768ef
--- /dev/null
+++ b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationTest.cs
@@ -0,0 +1,272 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+using Contentstack.Core.Configuration;
+using Contentstack.Core.Models;
+using Contentstack.Core.Internals;
+using Contentstack.Core.Tests.Helpers;
+
+namespace Contentstack.Core.Tests.Integration.Taxonomy
+{
+ ///
+ /// Integration tests for localized taxonomy and term delivery (CDA).
+ /// Covers: Taxonomy.Fetch(locale), TermQuery.SetLocale/IncludeFallback/Find,
+ /// Term.Fetch(locale), Term.Locales, Term.Ancestors, Term.Descendants.
+ /// Stack: gadgets taxonomy, locales en-us / hi-in.
+ ///
+ [Trait("Category", "TaxonomyLocalisation")]
+ public class TaxonomyLocalisationTest : IntegrationTestBase
+ {
+ public TaxonomyLocalisationTest(ITestOutputHelper output) : base(output) { }
+
+ ///
+ /// Creates a client scoped to the gadgets taxonomy-publish test stack.
+ /// Uses the default CDN host (no custom host override needed).
+ ///
+ private ContentstackClient CreateGadgetsClient()
+ {
+ var options = new ContentstackOptions
+ {
+ ApiKey = TestDataHelper.TaxPublishApiKey,
+ DeliveryToken = TestDataHelper.TaxPublishDeliveryToken,
+ Environment = TestDataHelper.TaxPublishEnvironment
+ };
+ var client = new ContentstackClient(options);
+ client.Plugins.Add(new RequestLoggingPlugin(TestOutput));
+ return client;
+ }
+
+ // ── 1. Fetch localized taxonomy ──────────────────────────────────────
+
+ [Fact(DisplayName = "TaxPublish - Fetch taxonomy returns object with uid and name")]
+ public async Task Fetch_Taxonomy_MasterLocale_ReturnsValidObject()
+ {
+ LogArrange("Fetching taxonomy without locale (master)");
+ LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid);
+
+ var client = CreateGadgetsClient();
+
+ LogAct("Calling Taxonomies(uid).Fetch()");
+ var result = await client
+ .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid)
+ .Fetch();
+
+ LogAssert("Verifying response");
+ Assert.NotNull(result);
+ Assert.NotNull(result["uid"]?.ToString());
+ Assert.Equal(TestDataHelper.TaxPublishTaxonomyUid, result["uid"]?.ToString());
+ }
+
+ [Fact(DisplayName = "TaxPublish - Fetch taxonomy with locale returns localized name")]
+ public async Task Fetch_Taxonomy_WithLocale_ReturnsLocalizedName()
+ {
+ LogArrange("Fetching taxonomy with locale");
+ LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid);
+ LogContext("Locale", TestDataHelper.TaxPublishLocale);
+
+ var client = CreateGadgetsClient();
+
+ LogAct("Calling Taxonomies(uid).Fetch(locale)");
+ var result = await client
+ .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid)
+ .Fetch(TestDataHelper.TaxPublishLocale);
+
+ LogAssert("Verifying response");
+ Assert.NotNull(result);
+ Assert.NotNull(result["uid"]?.ToString());
+ // The name should differ from the master locale (en-us) value
+ Assert.NotNull(result["name"]?.ToString());
+ }
+
+ // ── 2. Find all taxonomies ────────────────────────────────────────────
+
+ [Fact(DisplayName = "TaxPublish - Find all taxonomies returns non-empty collection")]
+ public async Task Find_AllTaxonomies_ReturnsCollection()
+ {
+ LogArrange("Fetching all published taxonomies");
+
+ var client = CreateGadgetsClient();
+
+ LogAct("Calling Taxonomies().Find()");
+ var result = await client.Taxonomies().Find();
+
+ LogAssert("Verifying response");
+ Assert.NotNull(result);
+ Assert.NotNull(result.Items);
+ }
+
+ // ── 3. Find terms with locale ─────────────────────────────────────────
+
+ [Fact(DisplayName = "TaxPublish - Find terms with locale returns localized terms")]
+ public async Task Find_Terms_WithLocale_ReturnsLocalizedTerms()
+ {
+ LogArrange("Finding terms with locale");
+ LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid);
+ LogContext("Locale", TestDataHelper.TaxPublishLocale);
+
+ var client = CreateGadgetsClient();
+
+ LogAct("Calling Terms().SetLocale(locale).Find()");
+ var result = await client
+ .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid)
+ .Terms()
+ .SetLocale(TestDataHelper.TaxPublishLocale)
+ .Find();
+
+ LogAssert("Verifying response");
+ Assert.NotNull(result);
+ Assert.NotNull(result.Items);
+ }
+
+ [Fact(DisplayName = "TaxPublish - Find terms with locale and fallback returns terms")]
+ public async Task Find_Terms_WithLocaleAndFallback_ReturnsTerms()
+ {
+ LogArrange("Finding terms with locale and include_fallback");
+ LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid);
+ LogContext("Locale", TestDataHelper.TaxPublishLocale);
+
+ var client = CreateGadgetsClient();
+
+ LogAct("Calling Terms().SetLocale(locale).IncludeFallback().Find()");
+ var result = await client
+ .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid)
+ .Terms()
+ .SetLocale(TestDataHelper.TaxPublishLocale)
+ .IncludeFallback()
+ .Find();
+
+ LogAssert("Verifying response");
+ Assert.NotNull(result);
+ Assert.NotNull(result.Items);
+ // Fallback means we should get at least as many terms as without fallback
+ Assert.True(result.Items.Any());
+ }
+
+ // ── 4–7. Single term methods ──────────────────────────────────────────
+
+ ///
+ /// Fetches the first available term UID from the gadgets taxonomy to use in subsequent tests.
+ ///
+ private async Task GetFirstTermUidAsync(ContentstackClient client)
+ {
+ var terms = await client
+ .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid)
+ .Terms()
+ .SetLocale(TestDataHelper.TaxPublishLocale)
+ .IncludeFallback()
+ .Find();
+
+ return terms?.Items?.FirstOrDefault()?["uid"]?.ToString();
+ }
+
+ [Fact(DisplayName = "TaxPublish - Fetch single term with locale returns localized term")]
+ public async Task Fetch_SingleTerm_WithLocale_ReturnsLocalizedTerm()
+ {
+ var client = CreateGadgetsClient();
+ var termUid = await GetFirstTermUidAsync(client);
+
+ if (string.IsNullOrEmpty(termUid))
+ {
+ Output.WriteLine("No term UID found — skipping test.");
+ return;
+ }
+
+ LogArrange("Fetching single term with locale");
+ LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid);
+ LogContext("TermUid", termUid);
+ LogContext("Locale", TestDataHelper.TaxPublishLocale);
+
+ LogAct("Calling Term(termUid).Fetch(locale)");
+ var result = await client
+ .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid)
+ .Term(termUid)
+ .Fetch(TestDataHelper.TaxPublishLocale);
+
+ LogAssert("Verifying response");
+ Assert.NotNull(result);
+ Assert.NotNull(result["uid"]?.ToString());
+ Assert.Equal(termUid, result["uid"]?.ToString());
+ }
+
+ [Fact(DisplayName = "TaxPublish - Term.Locales returns locales collection")]
+ public async Task Term_Locales_ReturnsLocalesCollection()
+ {
+ var client = CreateGadgetsClient();
+ var termUid = await GetFirstTermUidAsync(client);
+
+ if (string.IsNullOrEmpty(termUid))
+ {
+ Output.WriteLine("No term UID found — skipping test.");
+ return;
+ }
+
+ LogArrange("Fetching all locales for a term");
+ LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid);
+ LogContext("TermUid", termUid);
+
+ LogAct("Calling Term(termUid).Locales()");
+ var result = await client
+ .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid)
+ .Term(termUid)
+ .Locales();
+
+ LogAssert("Verifying response");
+ Assert.NotNull(result);
+ }
+
+ [Fact(DisplayName = "TaxPublish - Term.Ancestors returns ancestors collection")]
+ public async Task Term_Ancestors_ReturnsAncestorsCollection()
+ {
+ var client = CreateGadgetsClient();
+ var termUid = await GetFirstTermUidAsync(client);
+
+ if (string.IsNullOrEmpty(termUid))
+ {
+ Output.WriteLine("No term UID found — skipping test.");
+ return;
+ }
+
+ LogArrange("Fetching ancestors for a term");
+ LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid);
+ LogContext("TermUid", termUid);
+
+ LogAct("Calling Term(termUid).Ancestors()");
+ var result = await client
+ .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid)
+ .Term(termUid)
+ .Ancestors();
+
+ LogAssert("Verifying response");
+ Assert.NotNull(result);
+ }
+
+ [Fact(DisplayName = "TaxPublish - Term.Descendants returns descendants collection")]
+ public async Task Term_Descendants_ReturnsDescendantsCollection()
+ {
+ var client = CreateGadgetsClient();
+ var termUid = await GetFirstTermUidAsync(client);
+
+ if (string.IsNullOrEmpty(termUid))
+ {
+ Output.WriteLine("No term UID found — skipping test.");
+ return;
+ }
+
+ LogArrange("Fetching descendants for a term");
+ LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid);
+ LogContext("TermUid", termUid);
+
+ LogAct("Calling Term(termUid).Descendants()");
+ var result = await client
+ .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid)
+ .Term(termUid)
+ .Descendants();
+
+ LogAssert("Verifying response");
+ Assert.NotNull(result);
+ }
+ }
+}
diff --git a/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs b/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs
index f4f5e92..ac641d7 100644
--- a/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs
+++ b/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs
@@ -563,6 +563,169 @@ public void Below_WithZeroValue_AddsQueryParameter()
}
#endregion
+
+ #region Taxonomy UID Constructor Tests
+
+ [Fact]
+ public void Taxonomy_WithUid_DoesNotThrow()
+ {
+ var taxonomy = _client.Taxonomies("gadgets");
+ Assert.NotNull(taxonomy);
+ }
+
+ [Fact]
+ public void Taxonomy_WithNullUid_ThrowsTaxonomyException()
+ {
+ Assert.Throws(() => _client.Taxonomies(null));
+ }
+
+ [Fact]
+ public void Taxonomy_WithEmptyUid_ThrowsTaxonomyException()
+ {
+ Assert.Throws(() => _client.Taxonomies(string.Empty));
+ }
+
+ #endregion
+
+ #region Taxonomy.Term() Tests
+
+ [Fact]
+ public void Taxonomy_TermWithUid_ReturnsTaxonomyInstance()
+ {
+ var taxonomy = _client.Taxonomies("gadgets");
+ var term = taxonomy.Term("smartwatch");
+ Assert.NotNull(term);
+ Assert.IsType(term);
+ }
+
+ [Fact]
+ public void Taxonomy_TermWithoutUid_ThrowsTaxonomyException()
+ {
+ var taxonomy = _client.Taxonomies();
+ Assert.Throws(() => taxonomy.Term("smartwatch"));
+ }
+
+ #endregion
+
+ #region Taxonomy.Terms() Tests
+
+ [Fact]
+ public void Taxonomy_Terms_ReturnsTermQueryInstance()
+ {
+ var taxonomy = _client.Taxonomies("gadgets");
+ var termQuery = taxonomy.Terms();
+ Assert.NotNull(termQuery);
+ Assert.IsType(termQuery);
+ }
+
+ [Fact]
+ public void Taxonomy_Terms_WithoutUid_ThrowsTaxonomyException()
+ {
+ var taxonomy = _client.Taxonomies();
+ Assert.Throws(() => taxonomy.Terms());
+ }
+
+ #endregion
+
+ #region TermQuery Tests
+
+ [Fact]
+ public void TermQuery_SetLocale_ReturnsSelfForChaining()
+ {
+ var termQuery = _client.Taxonomies("gadgets").Terms();
+ var result = termQuery.SetLocale("hi-in");
+ Assert.Same(termQuery, result);
+ }
+
+ [Fact]
+ public void TermQuery_SetLocale_SetsLocaleParam()
+ {
+ var termQuery = _client.Taxonomies("gadgets").Terms();
+ termQuery.SetLocale("hi-in");
+
+ var field = typeof(TermQuery).GetField("_queryParams",
+ BindingFlags.NonPublic | BindingFlags.Instance);
+ var queryParams = (Dictionary)field?.GetValue(termQuery);
+
+ Assert.True(queryParams?.ContainsKey("locale") ?? false);
+ Assert.Equal("hi-in", queryParams["locale"]);
+ }
+
+ [Fact]
+ public void TermQuery_SetLocale_WithNull_ThrowsTaxonomyException()
+ {
+ var termQuery = _client.Taxonomies("gadgets").Terms();
+ Assert.Throws(() => termQuery.SetLocale(null));
+ }
+
+ [Fact]
+ public void TermQuery_IncludeFallback_ReturnsSelfForChaining()
+ {
+ var termQuery = _client.Taxonomies("gadgets").Terms();
+ var result = termQuery.IncludeFallback();
+ Assert.Same(termQuery, result);
+ }
+
+ [Fact]
+ public void TermQuery_IncludeFallback_SetsParam()
+ {
+ var termQuery = _client.Taxonomies("gadgets").Terms();
+ termQuery.IncludeFallback();
+
+ var field = typeof(TermQuery).GetField("_queryParams",
+ BindingFlags.NonPublic | BindingFlags.Instance);
+ var queryParams = (Dictionary)field?.GetValue(termQuery);
+
+ Assert.True(queryParams?.ContainsKey("include_fallback") ?? false);
+ Assert.Equal("true", queryParams["include_fallback"]);
+ }
+
+ [Fact]
+ public void TermQuery_SetLocale_Then_IncludeFallback_ChainsBoth()
+ {
+ var termQuery = _client.Taxonomies("gadgets").Terms()
+ .SetLocale("hi-in")
+ .IncludeFallback();
+
+ var field = typeof(TermQuery).GetField("_queryParams",
+ BindingFlags.NonPublic | BindingFlags.Instance);
+ var queryParams = (Dictionary)field?.GetValue(termQuery);
+
+ Assert.True(queryParams?.ContainsKey("locale") ?? false);
+ Assert.True(queryParams?.ContainsKey("include_fallback") ?? false);
+ }
+
+ #endregion
+
+ #region Term Constructor Validation Tests
+
+ [Fact]
+ public void Term_WithNullTaxonomyUid_ThrowsTaxonomyException()
+ {
+ Assert.Throws(() =>
+ {
+ var t = typeof(Term)
+ .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null,
+ new[] { typeof(ContentstackClient), typeof(string), typeof(string) }, null);
+ try { t?.Invoke(new object[] { _client, null, "smartwatch" }); }
+ catch (System.Reflection.TargetInvocationException ex) { throw ex.InnerException; }
+ });
+ }
+
+ [Fact]
+ public void Term_WithNullTermUid_ThrowsTaxonomyException()
+ {
+ Assert.Throws(() =>
+ {
+ var t = typeof(Term)
+ .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null,
+ new[] { typeof(ContentstackClient), typeof(string), typeof(string) }, null);
+ try { t?.Invoke(new object[] { _client, "gadgets", null }); }
+ catch (System.Reflection.TargetInvocationException ex) { throw ex.InnerException; }
+ });
+ }
+
+ #endregion
}
}
diff --git a/Contentstack.Core/ContentstackClient.cs b/Contentstack.Core/ContentstackClient.cs
index 3e2b390..f428528 100644
--- a/Contentstack.Core/ContentstackClient.cs
+++ b/Contentstack.Core/ContentstackClient.cs
@@ -491,6 +491,25 @@ public Taxonomy Taxonomies()
return tx;
}
+ ///
+ /// Returns a instance scoped to the given taxonomy UID,
+ /// providing access to published taxonomy data, terms, and locale-aware delivery via the CDA.
+ ///
+ /// The UID of the published taxonomy.
+ /// A instance scoped to the given UID.
+ ///
+ ///
+ /// ContentstackClient stack = new ContentstackClient("api_key", "delivery_token", "environment");
+ /// var taxonomy = await stack.Taxonomies("gadgets").Fetch<MyTaxonomy>();
+ /// var terms = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").Find<MyTerm>();
+ /// var term = await stack.Taxonomies("gadgets").Term("smartwatch").Fetch<MyTerm>("mr-in");
+ ///
+ ///
+ public Taxonomy Taxonomies(string uid)
+ {
+ return new Taxonomy(this, uid);
+ }
+
///
/// Get version.
///
diff --git a/Contentstack.Core/Models/Taxonomy.cs b/Contentstack.Core/Models/Taxonomy.cs
index 667915d..ba17794 100644
--- a/Contentstack.Core/Models/Taxonomy.cs
+++ b/Contentstack.Core/Models/Taxonomy.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Net;
+using System.Threading.Tasks;
using Contentstack.Core.Configuration;
using Contentstack.Core.Internals;
namespace Contentstack.Core.Models
@@ -14,6 +15,7 @@ public class Taxonomy: Query
private Dictionary _Headers = new Dictionary();
private Dictionary _StackHeaders = new Dictionary();
private Dictionary UrlQueries = new Dictionary();
+ private string _uid = null;
protected override string _Url
{
@@ -28,6 +30,8 @@ protected override string _Url
throw new TaxonomyException("Taxonomy Stack Config is null. Please ensure the ContentstackClient is properly configured.");
}
Config config = this.Stack.Config;
+ if (_uid != null)
+ return String.Format("{0}/taxonomies/{1}", config.BaseUrl, _uid);
return String.Format("{0}/taxonomies/entries", config.BaseUrl);
}
}
@@ -54,9 +58,92 @@ internal Taxonomy(ContentstackClient stack): base(stack)
this._StackHeaders = stack._LocalHeaders;
}
+ internal Taxonomy(ContentstackClient stack, string uid) : this(stack)
+ {
+ if (string.IsNullOrEmpty(uid))
+ throw new TaxonomyException("Taxonomy UID cannot be null or empty.");
+ _uid = uid;
+ }
+
#endregion
#region Public Functions
+ ///
+ /// Returns a Term instance for the given term UID within this taxonomy.
+ /// Requires the Taxonomy to be initialised with a UID via client.Taxonomies("uid").
+ ///
+ /// The UID of the term to retrieve.
+ /// A instance scoped to this taxonomy and term.
+ public Term Term(string termUid)
+ {
+ if (_uid == null)
+ throw new TaxonomyException("Term() requires a taxonomy UID. Use client.Taxonomies(\"uid\") to scope to a specific taxonomy.");
+ return new Term(Stack, _uid, termUid);
+ }
+
+ ///
+ /// Returns a TermQuery for listing all published terms within this taxonomy.
+ /// Requires the Taxonomy to be initialised with a UID via client.Taxonomies("uid").
+ ///
+ /// A instance for this taxonomy.
+ public TermQuery Terms()
+ {
+ if (_uid == null)
+ throw new TaxonomyException("Terms() requires a taxonomy UID. Use client.Taxonomies(\"uid\") to scope to a specific taxonomy.");
+ return new TermQuery(Stack, _uid);
+ }
+
+ ///
+ /// Fetches the published taxonomy by its UID from the CDA.
+ /// Requires the Taxonomy to be initialised with a UID via client.Taxonomies("uid").
+ ///
+ /// Optional locale code (e.g. "hi-in"). Omit for the master locale.
+ /// The deserialized taxonomy object.
+ ///
+ ///
+ /// ContentstackClient stack = new ContentstackClient("api_key", "delivery_token", "environment");
+ /// var taxonomy = await stack.Taxonomies("gadgets").Fetch<MyTaxonomy>();
+ /// var localized = await stack.Taxonomies("gadgets").Fetch<MyTaxonomy>("hi-in");
+ ///
+ ///
+ public async System.Threading.Tasks.Task Fetch(string locale = null)
+ {
+ if (_uid == null)
+ throw new TaxonomyException("Fetch() requires a taxonomy UID. Use client.Taxonomies(\"uid\") to scope to a specific taxonomy.");
+
+ try
+ {
+ var headerAll = new Dictionary();
+ foreach (var header in Stack._LocalHeaders)
+ headerAll[header.Key] = header.Value;
+
+ var mainJson = new Dictionary();
+ if (Stack.Config?.Environment != null)
+ mainJson["environment"] = Stack.Config.Environment;
+ if (!string.IsNullOrEmpty(locale))
+ mainJson["locale"] = locale;
+
+ var handler = new HttpRequestHandler(Stack);
+ var branch = Stack.Config?.Branch ?? "main";
+ var result = await handler.ProcessRequest(
+ _Url, headerAll, mainJson,
+ Branch: branch,
+ timeout: Stack.Config.Timeout,
+ proxy: Stack.Config.Proxy
+ );
+
+ var jObject = Newtonsoft.Json.Linq.JObject.Parse(result);
+ var token = jObject.SelectToken("$.taxonomy");
+ if (token != null)
+ return token.ToObject(Stack.Serializer);
+ return jObject.ToObject(Stack.Serializer);
+ }
+ catch (Exception ex)
+ {
+ throw TaxonomyException.CreateForProcessingError(ex);
+ }
+ }
+
///
/// Add a constraint to the query that requires a particular key entry to be less than the provided value.
///
diff --git a/Contentstack.Core/Models/Term.cs b/Contentstack.Core/Models/Term.cs
new file mode 100644
index 0000000..d45864f
--- /dev/null
+++ b/Contentstack.Core/Models/Term.cs
@@ -0,0 +1,184 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Contentstack.Core.Internals;
+using Newtonsoft.Json.Linq;
+
+namespace Contentstack.Core.Models
+{
+ ///
+ /// Represents a single published term within a taxonomy, providing methods to fetch
+ /// the term, its localized versions, ancestors, and descendants from the CDA.
+ ///
+ public class Term
+ {
+ private readonly ContentstackClient _stack;
+ private readonly string _taxonomyUid;
+ private readonly string _termUid;
+
+ private string BaseUrlPath =>
+ $"{_stack.Config.BaseUrl}/taxonomies/{_taxonomyUid}/terms/{_termUid}";
+
+ internal Term(ContentstackClient stack, string taxonomyUid, string termUid)
+ {
+ _stack = stack ?? throw new TaxonomyException("ContentstackClient cannot be null when creating a Term instance.");
+ if (string.IsNullOrEmpty(taxonomyUid)) throw new TaxonomyException("taxonomyUid cannot be null or empty.");
+ if (string.IsNullOrEmpty(termUid)) throw new TaxonomyException("termUid cannot be null or empty.");
+ _taxonomyUid = taxonomyUid;
+ _termUid = termUid;
+ }
+
+ ///
+ /// Fetches the published term from the CDA.
+ ///
+ /// Optional locale code (e.g. "mr-in"). Omit for the master locale.
+ /// The deserialized term object.
+ ///
+ ///
+ /// var term = await stack.Taxonomies("gadgets").Term("smartwatch").Fetch<MyTerm>();
+ /// var localizedTerm = await stack.Taxonomies("gadgets").Term("smartwatch").Fetch<MyTerm>("mr-in");
+ ///
+ ///
+ public async Task Fetch(string locale = null)
+ {
+ try
+ {
+ var queryParams = new Dictionary();
+ if (!string.IsNullOrEmpty(locale))
+ queryParams["locale"] = locale;
+
+ var result = await ExecuteRequest(BaseUrlPath, queryParams);
+ var jObject = JObject.Parse(result);
+ var token = jObject.SelectToken("$.term");
+ if (token != null)
+ return token.ToObject(_stack.Serializer);
+ return jObject.ToObject(_stack.Serializer);
+ }
+ catch (TaxonomyException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw TaxonomyException.CreateForProcessingError(ex);
+ }
+ }
+
+ ///
+ /// Fetches all published, localized versions of this term across every locale.
+ /// Maps to GET /taxonomies/{uid}/terms/{termUid}/locales.
+ ///
+ /// The deserialized locales collection.
+ ///
+ ///
+ /// var locales = await stack.Taxonomies("gadgets").Term("smartwatch").Locales<MyLocales>();
+ ///
+ ///
+ public async Task Locales()
+ {
+ try
+ {
+ var result = await ExecuteRequest($"{BaseUrlPath}/locales");
+ var jObject = JObject.Parse(result);
+ var token = jObject.SelectToken("$.locales");
+ if (token != null)
+ return token.ToObject(_stack.Serializer);
+ return jObject.ToObject(_stack.Serializer);
+ }
+ catch (TaxonomyException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw TaxonomyException.CreateForProcessingError(ex);
+ }
+ }
+
+ ///
+ /// Fetches all ancestors of this term up to the root.
+ /// Maps to GET /taxonomies/{uid}/terms/{termUid}/ancestors.
+ ///
+ /// The deserialized ancestors collection.
+ ///
+ ///
+ /// var ancestors = await stack.Taxonomies("gadgets").Term("smartwatch").Ancestors<MyTerms>();
+ ///
+ ///
+ public async Task Ancestors()
+ {
+ try
+ {
+ var result = await ExecuteRequest($"{BaseUrlPath}/ancestors");
+ var jObject = JObject.Parse(result);
+ var token = jObject.SelectToken("$.ancestors");
+ if (token != null)
+ return token.ToObject(_stack.Serializer);
+ return jObject.ToObject(_stack.Serializer);
+ }
+ catch (TaxonomyException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw TaxonomyException.CreateForProcessingError(ex);
+ }
+ }
+
+ ///
+ /// Fetches all descendants of this term.
+ /// Maps to GET /taxonomies/{uid}/terms/{termUid}/descendants.
+ ///
+ /// The deserialized descendants collection.
+ ///
+ ///
+ /// var descendants = await stack.Taxonomies("gadgets").Term("smartwatch").Descendants<MyTerms>();
+ ///
+ ///
+ public async Task Descendants()
+ {
+ try
+ {
+ var result = await ExecuteRequest($"{BaseUrlPath}/descendants");
+ var jObject = JObject.Parse(result);
+ var token = jObject.SelectToken("$.descendants");
+ if (token != null)
+ return token.ToObject(_stack.Serializer);
+ return jObject.ToObject(_stack.Serializer);
+ }
+ catch (TaxonomyException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw TaxonomyException.CreateForProcessingError(ex);
+ }
+ }
+
+ private async Task ExecuteRequest(string url, Dictionary extraParams = null)
+ {
+ var headerAll = new Dictionary();
+ foreach (var header in _stack._LocalHeaders)
+ headerAll[header.Key] = header.Value;
+
+ var mainJson = new Dictionary();
+ if (_stack.Config?.Environment != null)
+ mainJson["environment"] = _stack.Config.Environment;
+
+ if (extraParams != null)
+ foreach (var param in extraParams)
+ mainJson[param.Key] = param.Value;
+
+ var handler = new HttpRequestHandler(_stack);
+ var branch = _stack.Config?.Branch ?? "main";
+ return await handler.ProcessRequest(
+ url, headerAll, mainJson,
+ Branch: branch,
+ timeout: _stack.Config.Timeout,
+ proxy: _stack.Config.Proxy
+ );
+ }
+ }
+}
diff --git a/Contentstack.Core/Models/TermQuery.cs b/Contentstack.Core/Models/TermQuery.cs
new file mode 100644
index 0000000..d1d0876
--- /dev/null
+++ b/Contentstack.Core/Models/TermQuery.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Contentstack.Core.Internals;
+using Newtonsoft.Json.Linq;
+
+namespace Contentstack.Core.Models
+{
+ ///
+ /// Provides a fluent query builder for listing published terms within a taxonomy from the CDA.
+ /// Supports locale filtering and master-locale fallback.
+ ///
+ public class TermQuery
+ {
+ private readonly ContentstackClient _stack;
+ private readonly string _taxonomyUid;
+ private readonly Dictionary _queryParams = new Dictionary();
+
+ private string Url =>
+ $"{_stack.Config.BaseUrl}/taxonomies/{_taxonomyUid}/terms";
+
+ internal TermQuery(ContentstackClient stack, string taxonomyUid)
+ {
+ _stack = stack ?? throw new TaxonomyException("ContentstackClient cannot be null when creating a TermQuery instance.");
+ if (string.IsNullOrEmpty(taxonomyUid)) throw new TaxonomyException("taxonomyUid cannot be null or empty.");
+ _taxonomyUid = taxonomyUid;
+ }
+
+ ///
+ /// Filters terms to those published in the specified locale.
+ ///
+ /// The locale code (e.g. "hi-in", "en-us").
+ /// The current for chaining.
+ ///
+ ///
+ /// var terms = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").Find<MyTerm>();
+ ///
+ ///
+ public TermQuery SetLocale(string locale)
+ {
+ if (string.IsNullOrEmpty(locale))
+ throw new TaxonomyException("Locale cannot be null or empty.");
+ _queryParams["locale"] = locale;
+ return this;
+ }
+
+ ///
+ /// When a term is not localized in the requested locale, falls back to the master locale.
+ /// Must be used together with .
+ ///
+ /// The current for chaining.
+ ///
+ ///
+ /// var terms = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").IncludeFallback().Find<MyTerm>();
+ ///
+ ///
+ public TermQuery IncludeFallback()
+ {
+ _queryParams["include_fallback"] = "true";
+ return this;
+ }
+
+ ///
+ /// Executes the query and returns all matching published terms.
+ /// Maps to GET /taxonomies/{uid}/terms with any configured locale and fallback params.
+ ///
+ /// A containing the matched terms.
+ ///
+ ///
+ /// var result = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").IncludeFallback().Find<MyTerm>();
+ /// foreach (var term in result) { ... }
+ ///
+ ///
+ public async Task> Find()
+ {
+ try
+ {
+ var headerAll = new Dictionary();
+ foreach (var header in _stack._LocalHeaders)
+ headerAll[header.Key] = header.Value;
+
+ var mainJson = new Dictionary();
+ if (_stack.Config?.Environment != null)
+ mainJson["environment"] = _stack.Config.Environment;
+
+ foreach (var param in _queryParams)
+ mainJson[param.Key] = param.Value;
+
+ var handler = new HttpRequestHandler(_stack);
+ var branch = _stack.Config?.Branch ?? "main";
+ var result = await handler.ProcessRequest(
+ Url, headerAll, mainJson,
+ Branch: branch,
+ timeout: _stack.Config.Timeout,
+ proxy: _stack.Config.Proxy
+ );
+
+ var jObject = JObject.Parse(result);
+ var terms = jObject.SelectToken("$.terms")?.ToObject>(_stack.Serializer);
+ var collection = jObject.ToObject>(_stack.Serializer);
+ collection.Items = terms ?? new List();
+ return collection;
+ }
+ catch (TaxonomyException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw TaxonomyException.CreateForProcessingError(ex);
+ }
+ }
+ }
+}