From 765dab24ade698ad003e9036a2b6e87fae7ff7ab Mon Sep 17 00:00:00 2001 From: Andrei-Liviu Ungureanu Date: Thu, 11 Apr 2024 16:10:09 +0300 Subject: [PATCH 1/2] Compile C# expressions on demand --- .../CSharp/Activities/CSharpDesignerHelper.cs | 32 +++++++ .../CSharp/Activities/CSharpReference.cs | 90 ++++++++++++++++++- .../CSharp/Activities/CSharpValue.cs | 66 +++++++++++++- 3 files changed, 182 insertions(+), 6 deletions(-) diff --git a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpDesignerHelper.cs b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpDesignerHelper.cs index dd66abde..70211ff0 100644 --- a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpDesignerHelper.cs +++ b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpDesignerHelper.cs @@ -6,6 +6,7 @@ using System.Activities.ExpressionParser; using System.Activities.Expressions; using System.Collections.Generic; +using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using Microsoft.VisualBasic.Activities; @@ -22,7 +23,38 @@ protected override JustInTimeCompiler CreateCompiler(HashSet reference public CSharpHelper(string expressionText, HashSet assemblyReferences, HashSet namespaceImportsNames) : base(expressionText, assemblyReferences, namespaceImportsNames) { } + private CSharpHelper(string expressionText) : base(expressionText) { } + internal const string Language = "C#"; + + public static Expression> Compile(string expressionText, CodeActivityPublicEnvironmentAccessor publicAccessor, bool isLocationExpression) + { + GetAllImportReferences(publicAccessor.ActivityMetadata.CurrentActivity, false, + out var localNamespaces, + out var localAssemblies); + + var helper = new CSharpHelper(expressionText); + var localReferenceAssemblies = new HashSet(); + var localImports = new HashSet(localNamespaces); + foreach (var assemblyReference in localAssemblies) + { + if (assemblyReference.Assembly != null) + { + // directly add the Assembly to the list + // so that we don't have to go through + // the assembly resolution process + helper.ReferencedAssemblies ??= new HashSet(); + helper.ReferencedAssemblies.Add(assemblyReference.Assembly); + } + else if (assemblyReference.AssemblyName != null) + { + localReferenceAssemblies.Add(assemblyReference); + } + } + + helper.Initialize(localReferenceAssemblies, localImports); + return helper.Compile(publicAccessor, isLocationExpression); + } } internal class CSharpExpressionFactory : ExpressionFactory diff --git a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpReference.cs b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpReference.cs index 121afdad..de616fcc 100644 --- a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpReference.cs +++ b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpReference.cs @@ -3,13 +3,16 @@ using System; using System.Activities; +using System.Activities.ExpressionParser; using System.Activities.Expressions; using System.Activities.Internals; +using System.Activities.Runtime; using System.Activities.Validation; using System.ComponentModel; using System.Diagnostics; using System.Linq.Expressions; using System.Windows.Markup; +using ActivityContext = System.Activities.ActivityContext; namespace Microsoft.CSharp.Activities; @@ -18,6 +21,8 @@ namespace Microsoft.CSharp.Activities; public class CSharpReference : TextExpressionBase>, ITextExpression { private CompiledExpressionInvoker _invoker; + private LocationFactory _locationFactory; + private Expression> _expressionTree; public CSharpReference() => UseOldFastPath = true; @@ -28,13 +33,92 @@ public class CSharpReference : TextExpressionBase>, I [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public override string Language => CSharpHelper.Language; - public override Expression GetExpressionTree() => IsMetadataCached ? _invoker.GetExpressionTree() : throw FxTrace.Exception.AsError(new InvalidOperationException(SR.ActivityIsUncached)); + public override Expression GetExpressionTree() + { + if (IsMetadataCached) + { + if (_expressionTree == null) + { + if (_invoker != null) + { + return _invoker.GetExpressionTree(); + } + // it's safe to create this CodeActivityMetadata here, + // because we know we are using it only as lookup purpose. + var metadata = new CodeActivityMetadata(this, GetParentEnvironment(), false); + var publicAccessor = CodeActivityPublicEnvironmentAccessor.CreateWithoutArgument(metadata); + try + { + _expressionTree = CompileLocationExpression(publicAccessor, out var validationError); + if (validationError != null) + { + throw FxTrace.Exception.AsError(new InvalidOperationException(SR.ExpressionTamperedSinceLastCompiled(validationError))); + } + } + finally + { + metadata.Dispose(); + } + } + Fx.Assert(_expressionTree.NodeType == ExpressionType.Lambda, "Lambda expression required"); + return ExpressionUtilities.RewriteNonCompiledExpressionTree(_expressionTree); + } + throw FxTrace.Exception.AsError(new InvalidOperationException(SR.ActivityIsUncached)); + } + + protected override Location Execute(CodeActivityContext context) + { + if (_expressionTree == null) + { + return (Location)_invoker.InvokeExpression(context); + } + _locationFactory ??= ExpressionUtilities.CreateLocationFactory(_expressionTree); + return _locationFactory.CreateLocation(context); + } protected override void CacheMetadata(CodeActivityMetadata metadata) { + _expressionTree = null; _invoker = new CompiledExpressionInvoker(this, true, metadata); - QueueForValidation(metadata, true); + + if (QueueForValidation(metadata, true)) + { + return; + } + // If ICER is not implemented that means we haven't been compiled + var publicAccessor = CodeActivityPublicEnvironmentAccessor.Create(metadata); + _expressionTree = CompileLocationExpression(publicAccessor, out var validationError); + if (validationError != null) + { + metadata.AddValidationError(validationError); + } } - protected override Location Execute(CodeActivityContext context) => (Location)_invoker.InvokeExpression(context); + private Expression> CompileLocationExpression(CodeActivityPublicEnvironmentAccessor publicAccessor, out string validationError) + { + Expression> expressionTreeToReturn = null; + validationError = null; + try + { + expressionTreeToReturn = CSharpHelper.Compile(ExpressionText, publicAccessor, true); + // inspect the expressionTree to see if it is a valid location expression(L-value) + string extraErrorMessage = null; + if (!publicAccessor.ActivityMetadata.HasViolations && (expressionTreeToReturn == null || + !ExpressionUtilities.IsLocation(expressionTreeToReturn, typeof(TResult), out extraErrorMessage))) + { + var errorMessage = SR.InvalidLValueExpression; + if (extraErrorMessage != null) + { + errorMessage += ":" + extraErrorMessage; + } + expressionTreeToReturn = null; + validationError = SR.CompilerErrorSpecificExpression(ExpressionText, errorMessage); + } + } + catch (SourceExpressionException e) + { + validationError = e.Message; + } + return expressionTreeToReturn; + } } \ No newline at end of file diff --git a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpValue.cs b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpValue.cs index 044b4b6d..fd90788a 100644 --- a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpValue.cs +++ b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpValue.cs @@ -1,15 +1,19 @@ // This file is part of Core WF which is licensed under the MIT license. // See LICENSE file in the project root for full license information. +using Microsoft.VisualBasic.Activities; using System; using System.Activities; +using System.Activities.ExpressionParser; using System.Activities.Expressions; using System.Activities.Internals; +using System.Activities.Runtime; using System.Activities.Validation; using System.ComponentModel; using System.Diagnostics; using System.Linq.Expressions; using System.Windows.Markup; +using ActivityContext = System.Activities.ActivityContext; namespace Microsoft.CSharp.Activities; @@ -18,6 +22,8 @@ namespace Microsoft.CSharp.Activities; public class CSharpValue : TextExpressionBase { private CompiledExpressionInvoker _invoker; + private Func _compiledExpression; + private Expression> _expressionTree; public CSharpValue() => UseOldFastPath = true; @@ -28,13 +34,67 @@ public class CSharpValue : TextExpressionBase [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public override string Language => CSharpHelper.Language; - public override Expression GetExpressionTree() => IsMetadataCached ? _invoker.GetExpressionTree() : throw FxTrace.Exception.AsError(new InvalidOperationException(SR.ActivityIsUncached)); + public override Expression GetExpressionTree() + { + if (!IsMetadataCached) + { + throw FxTrace.Exception.AsError(new InvalidOperationException(SR.ActivityIsUncached)); + } + if (_expressionTree == null) + { + if (_invoker != null) + { + return _invoker.GetExpressionTree(); + } + // it's safe to create this CodeActivityMetadata here, + // because we know we are using it only as lookup purpose. + var metadata = new CodeActivityMetadata(this, GetParentEnvironment(), false); + var publicAccessor = CodeActivityPublicEnvironmentAccessor.CreateWithoutArgument(metadata); + try + { + _expressionTree = CSharpHelper.Compile(ExpressionText, publicAccessor, false); + } + catch (SourceExpressionException e) + { + throw FxTrace.Exception.AsError( + new InvalidOperationException(SR.ExpressionTamperedSinceLastCompiled(e.Message))); + } + finally + { + metadata.Dispose(); + } + } + Fx.Assert(_expressionTree.NodeType == ExpressionType.Lambda, "Lambda expression required"); + return ExpressionUtilities.RewriteNonCompiledExpressionTree(_expressionTree); + } protected override void CacheMetadata(CodeActivityMetadata metadata) { + _expressionTree = null; _invoker = new CompiledExpressionInvoker(this, false, metadata); - QueueForValidation(metadata, false); + + if (QueueForValidation(metadata, false)) + { + return; + } + try + { + var publicAccessor = CodeActivityPublicEnvironmentAccessor.Create(metadata); + _expressionTree = CSharpHelper.Compile(ExpressionText, publicAccessor, false); + } + catch (SourceExpressionException e) + { + metadata.AddValidationError(e.Message); + } } - protected override TResult Execute(CodeActivityContext context) => (TResult) _invoker.InvokeExpression(context); + protected override TResult Execute(CodeActivityContext context) + { + if (_expressionTree == null) + { + return (TResult)_invoker.InvokeExpression(context); + } + _compiledExpression ??= _expressionTree.Compile(); + return _compiledExpression(context); + } } \ No newline at end of file From da101cfe16ff1e8c870734ff4cdc1b8a1cf92d60 Mon Sep 17 00:00:00 2001 From: Andrei-Liviu Ungureanu Date: Wed, 24 Apr 2024 11:28:24 +0300 Subject: [PATCH 2/2] added unit tests --- .../TestCases.Workflows/ExpressionTests.cs | 61 +++++++++++++++++++ .../WF4Samples/Expressions.cs | 19 +++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/Test/TestCases.Workflows/ExpressionTests.cs b/src/Test/TestCases.Workflows/ExpressionTests.cs index 676c2d9c..c4905b01 100644 --- a/src/Test/TestCases.Workflows/ExpressionTests.cs +++ b/src/Test/TestCases.Workflows/ExpressionTests.cs @@ -158,6 +158,25 @@ public void Vb_CompareLambdas() validationResults.Errors[0].Message.ShouldContain("A null propagating operator cannot be converted into an expression tree."); } + [Fact] + public void Cs_CompareLambdas() + { + CSharpValue csv = new(@$"string.Concat(""alpha "", b?.Substring(0, 10), ""beta "", 1)"); + WriteLine writeLine = new(); + writeLine.Text = new InArgument(csv); + Sequence workflow = new(); + workflow.Activities.Add(writeLine); + workflow.Variables.Add(new Variable("b", "I'm a variable")); + + ValidationResults validationResults = ActivityValidationServices.Validate(workflow, _forceCache); + validationResults.Errors.Count.ShouldBe(1, string.Join("\n", validationResults.Errors.Select(e => e.Message))); + validationResults.Errors[0].Message.ShouldContain("An expression tree lambda may not contain a null propagating operator."); + + validationResults = ActivityValidationServices.Validate(workflow, _useValidator); + validationResults.Errors.Count.ShouldBe(1, string.Join("\n", validationResults.Errors.Select(e => e.Message))); + validationResults.Errors[0].Message.ShouldContain("An expression tree lambda may not contain a null propagating operator."); + } + [Fact] public void Vb_LambdaExtension() { @@ -172,6 +191,20 @@ public void Vb_LambdaExtension() validationResults.Errors.Count.ShouldBe(0, string.Join("\n", validationResults.Errors.Select(e => e.Message))); } + [Fact] + public void Cs_LambdaExtension() + { + CSharpValue csv = new("list.First()"); + WriteLine writeLine = new(); + writeLine.Text = new InArgument(csv); + Sequence workflow = new(); + workflow.Activities.Add(writeLine); + workflow.Variables.Add(new Variable>("list")); + + ValidationResults validationResults = ActivityValidationServices.Validate(workflow, _useValidator); + validationResults.Errors.Count.ShouldBe(0, string.Join("\n", validationResults.Errors.Select(e => e.Message))); + } + [Fact] public void Vb_Dictionary() { @@ -185,6 +218,20 @@ public void Vb_Dictionary() ValidationResults validationResults = ActivityValidationServices.Validate(workflow, _useValidator); validationResults.Errors.Count.ShouldBe(0, string.Join("\n", validationResults.Errors.Select(e => e.Message))); } + + [Fact] + public void Cs_Dictionary() + { + CSharpValue csv = new("something.FooDictionary[\"key\"].ToString()"); + WriteLine writeLine = new(); + writeLine.Text = new InArgument(csv); + Sequence workflow = new(); + workflow.Activities.Add(writeLine); + workflow.Variables.Add(new Variable("something")); + + ValidationResults validationResults = ActivityValidationServices.Validate(workflow, _useValidator); + validationResults.Errors.Count.ShouldBe(0, string.Join("\n", validationResults.Errors.Select(e => e.Message))); + } #region Check locations are not readonly [Fact] public void VB_Readonly_ThrowsError() @@ -291,6 +338,20 @@ public void Vb_IntOverflow() validationResults.Errors[0].Message.ShouldContain("Constant expression not representable in type 'Integer'"); } + [Fact] + public void Cs_IntOverflow() + { + VisualBasicValue csv = new("2147483648"); + Sequence workflow = new(); + workflow.Variables.Add(new Variable("someint")); + Assign assign = new() { To = new OutArgument(workflow.Variables[0]), Value = new InArgument(csv) }; + workflow.Activities.Add(assign); + + ValidationResults validationResults = ActivityValidationServices.Validate(workflow, _useValidator); + validationResults.Errors.Count.ShouldBe(1, string.Join("\n", validationResults.Errors.Select(e => e.Message))); + validationResults.Errors[0].Message.ShouldContain("Constant expression not representable in type 'Integer'"); + } + [Fact] public void VBValidator_StrictOn() { diff --git a/src/Test/TestCases.Workflows/WF4Samples/Expressions.cs b/src/Test/TestCases.Workflows/WF4Samples/Expressions.cs index 214a1091..573278a6 100644 --- a/src/Test/TestCases.Workflows/WF4Samples/Expressions.cs +++ b/src/Test/TestCases.Workflows/WF4Samples/Expressions.cs @@ -87,7 +87,7 @@ public class AheadOfTimeExpressions : ExpressionsBase static readonly string CSharpCalculationResult = "Result == XX^2" + Environment.NewLine; static readonly StringDictionary CSharpCalculationInputs = new() { ["XX"] = 16, ["YY"] = 16 }; [Fact] - public void SameTextDifferentTypes() + public void VBSameTextDifferentTypes() { var text = new VisualBasicValue("var"); var values = new VisualBasicValue>("var"); @@ -101,6 +101,23 @@ public void SameTextDifferentTypes() ((LambdaExpression)values.GetExpressionTree()).ReturnType.ShouldBe(typeof(IEnumerable)); } [Fact] + public void CSSameTextDifferentTypes() + { + var text = new CSharpValue("var"); + var values = new CSharpValue>("var"); + var root = new DynamicActivity + { + Implementation = () => new Sequence + { + Variables = { new Variable("var") }, + Activities = { new ForEach { Values = new InArgument>(values) }, new WriteLine { Text = new InArgument(text) } } + } + }; + ActivityXamlServices.Compile(root, new()); + ((LambdaExpression)text.GetExpressionTree()).ReturnType.ShouldBe(typeof(string)); + ((LambdaExpression)values.GetExpressionTree()).ReturnType.ShouldBe(typeof(IEnumerable)); + } + [Fact] public void CompileCSharpCalculation() { var activity = Compile(TestXamls.CSharpCalculation);