diff --git a/Frends.JSON.QuerySingle/CHANGELOG.md b/Frends.JSON.QuerySingle/CHANGELOG.md index 31a4240..42d25c0 100644 --- a/Frends.JSON.QuerySingle/CHANGELOG.md +++ b/Frends.JSON.QuerySingle/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.4.0] - 2026-08-05 +### Updated +- Upgraded target framework from .NET 6 to .NET 8. +- Added `CancellationToken` support to the `QuerySingle` method. +- Added `ThrowErrorOnFailure` and `ErrorMessageOnFailure` options so that failures can be returned as a result instead of throwing an exception. +- The result now includes an `Error` property with error details when the task fails and `ThrowErrorOnFailure` is set to false. + ## [1.3.0] - 2026-07-08 ### Fixed - Fixed issue where Options.ErrorWhenNotMatched did not throw an exception when a JSONPath filter expression (e.g. `[?(...)]`) matched no results. diff --git a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.UnitTests/ErrorHandlerTests.cs b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.UnitTests/ErrorHandlerTests.cs new file mode 100644 index 0000000..28a1f51 --- /dev/null +++ b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.UnitTests/ErrorHandlerTests.cs @@ -0,0 +1,54 @@ +using Frends.JSON.QuerySingle.Definitions; +using NUnit.Framework; +using System; +using System.Threading; + +namespace Frends.JSON.QuerySingle.UnitTests; + +[TestFixture] +public class ErrorHandlerTests +{ + private const string CustomErrorMessage = "CustomErrorMessage"; + + private static Input InvalidInput() => new Input + { + Json = "not valid json", + Query = "$.key" + }; + + private static Options DefaultOptions() => new Options + { + ErrorWhenNotMatched = false, + ThrowErrorOnFailure = true, + ErrorMessageOnFailure = string.Empty + }; + + [Test] + public void Should_Throw_Error_When_ThrowErrorOnFailure_Is_True() + { + var ex = Assert.Throws((TestDelegate)(() => + JSON.QuerySingle(InvalidInput(), DefaultOptions(), CancellationToken.None))); + Assert.That(ex, Is.Not.Null); + } + + [Test] + public void Should_Return_Failed_Result_When_ThrowErrorOnFailure_Is_False() + { + var options = DefaultOptions(); + options.ThrowErrorOnFailure = false; + var result = JSON.QuerySingle(InvalidInput(), options, CancellationToken.None); + Assert.That(result.Success, Is.False); + Assert.That(result.Error, Is.Not.Null); + } + + [Test] + public void Should_Use_Custom_ErrorMessageOnFailure() + { + var options = DefaultOptions(); + options.ErrorMessageOnFailure = CustomErrorMessage; + var ex = Assert.Throws((TestDelegate)(() => + JSON.QuerySingle(InvalidInput(), options, CancellationToken.None))); + Assert.That(ex, Is.Not.Null); + Assert.That(ex?.Message, Does.Contain(CustomErrorMessage)); + } +} diff --git a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.UnitTests/Frends.JSON.QuerySingle.UnitTests.csproj b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.UnitTests/Frends.JSON.QuerySingle.UnitTests.csproj index c26034e..ca5bdd1 100644 --- a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.UnitTests/Frends.JSON.QuerySingle.UnitTests.csproj +++ b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.UnitTests/Frends.JSON.QuerySingle.UnitTests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable @@ -12,6 +12,8 @@ + + diff --git a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.UnitTests/UnitTests.cs b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.UnitTests/UnitTests.cs index 4182421..50b3d79 100644 --- a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.UnitTests/UnitTests.cs +++ b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.UnitTests/UnitTests.cs @@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System.Threading; namespace Frends.JSON.QuerySingle.UnitTests; @@ -54,7 +55,7 @@ public void TestQuerySingle() ErrorWhenNotMatched = true, }; - var result = JSON.QuerySingle(input, options); + var result = JSON.QuerySingle(input, options, CancellationToken.None); Assert.IsTrue(result.Success); Assert.IsInstanceOfType(result.Data, typeof(JObject)); } @@ -73,8 +74,8 @@ public void QueryShouldThrowIfOptionSetAndNothingIsFound() ErrorWhenNotMatched = true, }; - var ex = Assert.ThrowsException(() => JSON.QuerySingle(input, options)); - Assert.IsTrue(ex.Message.Contains("Property 'Manufacturer' does not exist on JObject.")); + var ex = Assert.ThrowsException(() => JSON.QuerySingle(input, options, CancellationToken.None)); + Assert.IsTrue(ex.Message.Contains("Property 'Manufacturer' does not exist on JObject.") || (ex.InnerException != null && ex.InnerException.Message.Contains("Property 'Manufacturer' does not exist on JObject."))); } [TestMethod] @@ -91,7 +92,7 @@ public void QuerySingleShouldThrowIfOptionSetAndFilterMatchesNothing() ErrorWhenNotMatched = true, }; - Assert.ThrowsException(() => JSON.QuerySingle(input, options)); + Assert.ThrowsException(() => JSON.QuerySingle(input, options, CancellationToken.None)); } [TestMethod] @@ -108,7 +109,7 @@ public void QuerySingleShouldNotThrowIfOptionNotSetAndNothingIsFound() ErrorWhenNotMatched = false, }; - var result = JSON.QuerySingle(input, options); + var result = JSON.QuerySingle(input, options, CancellationToken.None); Assert.IsTrue(result.Success); Assert.IsNull(result.Data); } diff --git a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Definitions/Error.cs b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Definitions/Error.cs new file mode 100644 index 0000000..4767aa3 --- /dev/null +++ b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Definitions/Error.cs @@ -0,0 +1,19 @@ +using System; + +namespace Frends.JSON.QuerySingle.Definitions; + +/// +/// Error details. +/// +public class Error +{ + /// + /// Error message. + /// + public string Message { get; set; } + + /// + /// Additional information about the error (exception). + /// + public Exception AdditionalInfo { get; set; } +} diff --git a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Definitions/Options.cs b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Definitions/Options.cs index 3467752..b714c51 100644 --- a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Definitions/Options.cs +++ b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Definitions/Options.cs @@ -1,4 +1,7 @@ -namespace Frends.JSON.QuerySingle.Definitions; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Frends.JSON.QuerySingle.Definitions; /// /// Options parameters. @@ -10,4 +13,19 @@ public class Options /// /// true public bool ErrorWhenNotMatched { get; set; } + + /// + /// Throw an exception if the task fails. + /// + /// true + [DefaultValue(true)] + public bool ThrowErrorOnFailure { get; set; } = true; + + /// + /// Custom error message to include when the task fails. Leave empty to use the default error message. + /// + /// + [DisplayFormat(DataFormatString = "Text")] + [DefaultValue("")] + public string ErrorMessageOnFailure { get; set; } = string.Empty; } \ No newline at end of file diff --git a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Definitions/Result.cs b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Definitions/Result.cs index 1169b0e..0975e2d 100644 --- a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Definitions/Result.cs +++ b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Definitions/Result.cs @@ -9,17 +9,16 @@ public class Result /// Operation complete without errors. /// /// true - public bool Success { get; private set; } + public bool Success { get; set; } /// /// Result data. /// /// {{ "Name": "Foo", "Products": [{ "Name": "Bar", "Price": 1 }]}} - public dynamic Data { get; private set; } + public dynamic Data { get; set; } - internal Result(bool success, object data) - { - Success = success; - Data = data; - } -} \ No newline at end of file + /// + /// Error information when the task fails and ThrowErrorOnFailure is false. + /// + public Error Error { get; set; } +} diff --git a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.csproj b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.csproj index 0f8f7b2..e65896f 100644 --- a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.csproj +++ b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle.csproj @@ -1,8 +1,8 @@  - net6.0 - 1.3.0 + net8.0 + 1.4.0 Frends Frends Frends diff --git a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Helpers/ErrorHandler.cs b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Helpers/ErrorHandler.cs new file mode 100644 index 0000000..70d41d0 --- /dev/null +++ b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/Helpers/ErrorHandler.cs @@ -0,0 +1,45 @@ +using Frends.JSON.QuerySingle.Definitions; +using System; + +namespace Frends.JSON.QuerySingle.Helpers; + +internal static class ErrorHandler +{ + internal static Result Handle(this Exception exception, Options options, bool throwCanceled = true) + { + ThrowIfCanceled(exception, throwCanceled); + if (options.ThrowErrorOnFailure) ThrowBaseException(exception, options.ErrorMessageOnFailure); + + return ReturnResult(exception, options.ErrorMessageOnFailure); + } + + private static void ThrowIfCanceled(Exception exception, bool throwCanceled = true) + { + if (throwCanceled && exception is OperationCanceledException) throw exception; + } + + private static void ThrowBaseException(Exception exception, string customMessage = null) + { + if (string.IsNullOrEmpty(customMessage)) + throw new Exception(exception.Message, exception); + + throw new Exception(customMessage, exception); + } + + private static Result ReturnResult(Exception exception, string customMessage = null) + { + var errorMessage = string.IsNullOrEmpty(customMessage) + ? exception.Message + : $"{customMessage}: {exception.Message}"; + + return new Result + { + Success = false, + Error = new Error + { + Message = errorMessage, + AdditionalInfo = exception, + } + }; + } +} diff --git a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/QuerySingle.cs b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/QuerySingle.cs index 20e4fd5..e280f63 100644 --- a/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/QuerySingle.cs +++ b/Frends.JSON.QuerySingle/Frends.JSON.QuerySingle/QuerySingle.cs @@ -1,17 +1,20 @@ using Frends.JSON.QuerySingle.Definitions; +using Frends.JSON.QuerySingle.Helpers; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System; using System.ComponentModel; using System.IO; using System.Reflection; using System.Runtime.Loader; +using System.Threading; namespace Frends.JSON.QuerySingle; /// /// JSON Task. /// -public class JSON +public static class JSON { /// Mem cleanup. static JSON() @@ -28,16 +31,25 @@ static JSON() /// /// Input parameters. /// Optional parameters. - /// Object { bool Success, dynamic Data } - public static Result QuerySingle([PropertyTab] Input input, [PropertyTab] Options options) + /// Cancellation token. + /// Object { bool Success, dynamic Data, Error Error } + public static Result QuerySingle([PropertyTab] Input input, [PropertyTab] Options options, CancellationToken cancellationToken) { - JToken jToken = GetJTokenFromInput(input.Json); - JToken result = jToken.SelectToken(input.Query, options.ErrorWhenNotMatched); + try + { + cancellationToken.ThrowIfCancellationRequested(); + JToken jToken = GetJTokenFromInput(input.Json); + JToken result = jToken.SelectToken(input.Query, options.ErrorWhenNotMatched); - if (result == null && options.ErrorWhenNotMatched) - throw new JsonException($"No matches found for query '{input.Query}'."); + if (result == null && options.ErrorWhenNotMatched) + throw new JsonException($"No matches found for query '{input.Query}'."); - return new Result(true, result); + return new Result { Success = true, Data = result }; + } + catch (Exception ex) + { + return ex.Handle(options); + } } private static object GetJTokenFromInput(dynamic json)