Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compile all expressions at once when validating. #268

Merged
merged 6 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
65 changes: 64 additions & 1 deletion src/Test/TestCases.Workflows/ExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Activities.Validation;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace TestCases.Workflows;
Expand Down Expand Up @@ -47,7 +48,8 @@ static ExpressionTests()
// There's no programmatic way (that I know of) to add assembly references when creating workflows like in these tests.
// Adding the custom assembly directly to the expression validator to simulate XAML reference.
// The null is for testing purposes.
VbExpressionValidator.Instance = new VbExpressionValidator(new() { typeof(ClassWithCollectionProperties).Assembly, null });
VbExpressionValidator.Instance.AddRequiredAssembly(typeof(ClassWithCollectionProperties).Assembly);
CSharpExpressionValidator.Instance.AddRequiredAssembly(typeof(ClassWithCollectionProperties).Assembly);
}

[Theory]
Expand Down Expand Up @@ -424,6 +426,67 @@ public void CSRoslynValidator_ValidatesMoreThan16Arguments()
var result = ActivityValidationServices.Validate(sequence, _useValidator);
result.Errors.ShouldBeEmpty();
}
[Fact]
public void VB_Multithreaded_NoError()
{
var activities = new List<Activity>();
var tasks = new List<Task>();
var results = new List<ValidationResults>();
for (var i = 0; i < 20; i++)
{
var seq = new Sequence();
seq.Variables.Add(new Variable<int>("sum"));
for (var j = 0; j < 10000; j++)
{
seq.Activities.Add(new Assign
{
To = new OutArgument<int>(new VisualBasicReference<int>("sum")),
Value = new InArgument<int>(new VisualBasicValue<int>($"sum + {j}"))
});
}
}
foreach (var activity in activities)
{
tasks.Add(Task.Run(() =>
{
results.Add(ActivityValidationServices.Validate(activity, _useValidator));
}));
}
Task.WaitAll(tasks.ToArray());

results.All(r => !r.Errors.Any() && !r.Warnings.Any()).ShouldBeTrue();
}

[Fact]
public void CS_Multithreaded_NoError()
{
var activities = new List<Activity>();
var tasks = new List<Task>();
var results = new List<ValidationResults>();
for (var i = 0; i < 20; i++)
{
var seq = new Sequence();
seq.Variables.Add(new Variable<int>("sum"));
for (var j = 0; j < 10000; j++)
{
seq.Activities.Add(new Assign
{
To = new OutArgument<int>(new CSharpReference<int>("sum")),
Value = new InArgument<int>(new CSharpValue<int>($"sum + {j}"))
});
}
}
foreach (var activity in activities)
{
tasks.Add(Task.Run(() =>
{
results.Add(ActivityValidationServices.Validate(activity, _useValidator));
aoltean16 marked this conversation as resolved.
Show resolved Hide resolved
}));
}
Task.WaitAll(tasks.ToArray());

results.All(r => !r.Errors.Any() && !r.Warnings.Any()).ShouldBeTrue();
}

[Fact]
public void VBReferenceTypeIsChecked()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ internal sealed class ActivityLocationReferenceEnvironment : LocationReferenceEn
private Dictionary<string, LocationReference> _declarations;
private List<LocationReference> _unnamedDeclarations;

public ActivityLocationReferenceEnvironment() { }
public ActivityLocationReferenceEnvironment()
{
Extensions = new();
}

public ActivityLocationReferenceEnvironment(LocationReferenceEnvironment parent)
{
Expand All @@ -22,6 +25,7 @@ public ActivityLocationReferenceEnvironment(LocationReferenceEnvironment parent)
CompileExpressions = parent.CompileExpressions;
IsValidating = parent.IsValidating;
InternalRoot = parent.Root;
Extensions = parent.Extensions;
}
}

Expand Down
74 changes: 74 additions & 0 deletions src/UiPath.Workflow.Runtime/EnvironmentExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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.

namespace System.Activities
{
internal class EnvironmentExtensions
{
private readonly Dictionary<Type, object> _extensions = new();

/// <summary>
/// Gets the specified extension.
/// If the extension does not exist,
/// it will invoke the <paramref name="createExtensionFactory"/> parameter
/// </summary>
/// <typeparam name="T">The type of the extension</typeparam>
/// <param name="createExtensionFactory">The factory to create the extension</param>
/// <exception cref="ArgumentNullException"></exception>
public TInterface GetOrAdd<TInterface>(Func<TInterface> createExtensionFactory)
where TInterface : class
{
var type = typeof(TInterface);
if (_extensions.TryGetValue(type, out object extension))
{
return extension as TInterface;
}

return CreateAndAdd(createExtensionFactory, type);
}

/// <summary>
/// Retrieves the extension registered for the given type
/// or null otherwise
/// </summary>
/// <typeparam name="T">The type of the extension.</typeparam>
public T Get<T>() where T : class
{
if (_extensions.TryGetValue(typeof(T), out object extension))
return extension as T;
return null;
}

/// <summary>
/// Adds the specified extension to the list.
/// The extension is treated as a singleton,
/// so if a second extension with the same type is added, it will
/// throw an <see cref="InvalidOperationException"/>
/// </summary>
/// <typeparam name="TInterface">The type of the extension</typeparam>
/// <param name="extension">The extension</param>
/// <exception cref="InvalidOperationException"></exception>
public void Add<TInterface, TImplementation>(TImplementation extension)
where TInterface : class
where TImplementation : class, TInterface
{
if (_extensions.ContainsKey(typeof(TInterface)))
throw new InvalidOperationException($"Service '{typeof(TInterface).FullName}' already exists");

_extensions[typeof(TInterface)] = extension;
}


#region private methods
private T CreateAndAdd<T>(Func<T> createExtensionFactory, Type type) where T : class
{
var extension = createExtensionFactory();
if (extension is null)
throw new ArgumentNullException(nameof(extension));

_extensions[type] = extension;
return extension;
}
#endregion
}
}
2 changes: 2 additions & 0 deletions src/UiPath.Workflow.Runtime/LocationReferenceEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public abstract class LocationReferenceEnvironment
/// </summary>
internal bool IsValidating { get; set; }

internal EnvironmentExtensions Extensions { get; set; }

public abstract Activity Root { get; }

public LocationReferenceEnvironment Parent { get; protected set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ namespace System.Activities.Validation;

public static class ActivityValidationServices
{
internal static readonly ReadOnlyCollection<Activity> EmptyChildren = new(Array.Empty<Activity>());
private static readonly ValidationSettings defaultSettings = new();

internal static readonly ReadOnlyCollection<Activity> EmptyChildren = new(Array.Empty<Activity>());
internal static ReadOnlyCollection<ValidationError> EmptyValidationErrors = new(new List<ValidationError>(0));

public static ValidationResults Validate(Activity toValidate) => Validate(toValidate, defaultSettings);
Expand Down Expand Up @@ -245,7 +246,7 @@ private static string GenerateExceptionString(IList<ValidationError> validationE
return exceptionString;
}

static internal string GenerateValidationErrorPrefix(Activity toValidate, ActivityUtilities.ActivityCallStack parentChain, ProcessActivityTreeOptions options, out Activity source)
internal static string GenerateValidationErrorPrefix(Activity toValidate, ActivityUtilities.ActivityCallStack parentChain, ProcessActivityTreeOptions options, out Activity source)
{
bool parentVisible = true;
string prefix = "";
Expand Down Expand Up @@ -453,9 +454,28 @@ internal ValidationResults InternalValidate()
ActivityUtilities.CacheRootMetadata(_rootToValidate, _environment, _options, new ActivityUtilities.ProcessActivityCallback(ValidateElement), ref _errors);
}

var extension = GetValidationExtension(_environment);
aoltean16 marked this conversation as resolved.
Show resolved Hide resolved
if (extension is not null)
{
var results = extension.Validate(_rootToValidate);
if (_errors?.Any() == true)
{
foreach (var error in _errors)
{
results.Add(error);
}
}
return new ValidationResults(results);
}

return new ValidationResults(_errors);
}

private static IValidationExtension GetValidationExtension(LocationReferenceEnvironment environment)
{
return environment.Extensions.Get<IValidationExtension>();
}

private void ValidateElement(ActivityUtilities.ChildActivity childActivity, ActivityUtilities.ActivityCallStack parentChain)
{
Activity toValidate = childActivity.Activity;
Expand Down
15 changes: 15 additions & 0 deletions src/UiPath.Workflow.Runtime/Validation/ExpressionToValidate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

namespace System.Activities.Validation;

internal sealed class ExpressionToValidate
{
public Activity Activity { get; init; }

public string ExpressionText { get; init; }

public LocationReferenceEnvironment Environment { get; init; }

public Type ResultType { get; init; }

public bool IsLocation { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace System.Activities.Validation
{
internal interface IValidationExtension
{
IList<ValidationError> Validate(Activity activity);

void QueueExpressionForValidation<T>(ExpressionToValidate expressionToValidate, string language);
}
}
29 changes: 29 additions & 0 deletions src/UiPath.Workflow.Runtime/Validation/ValidationScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Immutable;

namespace System.Activities.Validation
{
internal sealed class ValidationScope
{
private readonly Dictionary<string, ExpressionToValidate> _expressionsToValidate = new();
private string _language;

internal void AddExpression<T>(ExpressionToValidate expressionToValidate, string language)
{
_language ??= language;
if (_language != language)
{
expressionToValidate.Activity.AddTempValidationError(new ValidationError(SR.DynamicActivityMultipleExpressionLanguages(language), expressionToValidate.Activity));
return;
}
_expressionsToValidate.Add(expressionToValidate.Activity.Id, expressionToValidate);
}

internal string Language => _language;

internal ExpressionToValidate GetExpression(string activityId) => _expressionsToValidate[activityId];

internal ImmutableArray<ExpressionToValidate> GetAllExpressions() => _expressionsToValidate.Values.ToImmutableArray();

internal void Clear() => _expressionsToValidate.Clear();
}
}
Loading