diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md index 622e4240e..acf0d9f1d 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -336,6 +336,9 @@ Assert.Equal(WorkflowStatus.Finished, resumed.WorkflowInstance.Status); | `WorkflowTestFixture.GetOutcomes` | Get all outcomes from specific activity | Asserting activity outcomes in integration tests | | `WorkflowTestFixture.HasOutcome` | Check if activity produced specific outcome | Asserting single outcome in integration tests | | `WorkflowTestFixture.GetActivityStatus` | Get execution status of specific activity | Asserting activity status (Faulted, Completed, etc.) | +| `WorkflowTestFixture.CreateWorkflowExecutionContextAsync` | Create workflow execution context without running workflow | Testing workflow-level concerns or base context setup | +| `WorkflowTestFixture.CreateActivityExecutionContextAsync` | Create activity execution context (2 overloads) | Testing activity scheduling/execution or when needing activity context | +| `WorkflowTestFixture.CreateExpressionExecutionContextAsync` | Create expression execution context (2 overloads) | Testing expression evaluators (JavaScript, Liquid, C#) with variables | | `context.GetActivityOutput` | Get output from activity using expression selector | Asserting activity outputs in unit tests | | `context.HasScheduledActivity` | Check if activity is scheduled | Verifying scheduling behavior in unit tests | | `context.GetOutcomes` | Get all outcomes from activity execution | Asserting multiple outcomes in unit tests | @@ -439,6 +442,95 @@ Assert.Equal(ActivityStatus.Faulted, status); - `ActivityStatus.Canceled` - Activity was canceled - `ActivityStatus.Faulted` - Activity encountered an error +### Creating Execution Contexts for Testing + +`WorkflowTestFixture` provides layered methods for creating execution contexts at different levels, giving you fine-grained control over test setup: + +#### Creating a Workflow Execution Context + +Use `CreateWorkflowExecutionContextAsync` to create a minimal workflow execution context without running the workflow: + +```csharp +var context = await _fixture.CreateWorkflowExecutionContextAsync(variables: new[] +{ + new Variable("Counter", 0) +}); +``` + +#### Creating an Activity Execution Context + +Use `CreateActivityExecutionContextAsync` to create an activity execution context. Two overloads available: + +**Without existing workflow context (creates one automatically):** +```csharp +var activityContext = await _fixture.CreateActivityExecutionContextAsync( + activity: myActivity, + variables: new[] { new Variable("MyVar", "value") } +); +``` + +**With existing workflow context:** +```csharp +var workflowContext = await _fixture.CreateWorkflowExecutionContextAsync(); +var activityContext = await _fixture.CreateActivityExecutionContextAsync( + workflowContext, + activity: myActivity +); +``` + +#### Creating an Expression Execution Context + +Use `CreateExpressionExecutionContextAsync` to create a context for testing expression evaluation (e.g., JavaScript, Liquid). Variables are properly registered and accessible via dynamic accessors. Two overloads available: + +**Without existing activity context (creates one automatically):** +```csharp +var expressionContext = await _fixture.CreateExpressionExecutionContextAsync(new[] +{ + new Variable("MyVariable", "test value") +}); + +// Variables are accessible via dynamic accessors in expressions +// e.g., getMyVariable() and setMyVariable(value) in JavaScript +``` + +**With existing activity context:** +```csharp +var activityContext = await _fixture.CreateActivityExecutionContextAsync(); +var expressionContext = await _fixture.CreateExpressionExecutionContextAsync( + activityContext, + variables: new[] { new Variable("Count", 42) } +); +``` + +**Example: Testing JavaScript Expression Evaluation** +```csharp +[Fact] +public async Task Dynamic_Variable_Accessors_Should_Work() +{ + // Arrange + var script = @" + setMyVariable('updated value'); + return getMyVariable(); + "; + var context = await _fixture.CreateExpressionExecutionContextAsync(new[] + { + new Variable("MyVariable", "initial value") + }); + + // Act + var evaluator = _fixture.Services.GetRequiredService(); + var result = await evaluator.EvaluateAsync(script, typeof(string), context); + + // Assert + Assert.Equal("updated value", result); +} +``` + +**When to use each method:** +- Use `CreateWorkflowExecutionContextAsync` when testing workflow-level concerns or when you need a base context for further customization +- Use `CreateActivityExecutionContextAsync` when testing activity scheduling, execution, or when you need access to activity-specific context +- Use `CreateExpressionExecutionContextAsync` when testing expression evaluators (JavaScript, Liquid, C#) or when you need variables to be accessible in expressions + --- ## Failure testing (faults & incidents) diff --git a/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs b/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs index 63f3cede3..6e4c9ecda 100644 --- a/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs +++ b/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs @@ -1,7 +1,8 @@ -using Elsa.Extensions; +using Elsa.Expressions.Models; using Elsa.Features.Services; using Elsa.Workflows; using Elsa.Workflows.Activities; +using Elsa.Workflows.Memory; using Elsa.Workflows.Models; using Elsa.Workflows.State; using JetBrains.Annotations; @@ -25,8 +26,8 @@ public class WorkflowTestFixture /// The test output helper public WorkflowTestFixture(ITestOutputHelper testOutputHelper) { - _testApplicationBuilder = new TestApplicationBuilder(testOutputHelper); - CapturingTextWriter = new CapturingTextWriter(); + _testApplicationBuilder = new(testOutputHelper); + CapturingTextWriter = new(); _testApplicationBuilder.WithCapturingTextWriter(CapturingTextWriter); } @@ -38,7 +39,7 @@ public class WorkflowTestFixture /// /// Gets the service provider. Throws if Build() hasn't been called yet. /// - private IServiceProvider Services => _services ?? throw new InvalidOperationException("Build() must be called before accessing services"); + public IServiceProvider Services => _services ?? throw new InvalidOperationException("Build() must be called before accessing services"); /// /// Configures Elsa features. @@ -192,4 +193,96 @@ public class WorkflowTestFixture return activityContext?.Status; } + + /// + /// Creates a WorkflowExecutionContext for testing. + /// This creates a minimal workflow execution context without executing the workflow. + /// + /// Optional workflow variables to include in the workflow + /// A WorkflowExecutionContext that can be used for testing + public async Task CreateWorkflowExecutionContextAsync(Variable[]? variables = null) + { + if (_services == null) + await BuildAsync(); + + // Create a minimal workflow with variables + var workflow = new Workflow + { + Root = new Sequence() + }; + + if (variables != null) + foreach (var variable in variables) + workflow.Variables.Add(variable); + + // Build the workflow graph + var workflowGraphBuilder = Services.GetRequiredService(); + var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow); + + // Create workflow execution context + return await WorkflowExecutionContext.CreateAsync( + Services, + workflowGraph, + $"test-instance-{Guid.NewGuid()}", + CancellationToken.None + ); + } + + /// + /// Creates an ActivityExecutionContext for testing. + /// Creates a workflow execution context first, then creates an activity execution context for the specified activity. + /// + /// The activity to create a context for. If null, uses the workflow itself. + /// Optional workflow variables to include + /// An ActivityExecutionContext that can be used for testing + public async Task CreateActivityExecutionContextAsync(IActivity? activity = null, Variable[]? variables = null) + { + var workflowExecutionContext = await CreateWorkflowExecutionContextAsync(variables); + return await CreateActivityExecutionContextAsync(workflowExecutionContext, activity); + } + + /// + /// Creates an ActivityExecutionContext for testing using an existing WorkflowExecutionContext. + /// + /// The workflow execution context to use + /// The activity to create a context for. If null, uses the workflow itself. + /// An ActivityExecutionContext that can be used for testing + public async Task CreateActivityExecutionContextAsync(WorkflowExecutionContext workflowExecutionContext, IActivity? activity = null) + { + // Use the workflow itself if no activity specified, as Workflow implements IVariableContainer. + // This ensures variables are accessible. + var targetActivity = activity ?? workflowExecutionContext.Workflow; + return await workflowExecutionContext.CreateActivityExecutionContextAsync(targetActivity); + } + + /// + /// Creates an ExpressionExecutionContext for testing expression evaluation. + /// This creates a minimal workflow and activity execution context, then initializes variables. + /// Variables are properly registered and accessible via dynamic accessors (e.g., getMyVariable, setMyVariable). + /// + /// Optional workflow variables to include in the execution context + /// An ExpressionExecutionContext that can be used for expression evaluation + public async Task CreateExpressionExecutionContextAsync(Variable[]? variables = null) + { + var activityContext = await CreateActivityExecutionContextAsync(activity: null, variables: variables); + return await CreateExpressionExecutionContextAsync(activityContext, variables); + } + + /// + /// Creates an ExpressionExecutionContext using an existing ActivityExecutionContext. + /// Initializes variables if provided. + /// + /// The activity execution context to use + /// Optional workflow variables to initialize + /// An ExpressionExecutionContext that can be used for expression evaluation + public Task CreateExpressionExecutionContextAsync(ActivityExecutionContext activityContext, Variable[]? variables = null) + { + // Initialize variables in the execution context if provided + // Use Variable.Set() to properly register variables (same approach as ActivityTestFixture) + if (variables != null) + foreach (var variable in variables) + variable.Set(activityContext.ExpressionExecutionContext, variable.Value); + + return Task.FromResult(activityContext.ExpressionExecutionContext); + } } diff --git a/test/integration/Elsa.JavaScript.IntegrationTests/JintJavaScriptEvaluatorTests.cs b/test/integration/Elsa.JavaScript.IntegrationTests/JintJavaScriptEvaluatorTests.cs new file mode 100644 index 000000000..4e4489ff0 --- /dev/null +++ b/test/integration/Elsa.JavaScript.IntegrationTests/JintJavaScriptEvaluatorTests.cs @@ -0,0 +1,197 @@ +using Elsa.Expressions.Contracts; +using Elsa.Expressions.JavaScript.Contracts; +using Elsa.Expressions.Models; +using Elsa.Testing.Shared; +using Elsa.Workflows.Memory; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.JavaScript.IntegrationTests; + +/// +/// Tests for JintJavaScriptEvaluator to ensure all custom functions remain available. +/// These tests protect against accidental renaming or removal of JavaScript functions. +/// +public class JintJavaScriptEvaluatorTests(ITestOutputHelper testOutputHelper) +{ + private readonly WorkflowTestFixture _fixture = new(testOutputHelper); + + [Theory(DisplayName = "Common workflow functions should be available")] + [InlineData("getWorkflowDefinitionId")] + [InlineData("getWorkflowDefinitionVersionId")] + [InlineData("getWorkflowDefinitionVersion")] + [InlineData("getWorkflowInstanceId")] + [InlineData("getCorrelationId")] + [InlineData("getWorkflowInstanceName")] + public async Task Common_Workflow_Functions_Should_Be_Available(string functionName) + { + // Arrange + var script = $"return typeof {functionName};"; + var context = await CreateExpressionExecutionContextAsync(); + var evaluator = _fixture.Services.GetRequiredService(); + + // Act + var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string; + + // Assert - function should exist (not 'undefined') + Assert.Equal("function", result); + } + + [Theory(DisplayName = "Workflow mutator functions should be available")] + [InlineData("setCorrelationId")] + [InlineData("setWorkflowInstanceName")] + [InlineData("setVariable")] + public async Task Workflow_Mutator_Functions_Should_Be_Available(string functionName) + { + // Arrange + var script = $"return typeof {functionName};"; + var context = await CreateExpressionExecutionContextAsync(); + var evaluator = _fixture.Services.GetRequiredService(); + + // Act + var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string; + + // Assert - function should exist (not 'undefined') + Assert.Equal("function", result); + } + + [Theory(DisplayName = "Variable and input/output accessor functions should be available")] + [InlineData("getVariable")] + [InlineData("getInput")] + [InlineData("getOutputFrom")] + [InlineData("getLastResult")] + public async Task Variable_And_IO_Functions_Should_Be_Available(string functionName) + { + // Arrange + var script = $"return typeof {functionName};"; + var context = await CreateExpressionExecutionContextAsync(); + var evaluator = _fixture.Services.GetRequiredService(); + + // Act + var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string; + + // Assert - function should exist (not 'undefined') + Assert.Equal("function", result); + } + + [Theory(DisplayName = "String utility functions should be available")] + [InlineData("isNullOrWhiteSpace")] + [InlineData("isNullOrEmpty")] + public async Task String_Utility_Functions_Should_Be_Available(string functionName) + { + // Arrange + var script = $"return typeof {functionName};"; + var context = await CreateExpressionExecutionContextAsync(); + var evaluator = _fixture.Services.GetRequiredService(); + + // Act + var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string; + + // Assert - function should exist (not 'undefined') + Assert.Equal("function", result); + } + + [Theory(DisplayName = "GUID functions should be available")] + [InlineData("parseGuid")] + [InlineData("newGuid")] + [InlineData("newGuidString")] + [InlineData("newShortGuid")] + [InlineData("getGuidString")] // Deprecated but should still exist + [InlineData("getShortGuid")] // Deprecated but should still exist + public async Task GUID_Functions_Should_Be_Available(string functionName) + { + // Arrange + var script = $"return typeof {functionName};"; + var context = await CreateExpressionExecutionContextAsync(); + var evaluator = _fixture.Services.GetRequiredService(); + + // Act + var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string; + + // Assert - function should exist (not 'undefined') + Assert.Equal("function", result); + } + + [Theory(DisplayName = "Encoding and serialization functions should be available")] + [InlineData("toJson")] + [InlineData("bytesToString")] + [InlineData("bytesFromString")] + [InlineData("bytesToBase64")] + [InlineData("bytesFromBase64")] + [InlineData("stringToBase64")] + [InlineData("stringFromBase64")] + [InlineData("streamToBytes")] + [InlineData("streamToBase64")] + public async Task Encoding_Functions_Should_Be_Available(string functionName) + { + // Arrange + var script = $"return typeof {functionName};"; + var context = await CreateExpressionExecutionContextAsync(); + var evaluator = _fixture.Services.GetRequiredService(); + + // Act + var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string; + + // Assert - function should exist (not 'undefined') + Assert.Equal("function", result); + } + + [Fact(DisplayName = "Variable accessors should be created for workflow variables")] + public async Task Variable_Accessors_Should_Be_Created() + { + // Arrange + var script = "return typeof getMyVariable;"; + var context = await CreateExpressionExecutionContextAsync(variables: + [ + new Variable("MyVariable", 42) + ]); + var evaluator = _fixture.Services.GetRequiredService(); + + // Act + var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string; + + // Assert - getter function should be created for the variable + Assert.Equal("function", result); + } + + [Fact(DisplayName = "Variable setter accessors should be created for workflow variables")] + public async Task Variable_Setter_Accessors_Should_Be_Created() + { + // Arrange + var script = "return typeof setMyVariable;"; + var context = await CreateExpressionExecutionContextAsync(variables: + [ + new Variable("MyVariable", 42) + ]); + var evaluator = _fixture.Services.GetRequiredService(); + + // Act + var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string; + + // Assert - setter function should be created for the variable + Assert.Equal("function", result); + } + + private async Task CreateExpressionExecutionContextAsync(Variable[]? variables = null) + { + await _fixture.BuildAsync(); + + var workflow = new Elsa.Workflows.Activities.Workflow(); + + if (variables != null) + { + foreach (var variable in variables) + { + workflow.Variables.Add(variable); + } + } + + var result = await _fixture.RunActivityAsync(workflow); + var activityContext = result.Journal.ActivityExecutionContexts.First(); + + return new ExpressionExecutionContext( + _fixture.Services, + activityContext.ExpressionExecutionContext.Memory, + cancellationToken: default); + } +} diff --git a/test/integration/Elsa.JavaScript.IntegrationTests/JintJavaScriptFunctionBehaviorTests.cs b/test/integration/Elsa.JavaScript.IntegrationTests/JintJavaScriptFunctionBehaviorTests.cs new file mode 100644 index 000000000..4edff2e92 --- /dev/null +++ b/test/integration/Elsa.JavaScript.IntegrationTests/JintJavaScriptFunctionBehaviorTests.cs @@ -0,0 +1,219 @@ +using Elsa.Expressions.JavaScript.Contracts; +using Elsa.Expressions.Models; +using Elsa.Testing.Shared; +using Elsa.Workflows.Memory; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.JavaScript.IntegrationTests; + +/// +/// Tests that validate the behavior of JavaScript custom functions, not just their existence. +/// +public class JintJavaScriptFunctionBehaviorTests(ITestOutputHelper testOutputHelper) +{ + private readonly WorkflowTestFixture _fixture = new(testOutputHelper); + + [Fact(DisplayName = "All JavaScript functions should execute without errors (smoke test)")] + public async Task All_Functions_Should_Execute_Without_Errors() + { + // Arrange + var script = @" + // Execute all functions that don't require arguments to ensure they work + var results = { + // Workflow info functions + workflowDefId: getWorkflowDefinitionId(), + workflowDefVersionId: getWorkflowDefinitionVersionId(), + workflowDefVersion: getWorkflowDefinitionVersion(), + workflowInstanceId: getWorkflowInstanceId(), + correlationId: getCorrelationId(), + workflowName: getWorkflowInstanceName(), + + // GUID functions + newGuidValue: newGuid(), + newGuidStringValue: newGuidString(), + newShortGuidValue: newShortGuid(), + + // Deprecated GUID functions + getGuidStringValue: getGuidString(), + getShortGuidValue: getShortGuid(), + + // String utility functions + isNullOrWhiteSpaceEmpty: isNullOrWhiteSpace(''), + isNullOrWhiteSpaceText: isNullOrWhiteSpace('text'), + isNullOrEmptyEmpty: isNullOrEmpty(''), + isNullOrEmptyText: isNullOrEmpty('text'), + + // Encoding functions + toJsonValue: toJson({ key: 'value' }), + stringToBase64Value: stringToBase64('test'), + bytesToStringValue: bytesToString([72, 101, 108, 108, 111]), + bytesToBase64Value: bytesToBase64([72, 101, 108, 108, 111]), + }; + + return toJson(results); + "; + + // Act + var result = await EvaluateScriptAsync(script); + + // Assert - script should execute and return JSON + Assert.NotNull(result); + Assert.Contains("workflowDefId", result); + } + + [Fact(DisplayName = "GUID functions should return valid formats")] + public async Task Guid_Functions_Should_Return_Valid_Formats() + { + // Arrange + var script = @" + return { + guid: newGuid().toString(), + guidString: newGuidString(), + shortGuid: newShortGuid(), + parsedGuid: parseGuid('12345678-1234-1234-1234-123456789abc').toString() + }; + "; + + // Act + var result = await EvaluateScriptAsync(script); + + // Assert + var dict = result as IDictionary; + Assert.NotNull(dict); + + // Validate GUID formats + var guidString = dict["guidString"].ToString(); + Assert.Matches("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", guidString ?? ""); + + var shortGuid = dict["shortGuid"]?.ToString(); + Assert.NotNull(shortGuid); + Assert.InRange(shortGuid.Length, 20, 22); // Base64 GUID without padding (can be 20-22 chars) + } + + [Theory(DisplayName = "Encoding functions should round-trip correctly")] + [InlineData("String encoding", @" + var original = 'Hello, World!'; + var base64 = stringToBase64(original); + var decoded = stringFromBase64(base64); + return decoded === original; + ")] + [InlineData("Bytes encoding", @" + var original = bytesFromString('Hello'); + var base64 = bytesToBase64(original); + var decoded = bytesFromBase64(base64); + var text = bytesToString(decoded); + return text === 'Hello'; + ")] + public async Task Encoding_Functions_Should_Round_Trip(string scenario, string script) + { + // Act + var result = await EvaluateScriptAsync(script); + + // Assert - round-trip should preserve the original data + Assert.True(result, $"{scenario} failed to round-trip correctly"); + } + + [Theory(DisplayName = "Setter/getter functions should update and retrieve values correctly")] + [InlineData("setVariable/getVariable", "setVariable('MyVar', 200); return getVariable('MyVar');", 200, "MyVar", 100)] + [InlineData("Dynamic variable accessors", "setMyVariable(999); return getMyVariable();", 999, "MyVariable", 42)] + public async Task Variable_Setters_Should_Update_Values(string scenario, string script, int expectedValue, string variableName, int initialValue) + { + // Arrange - Create context with variable + var context = await CreateExpressionExecutionContextAsync(variables: + [ + new Variable(variableName, initialValue) + ]); + + // Act + var result = await EvaluateScriptAsync(script, context); + + // Assert + Assert.True(result == expectedValue, $"{scenario}: Expected {expectedValue} but got {result}"); + } + + [Theory(DisplayName = "Workflow mutator functions should update workflow properties")] + [InlineData("setCorrelationId", "setCorrelationId('my-correlation-id'); return getCorrelationId();", "my-correlation-id")] + [InlineData("setWorkflowInstanceName", "setWorkflowInstanceName('My Custom Workflow Name'); return getWorkflowInstanceName();", "My Custom Workflow Name")] + public async Task Workflow_Mutators_Should_Update_Properties(string functionName, string script, string expectedValue) + { + // Act + var result = await EvaluateScriptAsync(script); + + // Assert + Assert.True(result == expectedValue, $"{functionName}: Expected '{expectedValue}' but got '{result}'"); + } + + [Fact(DisplayName = "String utility functions should validate correctly")] + public async Task String_Utility_Functions_Should_Validate_Correctly() + { + // Arrange + var script = @" + return { + emptyIsNullOrWhiteSpace: isNullOrWhiteSpace(''), + whitespaceIsNullOrWhiteSpace: isNullOrWhiteSpace(' '), + textIsNullOrWhiteSpace: isNullOrWhiteSpace('text'), + emptyIsNullOrEmpty: isNullOrEmpty(''), + whitespaceIsNullOrEmpty: isNullOrEmpty(' '), + textIsNullOrEmpty: isNullOrEmpty('text') + }; + "; + + // Act + var result = await EvaluateScriptAsync(script); + + // Assert + var dict = result as IDictionary; + Assert.NotNull(dict); + + Assert.True((bool)dict["emptyIsNullOrWhiteSpace"]); + Assert.True((bool)dict["whitespaceIsNullOrWhiteSpace"]); + Assert.False((bool)dict["textIsNullOrWhiteSpace"]); + + Assert.True((bool)dict["emptyIsNullOrEmpty"]); + Assert.False((bool)dict["whitespaceIsNullOrEmpty"]); // Whitespace is not considered empty + Assert.False((bool)dict["textIsNullOrEmpty"]); + } + + [Fact(DisplayName = "toJson should serialize objects correctly")] + public async Task ToJson_Should_Serialize_Objects() + { + // Arrange + var script = @" + var obj = { + name: 'Test', + value: 42, + nested: { inner: true } + }; + return toJson(obj); + "; + + // Act + var result = await EvaluateScriptAsync(script); + + // Assert - should produce valid JSON + Assert.NotNull(result); + Assert.Contains("\"name\"", result); + Assert.Contains("\"Test\"", result); + Assert.Contains("\"value\"", result); + Assert.Contains("42", result); + } + + private Task CreateExpressionExecutionContextAsync(Variable[]? variables = null) + { + return _fixture.CreateExpressionExecutionContextAsync(variables); + } + + /// + /// Helper method to evaluate a JavaScript script and return the result as the specified type. + /// Reduces boilerplate code in tests. + /// + private async Task EvaluateScriptAsync(string script, ExpressionExecutionContext? context = null) + { + context ??= await CreateExpressionExecutionContextAsync(); + var evaluator = _fixture.Services.GetRequiredService(); + var result = await evaluator.EvaluateAsync(script, typeof(T), context); + + return result is T typedResult ? typedResult : (T)Convert.ChangeType(result, typeof(T))!; + } +}