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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Frends.JSON.Query/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [1.3.0] - 2026-07-31
### Changed
- The task class is now properly declared as static, as required by Frends task standards.
- Added a `CancellationToken` parameter so that Frends can cancel the task when needed.
- Added `ThrowErrorOnFailure` and `ErrorMessageOnFailure` options. When `ThrowErrorOnFailure` is set to false, task failures are returned as a result object (with `Success = false` and an `Error` containing the message and exception) instead of raising an exception. The default behaviour (throwing on error) is unchanged.
- The result object now includes an `Error` property that is populated when `ThrowErrorOnFailure` is false.
- Updated target framework from .NET 6 to .NET 8.

## [1.2.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.
Expand Down
49 changes: 49 additions & 0 deletions Frends.JSON.Query/Frends.JSON.Query.UnitTests/ErrorHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Frends.JSON.Query.Definitions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Threading;

namespace Frends.JSON.Query.UnitTests;

[TestClass]
public class ErrorHandlerTests
{
private const string CustomErrorMessage = "CustomErrorMessage";

[TestMethod]
public void Should_Throw_Error_When_ThrowErrorOnFailure_Is_True()
{
var ex = Assert.ThrowsException<Exception>(() =>
JSON.Query(DefaultInput(), DefaultOptions(), CancellationToken.None));
Assert.IsNotNull(ex);
}

[TestMethod]
public void Should_Return_Failed_Result_When_ThrowErrorOnFailure_Is_False()
{
var options = DefaultOptions();
options.ThrowErrorOnFailure = false;
var result = JSON.Query(DefaultInput(), options, CancellationToken.None);
Assert.IsFalse(result.Success);
}

[TestMethod]
public void Should_Use_Custom_ErrorMessageOnFailure()
{
var options = DefaultOptions();
options.ErrorMessageOnFailure = CustomErrorMessage;
var ex = Assert.ThrowsException<Exception>(() =>
JSON.Query(DefaultInput(), options, CancellationToken.None));
Assert.IsNotNull(ex);
StringAssert.Contains(ex.Message, CustomErrorMessage);
}

private static Input DefaultInput() => new()
{
Json = "{\"key\":\"value\"}",
Query = "$.nonexistent"
};

private static Options DefaultOptions() =>
new() { ErrorWhenNotMatched = true, ThrowErrorOnFailure = true, ErrorMessageOnFailure = string.Empty };
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<PackageProjectUrl>https://frends.com/</PackageProjectUrl>
<Product>Frends</Product>
</PropertyGroup>

<ItemGroup>
Expand Down
9 changes: 5 additions & 4 deletions Frends.JSON.Query/Frends.JSON.Query.UnitTests/UnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Threading;

namespace Frends.JSON.Query.UnitTests;

Expand Down Expand Up @@ -53,7 +54,7 @@ public void QueryShouldWorkWithStringInput()
ErrorWhenNotMatched = true,
};

var result = JSON.Query(input, options);
var result = JSON.Query(input, options, CancellationToken.None);
Assert.IsTrue(result.Success);
Assert.AreEqual(2, result.Data.Count());
Assert.AreEqual("Anvil", result.Data.First().ToString());
Expand All @@ -74,7 +75,7 @@ public void QueryShouldWorkWithJTokenInput()
ErrorWhenNotMatched = true,
};

var result = JSON.Query(input, options);
var result = JSON.Query(input, options, CancellationToken.None);
Assert.IsTrue(result.Success);
Assert.AreEqual(2, result.Data.Count());
Assert.AreEqual("Anvil", result.Data.First().ToString());
Expand All @@ -94,7 +95,7 @@ public void QueryShouldThrowIfOptionSetAndFilterMatchesNothing()
ErrorWhenNotMatched = true,
};

Assert.ThrowsException<JsonException>(() => JSON.Query(input, options));
Assert.ThrowsException<Exception>(() => JSON.Query(input, options, CancellationToken.None));
}

[TestMethod]
Expand All @@ -111,7 +112,7 @@ public void QueryShouldNotThrowIfOptionNotSetAndNothingIsFound()
ErrorWhenNotMatched = false,
};

var result = JSON.Query(input, options);
var result = JSON.Query(input, options, CancellationToken.None);
Assert.IsTrue(result.Success);
Assert.IsNotNull(result.Data);
Assert.IsFalse(result.Data.Any());
Expand Down
19 changes: 19 additions & 0 deletions Frends.JSON.Query/Frends.JSON.Query/Definitions/Error.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;

namespace Frends.JSON.Query.Definitions;

/// <summary>
/// Error information returned when the task fails and ThrowErrorOnFailure is false.
/// </summary>
public class Error
{
/// <summary>
/// Error message.
/// </summary>
public string Message { get; internal set; }

/// <summary>
/// The exception that caused the error.
/// </summary>
public Exception AdditionalInfo { get; internal set; }
}
18 changes: 17 additions & 1 deletion Frends.JSON.Query/Frends.JSON.Query/Definitions/Options.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Frends.JSON.Query.Definitions;
using System.ComponentModel;

namespace Frends.JSON.Query.Definitions;

/// <summary>
/// Options parameters.
Expand All @@ -10,4 +12,18 @@ public class Options
/// </summary>
/// <example>true</example>
public bool ErrorWhenNotMatched { get; set; }

/// <summary>
/// If true, exceptions are thrown on failure. If false, errors are returned as part of the result.
/// </summary>
/// <example>true</example>
[DefaultValue(true)]
public bool ThrowErrorOnFailure { get; set; } = true;

/// <summary>
/// Custom error message to use when an error occurs. Leave empty to use the original exception message.
/// </summary>
/// <example></example>
[DefaultValue("")]
public string ErrorMessageOnFailure { get; set; } = string.Empty;
}
13 changes: 6 additions & 7 deletions Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@ public class Result
/// Operation complete without errors.
/// </summary>
/// <example>true</example>
public bool Success { get; private set; }
public bool Success { get; internal set; }

/// <summary>
/// Result data.
/// </summary>
/// <example>[ { Foo }, { Bar } ]</example>
public IEnumerable<object> Data { get; private set; }
public IEnumerable<object> Data { get; internal set; }

internal Result(bool success, IEnumerable<object> data)
{
Success = success;
Data = data;
}
/// <summary>
/// Error information, populated when the task fails and ThrowErrorOnFailure is false.
/// </summary>
public Error Error { get; internal set; }
}
4 changes: 2 additions & 2 deletions Frends.JSON.Query/Frends.JSON.Query/Frends.JSON.Query.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<Version>1.2.0</Version>
<TargetFrameworks>net8.0</TargetFrameworks>
<Version>1.3.0</Version>
<Authors>Frends</Authors>
<Copyright>Frends</Copyright>
<Company>Frends</Company>
Expand Down
54 changes: 54 additions & 0 deletions Frends.JSON.Query/Frends.JSON.Query/Helpers/ErrorHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Frends.JSON.Query.Definitions;
using System;

namespace Frends.JSON.Query.Helpers;

internal static class ErrorHandler
{
/// <summary>
/// Handle an exception according to the task options.
/// </summary>
/// <param name="exception">The exception to handle.</param>
/// <param name="options">Task options that control whether failures are returned as a Result object or thrown.</param>
/// <param name="throwCanceled">
/// When true, an OperationCanceledException is rethrown immediately.
/// When false, cancellation is handled like any other failure.
/// </param>
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,
},
};
}
}
28 changes: 20 additions & 8 deletions Frends.JSON.Query/Frends.JSON.Query/Query.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
using Frends.JSON.Query.Definitions;
using Frends.JSON.Query.Helpers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading;

namespace Frends.JSON.Query;

/// <summary>
/// JSON Task.
/// </summary>
public class JSON
public static class JSON
{
/// Mem cleanup.
static JSON()
Expand All @@ -29,16 +32,25 @@ static JSON()
/// </summary>
/// <param name="input">Input parameters</param>
/// <param name="options">Optional parameter.</param>
/// <returns>Object { bool Success, IEnumerable&lt;object&gt; Data }</returns>
public static Result Query([PropertyTab] Input input, [PropertyTab] Options options)
/// <param name="cancellationToken">Token generated by frends to stop this Task.</param>
/// <returns>Object { bool Success, IEnumerable&lt;object&gt; Data, Error Error }</returns>
public static Result Query([PropertyTab] Input input, [PropertyTab] Options options, CancellationToken cancellationToken)
{
JToken jToken = GetJTokenFromInput(input.Json);
var tokens = jToken.SelectTokens(input.Query, options.ErrorWhenNotMatched).ToList();
try
{
cancellationToken.ThrowIfCancellationRequested();
JToken jToken = GetJTokenFromInput(input.Json);
var tokens = jToken.SelectTokens(input.Query, options.ErrorWhenNotMatched).ToList();

if (tokens.Count == 0 && options.ErrorWhenNotMatched)
throw new JsonException($"No matches found for query '{input.Query}'.");
if (tokens.Count == 0 && options.ErrorWhenNotMatched)
throw new JsonException($"No matches found for query '{input.Query}'.");

return new Result(true, tokens);
return new Result { Success = true, Data = tokens };
}
catch (Exception ex)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot use Result Handle method that was provided in initial prompt instead this code in main method for error handler options. You can also remove newly added constructor with error and add error filed to current constructor, or remove both constructors if they are not needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the latest commit. The inline error handling has been extracted into a new Helpers/ErrorHandler.cs following the standard Handle extension method pattern. Query.cs now simply calls ex.Handle(options) in the catch block, and the success path uses an object initializer. Both Result constructors were removed — all three properties now use internal set, which is what allows ErrorHandler to use the object initializer syntax.

{
return ex.Handle(options);
}
}

private static object GetJTokenFromInput(dynamic json)
Expand Down
Loading