diff --git a/.talismanrc b/.talismanrc index 7654d1e..b1596ce 100644 --- a/.talismanrc +++ b/.talismanrc @@ -3,13 +3,15 @@ fileignoreconfig: ignore_detectors: - filecontent - filename: Contentstack.Core/Internals/HttpRequestHandler.cs - checksum: 94288e483056f41ff3a4c2ab652c3ce9ecb53dc0b9d4029456b34baed4f34891 + checksum: 62053e1b8772f44db054efc504d5d57f28fb7962c81021325854d478f570de09 - filename: Contentstack.Core/Models/Entry.cs checksum: 78a09b03b9fd6aefd0251353b2d8c70962bdfced16a6e1e28d10dc9af43da244 - filename: Contentstack.Core/ContentstackClient.cs checksum: 687dc0a5f20037509731cfe540dcec9c3cc2b6cf50373cd183ece4f3249dc88e - filename: Contentstack.Core/Models/AssetLibrary.cs - checksum: 7e05fd0bbb43b15e6b7f387d746cc64d709e17e0e8e26a7495a99025077ff507 + checksum: 0c67f8bb3b7ffdb9b04cd38ae096904b53d6d4372e86c91c1f935e36b6b0ce56 +- filename: Contentstack.Core.Tests/AssetTest.cs + checksum: 9e197065aa6ea46af795a8ddb9d652a4972d9d4b4bfc7b1772d304d848f1c3e1 - filename: Contentstack.Core/Models/Asset.cs checksum: d192718723e6cb2aa8f08f873d3a7ea7268c89cc15da3bdeea4c16fd304c410e - filename: Contentstack.Core/Models/Query.cs @@ -20,7 +22,5 @@ fileignoreconfig: checksum: 53ba4ce874c4d2362ad00deb23f5a6ec219318860352f997b945e9161a580651 - filename: Contentstack.Core.Tests/ContentstackClientTest.cs checksum: b63897181a8cb5993d1305248cfc3e711c4039b5677b6c1e4e2a639e4ecb391b -- filename: Contentstack.Core.Tests/AssetTest.cs - checksum: 3e7bf50c7223c458561f0217484d5e70cf3770490c569e0a7083b0a12af9ab86 - filename: Contentstack.Core.Tests/RegionHandlerTest.cs checksum: 69899138754908e156aa477d775d12fd6b3fefc1a6c2afec22cb409bd6e6446c diff --git a/CHANGELOG.md b/CHANGELOG.md index 171471c..b335753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +### Version: 2.25.0 +#### Date: Jan-07-2025 + +##### Feat: +- AssetLibrary + - Added new `Where` method for simple key-value pair queries + - Enhanced `Query` method to support multiple calls with intelligent merging +- Improved query handling with better null safety and error handling + ### Version: 2.24.0 #### Date: Sep-29-2025 diff --git a/Contentstack.Core.Tests/AssetTest.cs b/Contentstack.Core.Tests/AssetTest.cs index 25f4fd7..e716901 100644 --- a/Contentstack.Core.Tests/AssetTest.cs +++ b/Contentstack.Core.Tests/AssetTest.cs @@ -661,5 +661,295 @@ public async Task AssetTags_CaseSensitivityVerification_ShouldTestCaseBehavior_T } } } + + [Fact] + public void Query_MultipleCalls_ShouldMergeQueries_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + JObject firstQuery = new JObject + { + { "filename", "test1.png" }, + { "content_type", "image/png" } + }; + JObject secondQuery = new JObject + { + { "file_size", 1024 }, + { "tags", new JArray { "test", "image" } } + }; + + // Act + var result = assetLibrary.Query(firstQuery).Query(secondQuery); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + // The method should not throw an exception when called multiple times + } + + [Fact] + public void Query_SingleCall_ShouldWorkAsBefore_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + JObject queryObject = new JObject + { + { "filename", "test.png" } + }; + + // Act + var result = assetLibrary.Query(queryObject); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Query_WithEmptyObject_ShouldNotThrowException_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + JObject emptyQuery = new JObject(); + + // Act & Assert + var result = assetLibrary.Query(emptyQuery); + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Query_WithNullValues_ShouldHandleGracefully_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + JObject queryWithNulls = new JObject + { + { "filename", "test.png" }, + { "null_field", null } + }; + + // Act & Assert + var result = assetLibrary.Query(queryWithNulls); + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Query_ChainedWithOtherMethods_ShouldWork_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + JObject queryObject = new JObject + { + { "filename", "test.png" } + }; + + // Act + var result = assetLibrary + .Query(queryObject) + .Limit(10) + .Skip(0) + .IncludeMetadata(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Query_MultipleCallsWithSameKeys_ShouldMergeValues_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + JObject firstQuery = new JObject + { + { "tags", new JArray { "tag1", "tag2" } } + }; + JObject secondQuery = new JObject + { + { "tags", new JArray { "tag3", "tag4" } } + }; + + // Act + var result = assetLibrary.Query(firstQuery).Query(secondQuery); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + // The method should handle merging arrays without throwing exceptions + } + + [Fact] + public void Query_WithComplexNestedObjects_ShouldMergeCorrectly_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + JObject firstQuery = new JObject + { + { "metadata", new JObject + { + { "author", "John Doe" }, + { "version", 1 } + } + } + }; + JObject secondQuery = new JObject + { + { "metadata", new JObject + { + { "department", "IT" } + } + }, + { "filename", "test.png" } + }; + + // Act + var result = assetLibrary.Query(firstQuery).Query(secondQuery); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Where_SingleCall_ShouldAddKeyValuePair_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + string key = "filename"; + string value = "test.png"; + + // Act + var result = assetLibrary.Where(key, value); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Where_MultipleCalls_ShouldAddMultipleKeyValuePairs_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + + // Act + var result = assetLibrary + .Where("filename", "test.png") + .Where("content_type", "image/png") + .Where("file_size", "1024"); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Where_WithEmptyStrings_ShouldHandleGracefully_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + + // Act & Assert + var result = assetLibrary.Where("", ""); + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Where_WithNullKey_ShouldHandleGracefully_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + + // Act & Assert + var result = assetLibrary.Where(null, "value"); + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Where_WithNullValue_ShouldHandleGracefully_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + + // Act & Assert + var result = assetLibrary.Where("key", null); + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Where_ChainedWithOtherMethods_ShouldWork_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + + // Act + var result = assetLibrary + .Where("filename", "test.png") + .Limit(10) + .Skip(0) + .IncludeMetadata(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Where_WithQueryMethod_ShouldWorkTogether_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + JObject queryObject = new JObject + { + { "content_type", "image/png" } + }; + + // Act + var result = assetLibrary + .Query(queryObject) + .Where("filename", "test.png") + .Where("file_size", "1024"); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Where_OverwritesExistingKey_ShouldReplaceValue_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + + // Act + var result = assetLibrary + .Where("filename", "original.png") + .Where("filename", "updated.png"); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Where_WithSpecialCharacters_ShouldHandleCorrectly_Test() + { + // Arrange + AssetLibrary assetLibrary = client.AssetLibrary(); + + // Act + var result = assetLibrary + .Where("file_name", "test-file_123.png") + .Where("description", "File with special chars: @#$%"); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } } } \ No newline at end of file diff --git a/Contentstack.Core/Internals/HttpRequestHandler.cs b/Contentstack.Core/Internals/HttpRequestHandler.cs index 85f8a10..8d36749 100644 --- a/Contentstack.Core/Internals/HttpRequestHandler.cs +++ b/Contentstack.Core/Internals/HttpRequestHandler.cs @@ -12,43 +12,56 @@ namespace Contentstack.Core.Internals { internal class HttpRequestHandler { - ContentstackClient client - { - get; set; - } + ContentstackClient client { get; set; } + internal HttpRequestHandler(ContentstackClient contentstackClient) { client = contentstackClient; } - public async Task ProcessRequest(string Url, Dictionary Headers, Dictionary BodyJson, string FileName = null, string Branch = null, bool isLivePreview = false, int timeout = 30000, WebProxy proxy = null) - { - String queryParam = String.Join("&", BodyJson.Select(kvp => { - var value = ""; - if (kvp.Value is string[]) + public async Task ProcessRequest( + string Url, + Dictionary Headers, + Dictionary BodyJson, + string FileName = null, + string Branch = null, + bool isLivePreview = false, + int timeout = 30000, + WebProxy proxy = null + ) + { + String queryParam = String.Join( + "&", + BodyJson.Select(kvp => { - string[] vals = (string[])kvp.Value; - value = String.Join("&", vals.Select(item => + var value = ""; + if (kvp.Value is string[]) { - return String.Format("{0}={1}", kvp.Key, item); - })); - return value; - } - else if (kvp.Value is Dictionary) - value = JsonConvert.SerializeObject(kvp.Value); - else - return String.Format("{0}={1}", kvp.Key, kvp.Value); - - return String.Format("{0}={1}", kvp.Key, value); + string[] vals = (string[])kvp.Value; + value = String.Join( + "&", + vals.Select(item => + { + return String.Format("{0}={1}", kvp.Key, item); + }) + ); + return value; + } + else if (kvp.Value is Dictionary) + value = JsonConvert.SerializeObject(kvp.Value); + else + return String.Format("{0}={1}", kvp.Key, kvp.Value); - })); + return String.Format("{0}={1}", kvp.Key, value); + }) + ); - var uri = new Uri(Url+"?"+queryParam); + var uri = new Uri(Url + "?" + queryParam); var request = (HttpWebRequest)WebRequest.Create(uri); request.Method = "GET"; request.ContentType = "application/json"; - request.Headers["x-user-agent"]="contentstack-delivery-dotnet/2.24.0"; + request.Headers["x-user-agent"] = VersionUtility.GetSdkVersion(); request.Timeout = timeout; if (proxy != null) @@ -60,52 +73,68 @@ public async Task ProcessRequest(string Url, Dictionary { request.Headers["branch"] = Branch; } - if (Headers != default(IDictionary)) { - foreach (var header in Headers) { - try { + if (Headers != default(IDictionary)) + { + foreach (var header in Headers) + { + try + { request.Headers[header.Key] = header.Value.ToString(); - } catch { - } + catch { } } } foreach (var plugin in client.Plugins) { request = await plugin.OnRequest(client, request); - }; + } + ; var serializedresult = JsonConvert.SerializeObject(BodyJson); byte[] requestBody = Encoding.UTF8.GetBytes(serializedresult); StreamReader reader = null; HttpWebResponse response = null; - try { + try + { response = (HttpWebResponse)await request.GetResponseAsync(); - if (response != null) { + if (response != null) + { reader = new StreamReader(response.GetResponseStream()); string responseString = await reader.ReadToEndAsync(); foreach (var plugin in client.Plugins) { - responseString = await plugin.OnResponse(client, request, response, responseString); + responseString = await plugin.OnResponse( + client, + request, + response, + responseString + ); } return responseString; - } else { + } + else + { return null; } - } catch (Exception we) { + } + catch (Exception we) + { throw we; - } finally { - if (reader != null) { + } + finally + { + if (reader != null) + { reader.Dispose(); } if (response != null) { - response.Dispose(); + response.Dispose(); } } - } //internal void updateLivePreviewContent(JObject response) diff --git a/Contentstack.Core/Internals/VersionUtility.cs b/Contentstack.Core/Internals/VersionUtility.cs new file mode 100644 index 0000000..c8c4003 --- /dev/null +++ b/Contentstack.Core/Internals/VersionUtility.cs @@ -0,0 +1,94 @@ +using System; +using System.Reflection; + +namespace Contentstack.Core.Internals +{ + /// + /// Utility class for handling SDK version information and user-agent string generation. + /// + internal static class VersionUtility + { + /// + /// Gets the SDK version dynamically from the assembly. + /// + /// The SDK version string in format: contentstack-delivery-dotnet/Major.Minor.Patch + public static string GetSdkVersion() + { + try + { + var assembly = Assembly.GetExecutingAssembly(); + var version = assembly.GetName().Version; + + // Check if version is valid (not 0.0.0.0 which is default for unversioned assemblies) + if (version != null && (version.Major > 0 || version.Minor > 0 || version.Build > 0)) + { + return $"contentstack-delivery-dotnet/{version.Major}.{version.Minor}.{version.Build}"; + } + + // Try to get version from assembly file version as fallback + var fileVersion = assembly.GetCustomAttribute()?.Version; + if (!string.IsNullOrEmpty(fileVersion)) + { + // Parse file version and extract only Major.Minor.Build (first 3 parts) + var versionParts = fileVersion.Split('.'); + if (versionParts.Length >= 3) + { + return $"contentstack-delivery-dotnet/{versionParts[0]}.{versionParts[1]}.{versionParts[2]}"; + } + return $"contentstack-delivery-dotnet/{fileVersion}"; + } + + // Try to get version from assembly informational version + var infoVersion = assembly.GetCustomAttribute()?.InformationalVersion; + if (!string.IsNullOrEmpty(infoVersion)) + { + // Extract semantic version (Major.Minor.Patch) from informational version + // Handle formats like "2.25.0", "2.25.0-beta.1", "2.25.0+abc123" + var semanticVersion = ExtractSemanticVersion(infoVersion); + if (!string.IsNullOrEmpty(semanticVersion)) + { + return $"contentstack-delivery-dotnet/{semanticVersion}"; + } + return $"contentstack-delivery-dotnet/{infoVersion}"; + } + } + catch + { + // Ignore exceptions and continue to fallback + } + + // Final fallback - use a generic identifier that doesn't imply a specific version + return "contentstack-delivery-dotnet/dev"; + } + + /// + /// Extracts semantic version (Major.Minor.Patch) from informational version string. + /// + /// The informational version string. + /// Semantic version in Major.Minor.Patch format. + private static string ExtractSemanticVersion(string informationalVersion) + { + try + { + // Remove build metadata (everything after +) + var versionWithoutMetadata = informationalVersion.Split('+')[0]; + + // Split by dots to get version parts + var parts = versionWithoutMetadata.Split('.'); + + // Ensure we have at least 3 parts (Major.Minor.Patch) + if (parts.Length >= 3) + { + // Take only the first 3 parts (Major.Minor.Patch) + return $"{parts[0]}.{parts[1]}.{parts[2]}"; + } + + return null; + } + catch + { + return null; + } + } + } +} diff --git a/Contentstack.Core/Models/AssetLibrary.cs b/Contentstack.Core/Models/AssetLibrary.cs index 8ec17e8..cc7643c 100644 --- a/Contentstack.Core/Models/AssetLibrary.cs +++ b/Contentstack.Core/Models/AssetLibrary.cs @@ -12,8 +12,6 @@ namespace Contentstack.Core.Models { public class AssetLibrary { - - #region Internal Variables private Dictionary _ObjectAttributes = new Dictionary(); private Dictionary _Headers = new Dictionary(); @@ -30,17 +28,12 @@ private string _Url } #endregion - public ContentstackClient Stack - { - get; - set; - } + public ContentstackClient Stack { get; set; } #region Internal Constructors - internal AssetLibrary() - { - } + internal AssetLibrary() { } + internal AssetLibrary(ContentstackClient stack) { this.Stack = stack; @@ -74,13 +67,12 @@ public void SortWithKeyAndOrderBy(String key, OrderBy order) UrlQueries.Add("desc", key); } } + /// /// Provides only the number of assets. /// /// /// - - /// ContentstackClient stack = new ContentstackClinet("api_key", "delivery_token", "environment"); /// AssetLibrary assetLibrary = stack.AssetLibrary(); /// JObject jObject = await assetLibrary.Count(); @@ -96,7 +88,30 @@ public AssetLibrary Query(JObject QueryObject) { try { - UrlQueries.Add("query", QueryObject); + if (UrlQueries.ContainsKey("query")) + { + // If query already exists, append/merge the new query object + JObject existingQuery = UrlQueries["query"] as JObject; + if (existingQuery != null) + { + // Merge the new query object with the existing one + existingQuery.Merge(QueryObject, new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Union + }); + UrlQueries["query"] = existingQuery; + } + else + { + // If existing query is not a JObject, replace it + UrlQueries["query"] = QueryObject; + } + } + else + { + // If query doesn't exist, add it + UrlQueries.Add("query", QueryObject); + } } catch (Exception e) { @@ -104,6 +119,63 @@ public AssetLibrary Query(JObject QueryObject) } return this; } + + /// + /// Adds a key-value pair to the query object in UrlQueries. + /// + /// The key to add to the query object. + /// The value to add to the query object. + /// Current instance of AssetLibrary, this will be useful for a chaining calls. + /// + /// + /// ContentstackClient stack = new ContentstackClinet("api_key", "delivery_token", "environment"); + /// AssetLibrary assetLibrary = stack.AssetLibrary(); + /// assetLibrary.Where("filename", "image.png"); + /// ContentstackCollection contentstackCollection = await assetLibrary.FetchAll(); + /// + /// + public AssetLibrary Where(string key, string value) + { + try + { + // Handle null or empty key gracefully + if (string.IsNullOrEmpty(key)) + { + return this; + } + + if (UrlQueries.ContainsKey("query")) + { + // If query already exists, get it and add the key-value pair + JObject existingQuery = UrlQueries["query"] as JObject; + if (existingQuery != null) + { + existingQuery[key] = value; + UrlQueries["query"] = existingQuery; + } + else + { + // If existing query is not a JObject, create a new one + JObject newQuery = new JObject(); + newQuery[key] = value; + UrlQueries["query"] = newQuery; + } + } + else + { + // If query doesn't exist, create a new one with the key-value pair + JObject newQuery = new JObject(); + newQuery[key] = value; + UrlQueries.Add("query", newQuery); + } + } + catch (Exception e) + { + throw new Exception(StackConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + /// /// Include fallback locale publish content, if specified locale content is not publish. /// @@ -154,7 +226,6 @@ public AssetLibrary IncludeBranch() return this; } - /// /// Add param in URL query. /// @@ -167,7 +238,6 @@ public AssetLibrary IncludeBranch() /// ContentstackCollection contentstackCollection = await assetLibrary.FetchAll(); /// /// - /// Where function public AssetLibrary AddParam(string key, string value) { UrlQueries.Add(key, value); @@ -218,9 +288,6 @@ public void IncludeCount() UrlQueries.Add("include_count", "true"); } - - - /// /// This call includes metadata in the response. /// @@ -246,8 +313,6 @@ public AssetLibrary IncludeMetadata() return this; } - - /// /// This method includes the relative url of assets. /// @@ -465,7 +530,8 @@ public AssetLibrary RemoveHeader(string key) public async Task> FetchAll() { JObject json = await Exec(); - var assets = json.SelectToken("$.assets").ToObject>(this.Stack.Serializer); + var assets = json.SelectToken("$.assets") + .ToObject>(this.Stack.Serializer); var collection = json.ToObject>(this.Stack.Serializer); foreach (var entry in assets) { @@ -502,9 +568,15 @@ private async Task Exec() try { HttpRequestHandler RequestHandler = new HttpRequestHandler(this.Stack); - var outputResult = await RequestHandler.ProcessRequest(_Url, headers, mainJson, Branch: this.Stack.Config.Branch, timeout: this.Stack.Config.Timeout, proxy: this.Stack.Config.Proxy); + var outputResult = await RequestHandler.ProcessRequest( + _Url, + headers, + mainJson, + Branch: this.Stack.Config.Branch, + timeout: this.Stack.Config.Timeout, + proxy: this.Stack.Config.Proxy + ); return JObject.Parse(ContentstackConvert.ToString(outputResult, "{}")); - } catch (Exception ex) { @@ -540,19 +612,18 @@ private Dictionary GetHeader(Dictionary localHea } return classHeaders; - } else { return localHeader; } - } else { return _StackHeaders; } } + internal static ContentstackException GetContentstackError(Exception ex) { Int32 errorCode = 0; @@ -599,7 +670,7 @@ internal static ContentstackException GetContentstackError(Exception ex) ErrorCode = errorCode, ErrorMessage = errorMessage, StatusCode = statusCode, - Errors = errors + Errors = errors, }; return contentstackError; diff --git a/Directory.Build.props b/Directory.Build.props index 2263977..3645501 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 2.24.0 + 2.25.0