First commit - Adding all files.

This commit is contained in:
Travis Jensen 2019-09-04 13:02:25 -07:00
Коммит 57bd920101
30 изменённых файлов: 4287 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{C49A8494-13E5-4214-8434-708BA280C5B1}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Forge.DataContracts</RootNamespace>
<AssemblyName>Forge.DataContracts</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ForgeTree.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="ForgeSchemaValidationRules.json" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

Просмотреть файл

@ -0,0 +1,193 @@
{
"type": "object",
"definitions": {
"TreeDefinition": {
"type": "object",
"patternProperties": {
".*?": {
"$ref": "#/definitions/TreeNodeDefinition"
}
}
},
"TreeNodeDefinition": {
"oneOf": [
{
"$ref": "#/definitions/SelectionTypeNodeDefinition"
},
{
"$ref": "#/definitions/ActionTypeNodeDefinition"
},
{
"$ref": "#/definitions/LeafTypeNodeDefinition"
}
]
},
"SelectionTypeNodeDefinition": {
"type": "object",
"properties": {
"Type": {
"type": "string",
"enum": [ "Selection" ]
},
"ChildSelector": {
"type": "array",
"items": {
"$ref": "#/definitions/ChildSelectorDefinition"
},
"minItems": 1
},
"Properties": {
"type": "object"
}
},
"additionalProperties": false,
"required": [ "Type" ]
},
"ActionTypeNodeDefinition": {
"type": "object",
"properties": {
"Type": {
"type": "string",
"enum": [ "Action" ]
},
"Timeout": {
"type": [ "number", "string" ]
},
"Actions": {
"patternProperties": {
".*?": {
"$ref": "#/definitions/ActionDefinition"
}
},
"minProperties": 1
},
"ChildSelector": {
"type": "array",
"items": {
"$ref": "#/definitions/ChildSelectorDefinition"
},
"minItems": 1
},
"Properties": {
"type": "object"
}
},
"additionalProperties": false,
"required": [ "Type", "Actions" ]
},
"LeafTypeNodeDefinition": {
"type": "object",
"properties": {
"Type": {
"type": "string",
"enum": [ "Leaf" ]
},
"Actions": {
"patternProperties": {
".*?": {
"$ref": "#/definitions/LeafNodeSummaryActionDefinition"
}
},
"minProperties": 1,
"maxProperties": 1
},
"Properties": {
"type": "object"
}
},
"additionalProperties": false,
"required": [ "Type" ]
},
"ChildSelectorDefinition": {
"type": "object",
"properties": {
"Label": {
"type": "string"
},
"ShouldSelect": {
"type": "string"
},
"Child": {
"type": "string"
}
},
"additionalProperties": false,
"required": [ "Label", "Child" ]
},
"RetryPolicy": {
"type": "object",
"properties": {
"Type": {
"type": "string",
"enum": [ "None", "FixedInterval", "ExponentialBackoff" ]
},
"MinBackoffMs": {
"type": "number"
},
"MaxBackoffMs": {
"type": "number"
}
},
"required": [ "Type" ]
},
"ActionDefinition": {
"type": "object",
"properties": {
"Action": {
"type": "string"
},
"Input": {
"type": "object"
},
"Properties": {
"type": "object"
},
"Timeout": {
"type": [ "number", "string" ]
},
"ContinuationOnTimeout": {
"type": "boolean"
},
"RetryPolicy": {
"$ref": "#/definitions/RetryPolicy"
},
"ContinuationOnRetryExhaustion": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [ "Action" ]
},
"LeafNodeSummaryActionDefinition": {
"type": "object",
"properties": {
"Action": {
"enum": [ "LeafNodeSummaryAction" ]
},
"Input": {
"type": [ "object", "string" ],
"properties": {
"Status": {
"type": "string"
},
"StatusCode": {
"type": [ "number", "string" ]
},
"Output": {
"type": [ "object", "string" ]
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"required": [ "Action", "Input" ]
}
},
"properties": {
"Tree": {
"$ref": "#/definitions/TreeDefinition"
}
},
"additionalProperties": false
}

Просмотреть файл

@ -0,0 +1,252 @@
//-----------------------------------------------------------------------
// <copyright file="ForgeTree.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The Forge schema data contracts.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.DataContracts
{
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
/// <summary>
/// The Forge tree.
/// This outermost data structure holds the Forge schema.
/// </summary>
[DataContract]
public class ForgeTree
{
/// <summary>
/// Dictionary mapping unique TreeNodeKeys to TreeNodes.
/// </summary>
[DataMember]
public Dictionary<string, TreeNode> Tree { get; set; }
}
/// <summary>
/// The tree node.
/// Holds information to navigate the tree and perform actions.
/// </summary>
[DataContract]
public class TreeNode
{
/// <summary>
/// The tree node type.
/// </summary>
[DataMember(IsRequired = true)]
public TreeNodeType Type { get; private set; }
/// <summary>
/// Additional properties passed to wrapper class.
/// String properties starting with <see cref="TreeWalkerSession.RoslynLeadingText"/> represent a code-snippet that will be evaluated.
/// </summary>
[DataMember]
public dynamic Properties { get; set; }
/// <summary>
/// The child selectors.
/// </summary>
[DataMember]
public ChildSelector[] ChildSelector { get; private set; }
#region Properties used only by TreeNodeType.Action nodes
/// <summary>
/// The actions to execute when the TreeNodeType is Action.
/// Dictionary mapping unique TreeActionKeys to TreeActions.
/// </summary>
[DataMember]
public Dictionary<string, TreeAction> Actions { get; set; }
/// <summary>
/// Timeout in milliseconds for executing the TreeActions. Default to -1 (infinite) if not specified.
/// String properties starting with <see cref="TreeWalkerSession.RoslynLeadingText"/> represent a code-snippet that will be evaluated.
/// </summary>
[DataMember]
public dynamic Timeout { get; set; }
#endregion
}
/// <summary>
/// The child selector for the TreeNode.
/// Used to navigate the tree by referencing child TreeNodes.
/// </summary>
[DataContract]
public class ChildSelector
{
/// <summary>
/// String code-snippet that can be parsed and evaluated to a boolean value.
/// If the expression is true, visit the attached child TreeNode.
/// If the expression is empty, evaluate to true by default.
/// </summary>
[DataMember]
public string ShouldSelect { get; set; }
/// <summary>
/// Reader-friendly label that describes the intention of the ShouldSelect expression.
/// Used in ForgeEditor for display purposes.
/// </summary>
[DataMember]
public string Label { get; set; }
/// <summary>
/// String key pointer to a child TreeNode.
/// Visit this child if the attached ShouldSelect expression evaluates to true.
/// </summary>
[DataMember(IsRequired = true)]
public string Child { get; private set; }
}
/// <summary>
/// The tree action for the TreeNode.
/// Holds instructions and policies for executing an action.
/// </summary>
[DataContract]
public class TreeAction
{
/// <summary>
/// String name of the action that maps to an action-task.
/// These actions may be predefined Forge actions or action-tasks passed by a Wrapper class.
/// </summary>
[DataMember(IsRequired = true)]
public string Action { get; set; }
/// <summary>
/// Dynamic input parameters passed to the action-task.
/// Wrapper class is responsible for making sure the action-task input matches the input defined in the schema.
/// String properties starting with <see cref="TreeWalkerSession.RoslynLeadingText"/> represent a code-snippet that will be evaluated.
/// </summary>
[DataMember]
public dynamic Input { get; private set; }
/// <summary>
/// Additional properties passed to wrapper class.
/// String properties starting with <see cref="TreeWalkerSession.RoslynLeadingText"/> represent a code-snippet that will be evaluated.
/// </summary>
[DataMember]
public dynamic Properties { get; set; }
/// <summary>
/// Timeout in milliseconds for executing the action. Default to -1 (infinite) if not specified.
/// String properties starting with <see cref="TreeWalkerSession.RoslynLeadingText"/> represent a code-snippet that will be evaluated.
/// </summary>
[DataMember]
public dynamic Timeout { get; set; }
/// <summary>
/// A flag that represents how to handle the exit of the action due to timeout. If false (default), then the session will end on the
/// timeout. If true and a timeout is hit, the action will continue on as if it were successful after committing a "TimeoutOnAction" response.
/// </summary>
[DataMember]
public bool ContinuationOnTimeout { get; set; }
/// <summary>
/// Retry policy of the action.
/// </summary>
[DataMember]
public RetryPolicy RetryPolicy { get; private set; }
/// <summary>
/// A flag that represents how to handle the exit of the action due to retry exhaustion. If false (default), then the session will end once
/// retries are exhausted or no retries are specified. If true and retries are exhausted, the action will continue on as if it were successful
/// after committing a "RetryExhaustedOnAction" response.
/// </summary>
[DataMember]
public bool ContinuationOnRetryExhaustion { get; set; }
}
/// <summary>
/// The retry policy for the TreeAction.
/// </summary>
[DataContract]
public class RetryPolicy
{
/// <summary>
/// The retry policy type.
/// </summary>
[DataMember(IsRequired = true)]
public RetryPolicyType Type { get; private set; }
/// <summary>
/// Minimum backoff time in milliseconds.
/// When retrying an action, wait at least this long before your next attempt.
/// This is useful to ensure actions are not retried too quickly.
/// </summary>
[DataMember]
public long MinBackoffMs { get; private set; }
/// <summary>
/// Maximum backoff time in milliseconds.
/// When retrying an action, wait at most this long before your next attempt.
/// This is useful to ensure exponential backoff doesn't wait too long.
/// </summary>
[DataMember]
public long MaxBackoffMs { get; private set; }
}
/// <summary>
/// The retry policy types.
/// </summary>
[DataContract]
public enum RetryPolicyType
{
/// <summary>
/// Do not retry.
/// </summary>
[EnumMember]
None = 0,
/// <summary>
/// Retry at a fixed interval every MinBackoffMs.
/// </summary>
[EnumMember]
FixedInterval = 1,
/// <summary>
/// Retry with an exponential backoff.
/// Start with MinBackoffMs, then wait Math.Min(MinBackoffMs * 2^(retryCount), MaxBackoffMs).
/// </summary>
[EnumMember]
ExponentialBackoff = 2
// TODO: Add a FixedCount type that will give the full timeout duration for the set number of retries.
}
/// <summary>
/// The tree node types.
/// </summary>
[DataContract]
public enum TreeNodeType
{
/// <summary>
/// Undefined.
/// </summary>
[EnumMember]
Unknown = 0,
/// <summary>
/// Selection type node.
/// </summary>
[EnumMember]
Selection = 1,
/// <summary>
/// Action type node.
/// This node includes TreeAction(s).
/// </summary>
[EnumMember]
Action = 2,
/// <summary>
/// Leaf type node.
/// This represents an end state in tree.
/// </summary>
[EnumMember]
Leaf = 3
}
}

Просмотреть файл

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Forge.DataContracts")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Forge.DataContracts")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("c49a8494-13e5-4214-8434-708ba280c5b1")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Просмотреть файл

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props" Condition="Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{A33AF1FF-1291-4CB1-A733-3243E1FE967E}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Forge.TreeWalker.UnitTests</RootNamespace>
<AssemblyName>Forge.TreeWalker.UnitTests</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
<IsCodedUITest>False</IsCodedUITest>
<TestProjectType>UnitTest</TestProjectType>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<Compile Include="test\ExecutorUnitTests.cs" />
<Compile Include="test\ExternalTestType.cs" />
<Compile Include="test\ForgeSchemaHelper.cs" />
<Compile Include="test\TreeWalkerUnitTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Forge.DataContracts\Forge.DataContracts.csproj">
<Project>{c49a8494-13e5-4214-8434-708ba280c5b1}</Project>
<Name>Forge.DataContracts</Name>
</ProjectReference>
<ProjectReference Include="..\Forge.TreeWalker\Forge.TreeWalker.csproj">
<Project>{00fc1c22-6ae9-4f60-8a3e-05885ba34c9c}</Project>
<Name>Forge.TreeWalker</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props'))" />
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets'))" />
</Target>
<Import Project="..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets" Condition="Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets')" />
</Project>

Просмотреть файл

@ -0,0 +1,20 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Forge.TreeWalker.UnitTests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Forge.TreeWalker.UnitTests")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("a33af1ff-1291-4cb1-a733-3243e1fe967e")]
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Просмотреть файл

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-1.2.3.0" newVersion="1.2.3.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

Просмотреть файл

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="MSTest.TestAdapter" version="1.3.2" targetFramework="net462" />
<package id="MSTest.TestFramework" version="1.3.2" targetFramework="net462" />
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net462" />
</packages>

Просмотреть файл

@ -0,0 +1,311 @@
//-----------------------------------------------------------------------
// <copyright file="ExecutorUnitTests.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// Tests the ExpressionExecutor class.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker.UnitTests
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Forge.ExternalTypes;
using Forge.TreeWalker;
[TestClass]
public class ExecutorUnitTests
{
private dynamic UserContext;
[TestInitialize]
public void TestInitialize()
{
this.UserContext = new System.Dynamic.ExpandoObject();
this.UserContext.GetTopic = (Func<string, dynamic>)((topicName) =>
{
dynamic result = new System.Dynamic.ExpandoObject();
result.ResourceType = "Node";
return result;
});
this.UserContext.GetCount = (Func<int>)(() =>
{
return 1;
});
}
[TestMethod]
public void TestExecutor_Success_bool()
{
this.UserContext.Foo = "Bar";
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
string expression = "UserContext.Foo == \"Bar\"";
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
}
[TestMethod]
public void TestExecutor_Success_long()
{
// Note that long requires casting where int does not.
this.UserContext.Foo = (long)1000;
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
string expression = "UserContext.Foo";
Assert.AreEqual((long)1000, ex.Execute<long>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate the expression.");
}
[TestMethod]
public void TestExecutor_Success_int()
{
this.UserContext.Foo = 1000;
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
string expression = "UserContext.Foo";
Assert.AreEqual(1000, ex.Execute<int>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate the expression.");
}
[TestMethod]
public void TestExecutor_Success_ExecuteTwice()
{
this.UserContext.Foo = "Bar";
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
string expression = "UserContext.Foo == \"Bar\" && UserContext.GetTopic(\"TopicName\").ResourceType == \"Node\"";
// Test - confirm Execute can compile and execute the same code twice without crashing.
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
}
[TestMethod]
public void TestExecutor_Fail_MissingDefinitions()
{
this.UserContext.Foo = "Bar";
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
string expression = "UserContext.Bar == \"Bar\"";
try
{
ex.Execute<bool>(expression).GetAwaiter().GetResult();
Assert.Fail("Expected ExpressionExecutor to fail evaluating an expression when UserContext does not contain a necessary definitions.");
}
catch (Exception)
{
}
}
[TestMethod]
public void TestExecutor_Fail_BadExpression()
{
this.UserContext.Foo = "Bar";
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
string expression = "UserContext.Foo";
Assert.ThrowsException<InvalidCastException>(() =>
{
ex.Execute<bool>(expression).GetAwaiter().GetResult();
}, "Expected ExpressionExecutor to fail evaluating an expression that cannot be evaluated.");
}
[TestMethod]
public void TestExecutor_Success_CompileWithExternalDependencies()
{
this.UserContext.Foo = ExternalTestType.TestEnum;
List<Type> dependencies = new List<Type>();
dependencies.Add(typeof(ExternalTestType));
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, dependencies);
string expression = "UserContext.Foo == ExternalTestType.TestEnum";
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
}
[TestMethod]
public void TestExecutor_Fail_CompileExpressionWithMissingDependencies()
{
this.UserContext.Foo = ExternalTestType.ExampleEnum;
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
string expression = "UserContext.Foo == ExternalTestType.ExampleEnum";
try
{
ex.Execute<bool>(expression).GetAwaiter().GetResult();
Assert.Fail("Expected ExpressionExecutor to fail evaluating an expression because runtime assembly is missing required dependencies.");
}
catch (Exception)
{
}
}
[TestMethod]
public void TestExecutor_Fail_CompileExpressionWithExternalDependenciesAndMissingDependencies()
{
this.UserContext.Foo = ExternalTestType.TestEnum;
this.UserContext.Bar = DiffNamespaceType.TestOne;
List<Type> dependencies = new List<Type>();
dependencies.Add(typeof(ExternalTestType));
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, dependencies);
string expression = "UserContext.Foo == ExternalTestType.TestEnum && UserContext.Bar == DiffNamespaceType.TestOne";
try
{
ex.Execute<bool>(expression).GetAwaiter().GetResult();
Assert.Fail("Expected ExpressionExecutor to fail evaluating an expression because runtime assembly is missing required dependencies.");
}
catch (Exception)
{
}
}
[TestMethod]
public void TestExecutor_Success_CompileExpressionWithMissingDependenciesButOtherExternalTypesInSameNamespace()
{
this.UserContext.Foo = ExternalTestType.TestEnum;
this.UserContext.Bar = TestType.Test;
List<Type> dependencies = new List<Type>();
dependencies.Add(typeof(ExternalTestType));
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, dependencies);
string expression = "UserContext.Foo == ExternalTestType.TestEnum && UserContext.Bar == TestType.Test";
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
}
[TestMethod]
public void TestExecutor_Success_CompileExpressionWithForgeDefaultDependenciesBeingPassedInExternally()
{
this.UserContext.Foo = "Foo";
List<Type> dependencies = new List<Type>();
// Tasks dependency needed by Forge by default.
dependencies.Add(typeof(Task));
// Reflection dependency needed by Forge for runtime compilation.
dependencies.Add(typeof(Type));
// Default dependencies are expected to be tossed away internally in ExpressionExecutor.
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
string expression = "UserContext.Foo == \"Foo\"";
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult(), "Expected ExpressionExecutor to successfully evaluate a true expression.");
}
[TestMethod]
public void TestExecutor_Success_ChangingFunctionDefinition()
{
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
// Rewritting GetCount to return 2
string expression="UserContext.GetCount = new Func<int>(() => 2)";
Assert.AreEqual(this.UserContext.GetCount(), 1);
ex.Execute<Func<int>>(expression).GetAwaiter().GetResult();
Assert.AreEqual(this.UserContext.GetCount(), 2);
}
[TestMethod]
public void TestExecutor_Fail_ChangingFunctionReturnType()
{
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
// Changing return type of GetCount
string expression = "UserContext.GetCount = new Func<string>(() => \"Test\")";
Assert.ThrowsException<InvalidCastException>(() =>
{
// Since expected return type matches the original Func<int> type, this should throw an error
ex.Execute<Func<int>>(expression).GetAwaiter().GetResult();
}, "Expected ExpressionExecutor to fail evaluating can not change return type");
}
[TestMethod]
public void TestExecutor_Success_ChangingFunctionReturnType()
{
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
// Changing return type of GetCount
string expression = "UserContext.GetCount = new Func<string>(() => \"Test\")";
Assert.AreEqual(this.UserContext.GetCount(), 1);
// Since expected return type has been updated, this should pass
ex.Execute<Func<string>>(expression).GetAwaiter().GetResult();
Assert.AreEqual(this.UserContext.GetCount(), "Test");
}
[TestMethod]
public void TestExecutor_Fail_ExecutingMultipleStatements()
{
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
// Changing return type of GetCount
string expression = "int x = UserContext.GetCount() + 5; UserContext.GetCount = new Func<int>(() => x); return UserContext.GetCount()";
try
{
ex.Execute<Func<int>>(expression).GetAwaiter().GetResult();
Assert.Fail("Expected ExpressionExecutor to fail evaluating can not pass multiple statements to expression.");
}
catch (Exception)
{
}
}
[TestMethod]
public void TestExecutor_Success_WaitForDelegate()
{
this.UserContext.Foo = "Bar";
string expression = "(Func<bool>)(() => {return UserContext.Foo == \"Bar\";})";
// Casting the expression to Func<bool> since the executor will return a delegate of type Func<bool>
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
dynamic expressionResult = ex.Execute<Delegate>(expression).GetAwaiter().GetResult();
if (expressionResult.GetType() == typeof(Func<bool>))
{
Assert.IsTrue(expressionResult());
this.UserContext.Foo = "Far";
Assert.IsFalse(expressionResult());
}
else
{
Assert.Fail(string.Format("Expected expression to be of type bool but was {0}", expressionResult.GetType()));
}
}
[TestMethod]
public void TestExecutor_Success_WaitForDelegateAsync()
{
this.UserContext.Foo = "Bar";
string expression = "(Func<Task<bool>>)(() => {return Task.FromResult(UserContext.Foo == \"Bar\");})";
// Casting the expression to Func<bool> since the executor will return a delegate of type Func<bool>
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
dynamic expressionResult = ex.Execute<Delegate>(expression).GetAwaiter().GetResult();
if (expressionResult.GetType() == typeof(Func<Task<bool>>))
{
// expressionResult() return Task<bool>, doing .GetAwaiter().GetResult() again returns bool
Assert.IsTrue(expressionResult().GetAwaiter().GetResult());
this.UserContext.Foo = "Far";
Assert.IsFalse(expressionResult().GetAwaiter().GetResult());
}
else
{
Assert.Fail(string.Format("Expected expression to be of type Task<bool> but was {0}", expressionResult.GetType()));
}
}
[TestMethod]
public void TestExecutor_ScriptCacheContainsKey()
{
this.UserContext.Foo = "Bar";
string expression = "UserContext.Foo == \"Bar\" && UserContext.GetTopic(\"TopicName\").ResourceType == \"Node\"";
// Test - confirm ExpressionExecutor script cache does not contain script before executing.
ExpressionExecutor ex = new ExpressionExecutor(null, this.UserContext, null);
Assert.IsFalse(ex.ScriptCacheContainsKey(expression));
// Test - confirm ExpressionExecutor script cache does contain script after executing.
Assert.IsTrue(ex.Execute<bool>(expression).GetAwaiter().GetResult());
Assert.IsTrue(ex.ScriptCacheContainsKey(expression));
}
}
}

Просмотреть файл

@ -0,0 +1,36 @@
//-----------------------------------------------------------------------
// <copyright file="ExternalTestType.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// Test type used to test external Forge dependency injection.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.ExternalTypes
{
public enum ExternalTestType
{
ExampleEnum = 0,
TestEnum = 1,
}
public enum TestType
{
Example = 0,
Test = 1
}
}
namespace Forge.TreeWalker.UnitTests
{
public enum DiffNamespaceType
{
TestOne,
TestTwo
}
}

Просмотреть файл

@ -0,0 +1,425 @@
//-----------------------------------------------------------------------
// <copyright file="ForgeSchemaHelper.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// Helper class for TreeWalkerUnitTests that holds ForgeSchema examples.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker.UnitTests
{
public static class ForgeSchemaHelper
{
public const string TardigradeScenario = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Selection"",
""ChildSelector"":
[
{
""ShouldSelect"": ""C#|UserContext.ResourceType == \""Node\"""",
""Child"": ""Node""
},
{
""ShouldSelect"": ""C#|UserContext.ResourceType == \""Container\"""",
""Child"": ""Container""
}
]
},
""Container"": {
""Type"": ""Action"",
""Actions"":
{
""Container_CollectDiagnosticsAction"": {
""Action"": ""CollectDiagnosticsAction"",
""Input"":
{
""Command"": ""RunCollectDiagnostics.exe""
}
}
},
""ChildSelector"":
[
{
""ShouldSelect"": ""C#|Session.GetLastActionResponse().Status == \""Success\"""",
""Child"": ""Tardigrade""
}
]
},
""Tardigrade"": {
""Type"": ""Action"",
""Actions"":
{
""Tardigrade_TardigradeAction"": {
""Action"": ""TardigradeAction""
}
},
""ChildSelector"":
[
{
""ShouldSelect"": ""C#|Session.GetLastActionResponse().Status == \""Success\"""",
""Child"": ""Tardigrade_Success""
}
]
},
""Tardigrade_Success"": {
""Type"": ""Leaf""
}
}
}";
public const string ActionException_Fail = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Action"",
""Actions"":
{
""Root_TestDelayExceptionAction"": {
""Action"": ""TestDelayExceptionAction"",
""Input"":
{
""ThrowException"": true
}
}
}
}
}
}";
public const string ActionException_ContinuationOnRetryExhaustion = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Action"",
""Actions"":
{
""Root_TestDelayExceptionAction"": {
""Action"": ""TestDelayExceptionAction"",
""Input"":
{
""ThrowException"": true
},
""ContinuationOnRetryExhaustion"": true
}
}
}
}
}";
public const string ActionDelay_Fail = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Action"",
""Actions"":
{
""Root_TestDelayExceptionAction"": {
""Action"": ""TestDelayExceptionAction"",
""Input"":
{
""DelayMilliseconds"": 50
},
""Timeout"": 10,
}
}
}
}
}";
public const string ActionDelay_ContinuationOnTimeout = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Action"",
""Actions"":
{
""Root_TestDelayExceptionAction"": {
""Action"": ""TestDelayExceptionAction"",
""Input"":
{
""DelayMilliseconds"": 50
},
""Timeout"": 10,
""ContinuationOnTimeout"": true
}
}
}
}
}";
public const string ActionDelay_ContinuationOnTimeout_RetryPolicy_TimeoutInAction = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Action"",
""Actions"":
{
""Root_TestDelayExceptionAction"": {
""Action"": ""TestDelayExceptionAction"",
""Input"":
{
""DelayMilliseconds"": 50,
""ThrowException"": true
},
""Timeout"": 100,
""RetryPolicy"":
{
""Type"": ""FixedInterval"",
""MinBackoffMs"": 25
},
""ContinuationOnTimeout"": true
}
}
}
}
}";
public const string ActionDelay_ContinuationOnTimeout_RetryPolicy_TimeoutBetweenRetries = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Action"",
""Actions"":
{
""Root_TestDelayExceptionAction"": {
""Action"": ""TestDelayExceptionAction"",
""Input"":
{
""DelayMilliseconds"": 25,
""ThrowException"": true
},
""Timeout"": 50,
""RetryPolicy"":
{
""Type"": ""FixedInterval"",
""MinBackoffMs"": 100
},
""ContinuationOnTimeout"": true
}
}
}
}
}";
public const string NoChildMatch = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Selection"",
""ChildSelector"":
[
{
""ShouldSelect"": ""C#|false"",
""Child"": ""LeafNode""
}
]
},
""LeafNode"": {
""Type"": ""Leaf""
}
}
}
";
public const string TestEvaluateInputTypeAction = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Action"",
""Actions"":
{
""Root_TestEvaluateInputTypeAction"": {
""Action"": ""TestEvaluateInputTypeAction"",
""Input"":
{
""Command"": ""tasklist"",
""IntExpression"": ""C#<Int64>|UserContext.GetCount()"",
""BoolExpression"": ""C#|true"",
""NestedObject"":
{
""Name"": ""C#|string.Format(\""{0}_{1}\"", \""MyName\"", UserContext.Name)"",
""Value"": ""MyValue""
},
""ObjectArray"":
[
{
""Name"": ""C#|UserContext.Name"",
""Value"": ""FirstValue""
},
{
""Name"": ""SecondName"",
""Value"": ""SecondValue""
}
],
""StringArray"": [""value1"", ""value2""],
""LongArray"": [1, 3, 2],
""BoolDelegate"": ""C#|(Func<bool>)(() => {return UserContext.GetCount() == 1;})"",
""BoolDelegateAsync"": ""C#|(Func<Task<bool>>)(async() => { return await UserContext.GetCountAsync() == 2; })""
}
}
}
}
}
}
";
public const string TestEvaluateInputType_FailOnField_Action = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Action"",
""Actions"":
{
""Root_TestEvaluateInputType_FailOnField_Action"": {
""Action"": ""TestEvaluateInputType_FailOnField_Action"",
""Input"":
{
""UnexpectedField"": true
},
""ContinuationOnRetryExhaustion"": true
}
}
}
}
}
";
public const string TestEvaluateInputTypeAction_UnexpectedPropertyFail = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Action"",
""Actions"":
{
""Root_TestEvaluateInputTypeAction"": {
""Action"": ""TestEvaluateInputTypeAction"",
""Input"":
{
""UnexpectedProperty"": true
},
""ContinuationOnRetryExhaustion"": true
}
}
}
}
}
";
public const string TestEvaluateInputType_FailOnNonEmptyCtor_Action = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Action"",
""Actions"":
{
""Root_TestEvaluateInputType_FailOnNonEmptyCtor_Action"": {
""Action"": ""TestEvaluateInputType_FailOnNonEmptyCtor_Action"",
""Input"":
{
""BoolProperty"": true
},
""ContinuationOnRetryExhaustion"": true
}
}
}
}
}
";
public const string LeafNodeSummaryAction = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Leaf"",
""Actions"":
{
""Root_LeafNodeSummaryAction"": {
""Action"": ""LeafNodeSummaryAction"",
""Input"":
{
""Status"": ""Success"",
""StatusCode"": 1,
""Output"": ""TheResult""
}
}
}
}
}
}
";
public const string LeafNodeSummaryAction_InputIsActionResponse = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Action"",
""Actions"":
{
""Root_CollectDiagnosticsAction"": {
""Action"": ""CollectDiagnosticsAction"",
""Input"":
{
""Command"": ""TheCommand""
}
}
},
""ChildSelector"":
[
{
""Child"": ""LeafNodeSummaryTest""
}
]
},
""LeafNodeSummaryTest"": {
""Type"": ""Leaf"",
""Actions"":
{
""LeafNodeSummaryTest_LeafNodeSummaryAction"": {
""Action"": ""LeafNodeSummaryAction"",
""Input"": ""C#|Session.GetLastActionResponse()""
}
}
}
}
}
";
public const string ExternalExecutors = @"
{
""Tree"":
{
""Root"": {
""Type"": ""Leaf"",
""Actions"":
{
""Root_LeafNodeSummaryAction"": {
""Action"": ""LeafNodeSummaryAction"",
""Input"":
{
""Status"": ""External|StatusResult""
}
}
}
}
}
}
";
}
}

Просмотреть файл

@ -0,0 +1,762 @@
//-----------------------------------------------------------------------
// <copyright file="TreeWalkerUnitTests.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// Tests the TreeWalkerSession class.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker.UnitTests
{
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Forge.Attributes;
using Forge.DataContracts;
using Forge.TreeWalker;
using Forge.TreeWalker.ForgeExceptions;
using Newtonsoft.Json;
[TestClass]
public class TreeWalkerUnitTests
{
private Guid sessionId;
private IForgeDictionary forgeState = new ForgeDictionary(new Dictionary<string, object>(), Guid.Empty);
private dynamic UserContext = new System.Dynamic.ExpandoObject();
private ITreeWalkerCallbacks callbacks;
private CancellationToken token;
private TreeWalkerParameters parameters;
private TreeWalkerSession session;
public void TestInitialize(string jsonSchema)
{
// Initialize contexts, callbacks, and actions.
this.sessionId = Guid.NewGuid();
this.forgeState = new ForgeDictionary(new Dictionary<string, object>(), this.sessionId);
this.callbacks = new TreeWalkerCallbacks();
this.token = new CancellationTokenSource().Token;
this.UserContext.Name = "MyName";
this.UserContext.ResourceType = "Container";
this.UserContext.GetCount = (Func<Int32>)(() =>
{
return 1;
});
this.UserContext.GetCountAsync = (Func<Task<Int32>>)(() =>
{
return Task.FromResult(2);
});
this.parameters = new TreeWalkerParameters(
this.sessionId,
jsonSchema,
this.forgeState,
this.callbacks,
this.token)
{
UserContext = this.UserContext,
ForgeActionsAssembly = typeof(CollectDiagnosticsAction).Assembly
};
this.session = new TreeWalkerSession(this.parameters);
}
[TestMethod]
public void TestTreeWalkerSession_Constructor()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
// Test 1 - Verify jsonSchema was successfully deserialized in constructor.
Assert.AreEqual("Action", this.session.Schema.Tree["Tardigrade"].Type.ToString());
// Test 2 - Verify the Status is Initialized.
Assert.AreEqual("Initialized", this.session.Status, "Expected WalkTree status to be Initialized after initializing TreeWalkerSession.");
}
[TestMethod]
public void TestTreeWalkerSession_VisitNode_Success()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
// Test - VisitNode and expect the first child to be returned.
string expected = "Tardigrade";
string actualNextTreeNodeKey = this.session.VisitNode("Container").GetAwaiter().GetResult();
Assert.AreEqual(expected, actualNextTreeNodeKey, "Expected VisitNode(Container) to return Tardigrade.");
}
[TestMethod]
public void TestTreeWalkerSession_VisitNode_LeafNode_Success()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
// Test - VisitNode on node of Leaf type and confirm it does not throw.
string expected = null;
string actualNextTreeNodeKey = this.session.VisitNode("Tardigrade_Success").GetAwaiter().GetResult();
Assert.AreEqual(expected, actualNextTreeNodeKey, "Expected VisitNode(Tardigrade_Success) to return without throwing.");
}
[TestMethod]
public void TestTreeWalkerSession_VisitNode_NoTimeout_Success()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
// Test - VisitNode with no Timeout and execute an Action with no Timeout set. Confirm we do not throw exceptions.
string expected = "Tardigrade_Success";
string actualNextTreeNodeKey = this.session.VisitNode("Tardigrade").GetAwaiter().GetResult();
Assert.AreEqual(expected, actualNextTreeNodeKey, "Expected VisitNode(Tardigrade) to return Tardigrade_Success without throwing exception.");
}
[TestMethod]
public void TestTreeWalkerSession_WalkTree_Success()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
// Test - WalkTree and expect the Status to be RanToCompletion.
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to run to completion.");
}
[TestMethod]
public void TestTreeWalkerSession_WalkTree_ActionThrowsException_TimeoutOnAction()
{
// Initialize TreeWalkerSession with a schema containing Action that throws exception.
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionException_Fail);
// Test - WalkTree and expect the Status to be TimeoutOnAction due to unexpected exceptions thrown in action.
Assert.ThrowsException<ActionTimeoutException>(() =>
{
string temp = this.session.WalkTree("Root").GetAwaiter().GetResult();
}, "Expected WalkTree to timeout on action because the Action threw exceptions with no Continuation flags set.");
string actualStatus = this.session.Status;
Assert.AreEqual("TimeoutOnAction", actualStatus, "Expected WalkTree to timeout on action because the Action threw exceptions with no Continuation flags set.");
}
[TestMethod]
public void TestTreeWalkerSession_WalkTree_ActionThrowsException_ContinuationOnRetryExhaustion()
{
// Initialize TreeWalkerSession with a schema containing Action that throws exception but has ContinuationOnRetryExhaustion flag set.
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionException_ContinuationOnRetryExhaustion);
// Test - Expect WalkTree to be successful because the TreeAction exhausted retries but ContinuationOnRetryExhaustion flag was set.
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to be successful because the TreeAction exhausted retries but ContinuationOnRetryExhaustion flag was set.");
ActionResponse actionResponse = this.session.GetLastActionResponse();
Assert.AreEqual("RetryExhaustedOnAction", actionResponse.Status, "Expected WalkTree to be successful because the TreeAction exhausted retries but ContinuationOnRetryExhaustion flag was set.");
}
[TestMethod]
public void TestTreeWalkerSession_WalkTree_ActionHasDelay_TimeoutOnAction()
{
// Initialize TreeWalkerSession with a schema containing Action that will time out.
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionDelay_Fail);
// Test - WalkTree and expect the Status to be TimeoutOnAction due to Action timing out.
Assert.ThrowsException<ActionTimeoutException>(() =>
{
string temp = this.session.WalkTree("Root").GetAwaiter().GetResult();
}, "Expected WalkTree to timeout on action because the Action timed out with no Continuation flags set.");
string actualStatus = this.session.Status;
Assert.AreEqual("TimeoutOnAction", actualStatus, "Expected WalkTree to timeout on action because the Action timed out with no Continuation flags set.");
}
[TestMethod]
public void TestTreeWalkerSession_WalkTree_ActionHasDelay_ContinuationOnTimeout()
{
// Initialize TreeWalkerSession with a schema containing Action that will time out but has ContinuationOnTimeout flag set.
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionDelay_ContinuationOnTimeout);
// Test - Expect WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
ActionResponse actionResponse = this.session.GetLastActionResponse();
Assert.AreEqual("TimeoutOnAction", actionResponse.Status, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
}
[TestMethod]
public void TestTreeWalkerSession_WalkTree_ActionHasDelay_ContinuationOnTimeout_RetryPolicy_TimeoutInAction()
{
// Initialize TreeWalkerSession with a schema containing Action with RetryPolicy that will time out inside the Action but has ContinuationOnTimeout flag set.
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionDelay_ContinuationOnTimeout_RetryPolicy_TimeoutInAction);
// Test - Expect WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
ActionResponse actionResponse = this.session.GetLastActionResponse();
Assert.AreEqual("TimeoutOnAction", actionResponse.Status, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
}
[TestMethod]
public void TestTreeWalkerSession_WalkTree_ActionHasDelay_ContinuationOnTimeout_RetryPolicy_TimeoutBetweenRetries()
{
// Initialize TreeWalkerSession with a schema containing Action with RetryPolicy that will time out between retry attempts but has ContinuationOnTimeout flag set.
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionDelay_ContinuationOnTimeout_RetryPolicy_TimeoutBetweenRetries);
// Test - Expect WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
ActionResponse actionResponse = this.session.GetLastActionResponse();
Assert.AreEqual("TimeoutOnAction", actionResponse.Status, "Expected WalkTree to be successful because the TreeAction timed out but ContinuationOnTimeout flag was set.");
}
[TestMethod]
public void TestTreeWalkerSession_WalkTree_CancelledBeforeExecution()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
// Test - CancelWalkTree before WalkTree and expect the Status to be CancelledBeforeExecution.
this.session.CancelWalkTree();
Assert.ThrowsException<TaskCanceledException>(() =>
{
string temp = this.session.WalkTree("Root").GetAwaiter().GetResult();
}, "Expected WalkTree to throw exception after calling CancelWalkTree.");
string actualStatus = this.session.Status;
Assert.AreEqual("CancelledBeforeExecution", actualStatus, "Expected WalkTree to be cancelled before execution after calling CancelWalkTree.");
}
[TestMethod]
public void TestTreeWalkerSession_WalkTree_ActionWithDelay_CancelWalkTree()
{
// Initialize TreeWalkerSession with a schema containing Action with delay.
// This gives us time to start WalkTree before calling CancelWalkTree.
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ActionDelay_ContinuationOnTimeout_RetryPolicy_TimeoutInAction);
// Test - WalkTree then CancelWalkTree while WalkTree is running and expect the Status to be Cancelled.
Task<string> task = this.session.WalkTree("Root");
Thread.Sleep(25);
this.session.CancelWalkTree();
Assert.ThrowsException<OperationCanceledException>(() =>
{
string temp = task.GetAwaiter().GetResult();
}, "Expected WalkTree to throw exception after calling CancelWalkTree.");
string actualStatus = this.session.Status;
Assert.AreEqual("Cancelled", actualStatus, "Expected WalkTree to be cancelled after calling CancelWalkTree.");
}
[TestMethod]
public void TestTreeWalkerSession_WalkTree_Failed_MissingKey()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
// Test - WalkTree and expect the Status to be Failed because the key does not exist which threw an exception.
Assert.ThrowsException<KeyNotFoundException>(() =>
{
string temp = this.session.WalkTree("MissingKey").GetAwaiter().GetResult();
}, "Expected WalkTree to fail because the key does not exist.");
string actualStatus = this.session.Status;
Assert.AreEqual("Failed", actualStatus, "Expected WalkTree to fail because the key does not exist.");
}
[TestMethod]
public void TestTreeWalkerSession_WalkTree_NoChildMatched()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.NoChildMatch);
// Test - WalkTree and expect the Status to be NoChildMatched.
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("RanToCompletion_NoChildMatched", actualStatus, "Expected WalkTree to end with NoChildMatched status.");
}
[TestMethod]
public void TestGetCurrentTreeNode()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
// Test 1 - Confirm GetCurrentTreeNode returns null before walking tree.
Assert.AreEqual(null, this.session.GetCurrentTreeNode().GetAwaiter().GetResult(), "Expected CurrentTreeNode to be null before starting walk tree.");
// Test 2 - Confirm GetCurrentTreeNode returns last node visited after walking tree.
this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("Tardigrade_Success", this.session.GetCurrentTreeNode().GetAwaiter().GetResult(), "Expected CurrentTreeNode to equal the last node visited.");
}
[TestMethod]
public void TestGetLastTreeAction()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
// Test 1 - Confirm GetLastTreeAction returns null before walking tree.
Assert.AreEqual(null, this.session.GetLastTreeAction().GetAwaiter().GetResult(), "Expected LastTreeAction to be null before starting walk tree.");
// Test 2 - Confirm GetLastTreeAction returns last tree action executed after walking tree.
this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("Tardigrade_TardigradeAction", this.session.GetLastTreeAction().GetAwaiter().GetResult(), "Expected LastTreeAction to equal the last tree action executed.");
}
[TestMethod]
public void TestGetOutput()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to run to completion.");
// Test 1 - Confirm ActionResponse can be read from GetOutputAsync.
ActionResponse actionResponse = this.session.GetOutputAsync("Container_CollectDiagnosticsAction").GetAwaiter().GetResult();
Assert.AreEqual(
"RunCollectDiagnostics.exe_Results",
actionResponse.Output,
"Expected to successfully read ActionResponse.Output.");
Assert.AreEqual(
"Success",
actionResponse.Status,
"Expected to successfully read ActionResponse.Status.");
// Test 2 - Confirm ActionResponse can be read from GetOutput.
actionResponse = this.session.GetOutput("Container_CollectDiagnosticsAction");
Assert.AreEqual(
"RunCollectDiagnostics.exe_Results",
actionResponse.Output,
"Expected to successfully read ActionResponse.Output.");
Assert.AreEqual(
"Success",
actionResponse.Status,
"Expected to successfully read ActionResponse.Status.");
// Test 3 - Confirm ActionResponse can be read from GetLastActionResponseAsync.
actionResponse = this.session.GetLastActionResponseAsync().GetAwaiter().GetResult();
Assert.AreEqual(
"Success",
actionResponse.Status,
"Expected to successfully read ActionResponse.Status.");
// Test 4 - Confirm ActionResponse can be read from GetLastActionResponse.
actionResponse = this.session.GetLastActionResponse();
Assert.AreEqual(
"Success",
actionResponse.Status,
"Expected to successfully read ActionResponse.Status.");
}
// TODO: Add back test once we decide how we want to handle this case: Should people be able to change function definitions?
// TestScript in local jsonSchema is related to this test.
// [TestMethod]
// public void TestTreeWalkerSession_WalkTree_Success_ChangingFunctionDefinition()
// {
// this.TestInitialize(jsonSchema: ForgeSchemaHelper.TardigradeScenario);
// Assert.AreEqual(this.UserContext.GetCount(), 1);
// string actual = this.session.WalkTree("TestScript").GetAwaiter().GetResult();
// Assert.AreEqual(2, this.UserContext.GetCount());
// }
// TODO: Add back test once we decide how we want to handle this case: Should people be able to change function definitions?
// TestScript in local jsonSchema is related to this test.
[TestMethod]
public void Test_EvaluateInputType_Success()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TestEvaluateInputTypeAction);
// Test - WalkTree to execute an Action with its ActionInput type defined in the ActionDefinition.InputType.
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("RanToCompletion", actualStatus);
Assert.AreEqual(
true,
this.session.GetLastActionResponse().Output,
"Expected to successfully retrieve the Func output value from the action.");
}
[TestMethod]
public void Test_EvaluateInputType_UnexpectedFieldFail()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TestEvaluateInputType_FailOnField_Action);
// Test - WalkTree and expect the Status to be Failed_EvaluateDynamicProperty
// because ActionInput type defined in the ActionDefinition.InputType contained an unexpected public Field.
string actual;
Assert.ThrowsException<EvaluateDynamicPropertyException>(() =>
{
actual = this.session.WalkTree("Root").GetAwaiter().GetResult();
}, "Expected WalkTree to fail because ActionInput type defined in the ActionDefinition.InputType contained an unexpected public Field.");
actual = this.session.Status;
Assert.AreEqual(
"Failed_EvaluateDynamicProperty",
actual,
"Expected WalkTree to fail because ActionInput type defined in the ActionDefinition.InputType contained an unexpected public Field.");
}
[TestMethod]
public void Test_EvaluateInputType_UnexpectedPropertyFail()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TestEvaluateInputTypeAction_UnexpectedPropertyFail);
// Test - WalkTree and expect the Status to be Failed_EvaluateDynamicProperty
// because the schema contained a Property that does not exist in ActionDefinition.InputType.
string actual;
Assert.ThrowsException<EvaluateDynamicPropertyException>(() =>
{
actual = this.session.WalkTree("Root").GetAwaiter().GetResult();
}, "Expected WalkTree to fail because the schema contained a Property that does not exist in ActionDefinition.InputType.");
actual = this.session.Status;
Assert.AreEqual(
"Failed_EvaluateDynamicProperty",
actual,
"Expected WalkTree to fail because the schema contained a Property that does not exist in ActionDefinition.InputType.");
}
[TestMethod]
public void Test_EvaluateInputType_ParameterizedConstructorFail()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.TestEvaluateInputType_FailOnNonEmptyCtor_Action);
// Test - WalkTree and expect the Status to be Failed_EvaluateDynamicProperty
// because its ActionDefinition.InputType did not have a parameterless constructor.
string actual;
Assert.ThrowsException<EvaluateDynamicPropertyException>(() =>
{
actual = this.session.WalkTree("Root").GetAwaiter().GetResult();
}, "Expected WalkTree to fail because its ActionDefinition.InputType did not have a parameterless constructor.");
actual = this.session.Status;
Assert.AreEqual(
"Failed_EvaluateDynamicProperty",
actual,
"Expected WalkTree to fail because its ActionDefinition.InputType did not have a parameterless constructor.");
}
[TestMethod]
public void Test_LeafNodeSummaryAction_Success()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.LeafNodeSummaryAction);
// Test - WalkTree to execute a LeafNodeSummaryAction node with its ActionInput set to ActionResponse properties.
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("RanToCompletion", actualStatus);
ActionResponse leafActionResponse = this.session.GetLastActionResponse();
Assert.AreEqual(
"Success",
leafActionResponse.Status,
"Expected to successfully retrieve the Func output value from the action.");
Assert.AreEqual(
1,
leafActionResponse.StatusCode,
"Expected to successfully retrieve the Func output value from the action.");
Assert.AreEqual(
"TheResult",
leafActionResponse.Output,
"Expected to successfully retrieve the Func output value from the action.");
}
[TestMethod]
public void Test_LeafNodeSummaryAction_InputAsObject_Success()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.LeafNodeSummaryAction_InputIsActionResponse);
// Test - WalkTree to execute a LeafNodeSummaryAction node with its ActionInput set to ActionResponse object of the previously ran Action in the parent node.
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("RanToCompletion", actualStatus);
ActionResponse leafActionResponse = this.session.GetLastActionResponse();
Assert.AreEqual(
"Success",
leafActionResponse.Status,
"Expected to successfully retrieve the Func output value from the action.");
Assert.AreEqual(
"TheCommand_Results",
leafActionResponse.Output,
"Expected to successfully retrieve the Func output value from the action.");
}
[TestMethod]
public void Test_ExternalExecutors()
{
this.TestInitialize(jsonSchema: ForgeSchemaHelper.ExternalExecutors);
Dictionary<string, Func<string, CancellationToken, Task<object>>> externalExecutors = new Dictionary<string, Func<string, CancellationToken, Task<object>>>();
externalExecutors.Add("External|", External);
this.parameters.ExternalExecutors = externalExecutors;
this.session = new TreeWalkerSession(this.parameters);
// Test - WalkTree to execute an Action with an ActionInput that uses an external executor. Confirm expected results.
string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
Assert.AreEqual("RanToCompletion", actualStatus);
ActionResponse leafActionResponse = this.session.GetLastActionResponse();
Assert.AreEqual(
"StatusResult_Executed",
leafActionResponse.Status,
"Expected to successfully retrieve the Func output value from the action.");
}
private static async Task<object> External(string expression, CancellationToken token)
{
// External executes the expression and returns.
await Task.Delay(1, token);
return expression + "_Executed";
}
private sealed class TreeWalkerCallbacks : ITreeWalkerCallbacks
{
public async Task BeforeVisitNode(
Guid sessionId,
string treeNodeKey,
dynamic properties,
dynamic userContext,
CancellationToken token)
{
string serializeProperties = JsonConvert.SerializeObject(properties);
await Task.Run(() => Console.WriteLine(string.Format(
"OnBeforeVisitNode: SessionId: {0}, TreeNodeKey: {1}, Properties: {2}.",
sessionId,
treeNodeKey,
serializeProperties)));
}
public Task AfterVisitNode(
Guid sessionId,
string treeNodeKey,
dynamic properties,
dynamic userContext,
CancellationToken token)
{
Console.WriteLine(string.Format(
"OnAfterVisitNode: SessionId: {0}, TreeNodeKey: {1}, Properties: {2}.",
sessionId,
treeNodeKey,
JsonConvert.SerializeObject(properties)));
return Task.FromResult(0);
}
}
public abstract class BaseCommonAction : BaseAction
{
public object Input { get; private set; }
public CancellationToken Token { get; private set; }
public Guid SessionId { get; private set; }
public string TreeNodeKey { get; private set; }
private ActionContext actionContext;
public override Task<ActionResponse> RunAction(ActionContext actionContext)
{
this.Input = actionContext.ActionInput;
this.Token = actionContext.Token;
this.SessionId = actionContext.SessionId;
this.TreeNodeKey = actionContext.TreeNodeKey;
this.actionContext = actionContext;
return this.RunAction();
}
public abstract Task<ActionResponse> RunAction();
public Task CommitIntermediates<T>(T intermediates)
{
return this.actionContext.CommitIntermediates<T>(intermediates);
}
public Task<T> GetIntermediates<T>()
{
return this.actionContext.GetIntermediates<T>();
}
}
[ForgeAction(InputType: typeof(CollectDiagnosticsInput))]
public class CollectDiagnosticsAction : BaseCommonAction
{
public override async Task<ActionResponse> RunAction()
{
CollectDiagnosticsInput actionInput = (CollectDiagnosticsInput)this.Input;
// Collect diagnostics using the input command and commit results.
string intermediates = this.GetIntermediates<string>().GetAwaiter().GetResult();
Assert.AreEqual(null, intermediates);
string result = MockCollectDiagnosticsResult(actionInput.Command);
intermediates = result;
this.CommitIntermediates<string>(intermediates).GetAwaiter().GetResult();
Assert.AreEqual(intermediates, this.GetIntermediates<string>().GetAwaiter().GetResult());
await Task.Run(() => Console.WriteLine(string.Format(
"CollectDiagnosticsAction - SessionId: {0}, TreeNodeKey: {1}, ActionInput: {2}.",
this.SessionId,
this.TreeNodeKey,
JsonConvert.SerializeObject(actionInput))));
ActionResponse actionResponse = new ActionResponse() { Status = "Success", Output = result };
return actionResponse;
}
private static string MockCollectDiagnosticsResult(string command)
{
return command + "_Results";
}
}
public class CollectDiagnosticsInput
{
public string Command { get; set; }
}
[ForgeAction]
public class TardigradeAction : BaseCommonAction
{
public override async Task<ActionResponse> RunAction()
{
await Task.Run(() => Console.WriteLine(string.Format(
"TardigradeAction - SessionId: {0}, TreeNodeKey: {1}.",
this.SessionId,
this.TreeNodeKey)));
ActionResponse actionResponse = new ActionResponse() { Status = "Success" };
return actionResponse;
}
}
[ForgeAction(InputType: typeof(TestDelayExceptionInput))]
public class TestDelayExceptionAction : BaseCommonAction
{
public override async Task<ActionResponse> RunAction()
{
TestDelayExceptionInput actionInput = (TestDelayExceptionInput)this.Input;
Console.WriteLine(string.Format(
"TestDelayExceptionAction - SessionId: {0}, TreeNodeKey: {1}, ActionInput: {2}.",
this.SessionId,
this.TreeNodeKey,
JsonConvert.SerializeObject(actionInput)));
await Task.Delay(actionInput.DelayMilliseconds, this.Token);
if (actionInput.ThrowException)
{
throw new NullReferenceException("Throwing unexpected Exception!!");
}
ActionResponse actionResponse = new ActionResponse() { Status = "Success" };
return actionResponse;
}
}
public class TestDelayExceptionInput
{
public int DelayMilliseconds { get; set; } = 0;
public bool ThrowException { get; set; } = false;
}
[ForgeAction(InputType: typeof(FooActionInput))]
public class TestEvaluateInputTypeAction : BaseCommonAction
{
public override async Task<ActionResponse> RunAction()
{
FooActionInput actionInput = (FooActionInput)this.Input;
bool boolDelegateResult = actionInput.BoolDelegate();
bool boolDelegateAsyncResult = await actionInput.BoolDelegateAsync();
Console.WriteLine(string.Format(
"TestEvaluateInputTypeAction - SessionId: {0}, TreeNodeKey: {1}, ActionInput: {2}, BoolDelegateResult: {3}, BoolDelegateAsyncResult: {4}.",
this.SessionId,
this.TreeNodeKey,
JsonConvert.SerializeObject(actionInput),
boolDelegateResult,
boolDelegateAsyncResult));
ActionResponse actionResponse = new ActionResponse() { Status = "Success", Output = boolDelegateResult && boolDelegateAsyncResult };
return actionResponse;
}
}
public class FooActionInput
{
public string Command { get; set; }
public int IntExpression { get; set; }
public bool BoolExpression { get; set; }
public string PropertyNotInSchema { get; set; }
public FooActionObject NestedObject { get; set; }
public FooActionObject[] ObjectArray { get; set; }
public string[] StringArray { get; set; }
public long[] LongArray { get; set; }
public Func<bool> BoolDelegate { get; set; }
public Func<Task<bool>> BoolDelegateAsync { get; set; }
}
public class FooActionObject
{
public string Name { get; set; }
public string Value { get; set; }
}
[ForgeAction(InputType: typeof(FooActionInput_UnexpectedField))]
public class TestEvaluateInputType_FailOnField_Action : BaseCommonAction
{
public override async Task<ActionResponse> RunAction()
{
FooActionInput_UnexpectedField actionInput = (FooActionInput_UnexpectedField)this.Input;
await Task.Run(() => Console.WriteLine(string.Format(
"TestEvaluateInputType_FailOnField_Action - SessionId: {0}, TreeNodeKey: {1}, ActionInput: {2}.",
this.SessionId,
this.TreeNodeKey,
JsonConvert.SerializeObject(actionInput))));
ActionResponse actionResponse = new ActionResponse() { Status = "Success" };
return actionResponse;
}
}
public class FooActionInput_UnexpectedField
{
public bool UnexpectedField = false;
}
[ForgeAction(InputType: typeof(FooActionInput_NonEmptyConstructor))]
public class TestEvaluateInputType_FailOnNonEmptyCtor_Action : BaseCommonAction
{
public override async Task<ActionResponse> RunAction()
{
FooActionInput_NonEmptyConstructor actionInput = (FooActionInput_NonEmptyConstructor)this.Input;
await Task.Run(() => Console.WriteLine(string.Format(
"TestEvaluateInputType_FailOnNonEmptyCtor_Action - SessionId: {0}, TreeNodeKey: {1}, ActionInput: {2}.",
this.SessionId,
this.TreeNodeKey,
JsonConvert.SerializeObject(actionInput))));
ActionResponse actionResponse = new ActionResponse() { Status = "Success" };
return actionResponse;
}
}
public class FooActionInput_NonEmptyConstructor
{
public bool BoolProperty { get; set; }
public FooActionInput_NonEmptyConstructor(int unexpectedParameter) {}
}
}
}

Просмотреть файл

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Forge.TreeWalker</RootNamespace>
<AssemblyName>Forge.TreeWalker</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Collections.Immutable, Version=1.1.37.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Collections.Immutable.1.1.37\lib\dotnet\System.Collections.Immutable.dll</HintPath>
</Reference>
<Reference Include="System.IO" />
<Reference Include="System.Reflection" />
<Reference Include="System.Reflection.Metadata, Version=1.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Reflection.Metadata.1.2.0\lib\portable-net45+win8\System.Reflection.Metadata.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.CodeAnalysis, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.CodeAnalysis.Common.1.3.2\lib\net45\Microsoft.CodeAnalysis.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CodeAnalysis.CSharp, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.CodeAnalysis.CSharp.1.3.2\lib\net45\Microsoft.CodeAnalysis.CSharp.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CodeAnalysis.CSharp.Scripting, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.CodeAnalysis.CSharp.Scripting.1.3.2\lib\dotnet\Microsoft.CodeAnalysis.CSharp.Scripting.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CodeAnalysis.Scripting, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.CodeAnalysis.Scripting.Common.1.3.2\lib\dotnet\Microsoft.CodeAnalysis.Scripting.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="src\ActionContext.cs" />
<Compile Include="src\ActionDefinition.cs" />
<Compile Include="src\ActionResponse.cs" />
<Compile Include="src\BaseAction.cs" />
<Compile Include="src\ExpressionExecutor.cs" />
<Compile Include="src\ForgeActionAttribute.cs" />
<Compile Include="src\ForgeDictionary.cs" />
<Compile Include="src\ForgeExceptions.cs" />
<Compile Include="src\IForgeDictionary.cs" />
<Compile Include="src\ITreeSession.cs" />
<Compile Include="src\ITreeWalkerCallbacks.cs" />
<Compile Include="src\TreeWalkerParameters.cs" />
<Compile Include="src\TreeWalkerSession.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Forge.DataContracts\Forge.DataContracts.csproj">
<Project>{c49a8494-13e5-4214-8434-708ba280c5b1}</Project>
<Name>Forge.DataContracts</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

Просмотреть файл

@ -0,0 +1,38 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: InternalsVisibleTo("Forge.TreeWalker.UnitTests")]
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Forge")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Forge")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("00fc1c22-6ae9-4f60-8a3e-05885ba34c9c")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Просмотреть файл

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-1.2.3.0" newVersion="1.2.3.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

Просмотреть файл

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.CodeAnalysis" version="1.3.2" targetFramework="net462" />
<package id="Microsoft.CodeAnalysis.CSharp" version="1.3.2" targetFramework="net462" />
<package id="Microsoft.CodeAnalysis.CSharp.Scripting" version="1.3.2" targetFramework="net462" />
<package id="Microsoft.CodeAnalysis.Scripting" version="1.3.2" targetFramework="net462" />
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net462" />
<package id="System.Collections.Immutable" version="1.1.37" targetFramework="net462" />
<package id="System.Reflection.Metadata" version="1.2.0" targetFramework="net462" />
</packages>

Просмотреть файл

@ -0,0 +1,134 @@
//-----------------------------------------------------------------------
// <copyright file="ActionContext.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The ActionContext class.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker
{
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// The ActionContext class object is passed to Actions to give them contextual data and methods to help them execute.
/// </summary>
public class ActionContext
{
/// <summary>
/// The unique identifier for this tree walking session.
/// </summary>
public Guid SessionId { get; private set; }
/// <summary>
/// The tree node key where the Action resides.
/// </summary>
public string TreeNodeKey { get; private set; }
/// <summary>
/// The tree action key of this Action.
/// </summary>
public string TreeActionKey { get; private set; }
/// <summary>
/// The name of this Action.
/// </summary>
public string ActionName { get; private set; }
/// <summary>
/// The dynamic input for the Action given by the ForgeTree schema.
/// </summary>
public object ActionInput { get; private set; }
/// <summary>
/// The dynamic properties of this Action given by the ForgeTree schema.
/// </summary>
public object Properties { get; private set; }
/// <summary>
/// The dynamic user-defined context object that is able to be referenced when evaluating schema expressions or performing actions.
/// </summary>
public object UserContext { get; private set; }
/// <summary>
/// The cancellation token.
/// </summary>
public CancellationToken Token { get; private set; }
/// <summary>
/// The forgeState dictionary that holds information relevant to Forge and Actions.
/// </summary>
private IForgeDictionary forgeState;
/// <summary>
/// Instantiates an ActionContext object.
/// </summary>
/// <param name="sessionId">The unique identifier for this tree walking session.</param>
/// <param name="treeNodeKey">The TreeNode's key where the Action is taking place.</param>
/// <param name="treeActionKey">The TreeAction's key of the Action taking place.</param>
/// <param name="actionName">The name of the Action.</param>
/// <param name="actionInput">The input for this Action.</param>
/// <param name="properties">The properties of this Action.</param>
/// <param name="userContext">The user context for this Action.</param>
/// <param name="token">The cancellation token.</param>
/// <param name="forgeState">The forge state dictionary.</param>
public ActionContext(
Guid sessionId,
string treeNodeKey,
string treeActionKey,
string actionName,
object actionInput,
object properties,
object userContext,
CancellationToken token,
IForgeDictionary forgeState)
{
if (sessionId == null) throw new ArgumentNullException("sessionId");
if (string.IsNullOrWhiteSpace(treeNodeKey)) throw new ArgumentNullException("treeNodeKey");
if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentNullException("actionName");
if (userContext == null) throw new ArgumentNullException("userContext");
if (token == null) throw new ArgumentNullException("token");
if (forgeState == null) throw new ArgumentNullException("forgeState");
this.SessionId = sessionId;
this.TreeNodeKey = treeNodeKey;
this.TreeActionKey = treeActionKey;
this.ActionName = actionName;
this.ActionInput = actionInput;
this.Properties = properties;
this.UserContext = userContext;
this.Token = token;
this.forgeState = forgeState;
}
/// <summary>
/// Commits an Intermediates object for this Action to the forgeState.
/// Since Intermediates are available to the Action on each retry, this allows Actions to persist state across retries.
/// </summary>
/// <param name="intermediates">The intermediates object to be committed for this Action.</param>
public Task CommitIntermediates<T>(T intermediates)
{
return this.forgeState.Set<T>(this.TreeActionKey + TreeWalkerSession.IntermediatesSuffix, intermediates);
}
/// <summary>
/// Gets the previously committed Intermediates data for this Action from the forgeState.
/// </summary>
/// <returns>The Intermediates data for this Action if it exists, otherwise default(T).</returns>
public async Task<T> GetIntermediates<T>()
{
try
{
return await this.forgeState.GetValue<T>(this.TreeActionKey + TreeWalkerSession.IntermediatesSuffix).ConfigureAwait(false);
}
catch
{
return default(T);
}
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
//-----------------------------------------------------------------------
// <copyright file="ActionDefinition.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The ActionDefinition class.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker
{
using System;
using System.Threading.Tasks;
/// <summary>
/// The ActionDefinition class holds definitions for the action.
/// </summary>
public class ActionDefinition
{
/// <summary>
/// The Type of the ForgeAction class.
/// </summary>
public Type ActionType { get; set; }
/// <summary>
/// The InputType for this Action that will be passed to the Action by Forge when executing the Action.
/// When given, Forge will instantiate this type when evaluating the ActionInput from the schema.
/// When null, Forge will instead create a dynamic object.
/// Restrictions: Only the public Properties of the InputType will be instantiated.
/// Objects lacking a parameterless constructor are not supported.
/// Objects with public fields are not supported.
/// </summary>
public Type InputType { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,32 @@
//-----------------------------------------------------------------------
// <copyright file="ActionResponse.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The ActionResponse class.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker
{
/// <summary>
/// The ActionResponse class holds the response information from actions.
/// </summary>
public class ActionResponse
{
/// <summary>
/// The status code of this action response.
/// </summary>
public int StatusCode { get; set; }
/// <summary>
/// The status of this action response.
/// </summary>
public string Status { get; set; }
/// <summary>
/// The dynamic output of this action response.
/// </summary>
public object Output { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,28 @@
//-----------------------------------------------------------------------
// <copyright file="BaseAction.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The BaseAction abstract class.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker
{
using System;
using System.Threading.Tasks;
/// <summary>
/// The BaseAction abstract class must be inherited by all ForgeActions tagged with the ForgeActionAttribute.
/// All ForgeActionAttribute tagged classes should reside in the Assembly that is passed to Forge TreeWalkerSession.
/// When Forge encounters an ActionName while walking the tree, it will instantiate the ForgeAction type and then call RunAction.
/// </summary>
public abstract class BaseAction
{
/// <summary>
/// The RunAction method is called when Forge encounters an ActionName while walking the tree.
/// </summary>
/// <param name="actionContext">The action context holding relevant information for this Action.</param>
public abstract Task<ActionResponse> RunAction(ActionContext actionContext);
}
}

Просмотреть файл

@ -0,0 +1,155 @@
//-----------------------------------------------------------------------
// <copyright file="ExpressionExecutor.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The ExpressionExecutor class.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
/// <summary>
/// The ExpressionExecutor dynamically compiles code and executes it using Roslyn.
/// </summary>
public class ExpressionExecutor
{
/// <summary>
/// List of external type dependencies needed to compile expressions.
/// </summary>
private List<Type> dependencies;
/// <summary>
/// Script cache used to cache and re-use compiled Roslyn scripts.
/// </summary>
private ConcurrentDictionary<string, Script<object>> scriptCache;
/// <summary>
/// Roslyn script options.
/// </summary>
private ScriptOptions scriptOptions;
/// <summary>
/// Global parameters passed to Roslyn scripts that can be referenced inside expressions.
/// </summary>
private CodeGenInputParams parameters;
/// <summary>
/// Instantiates the ExpressionExecutor class with objects that can be referenced in the schema.
/// </summary>
/// <param name="session">The tree session.</param>
/// <param name="userContext">The dynamic user context.</param>
/// <param name="dependencies">Type dependencies required to compile the schema. Can be null if no external dependencies required.</param>
/// <param name="scriptCache">Script cache used to cache and re-use compiled Roslyn scripts.</param>
public ExpressionExecutor(ITreeSession session, object userContext, List<Type> dependencies, ConcurrentDictionary<string, Script<object>> scriptCache)
{
this.dependencies = dependencies;
this.parameters = new CodeGenInputParams
{
UserContext = userContext,
Session = session
};
this.scriptCache = scriptCache ?? new ConcurrentDictionary<string, Script<object>>();
this.Initialize();
}
/// <summary>
/// Instantiates the ExpressionExecutor class with objects that can be referenced in the schema.
/// </summary>
/// <param name="session">The tree session.</param>
/// <param name="userContext">The dynamic user context.</param>
/// <param name="dependencies">Type dependencies required to compile the schema. Can be null if no external dependencies required.</param>
public ExpressionExecutor(ITreeSession session, object userContext, List<Type> dependencies)
: this(session, userContext, dependencies, new ConcurrentDictionary<string, Script<object>>())
{
}
/// <summary>
/// Executes the given expression and returns the result as the given generic type.
/// </summary>
/// <param name="expression">The expression to evaluate.</param>
/// <returns>The T value of the evaluated code.</returns>
public async Task<T> Execute<T>(string expression)
{
var script = this.scriptCache.GetOrAdd(
expression,
(key) => CSharpScript.Create<object>(
string.Format("return {0};", expression),
this.scriptOptions,
typeof(CodeGenInputParams)));
return (T)(await script.RunAsync(this.parameters).ConfigureAwait(false)).ReturnValue;
}
/// <summary>
/// Initializes Roslyn script options with all the required assemblies, references, and external dependencies.
/// </summary>
private void Initialize()
{
this.scriptOptions = ScriptOptions.Default;
// Add references to required assemblies.
Assembly mscorlib = typeof(object).Assembly;
Assembly cSharpAssembly = typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly;
this.scriptOptions = this.scriptOptions.AddReferences(mscorlib, cSharpAssembly);
// Add required namespaces.
this.scriptOptions = this.scriptOptions.AddImports("System");
this.scriptOptions = this.scriptOptions.AddImports("System.Threading.Tasks");
string systemCoreAssemblyName = mscorlib.GetName().Name;
// Add external dependencies.
if (this.dependencies != null)
{
foreach (Type type in this.dependencies)
{
string fullAssemblyName = type.Assembly.GetName().Name;
// While adding the reference again is okay, we can not AddImports for systemCoreAssembly.
if (fullAssemblyName == systemCoreAssemblyName)
{
continue;
}
this.scriptOptions = this.scriptOptions.AddReferences(type.Assembly).AddImports(type.Namespace);
}
}
}
/// <summary>
/// Internal method used for testing if ScriptCache contains the expression key.
/// </summary>
/// <param name="expression">The expression key.</param>
/// <returns>True if the expression key exists, otherwise false.</returns>
internal bool ScriptCacheContainsKey(string expression)
{
return this.scriptCache.ContainsKey(expression);
}
/// <summary>
/// This class defines the global parameter that will be
/// passed into the Roslyn expression evaluator.
/// </summary>
public class CodeGenInputParams
{
/// <summary>
/// The dynamic UserContext object that holds properties and methods that can be referenced in the schema.
/// </summary>
public dynamic UserContext { get; set; }
/// <summary>
/// The ITreeSession interface that holds accessor methods into the forgeState dictionary that can be referenced in the schema.
/// </summary>
public ITreeSession Session { get; set; }
}
}
}

Просмотреть файл

@ -0,0 +1,52 @@
//-----------------------------------------------------------------------
// <copyright file="ForgeActionAttribute.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The ForgeActionAttribute class.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.Attributes
{
using System;
/// <summary>
/// The ForgeActionAttribute class defines the ForgeAction attribute.
///
/// This attribute should be added to all Forge Action classes that wish to be called by Forge while walking the tree.
/// The Name of the Action class will be used in the ForgeTree schema file to map to the Action with this ForgeAction attribute.
/// Action classes must inherit from Forge's BaseAction class, as Forge will call the Setup and RunAction methods on these classes.
///
/// InputType should be included if the Action wishes for Forge to instantiate that object from the ForgeSchema and pass it to the Action.
/// Otherwise, Forge will create a dynamic object from the Input in ForgeSchema if it exists, and pass it to the Action.
///
/// Ex) [ForgeAction(InputType: typeof(FooInput))]
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class ForgeActionAttribute : Attribute
{
/// <summary>
/// InputType should be included if the Action wishes for Forge to instantiate that object from the ForgeSchema and pass it to the Action.
/// If null, Forge will create a dynamic object from the Input in ForgeSchema if it exists and pass it to the Action.
/// </summary>
public Type InputType { get; private set; }
/// <summary>
/// Instantiates a ForgeActionAttribute.
/// </summary>
/// <param name="InputType">The input Type for this Action.</param>
public ForgeActionAttribute(Type InputType)
{
this.InputType = InputType;
}
/// <summary>
/// Instantiates a ForgeActionAttribute.
/// </summary>
public ForgeActionAttribute()
: this(null)
{
}
}
}

Просмотреть файл

@ -0,0 +1,165 @@
//-----------------------------------------------------------------------
// <copyright file="ForgeDictionary.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The ForgeDictionary class.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
/// <summary>
/// The ForgeDictionary class defines the methods for accessing forge state.
/// Note: The KeyPrefix should always precede the key when using the forgeStateTable to limit the scope to the current SessionId.
/// </summary>
public class ForgeDictionary : IForgeDictionary
{
/// <summary>
/// The unique identifier for this session.
/// </summary>
public Guid SessionId { get; set; }
/// <summary>
/// The key prefix that should precede the keys when using the forgeStateTable.
/// This ensures the scope of the table is limited to the current SessionId.
/// </summary>
private string keyPrefix;
/// <summary>
/// The dictionary holding the forge state.
/// Maps the string key to object value.
/// The key should always be preceded by the KeyPrefix. This ensures the scope of the table is limited to the current SessionId.
/// </summary>
private IDictionary<string, object> forgeStateTable;
/// <summary>
/// ForgeDictionary Constructor.
/// </summary>
/// <param name="forgeStateTable">The forge state table dictionary object.</param>
/// <param name="sessionId">The unique identifier for this session.</param>
public ForgeDictionary(IDictionary<string, object> forgeStateTable, Guid sessionId)
{
this.forgeStateTable = forgeStateTable;
this.SessionId = sessionId;
this.keyPrefix = this.SessionId + "_";
}
/// <summary>
/// Sets an element with the provided key and value to the backing store.
/// </summary>
/// <param name="key">The key of the element to set.</param>
/// <param name="value">The value of the element to be set.</param>
public Task Set<T>(string key, T value)
{
this.forgeStateTable[this.keyPrefix + key] = (object)value;
return Task.FromResult(0);
}
/// <summary>
/// Sets a list of key value pairs to the backing store.
/// </summary>
/// <param name="kvps">The list of key value pairs to set.</param>
public Task SetRange<T>(List<KeyValuePair<string, T>> kvps)
{
foreach(KeyValuePair<string, T> kvp in kvps)
{
this.forgeStateTable[this.keyPrefix + kvp.Key] = (object)kvp.Value;
}
return Task.FromResult(0);
}
/// <summary>
/// Gets an element with the provided key from the backing store.
/// </summary>
/// <param name="key">The key of the element to get.</param>
/// <returns>The value of the element to get.</returns>
public Task<T> GetValue<T>(string key)
{
return Task.FromResult((T)this.forgeStateTable[this.keyPrefix + key]);
}
/// <summary>
/// Removes an element with the provided key from the backing store.
/// </summary>
/// <param name="key">The key of the element to remove.</param>
/// <returns>True of the element was removed, False otherwise.</returns>
public Task<bool> RemoveKey(string key)
{
return Task.FromResult(this.forgeStateTable.Remove(this.keyPrefix + key));
}
/// <summary>
/// Removes a list of elements with the provided keys from the backing store.
/// </summary>
/// <param name="keys">The list of keys to remove.</param>
public Task RemoveKeys(List<string> keys)
{
foreach(string key in keys)
{
this.forgeStateTable.Remove(this.keyPrefix + key);
}
return Task.FromResult(0);
}
/// <summary>
/// Sets an element with the provided key and value to the backing store.
/// </summary>
/// <param name="key">The key of the element to set.</param>
/// <param name="value">The value of the element to be set.</param>
public void SetSync<T>(string key, T value)
{
this.forgeStateTable[this.keyPrefix + key] = (object)value;
}
/// <summary>
/// Sets a list of key value pairs to the backing store.
/// </summary>
/// <param name="kvps">The list of key value pairs to set.</param>
public void SetRangeSync<T>(List<KeyValuePair<string, T>> kvps)
{
foreach(KeyValuePair<string, T> kvp in kvps)
{
this.forgeStateTable[this.keyPrefix + kvp.Key] = (object)kvp.Value;
}
}
/// <summary>
/// Gets an element with the provided key from the backing store.
/// </summary>
/// <param name="key">The key of the element to get.</param>
/// <returns>The value of the element to get.</returns>
public T GetValueSync<T>(string key)
{
return (T)this.forgeStateTable[this.keyPrefix + key];
}
/// <summary>
/// Removes an element with the provided key from the backing store.
/// </summary>
/// <param name="key">The key of the element to remove.</param>
/// <returns>True of the element was removed, False otherwise.</returns>
public bool RemoveKeySync(string key)
{
return this.forgeStateTable.Remove(this.keyPrefix + key);
}
/// <summary>
/// Removes a list of elements with the provided keys from the backing store.
/// </summary>
/// <param name="keys">The list of keys to remove.</param>
public void RemoveKeysSync(List<string> keys)
{
foreach(string key in keys)
{
this.forgeStateTable.Remove(this.keyPrefix + key);
}
}
}
}

Просмотреть файл

@ -0,0 +1,78 @@
//-----------------------------------------------------------------------
// <copyright file="ForgeExceptions.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The ForgeExceptions.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker.ForgeExceptions
{
using System;
/// <summary>
/// Exception thrown on action timeout.
/// </summary>
public class ActionTimeoutException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ActionTimeoutException"/> class.
/// </summary>
/// <param name="message">The message.</param>
public ActionTimeoutException(string message)
: base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ActionTimeoutException"/> class.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="inner">The inner exception.</param>
public ActionTimeoutException(string message, Exception inner)
: base(message, inner)
{
}
}
/// <summary>
/// Exception thrown when ChildSelector fails to select any child.
/// </summary>
public class NoChildMatchedException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="NoChildMatchedException"/> class.
/// </summary>
/// <param name="message">The message.</param>
public NoChildMatchedException(string message)
: base(message)
{
}
}
/// <summary>
/// Exception thrown when EvaluateDynamicProperty fails.
/// </summary>
public class EvaluateDynamicPropertyException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="EvaluateDynamicPropertyException"/> class.
/// </summary>
/// <param name="message">The message.</param>
public EvaluateDynamicPropertyException(string message)
: base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EvaluateDynamicPropertyException"/> class.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="inner">The inner exception.</param>
public EvaluateDynamicPropertyException(string message, Exception inner)
: base(message, inner)
{
}
}
}

Просмотреть файл

@ -0,0 +1,60 @@
//-----------------------------------------------------------------------
// <copyright file="IForgeDictionary.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The IForgeDictionary interface.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
/// <summary>
/// The IForgeDictionary interface defines the methods for accessing forge state.
/// Note: The KeyPrefix should always precede the key when using the forgeStateTable to limit the scope to the current SessionId.
/// </summary>
public interface IForgeDictionary
{
/// <summary>
/// The unique identifier for this session.
/// </summary>
Guid SessionId { get; set; }
/// <summary>
/// Sets an element with the provided key and value to the backing store.
/// </summary>
/// <param name="key">The key of the element to set.</param>
/// <param name="value">The value of the element to be set.</param>
Task Set<T>(string key, T value);
/// <summary>
/// Sets a list of key value pairs to the backing store.
/// </summary>
/// <param name="kvps">The list of key value pairs to set.</param>
Task SetRange<T>(List<KeyValuePair<string, T>> kvps);
/// <summary>
/// Gets an element with the provided key from the backing store.
/// </summary>
/// <param name="key">The key of the element to get.</param>
/// <returns>The value of the element to get.</returns>
Task<T> GetValue<T>(string key);
/// <summary>
/// Removes an element with the provided key from the backing store.
/// </summary>
/// <param name="key">The key of the element to remove.</param>
/// <returns>True of the element was removed, False otherwise.</returns>
Task<bool> RemoveKey(string key);
/// <summary>
/// Removes a list of elements with the provided keys from the backing store.
/// </summary>
/// <param name="keys">The list of keys to remove.</param>
Task RemoveKeys(List<string> keys);
}
}

Просмотреть файл

@ -0,0 +1,46 @@
//-----------------------------------------------------------------------
// <copyright file="ITreeSession.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The ITreeSession interface.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker
{
using System;
using System.Threading.Tasks;
/// <summary>
/// The ITreeSession interface holds accessor methods into the forgeState dictionary.
/// </summary>
public interface ITreeSession
{
/// <summary>
/// Gets the ActionResponse data from the forgeState for the given tree action key.
/// </summary>
/// <param name="treeActionKey">The TreeAction's key of the action that was executed.</param>
/// <returns>The ActionResponse data for the given tree action key if it exists, otherwise null.</returns>
ActionResponse GetOutput(string treeActionKey);
/// <summary>
/// Asynchronously gets the ActionResponse data from the forgeState for the given tree action key.
/// </summary>
/// <param name="treeActionKey">The TreeAction's key of the action that was executed.</param>
/// <returns>The ActionResponse data for the given tree action key if it exists, otherwise null.</returns>
Task<ActionResponse> GetOutputAsync(string treeActionKey);
/// <summary>
/// Gets the last executed TreeAction's ActionResponse data from the forgeState.
/// </summary>
/// <returns>The ActionResponse data for the last executed tree action key if it exists, otherwise null.</returns>
ActionResponse GetLastActionResponse();
/// <summary>
/// Asynchronously gets the last executed TreeAction's ActionResponse data from the forgeState.
/// </summary>
/// <returns>The ActionResponse data for the last executed tree action key if it exists, otherwise null.</returns>
Task<ActionResponse> GetLastActionResponseAsync();
}
}

Просмотреть файл

@ -0,0 +1,43 @@
//-----------------------------------------------------------------------
// <copyright file="ITreeWalkerCallbacks.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The ITreeWalkerCallbacks interface.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker
{
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// The ITreeWalkerCallbacks interface defines the callback Tasks that are awaited while walking the tree.
/// </summary>
public interface ITreeWalkerCallbacks
{
/// <summary>
/// The callback Task that is awaited before visiting each node.
/// </summary>
/// <param name="sessionId">The Id of this tree walking session.</param>
/// <param name="treeNodeKey">The key of the current tree node being visited by Forge.</param>
/// <param name="properties">The additional properties for this node.</param>
/// <param name="userContext">The dynamic user-defined context object.</param>
/// <param name="token">The cancellation token.</param>
Task BeforeVisitNode(Guid sessionId, string treeNodeKey, dynamic properties, object userContext, CancellationToken token);
/// <summary>
/// The callback Task that is awaited after visiting each node.
/// </summary>
/// <param name="sessionId">The Id of this tree walking session.</param>
/// <param name="treeNodeKey">The key of the current tree node being visited by Forge.</param>
/// <param name="properties">The additional properties for this node.</param>
/// <param name="userContext">The dynamic user-defined context object.</param>
/// <param name="token">The cancellation token.</param>
Task AfterVisitNode(Guid sessionId, string treeNodeKey, dynamic properties, object userContext, CancellationToken token);
}
}

Просмотреть файл

@ -0,0 +1,114 @@
//-----------------------------------------------------------------------
// <copyright file="TreeWalkerParameters.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The TreeWalkerParameters class.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Scripting;
/// <summary>
/// The TreeWalkerParameters class contains the required and optional properties used by the TreeWalkerSession.
/// </summary>
public class TreeWalkerParameters
{
#region Required Properties
/// <summary>
/// The unique identifier for this session.
/// </summary>
public Guid SessionId { get; private set; }
/// <summary>
/// The string representation of the JSON schema.
/// </summary>
public string JsonSchema { get; private set; }
/// <summary>
/// The state given to TreeWalker on construction by a wrapper class.
/// The state holds information that is relevant to TreeWalker while walking the tree.
/// </summary>
public IForgeDictionary ForgeState { get; private set; }
/// <summary>
/// The ITreeWalkerCallbacks interface defines the callback Tasks that are awaited while walking the tree.
/// </summary>
public ITreeWalkerCallbacks Callbacks { get; private set; }
/// <summary>
/// The cancellation token.
/// </summary>
public CancellationToken Token { get; private set; }
#endregion
#region Optional Properties
/// <summary>
/// The dynamic object that is able to be referenced when evaluating schema expressions or performing actions.
/// </summary>
public object UserContext { get; set; }
/// <summary>
/// The Assembly containing ForgeActionAttribute tagged classes.
/// </summary>
public Assembly ForgeActionsAssembly { get; set; }
/// <summary>
/// Script cache used by ExpressionExecutor to cache and re-use compiled Roslyn scripts.
/// </summary>
public ConcurrentDictionary<string, Script<object>> ScriptCache { get; set; }
/// <summary>
/// Dependencies required to compile and execute the schema. Null if no external dependencies required.
/// </summary>
public List<Type> Dependencies { get; set; }
/// <summary>
/// The external executors work similarly to the built-in ExpressionExecutor, but use their own string match and evaluation logic on schema expressions.
/// The key is the string that EvaluateDynamicProperty will attempt to match string properties against. Similar to "C#|" for Roslyn expressions.
/// The value is the Func that is called when the string key matches. This Func takes the expression and token, and should return the expected value type.
/// </summary>
public Dictionary<string, Func<string, CancellationToken, Task<object>>> ExternalExecutors { get; set; }
#endregion
/// <summary>
/// Instantiates a TreeWalkerParameters object with the properies that are required to instantiate a TreeWalkerSession object.
/// </summary>
/// <param name="sessionId">The unique identifier for this session.</param>
/// <param name="jsonSchema">The JSON schema.</param>
/// <param name="forgeState">The Forge state.</param>
/// <param name="callbacks">The callbacks object.</param>
/// <param name="token">The cancellation token.</param>
public TreeWalkerParameters(
Guid sessionId,
string jsonSchema,
IForgeDictionary forgeState,
ITreeWalkerCallbacks callbacks,
CancellationToken token)
{
if (sessionId == Guid.Empty) throw new ArgumentNullException("sessionId");
if (string.IsNullOrWhiteSpace(jsonSchema)) throw new ArgumentNullException("jsonSchema");
if (forgeState == null) throw new ArgumentNullException("forgeState");
if (callbacks == null) throw new ArgumentNullException("callbacks");
if (token == null) throw new ArgumentNullException("token");
this.SessionId = sessionId;
this.JsonSchema = jsonSchema;
this.ForgeState = forgeState;
this.Callbacks = callbacks;
this.Token = token;
}
}
}

Просмотреть файл

@ -0,0 +1,969 @@
//-----------------------------------------------------------------------
// <copyright file="TreeWalkerSession.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// The TreeWalkerSession class.
// </summary>
//-----------------------------------------------------------------------
namespace Forge.TreeWalker
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Scripting;
using Forge.Attributes;
using Forge.DataContracts;
using Forge.TreeWalker.ForgeExceptions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
/// <summary>
/// The TreeWalkerSession tries to walk the given tree schema to completion.
/// It holds the logic for walking the tree schema, executing actions, and calling callbacks.
/// </summary>
public class TreeWalkerSession : ITreeSession
{
/// <summary>
/// The ActionResponse suffix appended to the end of the key in forgeState that maps to an ActionResponse.
/// Key: <SessionId>_<TreeActionKey>_AR
/// </summary>
public static string ActionResponseSuffix = "_AR";
/// <summary>
/// The CurrentTreeNode suffix appended to the end of the key in forgeState that maps to the current TreeNode being walked.
/// Key: <SessionId>_CTN
/// </summary>
public static string CurrentTreeNodeSuffix = "CTN";
/// <summary>
/// The LastTreeAction suffix appended to the end of the key in forgeState that maps to the last TreeAction that was committed.
/// Key: <SessionId>_LTA
/// </summary>
public static string LastTreeActionSuffix = "LTA";
/// <summary>
/// The Intermediates suffix appended to the end of the key in forgeState that maps to an ActionContext's GetIntermediates object.
/// Key: <SessionId>_<TreeActionKey>_Int
/// </summary>
public static string IntermediatesSuffix = "_Int";
/// <summary>
/// The name of the native LeafNodeSummaryAction.
/// This Action can only live on Leaf type TreeNodes. Similarly, Leaf type TreeNodes can only have this Action.
/// This Action takes an ActionResponse as the ActionInput, either as an object or as properties, and commits this object as the ActionResponse.
/// This Action is intended to give schema authors the ability to cleanly end a tree walking path with a summary.
/// </summary>
public static string LeafNodeSummaryAction = "LeafNodeSummaryAction";
/// <summary>
/// The Roslyn regex expression. Used to check if dynamic schema values should be evaluated with Roslyn.
/// Type can be added to indicate that Roslyn should evaluate the expression and return the specified type.
/// If the property is a "KnownType", the KnownType is used even if a <type> is specified in the expression. ActionDefinition.InputType is an example of a KnownType.
/// If the property is not a KnownType and no type is specified in the expression, string is used by default.
/// Ex) C#|"expression"
/// Ex) C#<Boolean>|"expression"
/// </summary>
private static Regex RoslynRegex = new Regex(@"^C#(\<(\w+)\>)?\|");
/// <summary>
/// The leading text to add to Schema strings to indicate the property value should be evaluated with Roslyn.
/// The property value must match the RoslynRegex to be evaluated with Roslyn.
/// </summary>
private static string RoslynLeadingText = "C#";
/// <summary>
/// The TreeWalkerParameters contains the required and optional properties used by the TreeWalkerSession.
/// </summary>
public TreeWalkerParameters Parameters { get; private set; }
/// <summary>
/// The JSON schema holding the tree to navigate during WalkTree.
/// </summary>
public ForgeTree Schema { get; private set; }
/// <summary>
/// The current status of the tree walker.
/// </summary>
public string Status { get; private set; }
/// <summary>
/// The WalkTree cancellation token source.
/// Used to send cancellation signal to action tasks and to stop tree walker from visiting future nodes.
/// </summary>
private CancellationTokenSource walkTreeCts;
/// <summary>
/// The ExpressionExecutor dynamically compiles code and executes it.
/// </summary>
private ExpressionExecutor expressionExecutor;
/// <summary>
/// The map of string ActionNames to ActionDefinitions.
/// This map is generated using reflection to find all the classes with the applied ForgeActionAttribute from the given Assembly.
/// The string key is the Action class name.
/// The ActionDefinition value contains the Action class type, and the InputType for the Action.
/// </summary>
private Dictionary<string, ActionDefinition> actionsMap;
/// <summary>
/// Instantiates a tree walker session with the required parameters.
/// </summary>
/// <param name="parameters">The parameters object contains the required and optional properties used by the TreeWalkerSession.</param>
public TreeWalkerSession(TreeWalkerParameters parameters)
{
if (parameters == null) throw new ArgumentNullException("parameters");
this.Parameters = parameters;
// Initialize properties from required TreeWalkerParameters properties.
this.Schema = JsonConvert.DeserializeObject<ForgeTree>(parameters.JsonSchema);
this.walkTreeCts = CancellationTokenSource.CreateLinkedTokenSource(parameters.Token);
// Initialize properties from optional TreeWalkerParameters properties.
GetActionsMapFromAssembly(parameters.ForgeActionsAssembly, out this.actionsMap);
parameters.ExternalExecutors = parameters.ExternalExecutors ?? new Dictionary<string, Func<string, CancellationToken, Task<object>>>();
this.expressionExecutor = new ExpressionExecutor(this as ITreeSession, parameters.UserContext, parameters.Dependencies, parameters.ScriptCache);
this.Status = "Initialized";
}
/// <summary>
/// Gets the ActionResponse data from the forgeState for the given tree action key.
/// </summary>
/// <param name="treeActionKey">The TreeAction's key of the action that was executed.</param>
/// <returns>The ActionResponse data for the given tree action key if it exists, otherwise null.</returns>
public ActionResponse GetOutput(string treeActionKey)
{
try
{
return this.Parameters.ForgeState.GetValue<ActionResponse>(treeActionKey + ActionResponseSuffix).GetAwaiter().GetResult();
}
catch
{
return null;
}
}
/// <summary>
/// Asynchronously gets the ActionResponse data from the forgeState for the given tree action key.
/// </summary>
/// <param name="treeActionKey">The TreeAction's key of the action that was executed.</param>
/// <returns>The ActionResponse data for the given tree action key if it exists, otherwise null.</returns>
public async Task<ActionResponse> GetOutputAsync(string treeActionKey)
{
try
{
return await this.Parameters.ForgeState.GetValue<ActionResponse>(treeActionKey + ActionResponseSuffix).ConfigureAwait(false);
}
catch
{
return null;
}
}
/// <summary>
/// Gets the last executed TreeAction's ActionResponse data from the forgeState.
/// </summary>
/// <returns>The ActionResponse data for the last executed tree action key if it exists, otherwise null.</returns>
public ActionResponse GetLastActionResponse()
{
try
{
return this.Parameters.ForgeState.GetValue<ActionResponse>(this.GetLastTreeAction().GetAwaiter().GetResult() + ActionResponseSuffix).GetAwaiter().GetResult();
}
catch
{
return null;
}
}
/// <summary>
/// Asynchronously gets the last executed TreeAction's ActionResponse data from the forgeState.
/// </summary>
/// <returns>The ActionResponse data for the last executed tree action key if it exists, otherwise null.</returns>
public async Task<ActionResponse> GetLastActionResponseAsync()
{
try
{
return await this.Parameters.ForgeState.GetValue<ActionResponse>(await this.GetLastTreeAction().ConfigureAwait(false) + ActionResponseSuffix).ConfigureAwait(false);
}
catch
{
return null;
}
}
/// <summary>
/// Gets the current tree node being walked from the forgeState.
/// </summary>
/// <returns>The current tree node if it has been persisted, otherwise null.</returns>
public async Task<string> GetCurrentTreeNode()
{
try
{
return await this.Parameters.ForgeState.GetValue<string>(CurrentTreeNodeSuffix).ConfigureAwait(false);
}
catch
{
return null;
}
}
/// <summary>
/// Gets the last committed tree action from the forgeState.
/// </summary>
/// <returns>The last committed tree action if it has been persisted, otherwise null.</returns>
public async Task<string> GetLastTreeAction()
{
try
{
return await this.Parameters.ForgeState.GetValue<string>(LastTreeActionSuffix).ConfigureAwait(false);
}
catch
{
return null;
}
}
/// <summary>
/// Signals the WalkTree and VisitNode cancellation token sources to cancel.
/// </summary>
public void CancelWalkTree()
{
this.walkTreeCts.Cancel();
}
/// <summary>
/// Walks the tree schema starting at the given tree node key.
/// </summary>
/// <param name="treeNodeKey">The TreeNode key to start walking.</param>
/// <returns>The string status of the tree walker.</returns>
public async Task<string> WalkTree(string treeNodeKey)
{
this.Status = "Running";
// Start a single task to walk the tree to completion.
// This task will only start new tasks to execute actions.
Task walkTreeTask = Task.Run(async () =>
{
string current = treeNodeKey;
string next;
// Starting with the given tree node key, visit the returned children nodes until you hit a node with no matching children.
// Call the callbacks before/after visiting each node.
do
{
await this.CommitCurrentTreeNode(current).ConfigureAwait(false);
if (this.walkTreeCts.Token.IsCancellationRequested)
{
this.Status = "Cancelled";
this.walkTreeCts.Token.ThrowIfCancellationRequested();
}
await this.Parameters.Callbacks.BeforeVisitNode(
this.Parameters.SessionId,
current,
await this.EvaluateDynamicProperty(this.Schema.Tree[current].Properties, null),
this.Parameters.UserContext,
this.walkTreeCts.Token).ConfigureAwait(false);
try
{
// Exceptions are thrown here if VisitNode hit a timeout, was cancelled, or failed.
next = await this.VisitNode(current).ConfigureAwait(false);
}
finally
{
// Always call this callback, whether or not visit node was successful.
await this.Parameters.Callbacks.AfterVisitNode(
this.Parameters.SessionId,
current,
await this.EvaluateDynamicProperty(this.Schema.Tree[current].Properties, null),
this.Parameters.UserContext,
this.walkTreeCts.Token).ConfigureAwait(false);
}
current = next;
} while (!string.IsNullOrWhiteSpace(current));
if (string.IsNullOrWhiteSpace(current))
{
// Null child means the tree ran to completion.
this.Status = "RanToCompletion";
}
}, this.walkTreeCts.Token);
try
{
// Exceptions are thrown here if tree walker hit a timeout, was cancelled, or failed.
// Let's update the Status according to the exception thrown before rethrowing the exception.
await walkTreeTask;
}
catch (TaskCanceledException)
{
// Tree walker was cancelled before calling WalkTree.
this.Status = "CancelledBeforeExecution";
}
catch (OperationCanceledException)
{
// Tree walker was cancelled after calling WalkTree (no timeouts hit).
this.Status = "Cancelled";
}
catch (ActionTimeoutException)
{
// An action-level timeout was hit.
this.Status = "TimeoutOnAction";
}
catch (TimeoutException)
{
// A node-level timeout was hit.
this.Status = "TimeoutOnNode";
}
catch (NoChildMatchedException)
{
// ChildSelector couldn't select any child.
this.Status = "RanToCompletion_NoChildMatched";
}
catch (EvaluateDynamicPropertyException)
{
this.Status = "Failed_EvaluateDynamicProperty";
}
catch (Exception)
{
// TODO: Consider checking the exception for specific Data entry and setting Status to that.
// This would allow callbacks such as BeforeVisitNode to throw exceptions and control the status instead of it being Failed.
// Unexpected exception was thrown in actions, callbacks, or elsewhere, resulting in failure and cancellation.
this.Status = "Failed";
}
finally
{
this.CancelWalkTree();
}
try
{
// Exceptions are thrown here if tree walker hit a timeout, was cancelled, or failed.
await walkTreeTask;
}
catch (NoChildMatchedException)
{
// For now, suppressing this exception so that its treated as successful end stage.
}
return this.Status;
}
/// <summary>
/// Visits a TreeNode in the ForgeTree, performing type-specific behavior as necessary before selecting the next child to visit.
/// </summary>
/// <param name="treeNodeKey">The TreeNode key to visit.</param>
/// <exception cref="TimeoutException">If the node-level timeout was hit.</exception>
/// <exception cref="ActionTimeoutException">If the action-level timeout was hit.</exception>
/// <exception cref="OperationCanceledException">If the cancellation token was triggered.</exception>
/// <exception cref="Exception">If an unexpected exception was thrown.</exception>
/// <returns>The key of the next child to visit, or <c>null</c> if no match was found.</returns>
internal async Task<string> VisitNode(string treeNodeKey)
{
TreeNode treeNode = this.Schema.Tree[treeNodeKey];
// Do type-specific behavior.
switch (treeNode.Type)
{
case TreeNodeType.Leaf:
{
await this.PerformLeafTypeBehavior(treeNode).ConfigureAwait(false);
// Leaf type can't have ChildSelector so we return here.
return null;
}
case TreeNodeType.Action:
{
// Exceptions are thrown here if the actions hit a timeout, were cancelled, or failed.
await this.PerformActionTypeBehavior(treeNode, treeNodeKey).ConfigureAwait(false);
break;
}
default:
{
break;
}
}
// Return next child to visit, if possible.
return await this.SelectChild(treeNode).ConfigureAwait(false);
}
/// <summary>
/// Iterates the child selectors for a matching child.
/// </summary>
/// <param name="treeNode">The TreeNode to select a child from.</param>
/// <returns>The key of the next child to visit, or <c>null</c> if no match was found.</returns>
internal async Task<string> SelectChild(TreeNode treeNode)
{
if (treeNode.ChildSelector == null)
{
// No children to select, we are done walking the tree.
return null;
}
foreach (ChildSelector cs in treeNode.ChildSelector)
{
// Empty expressions default to true. Otherwise, evaluate the expression.
if (string.IsNullOrWhiteSpace(cs.ShouldSelect) && !string.IsNullOrWhiteSpace(cs.Child))
{
return cs.Child;
}
if ((bool)await this.EvaluateDynamicProperty(cs.ShouldSelect, typeof(bool)).ConfigureAwait(false))
{
return cs.Child;
}
}
// No children were successfully matched.
throw new NoChildMatchedException("ChildSelector couldn't match any child.");
}
/// <summary>
/// Performs Leaf TreeNodeType behavior.
/// </summary>
/// <param name="treeNode">The Leaf TreeNode.</param>
internal async Task PerformLeafTypeBehavior(TreeNode treeNode)
{
// Check if Leaf node contains the LeafNodeSummaryAction and commit ActionInput as ActionResponse if it does.
if (treeNode.Actions == null || treeNode.Actions.Count != 1)
{
return;
}
foreach (KeyValuePair<string, TreeAction> kvp in treeNode.Actions)
{
string treeActionKey = kvp.Key;
TreeAction treeAction = kvp.Value;
if (treeAction.Action != LeafNodeSummaryAction)
{
return;
}
ActionResponse actionResponse = await this.EvaluateDynamicProperty(treeAction.Input, typeof(ActionResponse)).ConfigureAwait(false);
await this.CommitActionResponse(treeActionKey, actionResponse).ConfigureAwait(false);
return;
}
}
/// <summary>
/// Executes the actions for the given tree node.
/// Returns without throwing exception if all actions were completed successfully.
/// </summary>
/// <param name="treeNode">The TreeNode containing actions to execute.</param>
/// <param name="treeNodeKey">The TreeNode's key where the actions are taking place.</param>
/// <exception cref="TimeoutException">If the node-level timeout was hit.</exception>
/// <exception cref="ActionTimeoutException">If the action-level timeout was hit.</exception>
/// <exception cref="OperationCanceledException">If the cancellation token was triggered.</exception>
internal async Task PerformActionTypeBehavior(TreeNode treeNode, string treeNodeKey)
{
List<Task> actionTasks = new List<Task>();
if (treeNode.Actions == null)
{
return;
}
// Start new parallel tasks for each action on this node.
foreach (KeyValuePair<string, TreeAction> kvp in treeNode.Actions)
{
string treeActionKey = kvp.Key;
TreeAction treeAction = kvp.Value;
if (await this.GetOutputAsync(treeActionKey).ConfigureAwait(false) != null)
{
// Handle rehydration case. Commit LastTreeAction if it was not committed. Do not execute actions for which we have already received a response.
if (await this.GetLastTreeAction().ConfigureAwait(false) == null)
{
await this.CommitLastTreeAction(treeActionKey).ConfigureAwait(false);
}
continue;
}
if (this.actionsMap.TryGetValue(treeAction.Action, out ActionDefinition actionDefinition))
{
actionTasks.Add(this.ExecuteActionWithRetry(treeNodeKey, treeActionKey, treeAction, actionDefinition, this.walkTreeCts.Token));
}
}
// Wait for all parallel tasks to complete until the given timout.
// If any task hits a timeout, gets cancelled, or fails, an exception will be thrown.
// Note: CancelWalkTree is called at the end of every session to ensure all Actions/Tasks see the triggered cancellation token.
Task nodeTimeoutTask = Task.Delay((int)await this.EvaluateDynamicProperty(treeNode.Timeout ?? -1, typeof(int)).ConfigureAwait(false), this.walkTreeCts.Token);
actionTasks.Add(nodeTimeoutTask);
while (actionTasks.Count > 1)
{
// Throw if cancellation was requested between actions completing.
this.walkTreeCts.Token.ThrowIfCancellationRequested();
Task completedTask = await Task.WhenAny(actionTasks).ConfigureAwait(false);
actionTasks.Remove(completedTask);
if (completedTask == nodeTimeoutTask)
{
// Throw on cancellation requested if that's the reason the timeout task completed.
this.walkTreeCts.Token.ThrowIfCancellationRequested();
// NodeTimeout was hit, throw a special exception to differentiate between timeout and cancellation.
throw new TimeoutException("Hit node-level timeout in TreeNodeKey: " + treeNodeKey);
}
// Await the completed task to propagate any exceptions.
// Exceptions are thrown here if the action hit a timeout, was cancelled, or failed.
await completedTask;
}
}
/// <summary>
/// Executes the given action. Attempts retries according to the retry policy and timeout.
/// Returns without throwing exception if the action was completed successfully.
/// </summary>
/// <param name="treeNodeKey">The TreeNode's key where the actions are taking place.</param>
/// <param name="treeActionKey">The TreeAction's key of the action taking place.</param>
/// <param name="treeAction">The TreeAction object that holds properties of the action.</param>
/// <param name="actionDefinition">The object holding definitions for the action to execute.</param>
/// <param name="token">The cancellation token.</param>
/// <exception cref="ActionTimeoutException">If the action-level timeout was hit.</exception>
/// <exception cref="OperationCanceledException">If the cancellation token was triggered.</exception>
internal async Task ExecuteActionWithRetry(
string treeNodeKey,
string treeActionKey,
TreeAction treeAction,
ActionDefinition actionDefinition,
CancellationToken token)
{
// Initialize values. Default infinite timeout. Default RetryPolicyType.None.
int retryCount = 0;
Exception innerException = null;
Stopwatch stopwatch = new Stopwatch();
int actionTimeout = (int)await this.EvaluateDynamicProperty(treeAction.Timeout ?? -1, typeof(int)).ConfigureAwait(false);
RetryPolicyType retryPolicyType = treeAction.RetryPolicy != null ? treeAction.RetryPolicy.Type : RetryPolicyType.None;
TimeSpan waitTime = treeAction.RetryPolicy != null ? TimeSpan.FromMilliseconds(treeAction.RetryPolicy.MinBackoffMs) : new TimeSpan();
// Kick off timers.
Task actionTimeoutTask = Task.Delay(actionTimeout, token);
stopwatch.Start();
// Attmpt to ExecuteAction based on RetryPolicy and Timeout.
// Throw on non-retriable exceptions.
while (actionTimeout == -1 || stopwatch.ElapsedMilliseconds < actionTimeout)
{
token.ThrowIfCancellationRequested();
try
{
await this.ExecuteAction(treeNodeKey, treeActionKey, treeAction, actionDefinition, actionTimeoutTask, token).ConfigureAwait(false);
return; // success!
}
catch (OperationCanceledException)
{
throw; // non-retriable exception
}
catch (ActionTimeoutException)
{
throw; // non-retriable exception
}
catch (EvaluateDynamicPropertyException)
{
throw; // non-retriable exception
}
catch (Exception e)
{
// Cache exception as innerException in case we need to throw ActionTimeoutException.
innerException = e;
// Hit retriable exception. Retry according to RetryPolicy.
// When retries are exhausted, throw ActionTimeoutException with Exception e as the innerException.
switch (retryPolicyType)
{
case RetryPolicyType.FixedInterval:
{
// FixedInterval retries every MinBackoffMs until the timeout.
// Ex) 200ms, 200ms, 200ms...
waitTime = TimeSpan.FromMilliseconds(treeAction.RetryPolicy.MinBackoffMs);
break;
}
case RetryPolicyType.ExponentialBackoff:
{
// ExponentialBackoff retries every Math.Min(MinBackoffMs * 2^(retryCount), MaxBackoffMs) until the timeout.
// Ex) 100ms, 200ms, 400ms...
waitTime = TimeSpan.FromMilliseconds(Math.Min(treeAction.RetryPolicy.MaxBackoffMs, waitTime.TotalMilliseconds * 2));
break;
}
case RetryPolicyType.None:
default:
{
// No retries. Break out below to throw non-retriable exception.
break;
}
}
}
// Break out if no retry policy set.
if (retryPolicyType == RetryPolicyType.None)
{
// If the retries have exhausted and the ContinuationOnRetryExhaustion flag is set, commit a new ActionResponse
// with the status set to RetryExhaustedOnAction and return.
if (treeAction.ContinuationOnRetryExhaustion)
{
ActionResponse timeoutResponse = new ActionResponse
{
Status = "RetryExhaustedOnAction"
};
await this.CommitActionResponse(treeActionKey, timeoutResponse).ConfigureAwait(false);
return;
}
break;
}
// Break out early if we would hit timeout before next retry.
if (actionTimeout != -1 && stopwatch.ElapsedMilliseconds + waitTime.TotalMilliseconds >= actionTimeout)
{
// If the timeout is hit and the ContinuationOnTimeout flag is set, commit a new ActionResponse
// with the status set to TimeoutOnAction and return.
if (treeAction.ContinuationOnTimeout)
{
ActionResponse timeoutResponse = new ActionResponse
{
Status = "TimeoutOnAction"
};
await this.CommitActionResponse(treeActionKey, timeoutResponse).ConfigureAwait(false);
return;
}
break;
}
token.ThrowIfCancellationRequested();
await Task.Delay(waitTime, token).ConfigureAwait(false);
retryCount++;
}
// Retries are exhausted. Throw ActionTimeoutException with executeAction exception as innerException.
throw new ActionTimeoutException(
string.Format(
"Action did not complete successfully. TreeNodeKey: {0}, TreeActionKey: {1}, ActionName: {2}, RetryCount: {3}, RetryPolicy: {4}",
treeNodeKey,
treeActionKey,
treeAction.Action,
retryCount,
retryPolicyType),
innerException);
}
/// <summary>
/// Executes the given actionTask and commits the ActionResponse to forgeState on success.
/// </summary>
/// <param name="treeNodeKey">The TreeNode's key where the actions are taking place.</param>
/// <param name="treeActionKey">The TreeAction's key of the action taking place.</param>
/// <param name="treeAction">The TreeAction object that holds properties of the action.</param>
/// <param name="actionDefinition">The object holding definitions for the action to execute.</param>
/// <param name="actionTimeoutTask">The delay task tied to the action timeout.</param>
/// <param name="token">The cancellation token.</param>
/// <exception cref="ActionTimeoutException">If the action-level timeout was hit.</exception>
/// <exception cref="OperationCanceledException">If the cancellation token was triggered.</exception>
/// <returns>
/// RanToCompletion if the action was completed successfully.
/// Exceptions are thrown on timeout, cancellation, or retriable failures.
/// </returns>
internal async Task ExecuteAction(
string treeNodeKey,
string treeActionKey,
TreeAction treeAction,
ActionDefinition actionDefinition,
Task actionTimeoutTask,
CancellationToken token)
{
// Set up a linked cancellation token to trigger on timeout if ContinuationOnTimeout is set.
// This ensures the runActionTask gets canceled when Forge timeout is hit.
CancellationTokenSource actionCts = CancellationTokenSource.CreateLinkedTokenSource(token);
token = treeAction.ContinuationOnTimeout ? actionCts.Token : token;
// Evaluate the dynamic properties that are used by the actionTask.
ActionContext actionContext = new ActionContext(
this.Parameters.SessionId,
treeNodeKey,
treeActionKey,
treeAction.Action,
await this.EvaluateDynamicProperty(treeAction.Input, actionDefinition.InputType).ConfigureAwait(false),
await this.EvaluateDynamicProperty(treeAction.Properties, null).ConfigureAwait(false),
this.Parameters.UserContext,
token,
this.Parameters.ForgeState
);
// Instantiate the BaseAction-derived ActionType class and invoke the RunAction method on it.
var actionObject = Activator.CreateInstance(actionDefinition.ActionType);
MethodInfo method = typeof(BaseAction).GetMethod("RunAction");
Task<ActionResponse> runActionTask = (Task<ActionResponse>) method.Invoke(actionObject, new object[] { actionContext });
// Await for the first completed task between our runActionTask and the timeout task.
// This allows us to continue without awaiting the runActionTask upon timeout.
var completedTask = await Task.WhenAny(runActionTask, actionTimeoutTask).ConfigureAwait(false);
if (completedTask == actionTimeoutTask)
{
// Throw on cancellation requested if that's the reason the timeout task completed.
token.ThrowIfCancellationRequested();
// If the timeout is hit and the ContinuationOnTimeout flag is set, commit a new ActionResponse
// with the status set to TimeoutOnAction and return.
if (treeAction.ContinuationOnTimeout)
{
// Trigger linked cancellation token before continuing to ensure the runActionTask gets cancelled.
actionCts.Cancel();
ActionResponse timeoutResponse = new ActionResponse
{
Status = "TimeoutOnAction"
};
await this.CommitActionResponse(treeActionKey, timeoutResponse).ConfigureAwait(false);
return;
}
// ActionTimeout has been hit. Throw special exception to indicate this.
throw new ActionTimeoutException(string.Format(
"ActionTimeoutTask timed out before Action could complete. TreeNodeKey: {0}, TreeActionKey: {1}, ActionName: {2}.",
treeNodeKey,
treeActionKey,
treeAction.Action));
}
else
{
// Handle the completed runActionTask.
if (runActionTask.Status == TaskStatus.RanToCompletion)
{
await this.CommitActionResponse(treeActionKey, await runActionTask).ConfigureAwait(false);
}
// Await the completed task to propagate any exceptions.
// Exceptions are thrown here if the action hit a timeout, was cancelled, or failed.
await runActionTask;
}
}
/// <summary>
/// Iterates through the given schema object, evaluating any Roslyn expressions found in the values.
/// When a knownType is given, Forge will instantiate that type instead of a dynamic object.
/// String properties matching the <see cref="RoslynRegex"/> represent a code-snippet that will be evaluated.
/// </summary>
/// <param name="schemaObj">The object given from the schema.</param>
/// <param name="knownType">The type of the object being evaluated. Null here represents an unknown type that will be evaluated dynamically.</param>
/// <exception cref="EvaluateDynamicPropertyException">If exceptions are thrown while evaluating the dynamic property.</exception>
/// <returns>The properties after evaluation.</returns>
public async Task<object> EvaluateDynamicProperty(dynamic schemaObj, Type knownType)
{
try
{
if (schemaObj == null)
{
return null;
}
else if (schemaObj is string && schemaObj.StartsWith(RoslynLeadingText))
{
// Case when schema property is a Roslyn expression.
// Evaluate it as either the knownType if it exists, the <type> embeded in the RoslynRegex, or a string by default, in that order.
Match result = RoslynRegex.Match(schemaObj);
if (result.Success)
{
string typeStr = string.IsNullOrWhiteSpace(result.Groups[2].Value) ? "String" : result.Groups[2].Value;
Type type = knownType ?? Type.GetType("System." + typeStr);
MethodInfo method = typeof(ExpressionExecutor).GetMethod("Execute");
MethodInfo genericMethod = method.MakeGenericMethod(type);
string expression = schemaObj.Substring(result.Groups[0].Value.Length);
var res = await (dynamic) genericMethod.Invoke(this.expressionExecutor, new object[] { expression });
return res;
}
}
else if (schemaObj is string)
{
foreach (var kvp in this.Parameters.ExternalExecutors)
{
string regexString = kvp.Key;
Func<string, CancellationToken, Task<object>> externalExecutor = kvp.Value;
if (schemaObj.StartsWith(regexString))
{
// Case when schema property matches an external executor.
// Evaluate it and return the result as the knownType if it exists or as a string otherwise.
var result = await externalExecutor(schemaObj.Substring(regexString.Length), this.walkTreeCts.Token).ConfigureAwait(false);
return knownType != null ? Convert.ChangeType(result, knownType) : result;
}
}
return schemaObj;
}
else if (schemaObj is JObject)
{
// Case when schema object has properties (i.e. is a dictionary).
// Instantiate and use the knownType if given, then evaluate each property using recursion.
dynamic knownObj = Activator.CreateInstance(knownType ?? typeof(object));
IDictionary<string, dynamic> propertyValues = schemaObj.ToObject<IDictionary<string, dynamic>>();
foreach (string key in new List<string>(propertyValues.Keys))
{
if (knownType != null)
{
var prop = knownType.GetProperty(key);
prop.SetValue(knownObj, await this.EvaluateDynamicProperty(propertyValues[key], prop.PropertyType).ConfigureAwait(false));
}
else
{
propertyValues[key] = await this.EvaluateDynamicProperty(propertyValues[key], null).ConfigureAwait(false);
}
}
return knownType != null ? knownObj : (dynamic)JObject.FromObject(propertyValues);
}
else if (schemaObj is JArray)
{
// Case when schema object is an array.
// Create an array with the knownType if given, then use recursion to evaluate each index.
dynamic knownObj = Activator.CreateInstance(knownType ?? typeof(object[]), schemaObj.Count);
for (int i = 0; i < schemaObj.Count; i++)
{
if (knownType != null)
{
knownObj.SetValue(await this.EvaluateDynamicProperty(schemaObj[i], knownType.GetElementType()).ConfigureAwait(false), i);
}
else
{
schemaObj[i] = await this.EvaluateDynamicProperty(schemaObj[i], null).ConfigureAwait(false);
}
}
return knownType != null ? knownObj : schemaObj;
}
else if (schemaObj is JValue)
{
// Case when schema object is a value type.
// Return the value as the knownType if given.
return knownType != null ? Convert.ChangeType(schemaObj.Value, knownType) : schemaObj.Value;
}
else
{
// Case when schema object is a value type.
// Return the value as the knownType if given.
return knownType != null ? Convert.ChangeType(schemaObj, knownType) : schemaObj;
}
}
catch (OperationCanceledException)
{
throw; // rethrow on cancelled.
}
catch (Exception e)
{
throw new EvaluateDynamicPropertyException(
string.Format("EvaluateDynamicProperty failed to parse schemaObj: {0}, knownType: {1}.", schemaObj?.ToString(), knownType),
e);
}
return null;
}
/// <summary>
/// Commits the ActionResponse to the forgeState.
/// This allows the ActionResponses to be dynamically referenced in the ForgeTree through ITreeSession interface.
/// </summary>
/// <param name="treeActionKey">The TreeAction's key of the action that was executed.</param>
/// <param name="actionResponse">The action response object returned from the action.</param>
private async Task CommitActionResponse(string treeActionKey, ActionResponse actionResponse)
{
await this.Parameters.ForgeState.Set<ActionResponse>(treeActionKey + ActionResponseSuffix, actionResponse).ConfigureAwait(false);
await this.CommitLastTreeAction(treeActionKey).ConfigureAwait(false);
}
/// <summary>
/// Commits the current tree node to the forgeState.
/// The wrapper class has access to this state, allowing it to rehydrate/retry on failures if desired.
/// </summary>
/// <param name="treeNodeKey">The TreeNode's key that tree walker is currently walking.</param>
private Task CommitCurrentTreeNode(string treeNodeKey)
{
// TODO: Consider adding a "nodeStep" to save if we already completed the BeforeVisitNode step and should skip straight to VisitNode.
return this.Parameters.ForgeState.Set<string>(CurrentTreeNodeSuffix, treeNodeKey);
}
/// <summary>
/// Commits the last tree action to the forgeState.
/// </summary>
/// <param name="treeActionKey">The TreeAction's key of the last action that was committed.</param>
private Task CommitLastTreeAction(string treeActionKey)
{
return this.Parameters.ForgeState.Set<string>(LastTreeActionSuffix, treeActionKey);
}
/// <summary>
/// Initializes the actionsMap from the given assembly.
/// This map is generated using reflection to find all the classes with the applied ForgeActionAttribute from the given Assembly.
/// </summary>
/// <param name="forgeActionsAssembly">The Assembly containing ForgeActionAttribute tagged classes.</param>
/// <param name="actionsMap">The map of string ActionNames to ActionDefinitions.</param>
public static void GetActionsMapFromAssembly(Assembly forgeActionsAssembly, out Dictionary<string, ActionDefinition> actionsMap)
{
actionsMap = new Dictionary<string, ActionDefinition>();
if (forgeActionsAssembly == null)
{
return;
}
foreach (Type type in forgeActionsAssembly.GetExportedTypes())
{
// Find all classes with the applied ForgeActionAttribute.
ForgeActionAttribute forgeAction = (ForgeActionAttribute) type.GetCustomAttribute(typeof(ForgeActionAttribute), false);
if (forgeAction != null)
{
// Confirm that ForgeActionAttribute is attached to only BaseAction classes and there are no duplicates.
Type derived = type;
bool isBaseAction = false;
do
{
if (derived == typeof(BaseAction))
{
isBaseAction = true;
break;
}
derived = derived.BaseType;
} while (derived != null);
if (isBaseAction)
{
actionsMap.Add(type.Name, new ActionDefinition() { ActionType = type, InputType = forgeAction.InputType });
continue;
}
else
{
throw new CustomAttributeFormatException(
string.Format(
"The given type: {0} must implement the BaseAction abstract class in order to apply the ForgeActionAttribute.",
type.ToString()));
}
}
}
}
}
}

37
Forge/Forge.sln Normal file
Просмотреть файл

@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29215.179
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Forge.TreeWalker", "Forge.TreeWalker\Forge.TreeWalker.csproj", "{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Forge.DataContracts", "Forge.DataContracts\Forge.DataContracts.csproj", "{C49A8494-13E5-4214-8434-708BA280C5B1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Forge.TreeWalker.UnitTests", "Forge.TreeWalker.UnitTests\Forge.TreeWalker.UnitTests.csproj", "{A33AF1FF-1291-4CB1-A733-3243E1FE967E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00FC1C22-6AE9-4F60-8A3E-05885BA34C9C}.Release|Any CPU.Build.0 = Release|Any CPU
{C49A8494-13E5-4214-8434-708BA280C5B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C49A8494-13E5-4214-8434-708BA280C5B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C49A8494-13E5-4214-8434-708BA280C5B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C49A8494-13E5-4214-8434-708BA280C5B1}.Release|Any CPU.Build.0 = Release|Any CPU
{A33AF1FF-1291-4CB1-A733-3243E1FE967E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A33AF1FF-1291-4CB1-A733-3243E1FE967E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A33AF1FF-1291-4CB1-A733-3243E1FE967E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A33AF1FF-1291-4CB1-A733-3243E1FE967E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A8067EC1-B359-4193-8603-5E6524B2B3A1}
EndGlobalSection
EndGlobal