From 76ee7b81a4c23c6d290c4cad3f77bb4b55558a0c Mon Sep 17 00:00:00 2001 From: OMpawar-21 Date: Wed, 10 Jun 2026 17:16:01 +0530 Subject: [PATCH 1/2] feat: add multi-region endpoint resolution with self-heal regions registry --- .gitignore | 3 +- CHANGELOG.md | 6 + Contentstack.Utils.Tests/EndpointTest.cs | 263 +++++++++++++++++ Contentstack.Utils/Contentstack.Utils.csproj | 20 +- Contentstack.Utils/Endpoints/Endpoint.cs | 287 +++++++++++++++++++ Contentstack.Utils/Utils.cs | 14 + Scripts/refresh-region.cs | 77 +++++ build/contentstack.utils.targets | 19 ++ 8 files changed, 687 insertions(+), 2 deletions(-) create mode 100644 Contentstack.Utils.Tests/EndpointTest.cs create mode 100644 Contentstack.Utils/Endpoints/Endpoint.cs create mode 100644 Scripts/refresh-region.cs create mode 100644 build/contentstack.utils.targets diff --git a/.gitignore b/.gitignore index 000500d..353cc9c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ packages/ *.trx */TestResults/ */app.config -.dccache \ No newline at end of file +.dccache +*/Assets/regions.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fab7d75..58e7522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ - JSON serialization uses the same model attributes with `System.Text.Json.Serialization` (`JsonPropertyName`, `JsonConverter`), including custom converters for RTE/GQL-shaped JSON and **path-mapped** embedded models (`PathMappedJsonConverter`). - RTE JSON deserialization tolerates **trailing commas** when using the documented test/helper patterns (`AllowTrailingCommas`); attribute dictionaries may surface **`JsonElement`** values instead of boxed strings—use helpers or unwrap explicitly if you access `Node.attrs` directly. - Internal: `LangVersion` set to **latest** for multi-target builds; utilities normalize attribute values where the HTML pipeline expects strings. +- **New:** Multi-region endpoint resolution via `Endpoint.GetContentstackEndpoint(region, service)` — resolves Contentstack service URLs for all 7 supported regions (NA, EU, AU, Azure-NA, Azure-EU, GCP-NA, GCP-EU) and 18 service keys (contentDelivery, contentManagement, auth, graphqlDelivery, preview, images, assets, automate, launch, developerHub, brandKit, genAI, personalizeManagement, personalizeEdge, composableStudio, assetManagement, and more). +- **New:** `Utils.GetContentstackEndpoint(region, service)` proxy — access endpoint resolution directly from the `Utils` class without importing `Contentstack.Utils.Endpoints`. +- **New:** `omitHttps` flag strips the `https://` scheme from returned URLs — pass directly to SDK host configuration (e.g. `new ContentstackOptions { Host = Endpoint.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true) }`). +- **New:** Case-insensitive region alias support — `"us"`, `"NA"`, `"AWS-NA"`, `"azure_na"` all resolve correctly to the same region. +- **New:** `regions.json` registry auto-downloaded from `artifacts.contentstack.com` on first use and cached on disk — no setup required. The SDK self-heals if the file is missing. +- **New:** `Scripts/refresh-region.cs` bundled inside the NuGet package — automatically placed in your project's `Scripts/` folder on first `dotnet build`. Run `dotnet run Scripts/refresh-region.cs` anytime to pull the latest regions from CDN. ### Version: 2.0.0-beta.1 #### Date: April-27-2026 diff --git a/Contentstack.Utils.Tests/EndpointTest.cs b/Contentstack.Utils.Tests/EndpointTest.cs new file mode 100644 index 0000000..33a918b --- /dev/null +++ b/Contentstack.Utils.Tests/EndpointTest.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Contentstack.Utils.Endpoints; +using Xunit; + +namespace Contentstack.Utils.Tests +{ + public class EndpointTest : IDisposable + { + // Reset cache before each test so tests are isolated. + public EndpointTest() + { + Endpoint.ResetCache(); + } + + public void Dispose() + { + Endpoint.ResetCache(); + } + + // ------------------------------------------------------------------ + // Basic resolution + // ------------------------------------------------------------------ + + [Fact] + public void GetContentstackEndpoint_Na_ReturnsCorrectCdnUrl() + { + string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + Assert.Equal("https://cdn.contentstack.io", url); + } + + [Theory] + [InlineData("na")] + [InlineData("eu")] + [InlineData("au")] + [InlineData("azure-na")] + [InlineData("azure-eu")] + [InlineData("gcp-na")] + [InlineData("gcp-eu")] + public void GetContentstackEndpoint_AllRegionIds_Resolve(string regionId) + { + string url = Endpoint.GetContentstackEndpoint(regionId, "contentDelivery"); + Assert.False(string.IsNullOrEmpty(url)); + Assert.StartsWith("https://", url); + } + + // ------------------------------------------------------------------ + // Alias resolution (case-insensitive, dash/underscore variants) + // ------------------------------------------------------------------ + + [Theory] + [InlineData("na")] + [InlineData("us")] + [InlineData("NA")] + [InlineData("US")] + [InlineData("AWS-NA")] + [InlineData("aws_na")] + [InlineData("AWS_NA")] + public void GetContentstackEndpoint_NaAliasVariants_AllResolveToSameCdn(string alias) + { + string url = Endpoint.GetContentstackEndpoint(alias, "contentDelivery"); + Assert.Equal("https://cdn.contentstack.io", url); + } + + [Theory] + [InlineData("azure-na")] + [InlineData("azure_na")] + [InlineData("AZURE-NA")] + [InlineData("AZURE_NA")] + public void GetContentstackEndpoint_AzureNaAliasVariants_AllResolveToSameUrl(string alias) + { + string expected = Endpoint.GetContentstackEndpoint("azure-na", "contentDelivery"); + string result = Endpoint.GetContentstackEndpoint(alias, "contentDelivery"); + Assert.Equal(expected, result); + } + + // ------------------------------------------------------------------ + // omitHttps flag + // ------------------------------------------------------------------ + + [Fact] + public void GetContentstackEndpoint_OmitHttps_StripsScheme() + { + string host = Endpoint.GetContentstackEndpoint("na", "contentDelivery", omitHttps: true); + Assert.Equal("cdn.contentstack.io", host); + } + + [Fact] + public void GetContentstackEndpoint_OmitHttps_EuRegion_StripsScheme() + { + string host = Endpoint.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true); + Assert.Equal("eu-cdn.contentstack.com", host); + } + + [Fact] + public void GetContentstackEndpoint_OmitHttpsFalse_ReturnsFullUrl() + { + string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery", omitHttps: false); + Assert.StartsWith("https://", url); + } + + // ------------------------------------------------------------------ + // All-endpoints (dict) overload + // ------------------------------------------------------------------ + + [Theory] + [InlineData("na")] + [InlineData("eu")] + [InlineData("au")] + [InlineData("azure-na")] + [InlineData("azure-eu")] + [InlineData("gcp-na")] + [InlineData("gcp-eu")] + public void GetContentstackEndpoint_AllEndpoints_ContainsExpectedKeys(string regionId) + { + var endpoints = Endpoint.GetContentstackEndpoint(regionId); + Assert.True(endpoints.Count > 0); + Assert.True(endpoints.ContainsKey("contentDelivery")); + Assert.True(endpoints.ContainsKey("contentManagement")); + Assert.True(endpoints.ContainsKey("auth")); + } + + [Fact] + public void GetContentstackEndpoint_NaAllEndpoints_Has18Keys() + { + var endpoints = Endpoint.GetContentstackEndpoint("na"); + Assert.Equal(18, endpoints.Count); + } + + [Fact] + public void GetContentstackEndpoint_AllEndpoints_OmitHttps_AllValuesStripped() + { + var endpoints = Endpoint.GetContentstackEndpoint("na", omitHttps: true); + foreach (var url in endpoints.Values) + Assert.DoesNotContain("https://", url); + } + + // ------------------------------------------------------------------ + // Error handling + // ------------------------------------------------------------------ + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void GetContentstackEndpoint_EmptyRegion_ThrowsArgumentException(string emptyRegion) + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint(emptyRegion, "contentDelivery")); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void GetContentstackEndpoint_EmptyRegion_DictOverload_ThrowsArgumentException(string emptyRegion) + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint(emptyRegion)); + } + + [Fact] + public void GetContentstackEndpoint_UnknownRegion_ThrowsKeyNotFoundException() + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint("xyz", "contentDelivery")); + } + + [Fact] + public void GetContentstackEndpoint_UnknownService_ThrowsKeyNotFoundException() + { + var ex = Assert.Throws(() => + Endpoint.GetContentstackEndpoint("na", "nonExistentService")); + Assert.Contains("nonExistentService", ex.Message); + } + + // ------------------------------------------------------------------ + // Cache behaviour + // ------------------------------------------------------------------ + + [Fact] + public void ResetCache_AllowsReload_NoError() + { + // First call populates cache + Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + + // Reset and call again — should reload without error + Endpoint.ResetCache(); + string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + Assert.Equal("https://cdn.contentstack.io", url); + } + + [Fact] + public void GetContentstackEndpoint_ConsecutiveCalls_ReturnConsistentResults() + { + string first = Endpoint.GetContentstackEndpoint("eu", "contentManagement"); + string second = Endpoint.GetContentstackEndpoint("eu", "contentManagement"); + Assert.Equal(first, second); + } + + // ------------------------------------------------------------------ + // Utils proxy parity + // ------------------------------------------------------------------ + + [Fact] + public void Utils_Proxy_String_MatchesEndpoint() + { + string direct = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + string proxy = Utils.GetContentstackEndpoint("na", "contentDelivery"); + Assert.Equal(direct, proxy); + } + + [Fact] + public void Utils_Proxy_Dict_MatchesEndpoint() + { + var direct = Endpoint.GetContentstackEndpoint("eu"); + var proxy = Utils.GetContentstackEndpoint("eu"); + Assert.Equal(direct.Count, proxy.Count); + foreach (var kvp in direct) + Assert.Equal(kvp.Value, proxy[kvp.Key]); + } + + [Fact] + public void Utils_Proxy_OmitHttps_MatchesEndpoint() + { + string direct = Endpoint.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true); + string proxy = Utils.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true); + Assert.Equal(direct, proxy); + } + + // ------------------------------------------------------------------ + // Local file self-heal (mirrors Python _download_and_save behaviour) + // ------------------------------------------------------------------ + + [Fact] + public void LocalFile_WhenWritten_IsReadOnNextCacheReset() + { + // First call: either reads from disk or downloads from CDN and writes to disk. + Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + + string localPath = Endpoint.GetLocalFilePath(); + + // If CDN download failed (no network in CI), skip this assertion — the + // self-heal path is covered by the CDN download succeeding at call time. + if (!File.Exists(localPath)) + return; + + // Reset cache — next call must read from local file (step 2 in LoadRegions), + // not re-download. Mirrors Python: os.path.exists(_REGIONS_FILE) → open(). + Endpoint.ResetCache(); + string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + + Assert.Equal("https://cdn.contentstack.io", url); + } + + [Fact] + public void LocalFilePath_IsNextToDll_NotSourceDirectory() + { + string localPath = Endpoint.GetLocalFilePath(); + Assert.Contains("Assets", localPath); + Assert.EndsWith("regions.json", localPath); + } + } +} diff --git a/Contentstack.Utils/Contentstack.Utils.csproj b/Contentstack.Utils/Contentstack.Utils.csproj index 881b6eb..287e73f 100644 --- a/Contentstack.Utils/Contentstack.Utils.csproj +++ b/Contentstack.Utils/Contentstack.Utils.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net47;net472; + net10.0 latest contentstack.utils Contentstack.Utils @@ -38,9 +38,27 @@ + + + + + + true + contentFiles/cs/any/Scripts/refresh-region.cs + Content + false + + + true + build/contentstack.utils.targets + + + diff --git a/Contentstack.Utils/Endpoints/Endpoint.cs b/Contentstack.Utils/Endpoints/Endpoint.cs new file mode 100644 index 0000000..3e99f2f --- /dev/null +++ b/Contentstack.Utils/Endpoints/Endpoint.cs @@ -0,0 +1,287 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Contentstack.Utils.Endpoints +{ + /// + /// Resolves Contentstack service URLs for any supported region. + /// + /// All public methods are static — no instantiation required. + /// + /// Example: + /// + /// // Full URL + /// string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + /// // → "https://cdn.contentstack.io" + /// + /// // Host only (omit https://) — useful for SDK host configuration + /// string host = Endpoint.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true); + /// // → "eu-cdn.contentstack.com" + /// + /// // All endpoints for a region + /// Dictionary<string, string> all = Endpoint.GetContentstackEndpoint("azure-na"); + /// // → { "contentDelivery": "...", "contentManagement": "...", ... } + /// + /// + public static class Endpoint + { + private const string RegionsUrl = "https://artifacts.contentstack.com/regions.json"; + + // Module-level cache — loaded once per process, shared across all calls. + private static JsonElement[]? _regionsData; + private static readonly object _cacheLock = new object(); + private static readonly HttpClient _httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(30) + }; + + /// + /// Resolves a single Contentstack service endpoint URL for the given region. + /// + /// + /// Region ID or alias (case-insensitive). Examples: "na", "us", "eu", "AWS-NA", "azure_eu", "gcp-na". + /// + /// + /// Service key. Valid keys include: "contentDelivery", "contentManagement", "auth", + /// "graphqlDelivery", "preview", "images", "assets", "automate", "launch", + /// "developerHub", "brandKit", "genAI", "personalizeManagement", + /// "personalizeEdge", "composableStudio", "assetManagement". + /// + /// + /// When true, strips the https:// scheme from the returned URL. + /// Useful when passing the host to an SDK that constructs its own URLs. + /// + /// The endpoint URL string for the specified service. + /// If region is empty or whitespace. + /// If region or service is not found. + /// If the registry cannot be read or is corrupt. + public static string GetContentstackEndpoint(string region, string service, bool omitHttps = false) + { + if (string.IsNullOrWhiteSpace(region)) + throw new ArgumentException("Empty region provided. Please put valid region.", nameof(region)); + + var regions = LoadRegions(); + string normalized = region.Trim().ToLowerInvariant(); + var regionEl = FindRegion(regions, normalized); + + if (regionEl == null) + throw new KeyNotFoundException($"Invalid region: {region}"); + + var endpoints = regionEl.Value.GetProperty("endpoints"); + if (!endpoints.TryGetProperty(service, out var urlEl)) + { + string regionId = regionEl.Value.GetProperty("id").GetString()!; + throw new KeyNotFoundException($"Service \"{service}\" not found for region \"{regionId}\""); + } + + string url = urlEl.GetString()!; + return omitHttps ? StripHttps(url) : url; + } + + /// + /// Returns all service endpoint URLs for the given region. + /// + /// Region ID or alias (case-insensitive). + /// When true, strips the https:// scheme from all returned URLs. + /// Dictionary mapping service keys to endpoint URLs. + /// If region is empty or whitespace. + /// If region is not found. + /// If the registry cannot be read or is corrupt. + public static Dictionary GetContentstackEndpoint(string region, bool omitHttps = false) + { + if (string.IsNullOrWhiteSpace(region)) + throw new ArgumentException("Empty region provided. Please put valid region.", nameof(region)); + + var regions = LoadRegions(); + string normalized = region.Trim().ToLowerInvariant(); + var regionEl = FindRegion(regions, normalized); + + if (regionEl == null) + throw new KeyNotFoundException($"Invalid region: {region}"); + + var result = new Dictionary(); + var endpoints = regionEl.Value.GetProperty("endpoints"); + foreach (var ep in endpoints.EnumerateObject()) + { + string url = ep.Value.GetString()!; + result[ep.Name] = omitHttps ? StripHttps(url) : url; + } + return result; + } + + /// + /// Clears the in-memory region cache. Intended for testing only — forces the + /// next call to re-read regions.json from disk or re-download from CDN. + /// + public static void ResetCache() + { + lock (_cacheLock) + { + _regionsData = null; + } + } + + // ------------------------------------------------------------------ + // Internal helpers + // ------------------------------------------------------------------ + + /// + /// Load and cache the regions registry. + /// + /// Resolution order: + /// 1. In-memory cache — zero I/O after the first call in a process + /// 2. Local file on disk — Assets/regions.json next to the DLL + /// (written by DownloadAndSave or refresh-region.cs) + /// 3. CDN download — fetches from artifacts.contentstack.com, + /// writes to disk for future calls (silent on failure) + /// + private static JsonElement[] LoadRegions() + { + lock (_cacheLock) + { + if (_regionsData != null) + return _regionsData; + + string localFile = GetLocalFilePath(); + + // Step 2 — local file on disk + string? json = ReadLocalFile(localFile); + + // Step 3 — CDN download, writes to disk so next startup skips this step + if (json == null) + json = DownloadAndSave(localFile); + + if (json == null) + throw new InvalidOperationException( + "contentstack_utils: regions.json not found and could not be downloaded. " + + "Run 'dotnet run Scripts/refresh-region.cs' and ensure network access."); + + JsonDocument doc; + try + { + doc = JsonDocument.Parse(json); + } + catch (JsonException ex) + { + throw new InvalidOperationException( + "contentstack_utils: regions.json is corrupt. " + + "Run 'dotnet run Scripts/refresh-region.cs' to re-download it.", ex); + } + + if (!doc.RootElement.TryGetProperty("regions", out var regionsEl) || + regionsEl.ValueKind != JsonValueKind.Array) + { + throw new InvalidOperationException( + "contentstack_utils: regions.json is corrupt. " + + "Run 'dotnet run Scripts/refresh-region.cs' to re-download it."); + } + + var list = new List(); + foreach (var r in regionsEl.EnumerateArray()) + list.Add(r.Clone()); + + _regionsData = list.ToArray(); + return _regionsData; + } + } + + /// + /// Returns the path to regions.json next to the DLL + /// + internal static string GetLocalFilePath() + { + string assemblyDir = Path.GetDirectoryName(typeof(Endpoint).Assembly.Location) + ?? AppContext.BaseDirectory; + return Path.Combine(assemblyDir, "Assets", "regions.json"); + } + + /// + /// Reads regions.json from disk. Returns null if the file does not exist. + /// + private static string? ReadLocalFile(string path) + { + if (!File.Exists(path)) + return null; + try + { + return File.ReadAllText(path); + } + catch + { + return null; + } + } + + /// + /// Downloads regions.json from the CDN and writes it to disk so that future + /// process startups read from the local file instead of downloading again. + /// Silent on all failures (network error, permission denied). + /// + private static string? DownloadAndSave(string dest) + { + try + { + var task = _httpClient.GetStringAsync(RegionsUrl); + task.Wait(); + string data = task.Result; + + using var doc = JsonDocument.Parse(data); + if (!doc.RootElement.TryGetProperty("regions", out _)) + return null; + + // Write to disk — next startup reads from local file (Step 2). + // Silent on PermissionError or read-only filesystem + try + { + Directory.CreateDirectory(Path.GetDirectoryName(dest)!); + File.WriteAllText(dest, data); + } + catch { } + + return data; + } + catch + { + return null; + } + } + + /// + /// Two-pass region lookup: ID match wins over alias match. + /// Input must already be lowercased and trimmed. + /// + private static JsonElement? FindRegion(JsonElement[] regions, string normalized) + { + // Pass 1 — exact id match + foreach (var r in regions) + { + if (r.TryGetProperty("id", out var id) && + id.GetString()?.ToLowerInvariant() == normalized) + return r; + } + + // Pass 2 — alias match + foreach (var r in regions) + { + if (!r.TryGetProperty("alias", out var aliases)) continue; + foreach (var alias in aliases.EnumerateArray()) + { + if (alias.GetString()?.ToLowerInvariant() == normalized) + return r; + } + } + + return null; + } + + private static string StripHttps(string url) + { + return Regex.Replace(url, @"^https?://", string.Empty); + } + } +} diff --git a/Contentstack.Utils/Utils.cs b/Contentstack.Utils/Utils.cs index fdd51be..421896f 100644 --- a/Contentstack.Utils/Utils.cs +++ b/Contentstack.Utils/Utils.cs @@ -434,5 +434,19 @@ private static JsonArray ExtractVariantAliasesFromEntry(JsonObject entry) } return variantArray; } + + /// + /// Resolves a single Contentstack service endpoint URL for the given region. + /// Proxy for . + /// + public static string GetContentstackEndpoint(string region, string service, bool omitHttps = false) + => Endpoints.Endpoint.GetContentstackEndpoint(region, service, omitHttps); + + /// + /// Returns all service endpoint URLs for the given region. + /// Proxy for . + /// + public static Dictionary GetContentstackEndpoint(string region, bool omitHttps = false) + => Endpoints.Endpoint.GetContentstackEndpoint(region, omitHttps); } } diff --git a/Scripts/refresh-region.cs b/Scripts/refresh-region.cs new file mode 100644 index 0000000..c692dfc --- /dev/null +++ b/Scripts/refresh-region.cs @@ -0,0 +1,77 @@ +// Refresh regions.json from the Contentstack CDN. +// +// Works for both SDK developers and SDK consumers — no file copying needed. +// NuGet automatically places this file in your project's Scripts/ folder +// when you install the contentstack.utils package. +// +// Usage (run from your project root after dotnet build): +// dotnet run Scripts/refresh-region.cs +// +// Run whenever Contentstack adds a new region or service. + +using System.IO; +using System.Net.Http; +using System.Text.Json; + +const string RegionsUrl = "https://artifacts.contentstack.com/regions.json"; + +string root = Directory.GetCurrentDirectory(); + +Console.WriteLine($"Fetching {RegionsUrl} ..."); + +string json; +try +{ + using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; + json = await http.GetStringAsync(RegionsUrl); +} +catch (Exception ex) +{ + Console.Error.WriteLine($"ERROR: Could not download regions.json: {ex.Message}"); + return 1; +} + +JsonDocument doc; +try +{ + doc = JsonDocument.Parse(json); +} +catch (JsonException ex) +{ + Console.Error.WriteLine($"ERROR: Downloaded content is not valid JSON: {ex.Message}"); + return 1; +} + +if (!doc.RootElement.TryGetProperty("regions", out var regionsEl)) +{ + Console.Error.WriteLine("ERROR: Downloaded JSON does not contain a 'regions' key."); + return 1; +} + +int regionCount = regionsEl.GetArrayLength(); + +// ── 1. All bin output dirs — finds every Contentstack.Utils.dll in bin/ ─── +// Works for both the SDK repo and consumer projects after dotnet build. +// Writes Assets/regions.json next to each DLL found. +int binCount = 0; +foreach (string dll in Directory.GetFiles(root, "Contentstack.Utils.dll", SearchOption.AllDirectories)) +{ + if (!dll.Contains(Path.DirectorySeparatorChar + "bin" + Path.DirectorySeparatorChar)) + continue; + + string binDest = Path.Combine(Path.GetDirectoryName(dll)!, "Assets", "regions.json"); + await WriteFile(binDest, json); + Console.WriteLine($"[bin] Wrote {regionCount} regions → {binDest}"); + binCount++; +} + +if (binCount == 0) + Console.WriteLine("[bin] No build output found — run 'dotnet build' first, then re-run this script."); + +return 0; + +static async Task WriteFile(string path, string content) +{ + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + await File.WriteAllTextAsync(path, content); +} diff --git a/build/contentstack.utils.targets b/build/contentstack.utils.targets new file mode 100644 index 0000000..ce0f3e2 --- /dev/null +++ b/build/contentstack.utils.targets @@ -0,0 +1,19 @@ + + + + <_RefreshScriptDest>$(MSBuildProjectDirectory)/Scripts/refresh-region.cs + <_RefreshScriptSrc>$(MSBuildThisFileDirectory)../contentFiles/cs/any/Scripts/refresh-region.cs + + + + + + + + From 42e93b7b24d04a1eb9daae04fdcbe5a1a3b59994 Mon Sep 17 00:00:00 2001 From: OM PAWAR Date: Thu, 11 Jun 2026 13:44:42 +0530 Subject: [PATCH 2/2] Update comment for Local file self-heal --- Contentstack.Utils.Tests/EndpointTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contentstack.Utils.Tests/EndpointTest.cs b/Contentstack.Utils.Tests/EndpointTest.cs index 33a918b..2bb5ef8 100644 --- a/Contentstack.Utils.Tests/EndpointTest.cs +++ b/Contentstack.Utils.Tests/EndpointTest.cs @@ -228,7 +228,7 @@ public void Utils_Proxy_OmitHttps_MatchesEndpoint() } // ------------------------------------------------------------------ - // Local file self-heal (mirrors Python _download_and_save behaviour) + // Local file self-heal. // ------------------------------------------------------------------ [Fact]