Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<Activity x:Class="TestWorkflow" mc:Ignorable="sap sap2010"
xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sap="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation"
xmlns:sap2010="http://schemas.microsoft.com/netfx/2010/xaml/activities/presentation"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestCases.Workflows.WF4Samples;assembly=TestCases.Workflows"
xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System.Private.CoreLib"
xmlns:sco="clr-namespace:System.Collections.ObjectModel;assembly=System.Private.CoreLib"
xmlns:s="clr-namespace:System;assembly=System.Private.CoreLib">
<x:Members>
<x:Property Name="myEnumerable" Type="OutArgument(scg:IEnumerable(x:String))" />
<x:Property Name="dateTimeOutArgument" Type="OutArgument(s:DateTime)" />
<x:Property Name="nullableDateTimeOutArgument" Type="OutArgument(s:Nullable(s:DateTime))" />
</x:Members>
<sap2010:ExpressionActivityEditor.ExpressionActivityEditor>C#</sap2010:ExpressionActivityEditor.ExpressionActivityEditor>
<Sequence DisplayName="RootSequence">
<Sequence.Variables>
<Variable x:TypeArguments="x:String" Name="big" />
</Sequence.Variables>

<!-- Use your custom activity -->
<local:ImproveAssignabilityOutArgumentActivity>

<local:ImproveAssignabilityOutArgumentActivity.Result>
<OutArgument x:TypeArguments="scg:List(x:String)">
<CSharpReference x:TypeArguments="scg:List(x:String)">
myEnumerable
</CSharpReference>
</OutArgument>
</local:ImproveAssignabilityOutArgumentActivity.Result>

<local:ImproveAssignabilityOutArgumentActivity.DateTimeOutArgument>
<OutArgument x:TypeArguments="s:DateTime">
<CSharpReference x:TypeArguments="s:DateTime">
dateTimeOutArgument
</CSharpReference>
</OutArgument>
</local:ImproveAssignabilityOutArgumentActivity.DateTimeOutArgument>

<local:ImproveAssignabilityOutArgumentActivity.NullableDateTimeOutArgument>
<OutArgument x:TypeArguments="s:Nullable(s:DateTime)">
<CSharpReference x:TypeArguments="s:Nullable(s:DateTime)">
nullableDateTimeOutArgument
</CSharpReference>
</OutArgument>
</local:ImproveAssignabilityOutArgumentActivity.NullableDateTimeOutArgument>

</local:ImproveAssignabilityOutArgumentActivity>

<Assign sap2010:WorkflowViewState.IdRef="Assign_1">
<Assign.To>
<OutArgument x:TypeArguments="x:String">
<CSharpReference x:TypeArguments="x:String" sap2010:WorkflowViewState.IdRef="CSharpReference`1_1">big</CSharpReference>
</OutArgument>
</Assign.To>
<Assign.Value>
<InArgument x:TypeArguments="x:String">
<CSharpValue x:TypeArguments="x:String" sap2010:WorkflowViewState.IdRef="CSharpValue`1_13">new string('A', 100) ;</CSharpValue>
</InArgument>
</Assign.Value>
</Assign>
</Sequence>
</Activity>
1 change: 1 addition & 0 deletions src/Test/TestCases.Workflows/TestXamls/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ public enum TestXamls
SpecialCharacters,
ValueSpecialCharacterCSharp,
ValueSpecialCharacterVb,
ImproveAssignabilityOutArgumentActivity
}
}
6 changes: 5 additions & 1 deletion src/Test/TestCases.Workflows/WF4Samples/Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace TestCases.Workflows.WF4Samples
{
using StringDictionary = Dictionary<string, object>;

public abstract class ExpressionsBase
public abstract class ExpressionsBaseCommon
{
protected abstract bool CompileExpressions { get; }
protected Activity GetActivityFromXamlResource(TestXamls xamlName) => TestHelper.GetActivityFromXamlResource(xamlName, CompileExpressions);
Expand All @@ -31,6 +31,10 @@ protected Activity Compile(TestXamls xamlName)
Compiler.Run(activity);
return activity;
}
}

public abstract class ExpressionsBase : ExpressionsBaseCommon
{
protected const string CorrectOutput = @"John Doe earns $55000.00
Frank Kimono earns $89000.00
Salary statistics: minimum salary is $55000.00, maximum salary is $89000.00, average salary is $72000.00
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Activities;
using System.Collections.Generic;

namespace TestCases.Workflows.WF4Samples;

public class ImproveAssignabilityOutArgumentActivity : NativeActivity
{
// OutArgument of type List<string>
public OutArgument<List<string>> Result { get; set; }

// Additional OutArguments
public OutArgument<DateTime> DateTimeOutArgument { get; set; }
public OutArgument<DateTime?> NullableDateTimeOutArgument { get; set; }

public ImproveAssignabilityOutArgumentActivity() : base()
{
}

protected override void CacheMetadata(NativeActivityMetadata metadata)
{
// Result
var resultArg = new RuntimeArgument(
"Result",
typeof(List<string>),
ArgumentDirection.Out);
metadata.Bind(this.Result, resultArg);
metadata.AddArgument(resultArg);

// DateTimeOutArgument
var dateTimeArg = new RuntimeArgument(
"DateTimeOutArgument",
typeof(DateTime),
ArgumentDirection.Out);
metadata.Bind(this.DateTimeOutArgument, dateTimeArg);
metadata.AddArgument(dateTimeArg);

// NullableDateTimeOutArgument
var nullableDateTimeArg = new RuntimeArgument(
"NullableDateTimeOutArgument",
typeof(DateTime?),
ArgumentDirection.Out);
metadata.Bind(this.NullableDateTimeOutArgument, nullableDateTimeArg);
metadata.AddArgument(nullableDateTimeArg);
}

protected override void Execute(NativeActivityContext context)
{
var result = new List<string>
{
"aaa",
"bbb",
"ccc"
};

Result.Set(context, result);
DateTimeOutArgument.Set(context, DateTime.Now);
NullableDateTimeOutArgument.Set(context, DateTime.Now);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Shouldly;
using System;
using System.Activities;
using System.Activities.XamlIntegration;
using System.Collections.Generic;
using Xunit;

namespace TestCases.Workflows.WF4Samples;

public class ImproveAssignabilityOutArgumentTests : ExpressionsBaseCommon
{
protected override bool CompileExpressions => true;

[Fact]
public void CompileImproveAssignabilityOutArgumentActivity()
{
Activity activity = null;

// Assert that ActivityXamlServices.Load does not throw
Should.NotThrow(() =>
{
activity = ActivityXamlServices.Load(TestHelper.GetXamlStream(TestXamls.ImproveAssignabilityOutArgumentActivity),
new ActivityXamlServicesSettings
{
CompileExpressions = true
});
});

var invoker = new WorkflowInvoker(activity);
var result = invoker.Invoke();
result["myEnumerable"].ShouldBe(new List<string> { "aaa", "bbb", "ccc" });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace TestCases.Workflows.WF4Samples
//4. Assign: output = Dict.First.Key. => we expect the key is "KeyName"
//Results: the output has the unexpected value of "ValueName", instead of "KeyName", but if we enable parameter rename,
//everything works as expected
public class ValueSpecialCharacterTests : ExpressionsBase
public class ValueSpecialCharacterTests : ExpressionsBaseCommon
{
protected override bool CompileExpressions => true;

Expand Down
16 changes: 14 additions & 2 deletions src/Test/TestCases.Workflows/XamlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ public async Task CSharp_CompileAnonymousTypes(Type targetType)
Assert.Equal(typeof(IEnumerable<object>), compilationResult.ReturnType);
}

[Fact]
public async Task CSharp_CompileReferenceType()
{
SetupCompilation(out var location, out var namespaces, out var assemblyReferences);

var result = await CSharpDesignerHelper.CreatePrecompiledReferenceAsync(typeof(List<string>), "myEnumerable", namespaces, assemblyReferences, location);

Assert.Equal(typeof(IEnumerable<string>), result.ReturnType);
}

private static void SetupCompilation(out ActivityLocationReferenceEnvironment location, out string[] namespaces, out AssemblyReference[] assemblyReferences)
{
var seq = new Sequence();
Expand All @@ -233,7 +243,8 @@ private static void SetupCompilation(out ActivityLocationReferenceEnvironment lo
WorkflowInspectionServices.CacheMetadata(seq, location);
location.Declare(new Variable<string>("in_CountryName"), seq, ref errors);
location.Declare(new Variable<DataTable>("in_dt_OrderExport"), seq, ref errors);
namespaces = ["System", "System.Linq", "System.Data"];
location.Declare(new Variable<IEnumerable<string>>("myEnumerable"), seq, ref errors);
namespaces = ["System", "System.Linq", "System.Data", "System.Collections.Generic"];
assemblyReferences =
[
new AssemblyReference() { Assembly = typeof(string).Assembly },
Expand All @@ -242,7 +253,8 @@ private static void SetupCompilation(out ActivityLocationReferenceEnvironment lo
new AssemblyReference() { Assembly = typeof(System.ComponentModel.TypeConverter).Assembly },
new AssemblyReference() { Assembly = typeof(IServiceProvider).Assembly },
new AssemblyReference() { Assembly = Assembly.Load("System.Xml.ReaderWriter") },
new AssemblyReference() { Assembly = Assembly.Load("System.Private.Xml") }
new AssemblyReference() { Assembly = Assembly.Load("System.Private.Xml") },
new AssemblyReference() { Assembly = typeof(IEnumerable<>).Assembly }
];
}

Expand Down
26 changes: 10 additions & 16 deletions src/UiPath.Workflow/Activities/Utils/CSharpCompilerHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
using Microsoft.CodeAnalysis.Scripting.Hosting;
using ReflectionMagic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace System.Activities
{
public sealed class CSharpCompilerHelper : CompilerHelper
{
private static int crt = 0;
private static readonly dynamic s_typeNameFormatter = GetTypeNameFormatter();
private static readonly dynamic s_typeOptions = GetTypeOptions();

Expand Down Expand Up @@ -39,22 +36,19 @@ public override string CreateExpressionCode(string[] types, string[] names, stri
return $"{myDelegate} \n public static Expression<{name}<{typesStr}>> CreateExpression() => ({namesStr}) => {code};";
}

protected override (string, string) DefineDelegateCommon(int argumentsCount)
internal string CreateReferenceCode(string[] types, string returnType, string[] names, string code)
{
var crtValue = Interlocked.Add(ref crt, 1);

var part1 = new StringBuilder();
var part2 = new StringBuilder();
for (var i = 0; i < argumentsCount; i++)
{
part1.Append($"in T{i}, ");
part2.Append($" T{i} arg{i},");
}
part2.Remove(part2.Length - 1, 1);
var name = $"Func{crtValue}";
return ($"public delegate TResult {name}<{part1} out TResult>({part2});", name);
var strTypes = string.Join(Comma, types);
var strNames = string.Join(Comma, names);
return CSharpValidatorCommon.CreateReferenceCode(strTypes, returnType, strNames, code, string.Empty, 0);
}

internal string CreateValueCode(string[] types, string[] names, string code)
=> CSharpValidatorCommon.CreateValueCode(types, string.Join(Comma, names), code, string.Empty, 0);

protected override (string, string) DefineDelegateCommon(int argumentsCount)
=> CSharpValidatorCommon.DefineDelegateCommon(argumentsCount);

private static object GetTypeNameFormatter()
{
return typeof(CSharpScript)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,23 @@ protected override SyntaxTree GetSyntaxTreeForExpression(string expression, bool
.ToArray();

var names = resolvedIdentifiers.Select(var => var.Name).ToArray();
var types = resolvedIdentifiers.Select(var => var.Type).Concat(new[] { returnType }).Select(_compilerHelper.GetTypeName).ToArray();
var lambdaFuncCode = _compilerHelper.CreateExpressionCode(types, names, expression);
return CSharpSyntaxTree.ParseText(lambdaFuncCode, _compilerHelper.ScriptParseOptions);
var types = resolvedIdentifiers.Select(var => var.Type).Select(_compilerHelper.GetTypeName).ToArray();
string expressionCode;
if (isLocation)
{
expressionCode = _compilerHelper.CreateReferenceCode(types: types,
returnType: _compilerHelper.GetTypeName(returnType),
names: names,
code: expression);
}
else
{
types = types.Concat(new[] { _compilerHelper.GetTypeName(returnType) }).ToArray();
expressionCode = _compilerHelper.CreateValueCode(types: types,
names: names,
code: expression);
}

return CSharpSyntaxTree.ParseText(expressionCode, _compilerHelper.ScriptParseOptions);
}
}
51 changes: 51 additions & 0 deletions src/UiPath.Workflow/Utils/CSharpValidatorCommon.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace System.Activities;

internal static class CSharpValidatorCommon
{
private static int crt = 0;

// This is used in case the expression does not properly close (e.g. missing quotes, or multiline comment not closed)
private const string _expressionEnder = "// */ // \"";

private const string _valueValidationTemplate = "public static System.Linq.Expressions.Expression<System.Func<{0}>> CreateExpression{1}()//activityId:{4}\n => ({2}) => {3}; {5}";
private const string _delegateValueValidationTemplate = "{0}\npublic static System.Linq.Expressions.Expression<{1}<{2}>> CreateExpression{3}()//activityId:{6}\n => ({4}) => {5}; {7}";
private const string _referenceValidationTemplate = "public static {0} IsLocation{1}()//activityId:{5}\n => ({2}) => {3} = default({4}); {6}";

internal static string CreateReferenceCode(string types, string returnType, string names, string code, string activityId, int index)
{
var actionDefinition = !string.IsNullOrWhiteSpace(types)
? $"System.Action<{string.Join(CompilerHelper.Comma, types)}>"
: "System.Action";
return string.Format(_referenceValidationTemplate, actionDefinition, index, names, code, returnType, activityId, _expressionEnder);
}

internal static string CreateValueCode(IEnumerable<string> types, string names, string code, string activityId, int index)
{
var serializedArgumentTypes = string.Join(CompilerHelper.Comma, types);
if (types.Count() <= 16) // .net defines Func<TResult>...Func<T1,...T16,TResult)
Copy link

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

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

The comment has a typo in 'Funct' which should be 'Func'.

Copilot uses AI. Check for mistakes.

return string.Format(_valueValidationTemplate, serializedArgumentTypes, index, names, code, activityId, _expressionEnder);

var (myDelegate, name) = DefineDelegateCommon(types.Count() - 1);
return string.Format(_delegateValueValidationTemplate, myDelegate, name, serializedArgumentTypes, index, names, code, activityId, _expressionEnder);
}

internal static (string, string) DefineDelegateCommon(int argumentsCount)
{
var crtValue = Interlocked.Add(ref crt, 1);

var part1 = new StringBuilder();
var part2 = new StringBuilder();
for (var i = 0; i < argumentsCount; i++)
{
part1.Append($"in T{i}, ");
part2.Append($" T{i} arg{i},");
}
part2.Remove(part2.Length - 1, 1);
var name = $"Func{crtValue}";
return ($"public delegate TResult {name}<{part1} out TResult>({part2});", name);
}
}
Loading