diff --git a/src/modules/Elsa.Expressions.CSharp/Handlers/GenerateWorkflowVariableAccessors.cs b/src/modules/Elsa.Expressions.CSharp/Handlers/GenerateWorkflowVariableAccessors.cs index c04a2cbc5..d15ff70e9 100644 --- a/src/modules/Elsa.Expressions.CSharp/Handlers/GenerateWorkflowVariableAccessors.cs +++ b/src/modules/Elsa.Expressions.CSharp/Handlers/GenerateWorkflowVariableAccessors.cs @@ -38,7 +38,8 @@ public class GenerateWorkflowVariableAccessors(IOptions options) { foreach (var variable in variables.Where(x => x.Name.IsValidVariableName())) { - var variableName = variable.Name.Pascalize(); + var propertyName = variable.Name.Pascalize(); + var variableName = variable.Name; var variableType = variable.GetVariableType(); // Check if the variable type is ExpandoObject @@ -48,7 +49,7 @@ public class GenerateWorkflowVariableAccessors(IOptions options) var displayTypeName = isExpandoObject ? "dynamic" : variableType.GetFriendlyTypeName(Brackets.Angle); var retrieveTypeName = isExpandoObject ? "ExpandoObject" : displayTypeName; - sb.AppendLine($"\tpublic {displayTypeName} {variableName}"); + sb.AppendLine($"\tpublic {displayTypeName} {propertyName}"); sb.AppendLine("\t{"); sb.AppendLine($"\t\tget => Get<{retrieveTypeName}>(\"{variableName}\");"); sb.AppendLine($"\t\tset => Set(\"{variableName}\", value);"); diff --git a/test/unit/Elsa.Expressions.UnitTests/CSharp/GenerateWorkflowVariableAccessorsTests.cs b/test/unit/Elsa.Expressions.UnitTests/CSharp/GenerateWorkflowVariableAccessorsTests.cs new file mode 100644 index 000000000..c4236e0c5 --- /dev/null +++ b/test/unit/Elsa.Expressions.UnitTests/CSharp/GenerateWorkflowVariableAccessorsTests.cs @@ -0,0 +1,91 @@ +using Elsa.Expressions.CSharp.Handlers; +using Elsa.Expressions.CSharp.Notifications; +using Elsa.Expressions.CSharp.Options; +using Elsa.Expressions.CSharp.Services; +using Elsa.Expressions.Models; +using Elsa.Extensions; +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Memory; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; + +namespace Elsa.Expressions.UnitTests.CSharp; + +public class GenerateWorkflowVariableAccessorsTests +{ + [Fact] + public async Task PascalizedProperty_ReadsCamelCaseVariable_ByRealName() + { + var context = CreateContext(new Variable("orderId", "ORD-1")); + var evaluator = CreateEvaluator(); + + var result = await evaluator.EvaluateAsync("Variables.OrderId", typeof(string), context, new ExpressionEvaluatorOptions()); + + Assert.Equal("ORD-1", result); + } + + [Fact] + public async Task PascalizedProperty_WritesCamelCaseVariable_WithoutCreatingPascalizedShadow() + { + var context = CreateContext(new Variable("orderId", "ORD-1")); + var evaluator = CreateEvaluator(); + + var result = await evaluator.EvaluateAsync( + """ + Variables.OrderId = "ORD-2"; + Variables.OrderId + """, + typeof(string), + context, + new ExpressionEvaluatorOptions()); + + Assert.Equal("ORD-2", result); + Assert.Equal("ORD-2", context.GetVariable("orderId")); + Assert.Null(context.GetVariable("OrderId")); + } + + [Fact] + public async Task PascalizedProperty_ReadsAlreadyPascalCaseVariable() + { + var context = CreateContext(new Variable("OrderId", "ORD-3")); + var evaluator = CreateEvaluator(); + + var result = await evaluator.EvaluateAsync("Variables.OrderId", typeof(string), context, new ExpressionEvaluatorOptions()); + + Assert.Equal("ORD-3", result); + } + + private static ExpressionExecutionContext CreateContext(Variable variable) + { + var memoryRegister = new MemoryRegister(); + memoryRegister.Declare(variable); + return new ExpressionExecutionContext(new ServiceCollection().BuildServiceProvider(), memoryRegister); + } + + private static CSharpEvaluator CreateEvaluator() + { + var csharpOptions = new CSharpOptions + { + AllowHostCodeExecution = true + }; + var options = Microsoft.Extensions.Options.Options.Create(csharpOptions); + var accessors = new GenerateWorkflowVariableAccessors(options); + var assemblies = new AddAssembliesAndReferencesFromOptions(options); + var notificationSender = Substitute.For(); + + notificationSender + .SendAsync(Arg.Any(), Arg.Any()) + .Returns(async callInfo => + { + if (callInfo.Arg() is not EvaluatingCSharp evaluating) + return; + + var cancellationToken = callInfo.Arg(); + await assemblies.HandleAsync(evaluating, cancellationToken); + await accessors.HandleAsync(evaluating, cancellationToken); + }); + + return new CSharpEvaluator(notificationSender, options, new MemoryCache(new MemoryCacheOptions())); + } +}