├── .gitattributes ├── .gitignore ├── Images ├── MicroflowConsoleApp Nuget.PNG ├── MicroflowConsoleApp Solution.PNG ├── MicroflowFunctionApp Nuget.PNG ├── MicroflowFunctionApp Solution.PNG └── Tests.png ├── LICENSE ├── Microflow.sln ├── MicroflowApiFunctionApp ├── .gitignore ├── MicroflowApi │ ├── FlowControlApi.cs │ ├── ScaleGroupsApi.cs │ ├── StepsInProgressApi.cs │ └── WorkflowApi.cs ├── MicroflowApiFunctionApp.csproj ├── Properties │ ├── launchSettings.json │ ├── serviceDependencies.json │ └── serviceDependencies.local.json ├── host.json └── local.settings.json ├── MicroflowConsoleApp ├── MicroFlowConsole │ └── MicroflowConsole.csproj ├── MicroflowConsole │ ├── MicroflowConsole.csproj │ ├── Program.cs │ └── TestWorkflows.cs └── MicroflowConsoleApp.sln ├── MicroflowFunctionApp ├── .gitignore ├── API │ ├── HttpOrchestrators │ │ ├── HttpCallOrchestrator.cs │ │ └── WebhookOrchestrator.cs │ ├── Step │ │ └── MicroflowStepApi.cs │ └── Webhooks │ │ ├── Webhooks.cs │ │ └── WebhooksGet.cs ├── FlowControl │ ├── CanStepExecuteNow.cs │ ├── CanStepExecuteNowForScalingGroup.cs │ ├── Interfaces │ │ └── IMicroflowContext.cs │ ├── MicroflowContext.cs │ ├── MicroflowStart.cs │ └── MicroflowStep.cs ├── Helpers │ ├── MicroflowHelper.cs │ ├── MicroflowHttpHelper.cs │ ├── MicroflowOrchestrationHelper.cs │ ├── MicroflowStartupHelper.cs │ ├── MicroflowTableHelper.cs │ └── MicroflowWorkflowHelper.cs ├── MicroflowApp.csproj ├── Models │ ├── Interfaces │ │ └── IMicroflowHttpResponse.cs │ ├── MicroflowTableModels.cs │ └── Models.cs ├── Optional │ ├── ScaleGroupsApi.cs │ └── WorkflowApi.cs ├── Properties │ ├── ServiceDependencies │ │ └── MicroflowApp20210615143625 - Zip Deploy │ │ │ └── storage1.arm.json │ ├── serviceDependencies.MicroflowApp20210615143625 - Zip Deploy.json │ ├── serviceDependencies.json │ └── serviceDependencies.local.json ├── TableLogging │ ├── BlobLogResponse.cs │ ├── TableLogErrorActivity.cs │ ├── TableLogOrchestrationActivity.cs │ ├── TableLogStepActivity.cs │ └── TableLogWebhook.cs ├── cloudconfig.json ├── host.json └── local.settings.json ├── MicroflowModels ├── Constants.cs ├── Helpers │ └── TableHelper.cs ├── Interfaces │ ├── IHttpCall.cs │ ├── IHttpCallWithRetries.cs │ ├── IMicroflowRun.cs │ └── IStepEntity.cs ├── MicroflowModels.csproj ├── Models │ ├── MicroflowRun.cs │ └── Models.cs └── TableModels │ └── TableModels.cs ├── MicroflowSDK ├── CompilerDirectiveMaker.cs ├── HttpBlobDataManager.cs ├── MicroflowSDK.csproj ├── ScaleGroupsManager.cs ├── StepsManager.cs ├── WebhooksManager.cs └── WorkflowManager.cs ├── MicroflowShared ├── MicroflowShared.csproj ├── TableReferences.cs └── WorkflowHelper.cs ├── MicroflowTest ├── LogReader.cs ├── MicroflowTest.csproj ├── MicroflowTest.sln ├── Test1_WorkflowExecution.cs ├── Test2_Retries.cs ├── Test3_Webhooks.cs ├── Test4_ScaleGroups.cs ├── Test5_WaitForAllParents.cs ├── Test6_PassThroughParams.cs ├── Test7_RunFromSteps.cs ├── Test8_2TopStepsPostDataStart.cs ├── TestWorkflowHelper.cs └── config.json ├── MicroserviceEmulator ├── .gitignore ├── Function1.cs ├── HttpClient.cs ├── MicroflowExternalAPI.cs ├── MicroserviceEmulator.csproj ├── MicroserviceEmulator │ └── MicroserviceEmulator.sln ├── OrchestrationStateSimulations.cs ├── Properties │ ├── launchSettings.json │ ├── serviceDependencies.json │ └── serviceDependencies.local.json ├── ResponseProxies │ ├── Callbacks │ │ └── ResponseProxyCallbackDemoFunction.cs │ └── Inline │ │ └── ResponseProxyInlineDemoFunction.cs ├── SleepTestOrchestrator.cs ├── host.json └── local.settings.json ├── README.md └── local.settings.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /Images/MicroflowConsoleApp Nuget.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-maree/Microflow/fec9d282e189931b2181b5705bdb26e6b0fa4810/Images/MicroflowConsoleApp Nuget.PNG -------------------------------------------------------------------------------- /Images/MicroflowConsoleApp Solution.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-maree/Microflow/fec9d282e189931b2181b5705bdb26e6b0fa4810/Images/MicroflowConsoleApp Solution.PNG -------------------------------------------------------------------------------- /Images/MicroflowFunctionApp Nuget.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-maree/Microflow/fec9d282e189931b2181b5705bdb26e6b0fa4810/Images/MicroflowFunctionApp Nuget.PNG -------------------------------------------------------------------------------- /Images/MicroflowFunctionApp Solution.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-maree/Microflow/fec9d282e189931b2181b5705bdb26e6b0fa4810/Images/MicroflowFunctionApp Solution.PNG -------------------------------------------------------------------------------- /Images/Tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andre-maree/Microflow/fec9d282e189931b2181b5705bdb26e6b0fa4810/Images/Tests.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andre Maree 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Microflow.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32228.430 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroflowApp", "MicroflowFunctionApp\MicroflowApp.csproj", "{074D09C1-2F8E-4DBA-B838-F59BCBE0D3BD}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroflowModels", "MicroflowModels\MicroflowModels.csproj", "{3182A6A0-38B4-4A4D-951A-E88B6602502E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroflowApiFunctionApp", "MicroflowApiFunctionApp\MicroflowApiFunctionApp.csproj", "{8AB66346-AF3E-496C-BFE9-C8565BC3265F}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroflowShared", "MicroflowShared\MicroflowShared.csproj", "{C83CCA23-46D9-4819-8C45-ED01154A4121}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {074D09C1-2F8E-4DBA-B838-F59BCBE0D3BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {074D09C1-2F8E-4DBA-B838-F59BCBE0D3BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {074D09C1-2F8E-4DBA-B838-F59BCBE0D3BD}.Release|Any CPU.ActiveCfg = Debug|Any CPU 23 | {074D09C1-2F8E-4DBA-B838-F59BCBE0D3BD}.Release|Any CPU.Build.0 = Debug|Any CPU 24 | {3182A6A0-38B4-4A4D-951A-E88B6602502E}.Debug|Any CPU.ActiveCfg = DEBUG|Any CPU 25 | {3182A6A0-38B4-4A4D-951A-E88B6602502E}.Debug|Any CPU.Build.0 = DEBUG|Any CPU 26 | {3182A6A0-38B4-4A4D-951A-E88B6602502E}.Release|Any CPU.ActiveCfg = DDEBUG|Any CPU 27 | {3182A6A0-38B4-4A4D-951A-E88B6602502E}.Release|Any CPU.Build.0 = DDEBUG|Any CPU 28 | {8AB66346-AF3E-496C-BFE9-C8565BC3265F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {8AB66346-AF3E-496C-BFE9-C8565BC3265F}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {8AB66346-AF3E-496C-BFE9-C8565BC3265F}.Release|Any CPU.ActiveCfg = Debug|Any CPU 31 | {8AB66346-AF3E-496C-BFE9-C8565BC3265F}.Release|Any CPU.Build.0 = Debug|Any CPU 32 | {C83CCA23-46D9-4819-8C45-ED01154A4121}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {C83CCA23-46D9-4819-8C45-ED01154A4121}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {C83CCA23-46D9-4819-8C45-ED01154A4121}.Release|Any CPU.ActiveCfg = Debug|Any CPU 35 | {C83CCA23-46D9-4819-8C45-ED01154A4121}.Release|Any CPU.Build.0 = Debug|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {DCF5F864-D92D-4876-97D0-76F1FD7FBBD9} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /MicroflowApiFunctionApp/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /MicroflowApiFunctionApp/MicroflowApi/ScaleGroupsApi.cs: -------------------------------------------------------------------------------- 1 | #if DEBUG || RELEASE || !DEBUG_NO_FLOWCONTROL_SCALEGROUPS && !DEBUG_NO_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_SCALEGROUPS && !DEBUG_NO_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_UPSERT_FLOWCONTROL_SCALEGROUPS && !DEBUG_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_UPSERT_SCALEGROUPS && !DEBUG_NO_UPSERT_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_FLOWCONTROL_SCALEGROUPS && !RELEASE_NO_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_SCALEGROUPS && !RELEASE_NO_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_UPSERT_FLOWCONTROL_SCALEGROUPS && !RELEASE_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_UPSERT_SCALEGROUPS && !RELEASE_NO_UPSERT_SCALEGROUPS_STEPCOUNT 2 | using MicroflowModels; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 5 | using Microsoft.Azure.WebJobs.Extensions.Http; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Net; 12 | using System.Net.Http; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | using static MicroflowModels.Constants; 16 | 17 | namespace MicroflowApi 18 | { 19 | public class ScaleGroupsApi 20 | { 21 | /// 22 | /// Get max instance count for scale group 23 | /// 24 | [FunctionName("Get" + ScaleGroupCalls.ScaleGroup)] 25 | public static async Task GetScaleGroup([HttpTrigger(AuthorizationLevel.Anonymous, "get", 26 | Route = MicroflowPath + "/ScaleGroup/{scaleGroupId}")] HttpRequestMessage req, 27 | [DurableClient] IDurableEntityClient client, string scaleGroupId) 28 | { 29 | Dictionary result = new(); 30 | EntityQueryResult res = null; 31 | 32 | using (CancellationTokenSource cts = new()) 33 | { 34 | res = await client.ListEntitiesAsync(new EntityQuery() 35 | { 36 | PageSize = 99999999, 37 | EntityName = ScaleGroupCalls.ScaleGroupMaxConcurrentInstanceCount, 38 | FetchState = true 39 | }, cts.Token); 40 | } 41 | 42 | if (string.IsNullOrWhiteSpace(scaleGroupId)) 43 | { 44 | foreach (DurableEntityStatus rr in res.Entities) 45 | { 46 | result.Add(rr.EntityId.EntityKey, rr.State.Value()); 47 | } 48 | } 49 | else 50 | { 51 | foreach (DurableEntityStatus rr in res.Entities.Where(e => e.EntityId.EntityKey.Equals(scaleGroupId))) 52 | { 53 | result.Add(rr.EntityId.EntityKey, rr.State.Value()); 54 | } 55 | } 56 | 57 | StringContent content = new(JsonConvert.SerializeObject(result)); 58 | 59 | return new HttpResponseMessage(HttpStatusCode.OK) 60 | { 61 | Content = content 62 | }; 63 | } 64 | 65 | /// 66 | /// Set the max instance count for scale group, default to wait for 5 seconds 67 | /// 68 | [FunctionName("Set" + ScaleGroupCalls.ScaleGroup)] 69 | public static async Task ScaleGroup([HttpTrigger(AuthorizationLevel.Anonymous, "post", 70 | Route = MicroflowPath + "/ScaleGroup/{scaleGroupId}/{maxWaitSeconds:int?}")] HttpRequestMessage req, 71 | [DurableClient] IDurableOrchestrationClient client, string scaleGroupId, int? maxWaitSeconds) 72 | { 73 | ScaleGroupState state = JsonConvert.DeserializeObject(await req.Content.ReadAsStringAsync()); 74 | 75 | string instanceId = await client.StartNewAsync("SetScaleGroupMaxConcurrentCount", null, (scaleGroupId, state)); 76 | 77 | return await client.WaitForCompletionOrCreateCheckStatusResponseAsync(req, instanceId, TimeSpan.FromSeconds(maxWaitSeconds is null 78 | ? 5 79 | : maxWaitSeconds.Value)); 80 | } 81 | 82 | /// 83 | /// Set the scale group max concurrent instance count orchestration 84 | /// 85 | /// 86 | [Deterministic] 87 | [FunctionName("SetScaleGroupMaxConcurrentCount")] 88 | public static async Task SetScaleGroupMaxOrchestration([OrchestrationTrigger] IDurableOrchestrationContext context) 89 | { 90 | (string scaleGroupId, ScaleGroupState state) = context.GetInput<(string, ScaleGroupState)>(); 91 | 92 | EntityId scaleGroupCountId = new(ScaleGroupCalls.ScaleGroupMaxConcurrentInstanceCount, scaleGroupId); 93 | 94 | await context.CallEntityAsync(scaleGroupCountId, MicroflowEntityKeys.Set, state); 95 | } 96 | } 97 | } 98 | #endif -------------------------------------------------------------------------------- /MicroflowApiFunctionApp/MicroflowApi/StepsInProgressApi.cs: -------------------------------------------------------------------------------- 1 | #if DEBUG || RELEASE || !DEBUG_NO_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_FLOWCONTROL_STEPCOUNT && !DEBUG_NO_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_STEPCOUNT && !DEBUG_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_UPSERT_FLOWCONTROL_STEPCOUNT && !DEBUG_NO_UPSERT_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_UPSERT_STEPCOUNT && !RELEASE_NO_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_FLOWCONTROL_STEPCOUNT && !RELEASE_NO_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_STEPCOUNT && !RELEASE_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_UPSERT_FLOWCONTROL_STEPCOUNT && !RELEASE_NO_UPSERT_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_UPSERT_STEPCOUNT 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using Microsoft.Azure.WebJobs.Extensions.Http; 7 | 8 | namespace MicroflowApi 9 | { 10 | public class StepsInProgressApi 11 | { 12 | //[OpenApiOperation(operationId: "Run", tags: new[] { "name" })] 13 | //[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] 14 | //[OpenApiParameter(name: "name", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "The **Name** parameter")] 15 | //[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "text/plain", bodyType: typeof(string), Description = "The OK response")] 16 | [FunctionName("GetStepsCountInProgress")] 17 | public static async Task GetStepsCountInProgress([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "GetStepsCountInProgress/{workflowNameStepNumber}")] HttpRequestMessage req, 18 | [DurableClient] IDurableEntityClient client, 19 | string workflowNameStepNumber) 20 | { 21 | EntityId countId = new("StepCount", workflowNameStepNumber); 22 | 23 | EntityStateResponse result = await client.ReadEntityStateAsync(countId); 24 | 25 | return result.EntityState; 26 | } 27 | } 28 | } 29 | #endif -------------------------------------------------------------------------------- /MicroflowApiFunctionApp/MicroflowApi/WorkflowApi.cs: -------------------------------------------------------------------------------- 1 | #if DEBUG || RELEASE || !DEBUG_NO_UPSERT && !DEBUG_NO_UPSERT_FLOWCONTROL && !DEBUG_NO_UPSERT_FLOWCONTROL_SCALEGROUPS && !DEBUG_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_UPSERT_FLOWCONTROL_STEPCOUNT && !DEBUG_NO_UPSERT_SCALEGROUPS && !DEBUG_NO_UPSERT_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_UPSERT_STEPCOUNT && !RELEASE_NO_UPSERT && !RELEASE_NO_UPSERT_FLOWCONTROL && !RELEASE_NO_UPSERT_FLOWCONTROL_SCALEGROUPS && !RELEASE_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_UPSERT_FLOWCONTROL_STEPCOUNT && !RELEASE_NO_UPSERT_SCALEGROUPS && !RELEASE_NO_UPSERT_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_UPSERT_STEPCOUNT 2 | using System; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using MicroflowShared; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 9 | using Microsoft.Azure.WebJobs.Extensions.Http; 10 | 11 | namespace MicroflowApi 12 | { 13 | public static class WorkflowApi 14 | { 15 | /// 16 | /// Delete orchestration history 17 | /// 18 | /// 19 | [FunctionName("PurgeInstanceHistory")] 20 | public static async Task PurgeInstanceHistory([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "PurgeInstanceHistory/{workflowName?}")] 21 | HttpRequestMessage req, 22 | [DurableClient] IDurableOrchestrationClient client, 23 | string workflowName) 24 | { 25 | try 26 | { 27 | await client.PurgeInstanceHistoryAsync(workflowName); 28 | 29 | return new HttpResponseMessage(HttpStatusCode.OK); 30 | } 31 | catch (Exception e) 32 | { 33 | HttpResponseMessage resp = new(HttpStatusCode.InternalServerError) 34 | { 35 | Content = new StringContent(e.Message) 36 | }; 37 | 38 | return resp; 39 | } 40 | } 41 | 42 | /// 43 | /// This must be called at least once before a workflow runs, 44 | /// this is to prevent multiple concurrent instances from writing step data at workflow run, 45 | /// call UpsertWorkflow when something changed in the workflow, but do not always call this when concurrent multiple workflows 46 | /// 47 | [FunctionName("UpsertWorkflow")] 48 | public static async Task UpsertWorkflow([HttpTrigger(AuthorizationLevel.Anonymous, "post", 49 | Route = "UpsertWorkflow/{globalKey?}")] HttpRequestMessage req, 50 | [DurableClient] IDurableEntityClient client, string globalKey) 51 | { 52 | return await client.UpsertWorkflow(await req.Content.ReadAsStringAsync(), globalKey); 53 | } 54 | 55 | 56 | /// 57 | /// Returns the workflow Json that was saved with UpsertWorkflow 58 | /// 59 | [FunctionName("GetWorkflow")] 60 | public static async Task GetWorkflowJson([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "GetWorkflow/{workflowName}")] HttpRequestMessage req, 61 | string workflowName) 62 | { 63 | return await WorkflowHelper.GetWorkflowJson(workflowName); 64 | } 65 | } 66 | } 67 | #endif -------------------------------------------------------------------------------- /MicroflowApiFunctionApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "MicroflowApiFunctionApp": { 4 | "commandName": "Project", 5 | "commandLineArgs": "host start --pause-on-error --port 5860" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /MicroflowApiFunctionApp/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /MicroflowApiFunctionApp/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /MicroflowApiFunctionApp/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensions": { 4 | "durableTask": { 5 | "logReplayEvents": "false" 6 | }, 7 | "http": { 8 | "routePrefix": "microflow/v1" 9 | } 10 | }, 11 | "logging": { 12 | "applicationInsights": { 13 | "samplingSettings": { 14 | "isEnabled": false, 15 | "excludedTypes": "Request" 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /MicroflowApiFunctionApp/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 6 | "MicroflowStorage": "UseDevelopmentStorage=true", 7 | "AzureWebJobsSecretStorageType": "files" 8 | } 9 | } -------------------------------------------------------------------------------- /MicroflowConsoleApp/MicroFlowConsole/MicroflowConsole.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /MicroflowConsoleApp/MicroflowConsole/MicroflowConsole.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /MicroflowConsoleApp/MicroflowConsole/TestWorkflows.cs: -------------------------------------------------------------------------------- 1 | using MicroflowModels; 2 | using MicroflowSDK; 3 | using System.Collections.Generic; 4 | 5 | namespace MicroflowConsole 6 | { 7 | public static class TestWorkflows 8 | { 9 | public static List CreateTestWorkflow_SimpleSteps() 10 | { 11 | // create 12 | List steps = WorkflowManager.CreateSteps(4, 1,"{default_post_url}"); 13 | //steps[0].IsHttpGet = true; 14 | //steps[0].CallbackTimeoutSeconds = 30; 15 | //steps[0].WebhookAction = "approve"; 16 | //steps[0].CalloutUrl = "http://localhost:7071/SleepTestOrchestrator_HttpStart"; 17 | //steps[0].SetRetryForStep(1, 2, 1); 18 | //steps[0].StopOnActionFailed = true; 19 | //steps[0].CalloutTimeoutSeconds = 190; 20 | 21 | steps[1].AddSubSteps(steps[2], steps[3]); 22 | steps[4].AddParentSteps(steps[2], steps[3]); 23 | 24 | steps.Remove(steps[0]); 25 | 26 | return steps; 27 | } 28 | 29 | public static List CreateTestWorkflow_10StepsParallel() 30 | { 31 | List steps = WorkflowManager.CreateSteps(14, 1, "{default_post_url}"); 32 | 33 | steps[1].AddSubSteps(steps[2], steps[3]); 34 | steps[2].AddSubSteps(steps[4], steps[5], steps[6], steps[7], steps[8]); 35 | steps[3].AddSubSteps(steps[9], steps[10], steps[11], steps[12], steps[13]); 36 | // 2 groups of 5 parallel steps = 10 parallel steps 37 | steps[14].AddParentSteps(steps[4], steps[5], steps[6], steps[7], steps[8], steps[9], steps[10], steps[11], steps[12], steps[13]); 38 | 39 | steps.Remove(steps[0]); 40 | 41 | return steps; 42 | } 43 | 44 | public static List CreateTestWorkflow_Complex1() 45 | { 46 | List steps = WorkflowManager.CreateSteps(8, 1, "{default_post_url}"); 47 | 48 | steps[1].AddSubSteps(steps[2], steps[3], steps[4]); 49 | steps[2].AddSubSteps(steps[5], steps[6]); 50 | steps[3].AddSubSteps(steps[6], steps[7]); 51 | steps[4].AddSubSteps(steps[6], steps[8]); 52 | steps[5].AddSubSteps(steps[3], steps[6]); 53 | steps[6].AddSubSteps(steps[8]); 54 | steps[7].AddSubSteps(steps[8]); 55 | 56 | steps.Remove(steps[0]); 57 | 58 | return steps; 59 | } 60 | 61 | 62 | public static List CreateTestWorkflow_110Steps() 63 | { 64 | List steps = WorkflowManager.CreateSteps(110, 1, "{default_post_url}"); 65 | 66 | steps[1].AddSubStepRange(steps, 3, 11); 67 | steps[11].AddSubStepRange(steps, 13, 22); 68 | steps[22].AddSubStepRange(steps, 24, 33); 69 | steps[33].AddSubStepRange(steps, 35, 44); 70 | steps[44].AddSubStepRange(steps, 46, 55); 71 | steps[55].AddSubStepRange(steps, 57, 66); 72 | steps[66].AddSubStepRange(steps, 68, 77); 73 | steps[77].AddSubStepRange(steps, 79, 88); 74 | steps[88].AddSubStepRange(steps, 90, 99); 75 | steps[99].AddSubStepRange(steps, 101, 110); 76 | 77 | steps.Remove(steps[0]); 78 | 79 | return steps; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /MicroflowFunctionApp/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /MicroflowFunctionApp/API/Webhooks/Webhooks.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading.Tasks; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.Http; 5 | using System.Net.Http; 6 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 7 | using MicroflowModels.Helpers; 8 | using MicroflowModels; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Text.Json; 12 | using System.Linq; 13 | 14 | namespace Microflow.Webhooks 15 | { 16 | /// 17 | /// This is a generic webhooks implementation with /webhooks/{webhookId}/{action} 18 | /// Substeps to execute can be set with the step`s WebhookSubStepsMapping property, accociated to the action 19 | /// 20 | public static class Webhooks 21 | { 22 | ///// 23 | ///// For a webhook defined as {webhookId}/{action} 24 | ///// 25 | [FunctionName("Webhook")] 26 | public static async Task WebhookWithAction( 27 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", 28 | Route = Constants.MicroflowPath + "/Webhooks/{webhookId}/{action?}")] HttpRequestMessage req, 29 | [DurableClient] IDurableOrchestrationClient orchClient, 30 | string webhookId, string action) 31 | => await orchClient.ProcessWebhook(webhookId, action, req.RequestUri); 32 | 33 | private static async Task ProcessWebhook(this IDurableOrchestrationClient client, 34 | string webhookId, 35 | string action, 36 | Uri location) 37 | { 38 | Webhook webhook = await TableHelper.GetWebhook(webhookId); 39 | 40 | if (webhook == null) 41 | { 42 | return new(HttpStatusCode.NotFound); 43 | } 44 | 45 | try 46 | { 47 | // no action 48 | if (string.IsNullOrWhiteSpace(action)) 49 | { 50 | if (!string.IsNullOrWhiteSpace(webhook.WebhookSubStepsMapping)) 51 | { 52 | return new(HttpStatusCode.NotFound); 53 | } 54 | 55 | await client.RaiseEventAsync(webhookId, webhookId, new MicroflowHttpResponse() 56 | { 57 | Success = true, 58 | HttpResponseStatusCode = 200 59 | }); 60 | 61 | return new(HttpStatusCode.OK); 62 | } 63 | 64 | // with action 65 | if (string.IsNullOrWhiteSpace(webhook.WebhookSubStepsMapping)) 66 | { 67 | return new(HttpStatusCode.NotFound); 68 | } 69 | 70 | List webhookSubStepsMapping = JsonSerializer.Deserialize>(webhook.WebhookSubStepsMapping); 71 | 72 | SubStepsMappingForActions hook = webhookSubStepsMapping.FirstOrDefault(h => h.WebhookAction.Equals(action, StringComparison.OrdinalIgnoreCase)); 73 | 74 | MicroflowHttpResponse webhookResult; 75 | 76 | if (hook != null) 77 | { 78 | webhookResult = new() 79 | { 80 | Success = true, 81 | HttpResponseStatusCode = 200, 82 | SubStepsToRun = hook.SubStepsToRunForAction, 83 | Action = action 84 | }; 85 | } 86 | else 87 | { 88 | return new(HttpStatusCode.NotFound); 89 | } 90 | 91 | await client.RaiseEventAsync(webhookId, webhookId, webhookResult); 92 | 93 | return new(HttpStatusCode.OK); 94 | } 95 | catch (ArgumentException) 96 | { 97 | try 98 | { 99 | DurableOrchestrationStatus statusCheck = await client.GetStatusAsync("callout" + webhookId); 100 | 101 | if (statusCheck == null) 102 | { 103 | return new(HttpStatusCode.NotFound); 104 | } 105 | 106 | // unliky but possible that the event have not yet been created 107 | HttpResponseMessage response = new(HttpStatusCode.Accepted); 108 | response.Headers.Location = location; 109 | 110 | return response; 111 | } 112 | catch 113 | { 114 | return new(HttpStatusCode.InternalServerError); 115 | } 116 | } 117 | catch 118 | { 119 | return new(HttpStatusCode.InternalServerError); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /MicroflowFunctionApp/API/Webhooks/WebhooksGet.cs: -------------------------------------------------------------------------------- 1 | namespace Microflow.Webhooks 2 | { 3 | /// 4 | /// This is a generic webhooks implementation with /webhooks/{webhookId}/{stepId}/{action} 5 | /// Substeps to execute can be set with the step`s WebhookSubStepsMapping property, accociated to the action 6 | /// 7 | public static class WebhooksGet 8 | { 9 | /// 10 | /// For a webhook defined as "{webhookId}/{stepId}" 11 | /// 12 | //[FunctionName("GetWebhooks")] 13 | //public static async Task GetWebhooks( 14 | //[HttpTrigger(AuthorizationLevel.Function, "get", "post", 15 | //Route = Constants.MicroflowPath + "/GetWebhooks/{workflowName}/{webhookId}/{stepNumber}/{instanceGuid?}")] HttpRequestMessage req, 16 | //[DurableClient] IDurableOrchestrationClient orchClient, 17 | //string workflowName, string webhookId, int stepNumber, string instanceGuid = "") 18 | //{ 19 | // var li = await MicroflowTableHelper.GetWebhooks(workflowName, webhookId, stepNumber, instanceGuid); 20 | 21 | // return null; 22 | //} 23 | 24 | //private static async Task GetWebhooks() 25 | //{ 26 | // await Task.Delay(3); 27 | //} 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MicroflowFunctionApp/FlowControl/CanStepExecuteNow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microflow.Models; 4 | using MicroflowModels; 5 | using Microsoft.Azure.WebJobs; 6 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 7 | using static MicroflowModels.Constants; 8 | 9 | namespace Microflow.FlowControl 10 | { 11 | public static class CanStepExecuteNow 12 | { 13 | /// 14 | /// Calculate if a step is ready to execute by locking and counting the completed parents 15 | /// 16 | /// Bool to indicate if this step request can be executed or not 17 | [Deterministic] 18 | [FunctionName(CallNames.CanExecuteNow)] 19 | public static async Task CanExecuteNow([OrchestrationTrigger] IDurableOrchestrationContext context) 20 | { 21 | CanExecuteNowObject canExecuteNowObject = context.GetInput(); 22 | try 23 | { 24 | EntityId countId = new(MicroflowEntities.CanExecuteNowCount, 25 | canExecuteNowObject.RunId + canExecuteNowObject.StepNumber); 26 | 27 | CanExecuteResult canExecuteResult = null; 28 | 29 | using (await context.LockAsync(countId)) 30 | { 31 | int parentCompletedCount = await context.CallEntityAsync(countId, MicroflowEntityKeys.Read); 32 | 33 | if (parentCompletedCount + 1 >= canExecuteNowObject.ParentCount) 34 | { 35 | // cleanup with silnalentity 36 | //context.SignalEntity(countId, "delete"); 37 | 38 | canExecuteResult = new CanExecuteResult() 39 | { 40 | CanExecute = true, 41 | StepNumber = canExecuteNowObject.StepNumber 42 | }; 43 | } 44 | else 45 | { 46 | await context.CallEntityAsync(countId, MicroflowEntityKeys.Add); 47 | 48 | canExecuteResult = new CanExecuteResult() 49 | { 50 | CanExecute = false, 51 | StepNumber = canExecuteNowObject.StepNumber 52 | }; 53 | } 54 | } 55 | 56 | if(canExecuteResult.CanExecute) 57 | { 58 | context.SignalEntity(countId, MicroflowEntityKeys.Delete); 59 | } 60 | 61 | return canExecuteResult; 62 | } 63 | catch (Exception e) 64 | { 65 | // log to table error 66 | LogErrorEntity errorEntity = new(canExecuteNowObject.WorkflowName, 67 | Convert.ToInt32(canExecuteNowObject.StepNumber), 68 | e.Message, 69 | canExecuteNowObject.RunId); 70 | 71 | await context.CallActivityAsync(CallNames.LogError, errorEntity); 72 | 73 | return new CanExecuteResult() 74 | { 75 | CanExecute = false, 76 | StepNumber = canExecuteNowObject.StepNumber 77 | }; 78 | } 79 | } 80 | 81 | /// 82 | /// Durable entity to keep a count for each run and each step in the run 83 | /// 84 | [Deterministic] 85 | [FunctionName(MicroflowEntities.CanExecuteNowCount)] 86 | public static void CanExecuteNowCounter([EntityTrigger] IDurableEntityContext ctx) 87 | { 88 | switch (ctx.OperationName) 89 | { 90 | case MicroflowEntityKeys.Add: 91 | ctx.SetState(ctx.GetState() + 1); 92 | break; 93 | //case "reset": 94 | // ctx.SetState(0); 95 | // break; 96 | case MicroflowEntityKeys.Read: 97 | ctx.Return(ctx.GetState()); 98 | break; 99 | case MicroflowEntityKeys.Delete: 100 | ctx.DeleteState(); 101 | break; 102 | } 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/FlowControl/CanStepExecuteNowForScalingGroup.cs: -------------------------------------------------------------------------------- 1 | #if DEBUG || RELEASE || !DEBUG_NO_FLOWCONTROL_SCALEGROUPS && !DEBUG_NO_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_SCALEGROUPS && !DEBUG_NO_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_UPSERT_FLOWCONTROL_SCALEGROUPS && !DEBUG_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_UPSERT_SCALEGROUPS && !DEBUG_NO_UPSERT_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_FLOWCONTROL_SCALEGROUPS && !RELEASE_NO_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_SCALEGROUPS && !RELEASE_NO_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_UPSERT_FLOWCONTROL_SCALEGROUPS && !RELEASE_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_UPSERT_SCALEGROUPS && !RELEASE_NO_UPSERT_SCALEGROUPS_STEPCOUNT 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microflow.Models; 6 | using MicroflowModels; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 9 | using static MicroflowModels.Constants; 10 | 11 | namespace Microflow.FlowControl 12 | { 13 | /// 14 | /// Check if the step can execute taking into account the max concurrent steps in the scale group 15 | /// 16 | public static class CanStepExecuteNowForScalingGroup 17 | { 18 | [Deterministic] 19 | [FunctionName(ScaleGroupCalls.CanExecuteNowInScaleGroup)] 20 | public static async Task CheckMaxScaleCountForGroup([OrchestrationTrigger] IDurableOrchestrationContext context) 21 | { 22 | CanExecuteNowObject canExecuteNowObject = context.GetInput(); 23 | EntityId countId = new(ScaleGroupCalls.CanExecuteNowInScaleGroupCount, canExecuteNowObject.ScaleGroupId); 24 | 25 | EntityId scaleGroupCountId = new(ScaleGroupCalls.ScaleGroupMaxConcurrentInstanceCount, canExecuteNowObject.ScaleGroupId); 26 | ScaleGroupState scaleGroupState = await context.CallEntityAsync(scaleGroupCountId, MicroflowControlKeys.Read); 27 | 28 | if (scaleGroupState.ScaleGroupMaxConcurrentInstanceCount == 0) 29 | { 30 | return; 31 | } 32 | 33 | using (await context.LockAsync(countId)) 34 | { 35 | int scaleGroupInProcessCount = await context.CallEntityAsync(countId, MicroflowEntityKeys.Read); 36 | 37 | if (scaleGroupInProcessCount < scaleGroupState.ScaleGroupMaxConcurrentInstanceCount) 38 | { 39 | await context.CallEntityAsync(countId, MicroflowEntityKeys.Add); 40 | 41 | return; 42 | } 43 | } 44 | 45 | DateTime endDate = context.CurrentUtcDateTime.AddHours(scaleGroupState.PollingMaxHours); 46 | // start interval seconds 47 | int count = scaleGroupState.PollingIntervalSeconds; 48 | // max interval seconds 49 | int max = scaleGroupState.PollingIntervalMaxSeconds; 50 | 51 | using (CancellationTokenSource cts = new()) 52 | { 53 | try 54 | { 55 | while (context.CurrentUtcDateTime < endDate) 56 | { 57 | DateTime deadline = context.CurrentUtcDateTime.Add(TimeSpan.FromSeconds(count < max ? count : max)); 58 | await context.CreateTimer(deadline, cts.Token); 59 | count++; 60 | 61 | using (await context.LockAsync(countId)) 62 | { 63 | int scaleGroupInProcessCount = await context.CallEntityAsync(countId, MicroflowEntityKeys.Read); 64 | 65 | if (scaleGroupInProcessCount < scaleGroupState.ScaleGroupMaxConcurrentInstanceCount) 66 | { 67 | await context.CallEntityAsync(countId, MicroflowEntityKeys.Add); 68 | 69 | return; 70 | } 71 | } 72 | } 73 | } 74 | catch (TaskCanceledException tex) 75 | { 76 | // log to table error 77 | LogErrorEntity errorEntity = new(canExecuteNowObject.WorkflowName, 78 | Convert.ToInt32(canExecuteNowObject.StepNumber), 79 | tex.Message, 80 | canExecuteNowObject.RunId); 81 | 82 | await context.CallActivityAsync(CallNames.LogError, errorEntity); 83 | } 84 | catch (Exception e) 85 | { 86 | // log to table error 87 | LogErrorEntity errorEntity = new(canExecuteNowObject.WorkflowName, 88 | Convert.ToInt32(canExecuteNowObject.StepNumber), 89 | e.Message, 90 | canExecuteNowObject.RunId); 91 | 92 | await context.CallActivityAsync(CallNames.LogError, errorEntity); 93 | } 94 | finally 95 | { 96 | cts.Dispose(); 97 | } 98 | } 99 | } 100 | 101 | /// 102 | /// Durable entity to keep a count for each run and each step in the run 103 | /// 104 | [FunctionName(ScaleGroupCalls.CanExecuteNowInScaleGroupCount)] 105 | public static void CanExecuteNowInScalingGroupCounter([EntityTrigger] IDurableEntityContext ctx) 106 | { 107 | switch (ctx.OperationName) 108 | { 109 | case MicroflowEntityKeys.Add: 110 | ctx.SetState(ctx.GetState() + 1); 111 | break; 112 | case MicroflowEntityKeys.Subtract: 113 | int state = ctx.GetState(); 114 | ctx.SetState(state <= 0 ? 0 : state - 1); 115 | break; 116 | case MicroflowEntityKeys.Read: 117 | ctx.Return(ctx.GetState()); 118 | break; 119 | //case "delete": 120 | // ctx.DeleteState(); 121 | // break; 122 | } 123 | } 124 | 125 | [FunctionName(ScaleGroupCalls.ScaleGroupMaxConcurrentInstanceCount)] 126 | public static void ScaleGroupMaxConcurrentInstanceCounter([EntityTrigger] IDurableEntityContext ctx) 127 | { 128 | switch (ctx.OperationName.ToLowerInvariant()) 129 | { 130 | case MicroflowEntityKeys.Set: 131 | ctx.SetState(ctx.GetInput()); 132 | break; 133 | case MicroflowEntityKeys.Read: 134 | ctx.Return(ctx.GetState()); 135 | break; 136 | //case "delete": 137 | // ctx.DeleteState(); 138 | // break; 139 | } 140 | } 141 | } 142 | } 143 | #endif 144 | -------------------------------------------------------------------------------- /MicroflowFunctionApp/FlowControl/Interfaces/IMicroflowContext.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Microflow.FlowControl 4 | { 5 | public interface IMicroflowContext 6 | { 7 | Task RunMicroflow(); 8 | } 9 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/FlowControl/MicroflowStart.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.Http; 5 | using Microsoft.Extensions.Logging; 6 | using System.Net.Http; 7 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 8 | using Microflow.Helpers; 9 | using MicroflowModels; 10 | using Azure; 11 | using static MicroflowModels.Constants; 12 | 13 | namespace Microflow.FlowControl 14 | { 15 | /// 16 | /// UpsertWorkflow must be called to save workflow step meta data to table storage 17 | /// after this, Start can be called multiple times, 18 | /// if a change is made to the workflow, call UpsertWorkflow again to apply the changes 19 | /// 20 | public static class MicroflowStartFunctions 21 | { 22 | /// 23 | /// This is the entry point, workflow payload is in the http body 24 | /// 25 | /// If an instanceId is passed in, it will run as a singleton, else it will run concurrently with each with a new instanceId 26 | [FunctionName(CallNames.MicroflowStart)] 27 | public static async Task MicroflowStart([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = MicroflowPath + "/Start/{workflowName}/{instanceId?}")] 28 | HttpRequestMessage req, 29 | [DurableClient] IDurableOrchestrationClient client, 30 | string instanceId, string workflowName) 31 | { 32 | return await client.StartWorkflow(req, instanceId, workflowName); 33 | } 34 | 35 | /// 36 | /// This is called from Microflow_HttpStart, it does the looping and calls the ExecuteStep sub orchestration passing in the top step 37 | /// 38 | /// 39 | [Deterministic] 40 | [FunctionName(CallNames.MicroflowStartOrchestration)] 41 | public static async Task MicroflowStartOrchestration([OrchestrationTrigger] IDurableOrchestrationContext context, 42 | ILogger inLog) 43 | { 44 | ILogger log = context.CreateReplaySafeLogger(inLog); 45 | 46 | // read workflowRun payload 47 | MicroflowRun workflowRun = context.GetInput(); 48 | 49 | try 50 | { 51 | Task resp = context.MicroflowCheckAndWaitForReadyToRun(workflowRun.WorkflowName); 52 | 53 | if (!await resp) 54 | { 55 | return; 56 | } 57 | 58 | await context.StartMicroflow(log, workflowRun); 59 | } 60 | catch (RequestFailedException e) 61 | { 62 | // log to table workflow completed 63 | LogErrorEntity errorEntity = new(workflowRun.WorkflowName, 64 | Convert.ToInt32(workflowRun.RunObject.StepNumber), 65 | e.Message, 66 | workflowRun.RunObject.RunId); 67 | 68 | await context.CallActivityAsync(CallNames.LogError, errorEntity); 69 | } 70 | catch (Exception e) 71 | { 72 | // log to table workflow completed 73 | LogErrorEntity errorEntity = new(workflowRun.WorkflowName, 74 | Convert.ToInt32(workflowRun.RunObject.StepNumber), 75 | e.Message, 76 | workflowRun.RunObject.RunId); 77 | 78 | await context.CallActivityAsync(CallNames.LogError, errorEntity); 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/FlowControl/MicroflowStep.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using MicroflowModels; 4 | using MicroflowModels.Helpers; 5 | using Microsoft.Azure.WebJobs; 6 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 7 | using Microsoft.Extensions.Logging; 8 | using static MicroflowModels.Constants; 9 | 10 | namespace Microflow.FlowControl 11 | { 12 | public static class MicroflowStep 13 | { 14 | /// 15 | /// Get the current step table config 16 | /// 17 | [FunctionName(CallNames.GetStepInternal)] 18 | public static async Task GetStep([ActivityTrigger] MicroflowRun workflowRun) => await workflowRun.GetStep(); 19 | 20 | /// 21 | /// Recursive step and sub-step execution 22 | /// 23 | [Deterministic] 24 | [FunctionName(CallNames.ExecuteStep)] 25 | public static async Task ExecuteStep([OrchestrationTrigger] IDurableOrchestrationContext context, 26 | ILogger inLog) 27 | { 28 | MicroflowRun workflowRun = context.GetInput(); 29 | MicroflowContext microflowContext = null; 30 | 31 | try 32 | { 33 | microflowContext = new MicroflowContext(context, workflowRun, inLog); 34 | 35 | await microflowContext.RunMicroflow(); 36 | } 37 | catch (Exception e) 38 | { 39 | if (microflowContext != null) 40 | { 41 | string stepNumber = microflowContext.HttpCallWithRetries == null 42 | ? "-2" 43 | : microflowContext.HttpCallWithRetries.RowKey; 44 | 45 | // log to table workflow completed 46 | LogErrorEntity errorEntity = new(workflowRun?.WorkflowName, 47 | Convert.ToInt32(stepNumber), 48 | e.Message, 49 | workflowRun?.RunObject?.RunId); 50 | 51 | await context.CallActivityAsync(CallNames.LogError, errorEntity); 52 | 53 | throw; 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MicroflowFunctionApp/Helpers/MicroflowHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Microsoft.Azure.WebJobs; 6 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 7 | using Microsoft.Azure.WebJobs.Extensions.Http; 8 | using static MicroflowModels.Constants; 9 | 10 | namespace Microflow.Helpers 11 | { 12 | public static class MicroflowHelper 13 | { 14 | #if DEBUG || RELEASE || !DEBUG_NO_FLOWCONTROL && !DEBUG_NO_FLOWCONTROL_SCALEGROUPS && !DEBUG_NO_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_FLOWCONTROL_STEPCOUNT && !DEBUG_NO_UPSERT_FLOWCONTROL && !DEBUG_NO_UPSERT_FLOWCONTROL_SCALEGROUPS && !DEBUG_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_UPSERT_FLOWCONTROL_STEPCOUNT && !RELEASE_NO_FLOWCONTROL && !RELEASE_NO_FLOWCONTROL_SCALEGROUPS && !RELEASE_NO_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_FLOWCONTROL_STEPCOUNT && !RELEASE_NO_UPSERT_FLOWCONTROL && !RELEASE_NO_UPSERT_FLOWCONTROL_SCALEGROUPS && !RELEASE_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_UPSERT_FLOWCONTROL_STEPCOUNT 15 | /// 16 | /// Get workflow state 17 | /// Pause, run, or stop the workflow, cmd can be "run", "pause", or "stop" 18 | /// 19 | [FunctionName("WorkflowControl")] 20 | public static async Task WorkflowControl([HttpTrigger(AuthorizationLevel.Anonymous, "get", 21 | Route = MicroflowPath + "/WorkflowControl/{cmd}/{workflowName}/{workflowVersion}")] HttpRequestMessage req, 22 | [DurableClient] IDurableEntityClient client, string workflowName, string cmd, string workflowVersion) 23 | { 24 | string key = string.IsNullOrWhiteSpace(workflowVersion) 25 | ? workflowName 26 | : $"{workflowName}@{workflowVersion}"; 27 | 28 | if (cmd.Equals(MicroflowControlKeys.Read, StringComparison.OrdinalIgnoreCase)) 29 | { 30 | EntityId projStateId = new(MicroflowStateKeys.WorkflowState, key); 31 | EntityStateResponse stateRes = await client.ReadEntityStateAsync(projStateId); 32 | 33 | HttpResponseMessage resp = new(HttpStatusCode.OK) 34 | { 35 | Content = new StringContent(stateRes.EntityState) 36 | }; 37 | 38 | return resp; 39 | } 40 | 41 | return await client.SetRunState(nameof(WorkflowState), key, cmd); 42 | } 43 | 44 | /// 45 | /// Pause, run, or stop all with the same global key, cmd can be "run", "pause", or "stop" 46 | /// 47 | [FunctionName("GlobalControl")] 48 | public static async Task GlobalControl([HttpTrigger(AuthorizationLevel.Anonymous, "get", 49 | Route = MicroflowPath + "/GlobalControl/{cmd}/{globalKey}")] HttpRequestMessage req, 50 | [DurableClient] IDurableEntityClient client, string globalKey, string cmd) 51 | { 52 | if (cmd.Equals(MicroflowControlKeys.Read, StringComparison.OrdinalIgnoreCase)) 53 | { 54 | EntityId globStateId = new(MicroflowStateKeys.GlobalState, globalKey); 55 | EntityStateResponse stateRes = await client.ReadEntityStateAsync(globStateId); 56 | 57 | HttpResponseMessage resp = new(HttpStatusCode.OK) 58 | { 59 | Content = new StringContent(stateRes.EntityState) 60 | }; 61 | 62 | return resp; 63 | } 64 | 65 | return await client.SetRunState(nameof(GlobalState), globalKey, cmd); 66 | } 67 | #endif 68 | 69 | /// 70 | /// Durable entity check and set if the global state 71 | /// 72 | [FunctionName(MicroflowStateKeys.GlobalState)] 73 | public static void GlobalState([EntityTrigger] IDurableEntityContext ctx) 74 | { 75 | ctx.RunState(); 76 | } 77 | 78 | /// 79 | /// Durable entity check and set workflow state 80 | /// 81 | [FunctionName(MicroflowStateKeys.WorkflowState)] 82 | public static void WorkflowState([EntityTrigger] IDurableEntityContext ctx) 83 | { 84 | ctx.RunState(); 85 | } 86 | 87 | /// 88 | /// For workflow and global key states 89 | /// 90 | private static void RunState(this IDurableEntityContext ctx) 91 | { 92 | switch (ctx.OperationName) 93 | { 94 | case MicroflowControlKeys.Ready: 95 | ctx.SetState(MicroflowStates.Ready); 96 | break; 97 | case MicroflowControlKeys.Pause: 98 | ctx.SetState(MicroflowStates.Paused); 99 | break; 100 | case MicroflowControlKeys.Stop: 101 | ctx.SetState(MicroflowStates.Stopped); 102 | break; 103 | case MicroflowControlKeys.Read: 104 | ctx.Return(ctx.GetState()); 105 | break; 106 | } 107 | } 108 | 109 | /// 110 | /// Set the global or workflow state with the key, and the cmd can be "pause", "ready", or "stop" 111 | /// 112 | public static async Task SetRunState(this IDurableEntityClient client, 113 | string stateEntityId, 114 | string key, 115 | string cmd) 116 | { 117 | EntityId runStateId = new(stateEntityId, key); 118 | 119 | switch (cmd) 120 | { 121 | case MicroflowControlKeys.Pause: 122 | await client.SignalEntityAsync(runStateId, MicroflowControlKeys.Pause); 123 | break; 124 | case MicroflowControlKeys.Ready: 125 | await client.SignalEntityAsync(runStateId, MicroflowControlKeys.Ready); 126 | break; 127 | case MicroflowControlKeys.Stop: 128 | await client.SignalEntityAsync(runStateId, MicroflowControlKeys.Stop); 129 | break; 130 | } 131 | 132 | return new HttpResponseMessage(HttpStatusCode.OK); 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/Helpers/MicroflowHttpHelper.cs: -------------------------------------------------------------------------------- 1 | using MicroflowModels; 2 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 3 | using Microsoft.Extensions.Primitives; 4 | using System; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Text.Json; 8 | using static MicroflowModels.Constants; 9 | 10 | namespace Microflow.Helpers 11 | { 12 | public static class MicroflowHttpHelper 13 | { 14 | /// 15 | /// Used to handle callout responses 16 | /// 17 | [Deterministic] 18 | public static MicroflowHttpResponse GetMicroflowResponse(this DurableHttpResponse durableHttpResponse, bool forwardPostData) 19 | { 20 | int statusCode = (int)durableHttpResponse.StatusCode; 21 | 22 | if (statusCode <= 200 || ((statusCode > 201) && (statusCode < 300))) 23 | { 24 | return new MicroflowHttpResponse() { Success = true, HttpResponseStatusCode = statusCode, Content = forwardPostData ? durableHttpResponse.Content : string.Empty }; 25 | } 26 | 27 | // if 201 created try get the location header to save it in the steps log 28 | if (statusCode != 201) 29 | return new MicroflowHttpResponse() { Success = false, HttpResponseStatusCode = statusCode }; 30 | 31 | return durableHttpResponse.Headers.TryGetValue("location", out StringValues values) 32 | ? new MicroflowHttpResponse() { Success = true, HttpResponseStatusCode = statusCode, Content = values[0] } 33 | : new MicroflowHttpResponse() { Success = true, HttpResponseStatusCode = statusCode }; 34 | } 35 | 36 | [Deterministic] 37 | public static DurableHttpRequest CreateMicroflowDurableHttpRequest(this HttpCall httpCall, string instanceId, MicroflowHttpResponse microflowHttpResponse) 38 | { 39 | DurableHttpRequest newDurableHttpRequest; 40 | 41 | //string webhook = string.IsNullOrWhiteSpace(httpCall.WebhookId) 42 | // ? "" 43 | // : httpCall.WebhookId; 44 | 45 | httpCall.CalculateGlobalKey(); 46 | 47 | if (!httpCall.IsHttpGet) 48 | { 49 | MicroflowPostData postData = new() 50 | { 51 | WorkflowName = httpCall.PartitionKey, 52 | SubOrchestrationId = instanceId, 53 | RunId = httpCall.RunId, 54 | StepId = httpCall.StepId, 55 | StepNumber = Convert.ToInt32(httpCall.RowKey), 56 | MainOrchestrationId = httpCall.MainOrchestrationId, 57 | Webhook = httpCall.EnableWebhook ? $"{CallNames.BaseUrl}/webhooks/{httpCall.WebhookId}" : null, 58 | GlobalKey = httpCall.GlobalKey, 59 | PostData = microflowHttpResponse.Content 60 | }; 61 | 62 | string body = JsonSerializer.Serialize(postData); 63 | 64 | newDurableHttpRequest = new DurableHttpRequest( 65 | method: HttpMethod.Post, 66 | uri: new Uri(httpCall.CalloutUrl), 67 | timeout: TimeSpan.FromSeconds(httpCall.CalloutTimeoutSeconds), 68 | content: body, 69 | asynchronousPatternEnabled: httpCall.AsynchronousPollingEnabled 70 | //headers: durableHttpRequest.Headers, 71 | //tokenSource: durableHttpRequest.TokenSource 72 | 73 | ); 74 | 75 | // Do not copy over the x-functions-key header, as in many cases, the 76 | // functions key used for the initial request will be a Function-level key 77 | // and the status endpoint requires a master key. 78 | //newDurableHttpRequest.Headers.Remove("x-functions-key"); 79 | 80 | } 81 | else 82 | { 83 | newDurableHttpRequest = new DurableHttpRequest( 84 | method: HttpMethod.Get, 85 | uri: new Uri(httpCall.CalloutUrl), 86 | timeout: TimeSpan.FromSeconds(httpCall.CalloutTimeoutSeconds), 87 | asynchronousPatternEnabled: httpCall.AsynchronousPollingEnabled 88 | //headers: durableHttpRequest.Headers, 89 | //tokenSource: durableHttpRequest.TokenSource 90 | 91 | ); 92 | 93 | // Do not copy over the x-functions-key header, as in many cases, the 94 | // functions key used for the initial request will be a Function-level key 95 | // and the status endpoint requires a master key. 96 | //newDurableHttpRequest.Headers.Remove("x-functions-key"); 97 | } 98 | 99 | return newDurableHttpRequest; 100 | } 101 | 102 | [Deterministic] 103 | public static void ParseUrlMicroflowData(IHttpCallWithRetries httpCall, string instanceId) 104 | { 105 | // do not replace the querystring if it is a post 106 | if(!httpCall.IsHttpGet) 107 | { 108 | return; 109 | } 110 | 111 | StringBuilder sb = new(httpCall.CalloutUrl); 112 | 113 | sb.Replace("", httpCall.PartitionKey); 114 | sb.Replace("", httpCall.MainOrchestrationId); 115 | sb.Replace("", instanceId); 116 | if(httpCall.EnableWebhook) 117 | { 118 | sb.Replace("", $"{CallNames.BaseUrl}/webhooks/{httpCall.WebhookId}"); 119 | } 120 | sb.Replace("", httpCall.RunId); 121 | sb.Replace("", httpCall.StepId); 122 | sb.Replace("", httpCall.RowKey); 123 | sb.Replace("", httpCall.GlobalKey); 124 | 125 | httpCall.CalloutUrl = sb.ToString(); 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/Helpers/MicroflowStartupHelper.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using MicroflowModels; 3 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 4 | using System; 5 | using System.Collections.Specialized; 6 | using static MicroflowModels.Constants; 7 | using System.Net.Http; 8 | using System.Net; 9 | using System.Threading.Tasks; 10 | 11 | namespace Microflow.Helpers 12 | { 13 | public static class MicroflowStartupHelper 14 | { 15 | public static async Task StartWorkflow(this IDurableOrchestrationClient client, HttpRequestMessage req, string instanceId, string workflowNameVersion) 16 | { 17 | try 18 | { 19 | MicroflowRun workflowRun = MicroflowWorkflowHelper.CreateMicroflowRun(req, ref instanceId, workflowNameVersion); 20 | 21 | if (req.Method == HttpMethod.Post) 22 | { 23 | string content = await req.Content.ReadAsStringAsync(); 24 | 25 | if(!string.IsNullOrEmpty(content)) 26 | { 27 | workflowRun.RunObject.MicroflowStepResponseData = new() 28 | { 29 | Content = content 30 | }; 31 | } 32 | 33 | //workflowRun.RunObject.MicroflowStepResponseData.Content = content; 34 | } 35 | 36 | // start 37 | await client.StartNewAsync(CallNames.MicroflowStartOrchestration, instanceId, workflowRun); 38 | 39 | HttpResponseMessage response = client.CreateCheckStatusResponse(req, instanceId); 40 | 41 | if (response.StatusCode == HttpStatusCode.OK) 42 | { 43 | response.Content = new StringContent(instanceId); 44 | } 45 | 46 | return response; 47 | 48 | } 49 | catch (RequestFailedException ex) 50 | { 51 | HttpResponseMessage resp = new(HttpStatusCode.InternalServerError) 52 | { 53 | Content = new StringContent(ex.Message + " - workflow in error state, call 'UpsertWorkflow' at least once before running a workflow.") 54 | }; 55 | 56 | return resp; 57 | } 58 | catch (Exception e) 59 | { 60 | HttpResponseMessage resp = new(HttpStatusCode.InternalServerError) 61 | { 62 | Content = new StringContent(e.Message) 63 | }; 64 | 65 | return resp; 66 | } 67 | } 68 | 69 | /// 70 | /// Create a new workflowRun for stratup, set GlobalKey 71 | /// 72 | public static MicroflowRun CreateStartupRun(NameValueCollection data, 73 | ref string instanceId, 74 | string workflowName) 75 | { 76 | var input = new 77 | { 78 | Loop = Convert.ToInt32(data["loop"]), 79 | GlobalKey = data["globalkey"] 80 | }; 81 | 82 | // create a workflow run 83 | MicroflowRun workflowRun = new() 84 | { 85 | WorkflowName = workflowName, 86 | Loop = input.Loop != 0 87 | ? input.Loop 88 | : 1 89 | }; 90 | 91 | // create a new run object 92 | RunObject runObj = new(); 93 | workflowRun.RunObject = runObj; 94 | 95 | // instanceId is set/singleton 96 | if (!string.IsNullOrWhiteSpace(instanceId)) 97 | { 98 | // globalKey is set 99 | if (!string.IsNullOrWhiteSpace(input.GlobalKey)) 100 | { 101 | runObj.GlobalKey = input.GlobalKey; 102 | } 103 | else 104 | { 105 | runObj.GlobalKey = Guid.NewGuid().ToString(); 106 | } 107 | } 108 | // instanceId is not set/multiple concurrent instances 109 | else 110 | { 111 | instanceId = Guid.NewGuid().ToString(); 112 | // globalKey is set 113 | if (!string.IsNullOrWhiteSpace(input.GlobalKey)) 114 | { 115 | runObj.GlobalKey = input.GlobalKey; 116 | } 117 | else 118 | { 119 | runObj.GlobalKey = instanceId; 120 | } 121 | } 122 | 123 | //workflowRun.RunObject.StepNumber = "-1"; 124 | workflowRun.OrchestratorInstanceId = instanceId; 125 | 126 | return workflowRun; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /MicroflowFunctionApp/Helpers/MicroflowTableHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Azure; 3 | using Azure.Data.Tables; 4 | using Microflow.MicroflowTableModels; 5 | using System.Collections.Generic; 6 | using MicroflowModels.Helpers; 7 | 8 | namespace Microflow.Helpers 9 | { 10 | public static class MicroflowTableHelper 11 | { 12 | #region Table operations 13 | 14 | public static async Task LogStep(this LogStepEntity logEntity) 15 | { 16 | TableClient tableClient = TableHelper.GetLogStepsTable(); 17 | 18 | await tableClient.UpsertEntityAsync(logEntity); 19 | } 20 | 21 | public static async Task LogOrchestration(this LogOrchestrationEntity logEntity) 22 | { 23 | TableClient tableClient = TableHelper.GetLogOrchestrationTable(); 24 | 25 | await tableClient.UpsertEntityAsync(logEntity); 26 | } 27 | 28 | public static async Task LogWebhook(this LogWebhookEntity logEntity) 29 | { 30 | TableClient tableClient = TableHelper.GetLogWebhookTable(); 31 | 32 | await tableClient.UpsertEntityAsync(logEntity); 33 | } 34 | 35 | public static async Task> GetWebhooks(string workflowName, string webhookId, int stepNumber, string instanceGuid = "") 36 | { 37 | TableClient tableClient = TableHelper.GetLogWebhookTable(); 38 | 39 | string query = $"(PartitionKey eq '{workflowName}' or PartitionKey >= '{workflowName}~' and PartitionKey < '{workflowName}~~') and RowKey lt '{stepNumber + 1}' and RowKey gt '{stepNumber}~"; 40 | 41 | if (!string.IsNullOrEmpty(instanceGuid)) 42 | { 43 | query += $"{instanceGuid}~' and RowKey lt '{stepNumber}~{instanceGuid}~~'"; 44 | } 45 | else 46 | { 47 | query += "'"; 48 | } 49 | //"(PartitionKey eq 'Myflow_ClientX2@2.1' or PartitionKey >= 'Myflow_ClientX2@2.1~' and PartitionKey lt 'Myflow_ClientX2@2.1~~') and RowKey < '3' and RowKey gt '2~d17f312f-8b2f-5d34-aa4e-92c76236029d~'and RowKey < '3' and RowKey lt '2~d17f312f-8b2f-5d34-aa4e-92c76236029d~~'"; 50 | 51 | AsyncPageable queryResultsFilter = tableClient.QueryAsync(filter: query); 52 | 53 | List list = new(); 54 | 55 | // Iterate the to access all queried entities. 56 | await foreach (LogWebhookEntity qEntity in queryResultsFilter) 57 | { 58 | list.Add(qEntity); 59 | } 60 | 61 | return list; 62 | } 63 | 64 | #endregion 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /MicroflowFunctionApp/Helpers/MicroflowWorkflowHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Specialized; 3 | using System.Net.Http; 4 | using MicroflowModels; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using static MicroflowModels.Constants; 7 | 8 | namespace Microflow.Helpers 9 | { 10 | public static class MicroflowWorkflowHelper 11 | { 12 | public static MicroflowRun CreateMicroflowRun(HttpRequestMessage req, ref string instanceId, string workflowName) 13 | { 14 | return MicroflowStartupHelper.CreateStartupRun(req.RequestUri.ParseQueryString(), ref instanceId, workflowName); 15 | } 16 | 17 | public static Microsoft.Azure.WebJobs.Extensions.DurableTask.RetryOptions GetRetryOptions(this IHttpCallWithRetries httpCallWithRetries) 18 | { 19 | Microsoft.Azure.WebJobs.Extensions.DurableTask.RetryOptions ops = new(TimeSpan.FromSeconds(httpCallWithRetries.RetryDelaySeconds), 20 | httpCallWithRetries.RetryMaxRetries + 1) 21 | { 22 | RetryTimeout = TimeSpan.FromSeconds(httpCallWithRetries.RetryTimeoutSeconds), 23 | MaxRetryInterval = TimeSpan.FromSeconds(httpCallWithRetries.RetryMaxDelaySeconds), 24 | BackoffCoefficient = httpCallWithRetries.RetryBackoffCoefficient 25 | }; 26 | 27 | return ops; 28 | } 29 | 30 | /// 31 | /// Work out what the global key is for this call 32 | /// 33 | [Deterministic] 34 | public static void CalculateGlobalKey(this HttpCall httpCall) 35 | { 36 | // check if it is call to Microflow 37 | if (httpCall.CalloutUrl.StartsWith($"{CallNames.BaseUrl}start/")) 38 | { 39 | // parse query string 40 | NameValueCollection data = new Uri(httpCall.CalloutUrl).ParseQueryString(); 41 | // if there is query string data 42 | if (data.Count > 0) 43 | { 44 | // check if there is a global key (maybe if it is an assigned key) 45 | if (string.IsNullOrEmpty(data.Get("globalkey"))) 46 | { 47 | httpCall.CalloutUrl += $"&globalkey={httpCall.GlobalKey}"; 48 | } 49 | } 50 | else 51 | { 52 | httpCall.CalloutUrl += $"?globalkey={httpCall.GlobalKey}"; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MicroflowFunctionApp/Models/Interfaces/IMicroflowHttpResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Microflow.Models 2 | { 3 | //public interface IMicroflowHttpResponse 4 | //{ 5 | // int HttpResponseStatusCode { get; set; } 6 | // string Content { get; set; } 7 | // bool Success { get; set; } 8 | // public List SubStepsToRun { get; set; } 9 | //} 10 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/Models/MicroflowTableModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Azure; 3 | using Azure.Data.Tables; 4 | 5 | namespace Microflow.MicroflowTableModels 6 | { 7 | #region TableEntity 8 | 9 | public class WebhookEntity : ITableEntity 10 | { 11 | public WebhookEntity() { } 12 | 13 | public WebhookEntity(string webhookId, string webhookSubStepsMapping) 14 | { 15 | PartitionKey = webhookId; 16 | RowKey = "0"; 17 | WebhookSubStepsMapping = webhookSubStepsMapping; 18 | } 19 | 20 | public string WebhookSubStepsMapping { get; set; } 21 | public string PartitionKey { get; set; } 22 | public string RowKey { get; set; } 23 | public DateTimeOffset? Timestamp { get; set; } 24 | public ETag ETag { get; set; } 25 | } 26 | 27 | public class LogWebhookEntity : ITableEntity 28 | { 29 | public LogWebhookEntity() { } 30 | 31 | public LogWebhookEntity(bool isCreate, string workflowName, string rowKey, DateTime date, string runId = "", string action = "") 32 | { 33 | PartitionKey = workflowName; 34 | RowKey = rowKey; 35 | 36 | if(isCreate) 37 | { 38 | CreateDate = date; 39 | RunId = runId; 40 | } 41 | else 42 | { 43 | ActionDate = date; 44 | Action = action; 45 | } 46 | } 47 | 48 | public string Action { get; set; } 49 | public string RunId { get; set; } 50 | public DateTime? CreateDate { get; set; } 51 | public DateTime? ActionDate { get; set; } 52 | public string PartitionKey { get; set; } 53 | public string RowKey { get; set; } 54 | public DateTimeOffset? Timestamp { get; set; } 55 | public ETag ETag { get; set; } 56 | } 57 | 58 | /// 59 | /// Used for orchestration level logging 60 | /// 61 | public class LogOrchestrationEntity : ITableEntity 62 | { 63 | public LogOrchestrationEntity() { } 64 | 65 | public LogOrchestrationEntity(bool isStart, string workflowName, string rowKey, string logMessage, DateTime date, string orchestrationId, string globalKey) 66 | { 67 | PartitionKey = workflowName; 68 | LogMessage = logMessage; 69 | RowKey = rowKey; 70 | OrchestrationId = orchestrationId; 71 | GlobalKey = globalKey; 72 | 73 | if (isStart) 74 | StartDate = date; 75 | else 76 | EndDate = date; 77 | } 78 | 79 | public string GlobalKey { get; set; } 80 | public string OrchestrationId { get; set; } 81 | public string LogMessage { get; set; } 82 | public DateTime? StartDate { get; set; } 83 | public DateTime? EndDate { get; set; } 84 | public string PartitionKey { get; set; } 85 | public string RowKey { get; set; } 86 | public DateTimeOffset? Timestamp { get; set; } 87 | public ETag ETag { get; set; } 88 | } 89 | 90 | /// 91 | /// Used for step level logging 92 | /// 93 | public class LogStepEntity : ITableEntity 94 | { 95 | public LogStepEntity() { } 96 | 97 | public LogStepEntity(bool isStart, 98 | string workflowName, 99 | string rowKey, 100 | int stepNumber, 101 | string mainOrchestrationId, 102 | string runId, 103 | string globalKey, 104 | string? calloutUrl = null, 105 | bool? success = null, 106 | int? httpStatusCode = null, 107 | string message = null, 108 | string? subOrchestrationId = null, 109 | string? webhookId = null) 110 | { 111 | PartitionKey = workflowName + "__" + mainOrchestrationId; 112 | RowKey = rowKey; 113 | StepNumber = stepNumber; 114 | GlobalKey = globalKey; 115 | RunId = runId; 116 | CalloutUrl = calloutUrl; 117 | SubOrchestrationId = subOrchestrationId; 118 | 119 | if (isStart) 120 | StartDate = DateTime.UtcNow; 121 | else 122 | { 123 | Success = success; 124 | HttpStatusCode = httpStatusCode; 125 | Message = message; 126 | EndDate = DateTime.UtcNow; 127 | } 128 | } 129 | 130 | public bool? Success { get; set; } 131 | public int? HttpStatusCode { get; set; } 132 | public string Message { get; set; } 133 | public int StepNumber { get; set; } 134 | public string RunId { get; set; } 135 | public string GlobalKey { get; set; } 136 | public string CalloutUrl { get; set; } 137 | public string SubOrchestrationId { get; set; } 138 | public string WebhookId { get; set; } 139 | public DateTime? StartDate { get; set; } 140 | public DateTime? EndDate { get; set; } 141 | public string PartitionKey { get; set; } 142 | public string RowKey { get; set; } 143 | public DateTimeOffset? Timestamp { get; set; } 144 | public ETag ETag { get; set; } 145 | } 146 | 147 | #endregion 148 | } 149 | -------------------------------------------------------------------------------- /MicroflowFunctionApp/Models/Models.cs: -------------------------------------------------------------------------------- 1 | namespace Microflow.Models 2 | { 3 | #region POCOs 4 | 5 | /// 6 | /// Used when locking for parent step concurrency count checking 7 | /// 8 | public class CanExecuteNowObject 9 | { 10 | public string RunId { get; set; } 11 | public string StepNumber { get; set; } 12 | public int ParentCount { get; set; } 13 | public string ScaleGroupId { get; set; } 14 | public string WorkflowName { get; set; } 15 | } 16 | 17 | /// 18 | /// Used when locking for parent step concurrency count checking 19 | /// 20 | public class CanExecuteResult 21 | { 22 | public bool CanExecute { get; set; } 23 | public string StepNumber { get; set; } 24 | } 25 | 26 | #endregion 27 | } 28 | -------------------------------------------------------------------------------- /MicroflowFunctionApp/Optional/ScaleGroupsApi.cs: -------------------------------------------------------------------------------- 1 | #define INCLUDE_scalegroups 2 | #if INCLUDE_scalegroups 3 | using MicroflowModels; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using Microsoft.Azure.WebJobs.Extensions.Http; 7 | using Newtonsoft.Json.Linq; 8 | using Newtonsoft.Json; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Net; 13 | using System.Net.Http; 14 | using System.Text.Json; 15 | using System.Threading; 16 | using System.Threading.Tasks; 17 | using static MicroflowModels.Constants; 18 | 19 | namespace MicroflowApi 20 | { 21 | public class ScaleGroupsApi 22 | { 23 | /// 24 | /// Get max instance count for scale group 25 | /// 26 | [FunctionName("Get" + ScaleGroupCalls.ScaleGroup)] 27 | public static async Task GetScaleGroup([HttpTrigger(AuthorizationLevel.Anonymous, "get", 28 | Route = MicroflowPath + "/ScaleGroup/{scaleGroupId}")] HttpRequestMessage req, 29 | [DurableClient] IDurableEntityClient client, string scaleGroupId) 30 | { 31 | Dictionary result = new(); 32 | EntityQueryResult res = null; 33 | 34 | using (CancellationTokenSource cts = new()) 35 | { 36 | res = await client.ListEntitiesAsync(new EntityQuery() 37 | { 38 | PageSize = 99999999, 39 | EntityName = ScaleGroupCalls.ScaleGroupMaxConcurrentInstanceCount, 40 | FetchState = true 41 | }, cts.Token); 42 | } 43 | 44 | if (string.IsNullOrWhiteSpace(scaleGroupId)) 45 | { 46 | foreach (DurableEntityStatus rr in res.Entities) 47 | { 48 | result.Add(rr.EntityId.EntityKey, rr.State.Value()); 49 | } 50 | } 51 | else 52 | { 53 | foreach (DurableEntityStatus rr in res.Entities.Where(e => e.EntityId.EntityKey.Equals(scaleGroupId))) 54 | { 55 | result.Add(rr.EntityId.EntityKey, rr.State.ToObject()); 56 | } 57 | } 58 | 59 | StringContent content = new(JsonConvert.SerializeObject(result)); 60 | 61 | return new HttpResponseMessage(HttpStatusCode.OK) 62 | { 63 | Content = content 64 | }; 65 | } 66 | 67 | /// 68 | /// Set the max instance count for scale group, default to wait for 5 seconds 69 | /// 70 | [FunctionName("Set" + ScaleGroupCalls.ScaleGroup)] 71 | public static async Task ScaleGroup([HttpTrigger(AuthorizationLevel.Anonymous, "post", 72 | Route = MicroflowPath + "/ScaleGroup/{scaleGroupId}/{maxWaitSeconds:int?}")] HttpRequestMessage req, 73 | [DurableClient] IDurableOrchestrationClient client, string scaleGroupId, int? maxWaitSeconds) 74 | { 75 | ScaleGroupState state = JsonConvert.DeserializeObject(await req.Content.ReadAsStringAsync()); 76 | 77 | string instanceId = await client.StartNewAsync("SetScaleGroupMaxConcurrentCount", null, (scaleGroupId, state)); 78 | 79 | return await client.WaitForCompletionOrCreateCheckStatusResponseAsync(req, instanceId, TimeSpan.FromSeconds(maxWaitSeconds is null 80 | ? 5 81 | : maxWaitSeconds.Value)); 82 | } 83 | 84 | /// 85 | /// Set the scale group max concurrent instance count orchestration 86 | /// 87 | /// 88 | [Deterministic] 89 | [FunctionName("SetScaleGroupMaxConcurrentCount")] 90 | public static async Task SetScaleGroupMaxOrchestration([OrchestrationTrigger] IDurableOrchestrationContext context) 91 | { 92 | (string scaleGroupId, ScaleGroupState state) = context.GetInput<(string, ScaleGroupState)>(); 93 | 94 | EntityId scaleGroupCountId = new(ScaleGroupCalls.ScaleGroupMaxConcurrentInstanceCount, scaleGroupId); 95 | 96 | await context.CallEntityAsync(scaleGroupCountId, MicroflowEntityKeys.Set, state); 97 | } 98 | } 99 | } 100 | 101 | #endif -------------------------------------------------------------------------------- /MicroflowFunctionApp/Optional/WorkflowApi.cs: -------------------------------------------------------------------------------- 1 | #if DEBUG || RELEASE || !DEBUG_NO_UPSERT && !DEBUG_NO_UPSERT_FLOWCONTROL && !DEBUG_NO_UPSERT_FLOWCONTROL_SCALEGROUPS && !DEBUG_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_UPSERT_FLOWCONTROL_STEPCOUNT && !DEBUG_NO_UPSERT_SCALEGROUPS && !DEBUG_NO_UPSERT_SCALEGROUPS_STEPCOUNT && !DEBUG_NO_UPSERT_STEPCOUNT && !RELEASE_NO_UPSERT && !RELEASE_NO_UPSERT_FLOWCONTROL && !RELEASE_NO_UPSERT_FLOWCONTROL_SCALEGROUPS && !RELEASE_NO_UPSERT_FLOWCONTROL_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_UPSERT_FLOWCONTROL_STEPCOUNT && !RELEASE_NO_UPSERT_SCALEGROUPS && !RELEASE_NO_UPSERT_SCALEGROUPS_STEPCOUNT && !RELEASE_NO_UPSERT_STEPCOUN 2 | using System.Threading.Tasks; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.Http; 5 | using System.Net.Http; 6 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 7 | using MicroflowShared; 8 | using Microflow.Helpers; 9 | 10 | namespace MicroflowApi 11 | { 12 | public class WorkflowSplitCode 13 | { 14 | /// 15 | /// Inserts a workflow and starts it - skips workflow and global states, deletes, workflow save, create tables 16 | /// This will update the entire entity, so if its not the same steps then an unexpacted issue can occur 17 | /// 18 | [FunctionName("QuickInsertAndStartWorkflow")] 19 | public static async Task QuickInsertAndStartWorkflow([HttpTrigger(AuthorizationLevel.Anonymous, "post", 20 | Route = MicroflowModels.Constants.MicroflowPath + "/QuickInsertAndStartWorkflow/{workflowNameVersion}/{instanceId?}/{globalKey?}")] HttpRequestMessage req, 21 | [DurableClient] IDurableOrchestrationClient client, string workflowNameVersion, string instanceId, string globalKey) 22 | { 23 | await client.QuickInsert(await req.Content.ReadAsStringAsync(), globalKey); 24 | 25 | return await client.StartWorkflow(req, instanceId, workflowNameVersion); 26 | } 27 | 28 | /// 29 | /// This must be called at least once before a workflow runs, 30 | /// this is to prevent multiple concurrent instances from writing step data at workflow run, 31 | /// call UpsertWorkflow when something changed in the workflow, but do not always call this when concurrent multiple workflows 32 | /// 33 | [FunctionName("UpsertWorkflow")] 34 | public static async Task UpsertWorkflow([HttpTrigger(AuthorizationLevel.Anonymous, "post", 35 | Route = MicroflowModels.Constants.MicroflowPath + "/UpsertWorkflow/{globalKey?}")] HttpRequestMessage req, 36 | [DurableClient] IDurableEntityClient client, string globalKey) 37 | { 38 | return await client.UpsertWorkflow(await req.Content.ReadAsStringAsync(), globalKey); 39 | } 40 | 41 | 42 | /// 43 | /// Returns the workflow Json that was saved with UpsertWorkflow 44 | /// 45 | [FunctionName("GetWorkflow")] 46 | public static async Task GetWorkflowJson([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = MicroflowModels.Constants.MicroflowPath + "/GetWorkflow/{workflowName}")] HttpRequestMessage req, 47 | string workflowName) 48 | { 49 | return await WorkflowHelper.GetWorkflowJson(workflowName); 50 | } 51 | } 52 | } 53 | #endif -------------------------------------------------------------------------------- /MicroflowFunctionApp/Properties/ServiceDependencies/MicroflowApp20210615143625 - Zip Deploy/storage1.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourceGroupName": { 6 | "type": "string", 7 | "defaultValue": "ACloudGuru", 8 | "metadata": { 9 | "_parameterType": "resourceGroup", 10 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 11 | } 12 | }, 13 | "resourceGroupLocation": { 14 | "type": "string", 15 | "defaultValue": "southafricanorth", 16 | "metadata": { 17 | "_parameterType": "location", 18 | "description": "Location of the resource group. Resource groups could have different location than resources." 19 | } 20 | }, 21 | "resourceLocation": { 22 | "type": "string", 23 | "defaultValue": "[parameters('resourceGroupLocation')]", 24 | "metadata": { 25 | "_parameterType": "location", 26 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 27 | } 28 | } 29 | }, 30 | "resources": [ 31 | { 32 | "type": "Microsoft.Resources/resourceGroups", 33 | "name": "[parameters('resourceGroupName')]", 34 | "location": "[parameters('resourceGroupLocation')]", 35 | "apiVersion": "2019-10-01" 36 | }, 37 | { 38 | "type": "Microsoft.Resources/deployments", 39 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('csvimportservice', subscription().subscriptionId)))]", 40 | "resourceGroup": "[parameters('resourceGroupName')]", 41 | "apiVersion": "2019-10-01", 42 | "dependsOn": [ 43 | "[parameters('resourceGroupName')]" 44 | ], 45 | "properties": { 46 | "mode": "Incremental", 47 | "template": { 48 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 49 | "contentVersion": "1.0.0.0", 50 | "resources": [ 51 | { 52 | "sku": { 53 | "name": "Standard_LRS", 54 | "tier": "Standard" 55 | }, 56 | "kind": "Storage", 57 | "name": "csvimportservice", 58 | "type": "Microsoft.Storage/storageAccounts", 59 | "location": "[parameters('resourceLocation')]", 60 | "apiVersion": "2017-10-01" 61 | } 62 | ] 63 | } 64 | } 65 | } 66 | ], 67 | "metadata": { 68 | "_dependencyType": "storage.azure" 69 | } 70 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/Properties/serviceDependencies.MicroflowApp20210615143625 - Zip Deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "storage1": { 4 | "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.Storage/storageAccounts/csvimportservice", 5 | "type": "storage.azure", 6 | "connectionId": "AzureWebJobsStorage" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/TableLogging/BlobLogResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Azure; 4 | using Azure.Storage.Blobs; 5 | using Microsoft.Azure.WebJobs; 6 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 7 | using static MicroflowModels.Constants; 8 | 9 | namespace Microflow.Logging 10 | { 11 | public static class BlobLogResponseActivity 12 | { 13 | [FunctionName(CallNames.LogMicroflowHttpData)] 14 | public static async Task BlobLogResponse([ActivityTrigger] (string blobName, string data, bool isRequest) input) 15 | { 16 | string prefix = input.isRequest ? "request-" : "response-"; 17 | 18 | BlobContainerClient blobContainerClient = new("UseDevelopmentStorage=true", "microflow-httpdata"); 19 | 20 | try 21 | { 22 | await blobContainerClient.UploadBlobAsync(prefix + input.blobName, BinaryData.FromString(input.data)); 23 | } 24 | catch (RequestFailedException ex) when (ex.Status == 404) 25 | { 26 | await blobContainerClient.CreateIfNotExistsAsync(); 27 | 28 | await BlobLogResponse(input); 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/TableLogging/TableLogErrorActivity.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using MicroflowModels; 3 | using MicroflowModels.Helpers; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using static MicroflowModels.Constants; 7 | 8 | namespace Microflow.Logging 9 | { 10 | public static class TableLogginActivity 11 | { 12 | [FunctionName(CallNames.LogError)] 13 | public static async Task LogError([ActivityTrigger] LogErrorEntity logEntity) 14 | { 15 | await TableHelper.LogError(logEntity); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/TableLogging/TableLogOrchestrationActivity.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microflow.Helpers; 3 | using Microflow.MicroflowTableModels; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using static MicroflowModels.Constants; 7 | 8 | namespace Microflow.Logging 9 | { 10 | public static class TableLogOrchestrationActivity 11 | { 12 | [FunctionName(CallNames.LogOrchestration)] 13 | public static async Task TableLogOrchestration([ActivityTrigger] LogOrchestrationEntity logEntity) 14 | { 15 | await logEntity.LogOrchestration(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/TableLogging/TableLogStepActivity.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microflow.Helpers; 3 | using Microflow.MicroflowTableModels; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using static MicroflowModels.Constants; 7 | 8 | namespace Microflow.Logging 9 | { 10 | public static class BlobLogRequest 11 | { 12 | [FunctionName(CallNames.LogStep)] 13 | public static async Task TableLogActivity([ActivityTrigger] LogStepEntity logEntity) 14 | { 15 | await logEntity.LogStep(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/TableLogging/TableLogWebhook.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microflow.Helpers; 3 | using Microflow.MicroflowTableModels; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using static MicroflowModels.Constants; 7 | 8 | namespace Microflow.Logging 9 | { 10 | public static class TableLogWebhook 11 | { 12 | [FunctionName(CallNames.LogWebhook)] 13 | public static async Task TableLogWebhookActivity([ActivityTrigger] LogWebhookEntity logEntity) 14 | { 15 | await logEntity.LogWebhook(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/cloudconfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "AzureWebJobsDashboard", 4 | "value": "DefaultEndpointsProtocol=https;AccountName=microflowapp202202032142;AccountKey=lsGSaloGV9L6RCn7XlR/zDZe6xOnOv8BtdZACqxvJpkIFiqWSHiiAIg12LpwK5jt4Rk+WeeUG57ykIjgTiSaqg==;EndpointSuffix=core.windows.net", 5 | "slotSetting": false 6 | }, 7 | { 8 | "name": "AzureWebJobsStorage", 9 | "value": "DefaultEndpointsProtocol=https;AccountName=microflowapp202202032142;AccountKey=lsGSaloGV9L6RCn7XlR/zDZe6xOnOv8BtdZACqxvJpkIFiqWSHiiAIg12LpwK5jt4Rk+WeeUG57ykIjgTiSaqg==;EndpointSuffix=core.windows.net", 10 | "slotSetting": false 11 | }, 12 | { 13 | "name": "BaseUrl", 14 | "value": "https://microflowapp20220203214232.azurewebsites.net/", 15 | "slotSetting": false 16 | }, 17 | { 18 | "name": "CallBack", 19 | "value": "webhook", 20 | "slotSetting": false 21 | }, 22 | { 23 | "name": "FUNCTIONS_EXTENSION_VERSION", 24 | "value": "~4", 25 | "slotSetting": false 26 | }, 27 | { 28 | "name": "FUNCTIONS_WORKER_RUNTIME", 29 | "value": "dotnet", 30 | "slotSetting": false 31 | }, 32 | { 33 | "name": "MicroflowStorage", 34 | "value": "DefaultEndpointsProtocol=https;AccountName=microflowapp202202032142;AccountKey=lsGSaloGV9L6RCn7XlR/zDZe6xOnOv8BtdZACqxvJpkIFiqWSHiiAIg12LpwK5jt4Rk+WeeUG57ykIjgTiSaqg==;EndpointSuffix=core.windows.net", 35 | "slotSetting": false 36 | }, 37 | { 38 | "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", 39 | "value": "DefaultEndpointsProtocol=https;AccountName=microflowapp202202032142;AccountKey=lsGSaloGV9L6RCn7XlR/zDZe6xOnOv8BtdZACqxvJpkIFiqWSHiiAIg12LpwK5jt4Rk+WeeUG57ykIjgTiSaqg==;EndpointSuffix=core.windows.net", 40 | "slotSetting": false 41 | }, 42 | { 43 | "name": "WEBSITE_CONTENTSHARE", 44 | "value": "microflowapp20220203214232", 45 | "slotSetting": false 46 | } 47 | ] -------------------------------------------------------------------------------- /MicroflowFunctionApp/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensions": { 4 | "durableTask": { 5 | "logReplayEvents": "false", 6 | "hubName": "MicroflowTaskHub" 7 | }, 8 | "http": { 9 | "routePrefix": "" 10 | } 11 | }, 12 | "logging": { 13 | "logLevel": { 14 | "DurableTask.AzureStorage": "None", 15 | "DurableTask.Core": "None" 16 | }, 17 | "applicationInsights": { 18 | "samplingExcludedTypes": "Request", 19 | "samplingSettings": { 20 | "isEnabled": false 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /MicroflowFunctionApp/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "MicroflowStorage": "UseDevelopmentStorage=true", 6 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 7 | "BasePrefix": "http://", 8 | "PollingMaxHours": 240, 9 | "PollingIntervalSeconds": 5, 10 | "PollingIntervalMaxSeconds": 15, 11 | "AzureWebJobsSecretStorageType": "files" 12 | } 13 | } -------------------------------------------------------------------------------- /MicroflowModels/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MicroflowModels 4 | { 5 | public static class Constants 6 | { 7 | 8 | #if DEBUG || RELEASE || !DEBUG_NO_SCALEGROUPS && !RELEASE_NO_SCALEGROUPS 9 | public static class ScaleGroupCalls 10 | { 11 | public const string CanExecuteNowInScaleGroup = "CanExecuteNowInScaleGroup"; 12 | public const string CanExecuteNowInScaleGroupCount = "CanExecuteNowInScaleGroupCount"; 13 | public const string ScaleGroupMaxConcurrentInstanceCount = "ScaleGroupMaxConcurrentInstanceCount"; 14 | public const string ScaleGroup = "ScaleGroup"; 15 | } 16 | #endif 17 | 18 | public static class PollingConfig 19 | { 20 | public static readonly long PollingMaxHours = Convert.ToInt64(Environment.GetEnvironmentVariable("PollingMaxHours")); 21 | public static readonly int PollingIntervalMaxSeconds = Convert.ToInt32(Environment.GetEnvironmentVariable("PollingIntervalMaxSeconds")); 22 | public static readonly int PollingIntervalSeconds = Convert.ToInt32(Environment.GetEnvironmentVariable("PollingIntervalSeconds")); 23 | } 24 | 25 | public const string MicroflowPath = "microflow/v1"; 26 | 27 | public static class CallNames 28 | { 29 | public static readonly string BaseUrl = $"{Environment.GetEnvironmentVariable("BasePrefix")}{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/{MicroflowPath}"; 30 | public const string CanExecuteNow = "CanExecuteNow"; 31 | public const string ExecuteStep = "ExecuteStep"; 32 | public const string GetStepInternal = "GetStepInternal"; 33 | public const string LogError = "LogError"; 34 | public const string LogStep = "LogStep"; 35 | public const string LogWebhook = "TableLogWebhook"; 36 | public const string LogMicroflowHttpData = "BlobLogRequest"; 37 | public const string LogOrchestration = "LogOrchestration"; 38 | public const string HttpCallOrchestrator = "HttpCallOrchestrator"; 39 | public const string WebhookOrchestrator = "WebhookOrchestrator"; 40 | public const string MicroflowStart = "MicroflowStart"; 41 | public const string MicroflowStartOrchestration = "MicroflowStartOrchestration"; 42 | public const string StepFlowControl = "StepFlowControl"; 43 | public const string RunFromSteps = "RunFromSteps"; 44 | public const string RunFromStepsOrchestrator = "RunFromStepsOrchestrator"; 45 | } 46 | 47 | public static class MicroflowStates 48 | { 49 | public const int Ready = 0; 50 | public const int Paused = 1; 51 | public const int Stopped = 2; 52 | } 53 | 54 | public static class MicroflowEntities 55 | { 56 | public const string StepCount = "StepCount"; 57 | public const string CanExecuteNowCount = "CanExecuteNowCount"; 58 | public const string StepFlowState= "StepFlowState"; 59 | } 60 | 61 | public static class MicroflowEntityKeys 62 | { 63 | public const string Add = "add"; 64 | public const string Subtract = "subtract"; 65 | public const string Read = "get"; 66 | public const string Delete = "delete"; 67 | public const string Set = "set"; 68 | } 69 | 70 | public static class MicroflowStateKeys 71 | { 72 | public const string WorkflowState = "WorkflowState"; 73 | public const string GlobalState = "GlobalState"; 74 | } 75 | 76 | public static class MicroflowControlKeys 77 | { 78 | public const string Ready = "ready"; 79 | public const string Pause = "pause"; 80 | public const string Stop = "stop"; 81 | public const string Read = "get"; 82 | } 83 | 84 | public static readonly char[] Splitter = { ',', ';' }; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /MicroflowModels/Helpers/TableHelper.cs: -------------------------------------------------------------------------------- 1 | using Azure.Data.Tables; 2 | using System; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | namespace MicroflowModels.Helpers 8 | { 9 | public static class TableHelper 10 | { 11 | #region Formatting 12 | 13 | public static string GetTableLogRowKeyDescendingByDate(DateTime date, string postfix) 14 | { 15 | return $"{String.Format("{0:D19}", DateTime.MaxValue.Ticks - date.Ticks)}{postfix}"; 16 | } 17 | 18 | public static string GetTableRowKeyDescendingByDate() 19 | { 20 | return $"{String.Format("{0:D19}", DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks)}{Guid.NewGuid()}"; 21 | } 22 | 23 | #endregion 24 | 25 | public static async Task UpsertStep(this HttpCallWithRetries step) 26 | { 27 | TableClient tableClient = GetStepsTable(); 28 | 29 | await tableClient.UpsertEntityAsync(step); 30 | } 31 | 32 | public static async Task LogError(this LogErrorEntity logEntity) 33 | { 34 | TableClient tableClient = GetErrorsTable(); 35 | 36 | await tableClient.UpsertEntityAsync(logEntity); 37 | } 38 | 39 | public static async Task LogError(string workflowName, string globalKey, string runId, Exception e) 40 | { 41 | await new LogErrorEntity(workflowName, -999, e.Message, globalKey, runId).LogError(); 42 | 43 | HttpResponseMessage resp = new(HttpStatusCode.InternalServerError) 44 | { 45 | Content = new StringContent(e.Message) 46 | }; 47 | 48 | return resp; 49 | } 50 | 51 | public static async Task GetStep(this MicroflowRun workflowRun) 52 | { 53 | TableClient tableClient = GetStepsTable(); 54 | 55 | return await tableClient.GetEntityAsync(workflowRun.WorkflowName, workflowRun.RunObject.StepNumber); 56 | } 57 | 58 | public static async Task GetWebhook(string webhookId) 59 | { 60 | TableClient tableClient = GetWebhooksTable(); 61 | 62 | return await tableClient.GetEntityAsync(webhookId, "0", new string[] { "WebhookSubStepsMapping" }); 63 | } 64 | 65 | public static TableClient GetErrorsTable() 66 | { 67 | TableServiceClient tableClient = GetTableClient(); 68 | 69 | return tableClient.GetTableClient($"MicroflowLogErrors"); 70 | } 71 | 72 | public static TableClient GetStepsTable() 73 | { 74 | TableServiceClient tableClient = GetTableClient(); 75 | 76 | return tableClient.GetTableClient($"MicroflowStepConfigs"); 77 | } 78 | 79 | public static TableServiceClient GetTableClient() 80 | { 81 | return new TableServiceClient(Environment.GetEnvironmentVariable("MicroflowStorage")); 82 | } 83 | 84 | public static TableClient GetLogWebhookTable() 85 | { 86 | TableServiceClient tableClient = GetTableClient(); 87 | 88 | return tableClient.GetTableClient($"MicroflowLogWebhooks"); 89 | } 90 | 91 | public static TableClient GetWebhooksTable() 92 | { 93 | TableServiceClient tableClient = GetTableClient(); 94 | 95 | return tableClient.GetTableClient($"MicroflowWebhookConfigs"); 96 | } 97 | 98 | public static TableClient GetLogOrchestrationTable() 99 | { 100 | TableServiceClient tableClient = GetTableClient(); 101 | 102 | return tableClient.GetTableClient($"MicroflowLogOrchestrations"); 103 | } 104 | 105 | public static TableClient GetLogStepsTable() 106 | { 107 | TableServiceClient tableClient = GetTableClient(); 108 | 109 | return tableClient.GetTableClient($"MicroflowLogSteps"); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /MicroflowModels/Interfaces/IHttpCall.cs: -------------------------------------------------------------------------------- 1 | namespace MicroflowModels 2 | { 3 | public interface IHttpCall : IStepEntity 4 | { 5 | int WebhookTimeoutSeconds { get; set; } 6 | bool AsynchronousPollingEnabled { get; set; } 7 | public string WebhookId { get; set; } 8 | public bool EnableWebhook { get; set; } 9 | string CalloutUrl { get; set; } 10 | string GlobalKey { get; set; } 11 | bool IsHttpGet { get; set; } 12 | string MainOrchestrationId { get; set; } 13 | string RunId { get; set; } 14 | string StepId { get; set; } 15 | bool StopOnCalloutFailure { get; set; } 16 | string SubStepsToRunForCalloutFailure { get; set; } 17 | bool StopOnWebhookTimeout { get; set; } 18 | string ScaleGroupId { get; set; } 19 | string SubStepsToRunForWebhookTimeout { get; set; } 20 | bool ForwardResponseData { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /MicroflowModels/Interfaces/IHttpCallWithRetries.cs: -------------------------------------------------------------------------------- 1 | namespace MicroflowModels 2 | { 3 | public interface IHttpCallWithRetries : IHttpCall 4 | { 5 | double RetryBackoffCoefficient { get; set; } 6 | int RetryDelaySeconds { get; set; } 7 | int RetryMaxDelaySeconds { get; set; } 8 | int RetryMaxRetries { get; set; } 9 | int RetryTimeoutSeconds { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /MicroflowModels/Interfaces/IMicroflowRun.cs: -------------------------------------------------------------------------------- 1 | namespace MicroflowModels 2 | { 3 | public interface IMicroflowRun 4 | { 5 | int CurrentLoop { get; set; } 6 | int Loop { get; set; } 7 | string OrchestratorInstanceId { get; set; } 8 | int PausedStepId { get; set; } 9 | string WorkflowName { get; set; } 10 | RunObject RunObject { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /MicroflowModels/Interfaces/IStepEntity.cs: -------------------------------------------------------------------------------- 1 | using Azure.Data.Tables; 2 | using System.Collections.Generic; 3 | 4 | namespace MicroflowModels 5 | { 6 | public interface IStepEntity : ITableEntity 7 | { 8 | Dictionary MergeFields { get; set; } 9 | string SubSteps { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /MicroflowModels/MicroflowModels.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | DEBUG;RELEASE;DEBUG_NO_SCALEGROUPS;RELEASE_NO_SCALEGROUPS 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /MicroflowModels/Models/MicroflowRun.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MicroflowModels 4 | { 5 | /// 6 | /// This is the minimalist workflow object that is passed during execution 7 | /// 8 | public class MicroflowRun : IMicroflowRun 9 | { 10 | public string WorkflowName { get; set; } 11 | public RunObject RunObject { get; set; } 12 | public int PausedStepId { get; set; } 13 | public int Loop { get; set; } = 1; 14 | public int CurrentLoop { get; set; } = 1; 15 | public string OrchestratorInstanceId { get; set; } 16 | } 17 | 18 | /// 19 | /// This is the minimalist run object that is passed during execution inside the workflow run object 20 | /// 21 | public class RunObject 22 | { 23 | public string RunId { get; set; } 24 | public string StepNumber { get; set; } 25 | public string GlobalKey { get; set; } 26 | public MicroflowHttpResponse MicroflowStepResponseData { get; set; } 27 | } 28 | 29 | /// 30 | /// Used to hold Microflow specific http status code results 31 | /// 32 | public class MicroflowHttpResponseBase 33 | { 34 | public bool Success { get; set; } 35 | public int HttpResponseStatusCode { get; set; } 36 | public string Content { get; set; } 37 | } 38 | 39 | public class MicroflowHttpResponse : MicroflowHttpResponseBase//, IMicroflowHttpResponse 40 | { 41 | public List SubStepsToRun { get; set; } 42 | public string Action { get; set; } 43 | public CalloutOrWebhook CalloutOrWebhook { get;set;} 44 | } 45 | //public interface IMicroflowHttpResponse 46 | //{ 47 | // int HttpResponseStatusCode { get; set; } 48 | // string Content { get; set; } 49 | // bool Success { get; set; } 50 | // public List SubStepsToRun { get; set; } 51 | //} 52 | } 53 | -------------------------------------------------------------------------------- /MicroflowModels/Models/Models.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.Serialization; 4 | 5 | namespace MicroflowModels 6 | { 7 | #region POCOs 8 | 9 | public enum CalloutOrWebhook 10 | { 11 | Callout, 12 | Webhook 13 | } 14 | 15 | public class ScaleGroupState 16 | { 17 | public int ScaleGroupMaxConcurrentInstanceCount { get; set; } 18 | public int PollingIntervalSeconds { get; set; } 19 | public int PollingIntervalMaxSeconds { get; set; } 20 | public int PollingMaxHours { get; set; } 21 | } 22 | 23 | public class SubStepsMappingForActions 24 | { 25 | public string WebhookAction { get; set; } 26 | public List SubStepsToRunForAction { get; set; } = new(); 27 | } 28 | 29 | public class MicroflowPostData 30 | { 31 | public string WorkflowName { get; set; } 32 | public string MainOrchestrationId { get; set; } 33 | public string SubOrchestrationId { get; set; } 34 | public string Webhook { get; set; } 35 | public string RunId { get; set; } 36 | public int StepNumber { get; set; } 37 | public string StepId { get; set; } 38 | public string GlobalKey { get; set; } 39 | public string PostData { get; set; } 40 | } 41 | 42 | public class MicroflowBase 43 | { 44 | public string WorkflowName { get; set; } 45 | } 46 | 47 | public class Microflow : MicroflowBase 48 | { 49 | public List Steps { get; set; } 50 | 51 | [DataMember(Name = "MergeFields", EmitDefaultValue = false)] 52 | public Dictionary MergeFields { get; set; } = new Dictionary(); 53 | 54 | public RetryOptions DefaultRetryOptions { get; set; } 55 | 56 | public string WorkflowVersion { get;set; } 57 | } 58 | 59 | public class Step 60 | { 61 | public Step() { } 62 | 63 | public Step(int stepNumber, string calloutUrl, string stepId = null) 64 | { 65 | if (stepId == null) 66 | { 67 | StepId = $"{stepNumber}_{Guid.NewGuid()}"; 68 | } 69 | else 70 | { 71 | StepId = stepId; 72 | } 73 | CalloutUrl = calloutUrl; 74 | StepNumber = stepNumber; 75 | } 76 | 77 | public Step(string stepId, List subSteps) 78 | { 79 | StepId = stepId; 80 | SubSteps = subSteps; 81 | } 82 | 83 | public int StepNumber { get; set; } 84 | public string StepId { get; set; } 85 | 86 | [DataMember(Name = "SubSteps", EmitDefaultValue = false)] 87 | public List SubSteps { get; set; } = new List(); 88 | 89 | [DataMember(Name = "WaitForAllParents", EmitDefaultValue = false)] 90 | public bool WaitForAllParents { get; set; } = true; 91 | 92 | public string CalloutUrl { get; set; } 93 | public int CalloutTimeoutSeconds { get; set; } = 1000; 94 | public bool StopOnCalloutFailure { get; set; } 95 | public List SubStepsToRunForCalloutFailure { get; set; } 96 | public bool IsHttpGet { get; set; } 97 | public bool EnableWebhook { get; set; } 98 | public string WebhookId { get; set; } 99 | public bool StopOnWebhookTimeout { get; set; } = true; 100 | public List SubStepsToRunForWebhookTimeout { get; set; } 101 | public int WebhookTimeoutSeconds { get; set; } = 1000; 102 | 103 | [DataMember(Name = "WebhookSubStepsMapping", EmitDefaultValue = false)] 104 | public List WebhookSubStepsMapping { get; set; } 105 | 106 | public string ScaleGroupId { get; set; } 107 | public bool AsynchronousPollingEnabled { get; set; } = true; 108 | public bool ForwardResponseData { get; set; } 109 | 110 | [DataMember(Name = "RetryOptions", EmitDefaultValue = false)] 111 | public RetryOptions RetryOptions { get; set; } 112 | } 113 | 114 | public class RetryOptions 115 | { 116 | public int DelaySeconds { get; set; } = 5; 117 | public int MaxDelaySeconds { get; set; } = 120; 118 | public int MaxRetries { get; set; } = 15; 119 | public double BackoffCoefficient { get; set; } = 5; 120 | public int TimeOutSeconds { get; set; } = 300; 121 | } 122 | 123 | #endregion 124 | } 125 | -------------------------------------------------------------------------------- /MicroflowModels/TableModels/TableModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Azure; 4 | using Azure.Data.Tables; 5 | using MicroflowModels.Helpers; 6 | 7 | namespace MicroflowModels 8 | { 9 | #region TableEntity 10 | 11 | public class Webhook : ITableEntity 12 | { 13 | public Webhook() { } 14 | 15 | public Webhook(string webhookId, string webhookSubStepsMapping) 16 | { 17 | PartitionKey = webhookId; 18 | RowKey = "0"; 19 | WebhookSubStepsMapping = webhookSubStepsMapping; 20 | } 21 | 22 | public string WebhookSubStepsMapping { get; set; } 23 | public string PartitionKey { get; set; } 24 | public string RowKey { get; set; } 25 | public DateTimeOffset? Timestamp { get; set; } 26 | public ETag ETag { get; set; } 27 | } 28 | 29 | //public class WebhookSubStepsMappingEntity : ITableEntity 30 | //{ 31 | // public string WebhookSubStepsMapping { get; set; } 32 | 33 | // public string PartitionKey { get; set; } 34 | // public string RowKey { get; set; } 35 | // public DateTimeOffset? Timestamp { get; set; } 36 | // public ETag ETag { get; set; } 37 | //} 38 | 39 | /// 40 | /// Used for step level logging 41 | /// 42 | public class LogErrorEntity : ITableEntity 43 | { 44 | public LogErrorEntity() { } 45 | 46 | public LogErrorEntity(string workflowName, int stepNumber, string message, string globalKey, string runId = null) 47 | { 48 | PartitionKey = workflowName + "__" + runId; 49 | RowKey = TableHelper.GetTableRowKeyDescendingByDate(); 50 | StepNumber = stepNumber; 51 | Message = message; 52 | Date = DateTime.UtcNow; 53 | GlobalKey = globalKey; 54 | } 55 | 56 | public string GlobalKey { get; set; } 57 | public int StepNumber { get; set; } 58 | public DateTime Date { get; set; } 59 | public string Message { get; set; } 60 | public string PartitionKey { get; set; } 61 | public string RowKey { get; set; } 62 | public DateTimeOffset? Timestamp { get; set; } 63 | public ETag ETag { get; set; } 64 | } 65 | 66 | /// 67 | /// Base class for http calls 68 | /// 69 | public class StepEntity : ITableEntity, IStepEntity 70 | { 71 | public StepEntity() { } 72 | 73 | public string SubSteps { get; set; } 74 | public Dictionary MergeFields { get; set; } 75 | public string PartitionKey { get; set; } 76 | public string RowKey { get; set; } 77 | public DateTimeOffset? Timestamp { get; set; } 78 | public ETag ETag { get; set; } 79 | } 80 | 81 | /// 82 | /// Basic http call with no retries 83 | /// 84 | public class HttpCall : StepEntity, IHttpCall 85 | { 86 | public HttpCall() { } 87 | 88 | public HttpCall(string workflow, string stepNumber, string stepId, string subSteps) 89 | { 90 | PartitionKey = workflow; 91 | RowKey = stepNumber; 92 | SubSteps = subSteps; 93 | StepId = stepId; 94 | } 95 | public bool AsynchronousPollingEnabled { get; set; } 96 | public string CalloutUrl { get; set; } 97 | public string WebhookId { get; set; } 98 | public bool EnableWebhook { get; set; } 99 | public int WebhookTimeoutSeconds { get; set; } = 1000; 100 | public bool StopOnWebhookTimeout { get; set; } 101 | public int CalloutTimeoutSeconds { get; set; } 102 | public bool IsHttpGet { get; set; } 103 | public string StepId { get; set; } 104 | public string ScaleGroupId { get; set; } 105 | public bool ForwardResponseData { get; set; } 106 | public string SubStepsToRunForWebhookTimeout { get; set; } 107 | public bool StopOnCalloutFailure { get; set; } 108 | public string SubStepsToRunForCalloutFailure { get; set; } 109 | 110 | //[IgnoreDataMember] 111 | public string GlobalKey { get; set; } 112 | 113 | //[IgnoreDataMember] 114 | public string RunId { get; set; } 115 | 116 | //[IgnoreDataMember] 117 | public string MainOrchestrationId { get; set; } 118 | } 119 | 120 | /// 121 | /// Http call with retries 122 | /// 123 | public class HttpCallWithRetries : HttpCall, IHttpCallWithRetries 124 | { 125 | public HttpCallWithRetries() { } 126 | 127 | public HttpCallWithRetries(string workflow, string stepNumber, string stepId, string subSteps) 128 | { 129 | PartitionKey = workflow; 130 | RowKey = stepNumber; 131 | SubSteps = subSteps; 132 | StepId = stepId; 133 | } 134 | 135 | // retry options 136 | public int RetryDelaySeconds { get; set; } 137 | public int RetryMaxDelaySeconds { get; set; } 138 | public int RetryMaxRetries { get; set; } 139 | public double RetryBackoffCoefficient { get; set; } 140 | public int RetryTimeoutSeconds { get; set; } 141 | } 142 | 143 | /// 144 | /// This is used to check if all parents have completed 145 | /// 146 | public class ParentCountCompletedEntity : ITableEntity 147 | { 148 | public ParentCountCompletedEntity() { } 149 | 150 | public ParentCountCompletedEntity(string runId, string stepId) 151 | { 152 | PartitionKey = runId; 153 | RowKey = stepId; 154 | } 155 | 156 | public int ParentCountCompleted { get; set; } 157 | public string PartitionKey { get; set; } 158 | public string RowKey { get; set; } 159 | public DateTimeOffset? Timestamp { get; set; } 160 | public ETag ETag { get; set; } 161 | } 162 | 163 | #endregion 164 | } -------------------------------------------------------------------------------- /MicroflowSDK/CompilerDirectiveMaker.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace MicroflowSDK 5 | { 6 | public class CompilerDirectiveMaker 7 | { 8 | public static string MakeCompilerDirectiveString(string largestCombo) 9 | { 10 | string[] arr = largestCombo.Split('_'); 11 | 12 | List combinations = CreateCombinations(0, "", arr); 13 | List list = new List(); 14 | 15 | 16 | for (int i1 = 1; i1 < arr.Length; i1++) 17 | { 18 | string i = arr[i1]; 19 | 20 | for (int yy = 1; yy < combinations.Count; yy++) 21 | { 22 | string j = combinations[yy]; 23 | 24 | if (!j.StartsWith(i)) 25 | { 26 | int idx = j.IndexOf(i); 27 | if (idx > -1) 28 | { 29 | combinations[yy] = j.Insert(idx, "_"); 30 | } 31 | } 32 | } 33 | } 34 | 35 | for (int i1 = 0; i1 < combinations.Count; i1++) 36 | { 37 | list.Add("RELEASE_NO_" + combinations[i1]); 38 | list.Add("DEBUG_NO_" + combinations[i1]); 39 | } 40 | 41 | list.Add("DEBUG"); 42 | list.Add("RELEASE"); 43 | 44 | //var tuple = GetCSharpDirectiveForOption() 45 | list.Sort(); 46 | 47 | return $"{string.Join(";", list)}"; 48 | } 49 | 50 | private static List CreateCombinations(int startIndex, string pair, string[] initialArray) 51 | { 52 | List combinations = new List(); 53 | for (int i = startIndex; i < initialArray.Length; i++) 54 | { 55 | string value = $"{pair}{initialArray[i]}"; 56 | combinations.Add(value); 57 | combinations.AddRange(CreateCombinations(i + 1, value, initialArray)); 58 | } 59 | 60 | return combinations; 61 | } 62 | 63 | public static string GetCompilerDirectiveForOptionToExclude(bool IsDebug, string key, string config) 64 | { 65 | List li = config.Split(';').ToList(); 66 | 67 | string res = $"#if DEBUG || RELEASE || !{string.Join(" && !", li.FindAll(r => r.Contains(key)))}"; 68 | 69 | return res; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MicroflowSDK/HttpBlobDataManager.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs; 2 | using Azure.Storage.Blobs.Models; 3 | using System.Threading.Tasks; 4 | 5 | namespace MicroflowSDK 6 | { 7 | public static class HttpBlobDataManager 8 | { 9 | public static async Task GetHttpBlob(bool isRequest, string workflowName, int stepNumber, string runId, string subinstanceId) 10 | { 11 | string prefix = isRequest ? "request-" : "response-"; 12 | 13 | BlobContainerClient blobContainerClient = new("UseDevelopmentStorage=true", "microflow-httpdata"); 14 | 15 | var blobClient = blobContainerClient.GetBlobClient($"{prefix}{workflowName}@{stepNumber}@{runId}@{subinstanceId}"); 16 | 17 | BlobDownloadResult downloadResult = await blobClient.DownloadContentAsync(); 18 | 19 | return downloadResult.Content.ToString(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MicroflowSDK/MicroflowSDK.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /MicroflowSDK/ScaleGroupsManager.cs: -------------------------------------------------------------------------------- 1 | using MicroflowModels; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Net.Http.Json; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace MicroflowSDK 9 | { 10 | public static class ScaleGroupsManager 11 | { 12 | public static async Task SetMaxInstanceCountForScaleGroup(string scaleGroupId, ScaleGroupState scaleGroupState, string baseUrl, HttpClient httpClient, int waitForResultSeconds = 30) 13 | { 14 | 15 | return await httpClient.PostAsJsonAsync($"{baseUrl}/ScaleGroup/{scaleGroupId}/{waitForResultSeconds}", scaleGroupState, new JsonSerializerOptions(JsonSerializerDefaults.General)); 16 | } 17 | 18 | public static async Task> GetScaleGroupsWithMaxInstanceCounts(string scaleGroupId, string baseUrl, HttpClient httpClient) 19 | { 20 | Dictionary li = null; 21 | 22 | if (!string.IsNullOrWhiteSpace(scaleGroupId)) 23 | { 24 | string t = await httpClient.GetStringAsync($"{baseUrl}/ScaleGroup/{scaleGroupId}"); 25 | li = JsonSerializer.Deserialize>(t); 26 | } 27 | else 28 | { 29 | string t = await httpClient.GetStringAsync($"{baseUrl}/ScaleGroup"); 30 | li = JsonSerializer.Deserialize>(t); 31 | } 32 | 33 | return li; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MicroflowSDK/StepsManager.cs: -------------------------------------------------------------------------------- 1 | using MicroflowModels; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace MicroflowSDK 6 | { 7 | public static class StepsManager 8 | { 9 | public static void AddParentSteps(this Step step, params Step[] parents) 10 | { 11 | foreach (Step parentStep in parents) 12 | { 13 | parentStep.SubSteps.Add(step.StepNumber); 14 | } 15 | } 16 | 17 | public static void AddSubSteps(this Step step, params Step[] subSteps) 18 | { 19 | foreach (Step subStep in subSteps) 20 | { 21 | step.SubSteps.Add(subStep.StepNumber); 22 | } 23 | } 24 | 25 | public static void AddSubStepRange(this Step step, List steps, int fromId, int toId) 26 | { 27 | List li = steps.FindAll(s => s.StepNumber >= fromId && s.StepNumber <= toId); 28 | step.SubSteps.AddRange(from s in li select s.StepNumber); 29 | } 30 | 31 | public static void SetRetryForSteps(params Step[] steps) 32 | { 33 | foreach (Step step in steps) 34 | { 35 | step.RetryOptions = new RetryOptions(); 36 | } 37 | } 38 | 39 | public static void SetRetryForSteps(int delaySeconds, int maxDelaySeconds, int maxRetries, int timeOutSeconds, int backoffCoefficient, params Step[] steps) 40 | { 41 | RetryOptions retryOptions = new RetryOptions() 42 | { 43 | DelaySeconds = delaySeconds, 44 | MaxDelaySeconds = maxDelaySeconds, 45 | MaxRetries = maxRetries, 46 | TimeOutSeconds = timeOutSeconds, 47 | BackoffCoefficient = backoffCoefficient 48 | }; 49 | 50 | foreach(Step step in steps) 51 | { 52 | step.RetryOptions = retryOptions; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MicroflowSDK/WebhooksManager.cs: -------------------------------------------------------------------------------- 1 | namespace MicroflowSDK 2 | { 3 | public static class WebhooksManager 4 | { 5 | public static void GetWebhooks(string workflowName, int stepNumber, string runId) 6 | { 7 | 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /MicroflowSDK/WorkflowManager.cs: -------------------------------------------------------------------------------- 1 | using MicroflowModels; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Net.Http.Json; 6 | using System.Text.Json; 7 | using System.Threading.Tasks; 8 | 9 | namespace MicroflowSDK 10 | { 11 | public class PassThroughParams 12 | { 13 | public bool WorkflowName { get; set; } = true; 14 | public bool MainOrchestrationId { get; set; } = true; 15 | public bool SubOrchestrationId { get; set; } = true; 16 | public bool WebhookId { get; set; } = true; 17 | public bool RunId { get; set; } = true; 18 | public bool StepNumber { get; set; } = true; 19 | public bool GlobalKey { get; set; } = true; 20 | public bool StepId { get; set; } = true; 21 | } 22 | 23 | public static class WorkflowManager 24 | { 25 | public static HttpClient HttpClient = new(); 26 | 27 | public static Step Step(this Microflow microFlow, int stepNumber) => microFlow.Steps.First(s => s.StepNumber == stepNumber); 28 | 29 | public static Step StepNumber(this List steps, int stepNumber) => steps.First(s => s.StepNumber == stepNumber); 30 | 31 | public static List CreateSteps(int count, int fromId, string defaultCalloutURI = "") 32 | { 33 | List stepsList = new(); 34 | 35 | for (; fromId <= count; fromId++) 36 | { 37 | Step step = new(fromId, defaultCalloutURI, "myStep " + fromId); 38 | stepsList.Add(step); 39 | } 40 | 41 | return stepsList; 42 | } 43 | 44 | public static async Task UpsertWorkFlow(Microflow workflow, string baseUrl) 45 | { 46 | // Upsert 47 | HttpResponseMessage result = await HttpClient.PostAsJsonAsync(baseUrl + "/UpsertWorkflow/", workflow, new JsonSerializerOptions(JsonSerializerDefaults.General)); 48 | 49 | if (result.StatusCode == System.Net.HttpStatusCode.OK) 50 | { 51 | return true; 52 | } 53 | 54 | return false; 55 | } 56 | 57 | public static async Task QuickInsertAndStartWorkFlow(Microflow workflow, string baseUrl) 58 | { 59 | // Upsert and start 60 | return await HttpClient.PostAsJsonAsync(baseUrl + $"/QuickInsertAndStartWorkflow/{workflow.WorkflowName}@{workflow.WorkflowVersion}", workflow, new JsonSerializerOptions(JsonSerializerDefaults.General)); 61 | } 62 | 63 | public static async Task WaitForWorkflowCompleted(HttpResponseMessage resp) 64 | { 65 | OrchResult? res = JsonSerializer.Deserialize(await resp.Content.ReadAsStringAsync()); 66 | 67 | string res2result = ""; 68 | 69 | while (!res2result.Contains("\"runtimeStatus\":\"Completed\"")) 70 | { 71 | await Task.Delay(2000); 72 | 73 | HttpResponseMessage res2 = await HttpClient.GetAsync(res.statusQueryGetUri); 74 | res2result = await res2.Content.ReadAsStringAsync(); 75 | 76 | if (res2result.Contains("\"runtimeStatus\":\"Completed\"")) 77 | break; 78 | } 79 | 80 | return res.id; 81 | } 82 | } 83 | public class OrchResult 84 | { 85 | public string id { get; set; } 86 | public string purgeHistoryDeleteUri { get; set; } 87 | public string sendEventPostUri { get; set; } 88 | public string statusQueryGetUri { get; set; } 89 | public string terminatePostUri { get; set; } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /MicroflowShared/TableReferences.cs: -------------------------------------------------------------------------------- 1 | namespace MicroflowShared 2 | { 3 | public static class TableReferences 4 | { 5 | #region Get table references 6 | 7 | 8 | #endregion 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /MicroflowTest/LogReader.cs: -------------------------------------------------------------------------------- 1 | using Azure.Data.Tables; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microflow.MicroflowTableModels; 5 | 6 | namespace MicroflowTest 7 | { 8 | internal class LogReader 9 | { 10 | public static async Task> GetOrchLog(string workflowName) 11 | { 12 | List li = new(); 13 | TableClient tableClient = GetLogOrchestrationTable(); 14 | 15 | Azure.AsyncPageable logTask = tableClient.QueryAsync(filter: $"PartitionKey eq '{workflowName}'"); 16 | 17 | await foreach(LogOrchestrationEntity log in logTask) 18 | { 19 | li.Add(log); 20 | } 21 | 22 | return li; 23 | } 24 | 25 | public static async Task> GetStepsLog(string workflowName, string instanceId) 26 | { 27 | List li = new(); 28 | TableClient tableClient = GetStepsLogTable(); 29 | 30 | Azure.AsyncPageable logTask = tableClient.QueryAsync(filter: $"PartitionKey eq '{workflowName}__{instanceId}'"); 31 | 32 | await foreach (LogStepEntity log in logTask) 33 | { 34 | li.Add(log); 35 | } 36 | 37 | return li; 38 | } 39 | 40 | public static TableClient GetErrorsTable() 41 | { 42 | TableServiceClient tableClient = GetTableClient(); 43 | 44 | return tableClient.GetTableClient($"MicroflowLogErrors"); 45 | } 46 | 47 | public static TableClient GetStepsLogTable() 48 | { 49 | TableServiceClient tableClient = GetTableClient(); 50 | 51 | return tableClient.GetTableClient($"MicroflowLogSteps"); 52 | } 53 | 54 | public static TableClient GetStepsTable() 55 | { 56 | TableServiceClient tableClient = GetTableClient(); 57 | 58 | return tableClient.GetTableClient($"MicroflowStepConfigs"); 59 | } 60 | 61 | public static TableClient GetLogOrchestrationTable() 62 | { 63 | TableServiceClient tableClient = GetTableClient(); 64 | 65 | return tableClient.GetTableClient($"MicroflowLogOrchestrations"); 66 | } 67 | 68 | public static TableServiceClient GetTableClient() 69 | { 70 | return new TableServiceClient("UseDevelopmentStorage=true"); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /MicroflowTest/MicroflowTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ..\MicroflowFunctionApp\bin\Debug\net6.0\MicroflowApp.dll 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /MicroflowTest/MicroflowTest.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32901.215 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroflowTest", "MicroflowTest.csproj", "{F56DD770-E3B5-4846-9301-46748A9EAC43}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroflowSDK", "..\MicroflowSDK\MicroflowSDK.csproj", "{D52A21A8-E941-4627-A14D-C6E722C7ABD3}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroflowModels", "..\MicroflowModels\MicroflowModels.csproj", "{9C1D7EC0-68A8-48C8-9F68-A96707D93E06}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | DEBUG_NO_SCALEGROUPS|Any CPU = DEBUG_NO_SCALEGROUPS|Any CPU 15 | Debug|Any CPU = Debug|Any CPU 16 | RELEASE_NO_SCALEGROUPS|Any CPU = RELEASE_NO_SCALEGROUPS|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {F56DD770-E3B5-4846-9301-46748A9EAC43}.DEBUG_NO_SCALEGROUPS|Any CPU.ActiveCfg = Debug|Any CPU 21 | {F56DD770-E3B5-4846-9301-46748A9EAC43}.DEBUG_NO_SCALEGROUPS|Any CPU.Build.0 = Debug|Any CPU 22 | {F56DD770-E3B5-4846-9301-46748A9EAC43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {F56DD770-E3B5-4846-9301-46748A9EAC43}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {F56DD770-E3B5-4846-9301-46748A9EAC43}.RELEASE_NO_SCALEGROUPS|Any CPU.ActiveCfg = Release|Any CPU 25 | {F56DD770-E3B5-4846-9301-46748A9EAC43}.RELEASE_NO_SCALEGROUPS|Any CPU.Build.0 = Release|Any CPU 26 | {F56DD770-E3B5-4846-9301-46748A9EAC43}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {F56DD770-E3B5-4846-9301-46748A9EAC43}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {D52A21A8-E941-4627-A14D-C6E722C7ABD3}.DEBUG_NO_SCALEGROUPS|Any CPU.ActiveCfg = Debug|Any CPU 29 | {D52A21A8-E941-4627-A14D-C6E722C7ABD3}.DEBUG_NO_SCALEGROUPS|Any CPU.Build.0 = Debug|Any CPU 30 | {D52A21A8-E941-4627-A14D-C6E722C7ABD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {D52A21A8-E941-4627-A14D-C6E722C7ABD3}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {D52A21A8-E941-4627-A14D-C6E722C7ABD3}.RELEASE_NO_SCALEGROUPS|Any CPU.ActiveCfg = Release|Any CPU 33 | {D52A21A8-E941-4627-A14D-C6E722C7ABD3}.RELEASE_NO_SCALEGROUPS|Any CPU.Build.0 = Release|Any CPU 34 | {D52A21A8-E941-4627-A14D-C6E722C7ABD3}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {D52A21A8-E941-4627-A14D-C6E722C7ABD3}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {9C1D7EC0-68A8-48C8-9F68-A96707D93E06}.DEBUG_NO_SCALEGROUPS|Any CPU.ActiveCfg = DEBUG_NO_SCALEGROUPS|Any CPU 37 | {9C1D7EC0-68A8-48C8-9F68-A96707D93E06}.DEBUG_NO_SCALEGROUPS|Any CPU.Build.0 = DEBUG_NO_SCALEGROUPS|Any CPU 38 | {9C1D7EC0-68A8-48C8-9F68-A96707D93E06}.Debug|Any CPU.ActiveCfg = DEBUG|Any CPU 39 | {9C1D7EC0-68A8-48C8-9F68-A96707D93E06}.Debug|Any CPU.Build.0 = DEBUG|Any CPU 40 | {9C1D7EC0-68A8-48C8-9F68-A96707D93E06}.RELEASE_NO_SCALEGROUPS|Any CPU.ActiveCfg = RELEASE_NO_SCALEGROUPS|Any CPU 41 | {9C1D7EC0-68A8-48C8-9F68-A96707D93E06}.RELEASE_NO_SCALEGROUPS|Any CPU.Build.0 = RELEASE_NO_SCALEGROUPS|Any CPU 42 | {9C1D7EC0-68A8-48C8-9F68-A96707D93E06}.Release|Any CPU.ActiveCfg = RELEASE|Any CPU 43 | {9C1D7EC0-68A8-48C8-9F68-A96707D93E06}.Release|Any CPU.Build.0 = RELEASE|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(ExtensibilityGlobals) = postSolution 49 | SolutionGuid = {11422D0E-2C78-4241-933B-841495B865D6} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /MicroflowTest/Test2_Retries.cs: -------------------------------------------------------------------------------- 1 | using MicroflowModels; 2 | using MicroflowSDK; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Threading.Tasks; 9 | 10 | namespace MicroflowTest 11 | { 12 | [TestClass] 13 | public class Test2_Retries 14 | { 15 | [TestMethod] 16 | public async Task Reties() 17 | { 18 | List workflow = TestWorkflowHelper.CreateTestWorkflow_SimpleSteps(); 19 | 20 | (MicroflowModels.Microflow workflow, string workflowName) microflow = TestWorkflowHelper.CreateMicroflow(workflow); 21 | 22 | int loop = 1; 23 | string globalKey = Guid.NewGuid().ToString(); 24 | 25 | microflow.workflow.Step(1).EnableWebhook = true; 26 | 27 | microflow.workflow.Step(1).WebhookId = "mywebhook123-" + Guid.NewGuid().ToString(); 28 | 29 | microflow.workflow.Step(1).StopOnWebhookTimeout = false; 30 | 31 | microflow.workflow.Step(1).WebhookTimeoutSeconds = 3; 32 | microflow.workflow.Step(1).RetryOptions = new RetryOptions() { BackoffCoefficient = 1, DelaySeconds = 1, MaxDelaySeconds = 1, MaxRetries = 2, TimeOutSeconds = 300 }; 33 | 34 | // Upsert 35 | bool successUpsert = await WorkflowManager.UpsertWorkFlow(microflow.workflow, TestWorkflowHelper.BaseUrl); 36 | 37 | Assert.IsTrue(successUpsert); 38 | 39 | // start the upserted Microflow 40 | HttpResponseMessage startResult = await TestWorkflowHelper.StartMicroflow(microflow, loop, globalKey); 41 | 42 | string instanceId = await WorkflowManager.WaitForWorkflowCompleted(startResult); 43 | 44 | List log = await LogReader.GetOrchLog(microflow.workflowName); 45 | 46 | Assert.IsTrue(log.FindIndex(i=>i.OrchestrationId.Equals(instanceId))>=0); 47 | 48 | List steps = await LogReader.GetStepsLog(microflow.workflowName, instanceId); 49 | 50 | List s = steps.OrderBy(e => e.EndDate).ToList(); 51 | 52 | Assert.IsTrue(s[0].StepNumber == 1); 53 | 54 | if(s[1].StepNumber==2) 55 | Assert.IsTrue(s[2].StepNumber==3); 56 | else 57 | { 58 | Assert.IsTrue(s[1].StepNumber == 3); 59 | Assert.IsTrue(s[2].StepNumber == 2); 60 | } 61 | 62 | Assert.IsTrue(s[3].StepNumber == 4); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /MicroflowTest/Test3_Webhooks.cs: -------------------------------------------------------------------------------- 1 | using MicroflowModels; 2 | using MicroflowSDK; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Threading.Tasks; 9 | 10 | namespace MicroflowTest 11 | { 12 | [TestClass] 13 | public class Test3_Webhooks 14 | { 15 | [TestMethod] 16 | public async Task BasicWebhook() 17 | { 18 | List workflow = TestWorkflowHelper.CreateTestWorkflow_SimpleSteps(); 19 | 20 | (MicroflowModels.Microflow workflow, string workflowName) microflow = TestWorkflowHelper.CreateMicroflow(workflow); 21 | 22 | int loop = 1; 23 | string globalKey = Guid.NewGuid().ToString(); 24 | 25 | microflow.workflow.Step(2).WebhookId = Guid.NewGuid().ToString(); 26 | microflow.workflow.Step(2).EnableWebhook = true; 27 | 28 | // Upsert 29 | bool successUpsert = await WorkflowManager.UpsertWorkFlow(microflow.workflow, TestWorkflowHelper.BaseUrl); 30 | 31 | Assert.IsTrue(successUpsert); 32 | 33 | // start the upserted Microflow 34 | HttpResponseMessage startResult = await TestWorkflowHelper.StartMicroflow(microflow, loop, globalKey); 35 | 36 | // Emulator will reply on the webhook 37 | if (!TestWorkflowHelper.UseEmulator) 38 | { 39 | while (true) 40 | { 41 | await Task.Delay(2000); 42 | 43 | //HttpResponseMessage webhookcall = await TestWorkflowHelper.HttpClient.GetAsync( 44 | // $"{TestWorkflowHelper.BaseUrl}/getwebhooks/{microflow.workflowName}/{microflow.workflow.Step(2).WebhookId}/{microflow.workflow.Step(2).StepNumber}"); 45 | HttpResponseMessage webhookcall = await TestWorkflowHelper.HttpClient.GetAsync($"{TestWorkflowHelper.BaseUrl}/webhooks/{microflow.workflow.Step(2).WebhookId}"); 46 | 47 | // if the callout sent out a webhookid externally, and the events is not created yet, then a 202 will always return 48 | if (webhookcall.StatusCode == System.Net.HttpStatusCode.Accepted || webhookcall.StatusCode == System.Net.HttpStatusCode.NotFound) 49 | { 50 | continue; 51 | } 52 | else if (webhookcall.StatusCode == System.Net.HttpStatusCode.OK) 53 | { 54 | break; 55 | } 56 | } 57 | } 58 | 59 | string instanceId = await WorkflowManager.WaitForWorkflowCompleted(startResult); 60 | 61 | List log = await LogReader.GetOrchLog(microflow.workflowName); 62 | 63 | Assert.IsTrue(log.FindIndex(i => i.OrchestrationId.Equals(instanceId)) >= 0); 64 | 65 | List steps = await LogReader.GetStepsLog(microflow.workflowName, instanceId); 66 | 67 | List s = steps.OrderBy(e => e.EndDate).ToList(); 68 | 69 | Assert.IsTrue(s[0].StepNumber == 1); 70 | 71 | if (s[1].StepNumber == 2) 72 | Assert.IsTrue(s[2].StepNumber == 3); 73 | else 74 | { 75 | Assert.IsTrue(s[1].StepNumber == 3); 76 | Assert.IsTrue(s[2].StepNumber == 2); 77 | } 78 | 79 | Assert.IsTrue(s[3].StepNumber == 4); 80 | 81 | Assert.IsTrue(s.Count == 4); 82 | } 83 | 84 | [TestMethod] 85 | public async Task WebhookWithCallbackActionAndSubStepsIfElse() 86 | { 87 | List workflow = TestWorkflowHelper.CreateTestWorkflow_SimpleSteps(); 88 | 89 | (MicroflowModels.Microflow workflow, string workflowName) microflow = TestWorkflowHelper.CreateMicroflow(workflow); 90 | 91 | // Emulator will reply on the webhook, set /approve 92 | if (TestWorkflowHelper.UseEmulator) 93 | { 94 | TestWorkflowHelper.SetEmulatorWebhookAction(microflow.workflow); 95 | } 96 | 97 | int loop = 1; 98 | string globalKey = Guid.NewGuid().ToString(); 99 | 100 | string webhookId = Guid.NewGuid().ToString();// "mywebhook"; 101 | 102 | microflow.workflow.Step(1).WebhookId = webhookId; 103 | microflow.workflow.Step(1).EnableWebhook = true; 104 | microflow.workflow.Step(1).WebhookSubStepsMapping = new(); 105 | microflow.workflow.Step(1).WebhookSubStepsMapping.Add(new() 106 | { 107 | WebhookAction = "decline", 108 | SubStepsToRunForAction = new List() { 2 } 109 | }); 110 | microflow.workflow.Step(1).WebhookSubStepsMapping.Add(new() 111 | { 112 | WebhookAction = "approve", 113 | SubStepsToRunForAction = new List() { 3 } 114 | }); 115 | microflow.workflow.Step(4).WaitForAllParents = false; 116 | 117 | // Upsert 118 | bool successUpsert = await WorkflowManager.UpsertWorkFlow(microflow.workflow, TestWorkflowHelper.BaseUrl); 119 | 120 | Assert.IsTrue(successUpsert); 121 | 122 | // start the upserted Microflow 123 | HttpResponseMessage startResult = await TestWorkflowHelper.StartMicroflow(microflow, loop, globalKey); 124 | 125 | // Emulator will reply on the webhook 126 | if (!TestWorkflowHelper.UseEmulator) 127 | { 128 | while (true) 129 | { 130 | await Task.Delay(2000); 131 | 132 | HttpResponseMessage webhookcall = await TestWorkflowHelper.HttpClient.GetAsync($"{TestWorkflowHelper.BaseUrl}/webhooks/{webhookId}/approve"); 133 | 134 | // if the callout sent out a webhookid externally, and the events is not created yet, then a 202 will always return 135 | if (webhookcall.StatusCode == System.Net.HttpStatusCode.Accepted || webhookcall.StatusCode == System.Net.HttpStatusCode.NotFound) 136 | { 137 | continue; 138 | } 139 | else if (webhookcall.StatusCode == System.Net.HttpStatusCode.OK) 140 | { 141 | break; 142 | } 143 | } 144 | } 145 | 146 | string instanceId = await WorkflowManager.WaitForWorkflowCompleted(startResult); 147 | 148 | List log = await LogReader.GetOrchLog(microflow.workflowName); 149 | 150 | Assert.IsTrue(log.FindIndex(i => i.OrchestrationId.Equals(instanceId)) >= 0); 151 | 152 | List steps = await LogReader.GetStepsLog(microflow.workflowName, instanceId); 153 | 154 | List s = steps.OrderBy(e => e.EndDate).ToList(); 155 | 156 | Assert.IsTrue(s[0].StepNumber == 1); 157 | Assert.IsTrue(s[1].StepNumber == 3); 158 | Assert.IsTrue(s[2].StepNumber == 4); 159 | 160 | Assert.IsTrue(s.Count == 3); 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /MicroflowTest/Test4_ScaleGroups.cs: -------------------------------------------------------------------------------- 1 | using Microflow.MicroflowTableModels; 2 | using MicroflowModels; 3 | using MicroflowSDK; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Globalization; 8 | using System.Linq; 9 | using System.Net.Http; 10 | using System.Threading.Tasks; 11 | 12 | namespace MicroflowTest 13 | { 14 | [TestClass] 15 | public class Test4_ScaleGroups 16 | { 17 | string scaleGroupId = "mytestgroup"; 18 | 19 | [TestMethod] 20 | public async Task MaxInstanceCountForScaleGroupTo10() 21 | { 22 | // create a workflow with parent step 1, two child steps 2 and 3, 2 with five children and 3 with five children, 23 | // and step 14 as the child of the ten children of 2 and 3 24 | List workflow = TestWorkflowHelper.CreateTestWorkflow_10StepsParallel(); 25 | 26 | // create Microflow with the created workflow 27 | (MicroflowModels.Microflow workflow, string workflowName) microflow = TestWorkflowHelper.CreateMicroflow(workflow); 28 | 29 | // set loop to 1, how many time the workflow will execute 30 | int loop = 1; 31 | 32 | // set the global key if needed 33 | // the global key is needed to group related workflows and control the group 34 | string globalKey = Guid.NewGuid().ToString(); 35 | 36 | // set the ten parallel steps 4 - 13 to the same scale group with the max instance count = 10, will overlap 37 | microflow.workflow.Steps.Where(s => s.StepNumber > 3 && s.StepNumber < 14).ToList().ForEach(x => x.ScaleGroupId = scaleGroupId); 38 | 39 | // upsert the workflow 40 | await UpsertWorkflow(microflow); 41 | 42 | // set the maximum limit of concurrent executions for the scale group id tp 10 43 | await TestWorkflowHelper.SetScaleGroupMax(10, scaleGroupId); 44 | 45 | // start the upserted Microflow 46 | HttpResponseMessage startResult = await TestWorkflowHelper.StartMicroflow(microflow, loop, globalKey); 47 | 48 | // wait for completion 49 | string instanceId = await WorkflowManager.WaitForWorkflowCompleted(startResult); 50 | 51 | // test that 52 | await TestNoScaleGroupWithMax10(microflow, instanceId); 53 | } 54 | 55 | [TestMethod] 56 | public async Task MaxInstanceCountForScaleGroupTo1() 57 | { 58 | // create a workflow with parent step 1, two child steps 2 and 3, 2 with five children and 3 with five children, 59 | // and step 14 as the child of the ten children of 2 and 3 60 | List workflow = TestWorkflowHelper.CreateTestWorkflow_10StepsParallel(); 61 | 62 | // create Microflow with the created workflow 63 | (MicroflowModels.Microflow workflow, string workflowName) microflow = TestWorkflowHelper.CreateMicroflow(workflow); 64 | 65 | // set loop to 1, how many time the workflow will execute 66 | int loop = 1; 67 | 68 | // set the global key if needed 69 | // the global key is needed to group related workflows and control the group 70 | string globalKey = Guid.NewGuid().ToString(); 71 | 72 | microflow.workflow.Steps.Where(s => s.StepNumber > 3 && s.StepNumber < 14).ToList().ForEach(x => x.ScaleGroupId = scaleGroupId); 73 | 74 | await UpsertWorkflow(microflow); 75 | 76 | // set the ten parallel steps 4 - 13 to the same scale group with the max instance count = 1, no overlapping 77 | await TestWorkflowHelper.SetScaleGroupMax(1, scaleGroupId); 78 | 79 | // start the upserted Microflow 80 | HttpResponseMessage startResult = await TestWorkflowHelper.StartMicroflow(microflow, loop, globalKey); 81 | 82 | string instanceId = await WorkflowManager.WaitForWorkflowCompleted(startResult); 83 | 84 | await TestNoScaleGroupWithMax1(microflow, instanceId); 85 | } 86 | 87 | private static async Task TestNoScaleGroupWithMax1((MicroflowModels.Microflow workflow, string workflowName) microflow, string instanceId) 88 | { 89 | //// CHECK RESULTS //// 90 | 91 | // get the orchestration log to check the results 92 | List log = await LogReader.GetOrchLog(microflow.workflowName); 93 | 94 | Assert.IsTrue(log.FindIndex(i => i.OrchestrationId.Equals(instanceId)) >= 0); 95 | 96 | // get the steps log to check the results 97 | List steps = await LogReader.GetStepsLog(microflow.workflowName, instanceId); 98 | 99 | List sortedSteps = TestBasicFlow(steps); 100 | 101 | IEnumerable parallelSteps = sortedSteps.Where(s => s.StepNumber > 3 && s.StepNumber < 14); 102 | int count = 1; 103 | 104 | foreach (LogStepEntity paraStep in parallelSteps) 105 | { 106 | // these are now not overlapping since the scalegroup max instance count is 1 107 | // count the other step`s with start dates before this step`s end date 108 | if (parallelSteps.Count(s => s.StartDate < paraStep.EndDate) != count) 109 | { 110 | Assert.Fail(); 111 | break; 112 | } 113 | 114 | // now 1 more other step has a start date before the next step`s end date 115 | count++; 116 | } 117 | } 118 | 119 | private static async Task TestNoScaleGroupWithMax10((MicroflowModels.Microflow workflow, string workflowName) microflow, string instanceId) 120 | { 121 | //// CHECK RESULTS //// 122 | 123 | // get the orchestration log to check the results 124 | List log = await LogReader.GetOrchLog(microflow.workflowName); 125 | 126 | Assert.IsTrue(log.FindIndex(i => i.OrchestrationId.Equals(instanceId)) >= 0); 127 | 128 | // get the steps log to check the results 129 | List steps = await LogReader.GetStepsLog(microflow.workflowName, instanceId); 130 | 131 | List sortedSteps = TestBasicFlow(steps); 132 | 133 | IEnumerable parallelSteps = sortedSteps.Where(s => s.StepNumber > 3 && s.StepNumber < 14); 134 | bool foundOverlap = false; 135 | 136 | foreach (LogStepEntity paraStep in parallelSteps) 137 | { 138 | if (parallelSteps.Count(s => s.StartDate < paraStep.EndDate) > 0) 139 | { 140 | foundOverlap = true; 141 | break; 142 | } 143 | } 144 | 145 | Assert.IsTrue(foundOverlap); 146 | } 147 | 148 | private static List TestBasicFlow(List steps) 149 | { 150 | List sortedSteps = steps.OrderBy(e => e.EndDate).ToList(); 151 | 152 | Assert.IsTrue(sortedSteps[0].StepNumber == 1); 153 | Assert.IsTrue(sortedSteps[1].StepNumber == 2 || sortedSteps[1].StepNumber == 3); 154 | Assert.IsTrue(sortedSteps.Count(n => n.StepNumber == 14) == 1); 155 | Assert.IsTrue(sortedSteps.Count == 14); 156 | Assert.IsTrue(sortedSteps[13].StepNumber == 14); 157 | 158 | return sortedSteps; 159 | } 160 | 161 | private static async Task UpsertWorkflow((MicroflowModels.Microflow workflow, string workflowName) microflow) 162 | { 163 | // upsert Microflow json 164 | //string json = JsonSerializer.Serialize(microflow.workflow); 165 | bool successUpsert = await WorkflowManager.UpsertWorkFlow(microflow.workflow, TestWorkflowHelper.BaseUrl); 166 | 167 | Assert.IsTrue(successUpsert); 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /MicroflowTest/Test5_WaitForAllParents.cs: -------------------------------------------------------------------------------- 1 | using MicroflowModels; 2 | using MicroflowSDK; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Threading.Tasks; 9 | 10 | namespace MicroflowTest 11 | { 12 | [TestClass] 13 | public class Test5_WaitForAllParents 14 | { 15 | [TestMethod] 16 | public async Task WaitForAllParents() 17 | { 18 | // create a workflow with parent step 1, two child steps 2 and 3, 2 with five children and 3 with five children, 19 | // and step 14 as the child of the ten children of 2 and 3 20 | List workflow = TestWorkflowHelper.CreateTestWorkflow_10StepsParallel(); 21 | 22 | (MicroflowModels.Microflow workflow, string workflowName) microflow = TestWorkflowHelper.CreateMicroflow(workflow); 23 | 24 | int loop = 1; 25 | string globalKey = Guid.NewGuid().ToString(); 26 | 27 | microflow.workflow.Step(14).WaitForAllParents = false; 28 | 29 | // Upsert 30 | bool successUpsert = await WorkflowManager.UpsertWorkFlow(microflow.workflow, TestWorkflowHelper.BaseUrl); 31 | 32 | Assert.IsTrue(successUpsert); 33 | 34 | // start the upserted Microflow 35 | HttpResponseMessage startResult = await TestWorkflowHelper.StartMicroflow(microflow, loop, globalKey); 36 | 37 | string instanceId = await WorkflowManager.WaitForWorkflowCompleted(startResult); 38 | 39 | List steps = await LogReader.GetStepsLog(microflow.workflowName, instanceId); 40 | 41 | List sorted = steps.OrderBy(e => e.EndDate).ToList(); 42 | 43 | List log = await LogReader.GetOrchLog(microflow.workflowName); 44 | 45 | Assert.IsTrue(log.FindIndex(i => i.OrchestrationId.Equals(instanceId)) >= 0); 46 | 47 | Assert.IsTrue(sorted[0].StepNumber == 1); 48 | 49 | Assert.IsTrue(sorted[1].StepNumber == 2 || sorted[1].StepNumber == 3); 50 | 51 | Assert.IsTrue(sorted.Count() == 23); 52 | 53 | // with WaitForAllParents = true, step 14 will execute once when all parents are completed 54 | // with WaitForAllParents = false, step 14 will execute each time a parent completes 55 | Assert.IsTrue(sorted.Count(n => n.StepNumber == 14) == 10); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /MicroflowTest/Test6_PassThroughParams.cs: -------------------------------------------------------------------------------- 1 | using MicroflowModels; 2 | using MicroflowSDK; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Threading.Tasks; 9 | 10 | namespace MicroflowTest 11 | { 12 | [TestClass] 13 | public class Test6_PassThroughParams 14 | { 15 | [TestMethod] 16 | public async Task HttpGet_PassAllParams() 17 | { 18 | // create a simple workflow with parent step 1, subling children step 2 and 3, and child of 2 and 3 step 4 19 | // siblings steps 2 and 3 runs in parallel 20 | List stepsList = TestWorkflowHelper.CreateTestWorkflow_SimpleSteps(); 21 | stepsList.RemoveRange(1, 3); 22 | stepsList[0].IsHttpGet = true; 23 | stepsList[0].SubSteps.Clear(); 24 | stepsList[0].WebhookId = "mywebhook"; 25 | //stepsList[3].WaitForAllParents = false; 26 | 27 | // create Microflow with the created workflow 28 | (MicroflowModels.Microflow workflow, string workflowName) microflow = TestWorkflowHelper.CreateMicroflow(stepsList, isHttpGet: true); 29 | 30 | // set loop to 1, how many time the workflow will execute 31 | int loop = 1; 32 | 33 | // set the global key if needed 34 | // the global key is needed to group related workflows and control the group 35 | string globalKey = Guid.NewGuid().ToString(); 36 | 37 | // upsert Microflow json 38 | //string json = JsonSerializer.Serialize(microflow.workflow); 39 | bool successUpsert = await WorkflowManager.UpsertWorkFlow(microflow.workflow, TestWorkflowHelper.BaseUrl); 40 | 41 | Assert.IsTrue(successUpsert); 42 | 43 | // start the upserted Microflow 44 | HttpResponseMessage startResult = await TestWorkflowHelper.StartMicroflow(microflow, loop, globalKey); 45 | 46 | string instanceId = await WorkflowManager.WaitForWorkflowCompleted(startResult); 47 | 48 | //// CHECK RESULTS //// 49 | /// 50 | // get the steps log to check the results 51 | List steps = await LogReader.GetStepsLog(microflow.workflowName, instanceId); 52 | 53 | // get the orchestration log to check the results 54 | List log = await LogReader.GetOrchLog(microflow.workflowName); 55 | 56 | // check that the orchestraion id is logged 57 | Assert.IsTrue(log.FindIndex(i => i.OrchestrationId.Equals(instanceId)) >= 0); 58 | 59 | List sortedSteps = steps.OrderBy(e => e.EndDate).ToList(); 60 | 61 | Assert.IsTrue(sortedSteps[0].StepNumber == 1); 62 | 63 | string blobHttpRosponse = await HttpBlobDataManager.GetHttpBlob(false, microflow.workflowName, sortedSteps[0].StepNumber, sortedSteps[0].RunId, sortedSteps[0].SubOrchestrationId); 64 | 65 | Assert.IsTrue(blobHttpRosponse.Equals("{\"success\":\"true\"}\n")); 66 | 67 | var arr = sortedSteps[0].PartitionKey.Split("__"); 68 | 69 | var s = $"{TestWorkflowHelper.CalloutGETUrl}?WorkflowName={microflow.workflowName}&MainOrchestrationId={arr[1]}&SubOrchestrationId={sortedSteps[0].SubOrchestrationId}&WebhookId=&RunId={sortedSteps[0].RunId}&StepNumber=1&GlobalKey={sortedSteps[0].GlobalKey}&StepId={stepsList[0].StepId}"; 70 | 71 | Assert.IsTrue(s.Equals(sortedSteps[0].CalloutUrl)); 72 | } 73 | 74 | [TestMethod] 75 | public async Task HttpPost_PassAllParams() 76 | { 77 | // create a simple workflow with parent step 1, subling children step 2 and 3, and child of 2 and 3 step 4 78 | // siblings steps 2 and 3 runs in parallel 79 | List stepsList = TestWorkflowHelper.CreateTestWorkflow_SimpleSteps(); 80 | stepsList.RemoveRange(1, 3); 81 | stepsList[0].IsHttpGet = false; 82 | stepsList[0].SubSteps.Clear(); 83 | stepsList[0].WebhookId = "mywebhook"; 84 | //stepsList[3].WaitForAllParents = false; 85 | 86 | // create Microflow with the created workflow 87 | (MicroflowModels.Microflow workflow, string workflowName) microflow = TestWorkflowHelper.CreateMicroflow(stepsList, isHttpGet: false); 88 | 89 | // set loop to 1, how many time the workflow will execute 90 | int loop = 1; 91 | 92 | // set the global key if needed 93 | // the global key is needed to group related workflows and control the group 94 | string globalKey = Guid.NewGuid().ToString(); 95 | 96 | // upsert Microflow json 97 | //string json = JsonSerializer.Serialize(microflow.workflow); 98 | bool successUpsert = await WorkflowManager.UpsertWorkFlow(microflow.workflow, TestWorkflowHelper.BaseUrl); 99 | 100 | Assert.IsTrue(successUpsert); 101 | 102 | string input = "hello echo"; 103 | 104 | // start the upserted Microflow 105 | HttpResponseMessage startResult = await TestWorkflowHelper.StartMicroflow(microflow, loop, globalKey, input); 106 | 107 | string instanceId = await WorkflowManager.WaitForWorkflowCompleted(startResult); 108 | 109 | //// CHECK RESULTS //// 110 | /// 111 | // get the steps log to check the results 112 | List steps = await LogReader.GetStepsLog(microflow.workflowName, instanceId); 113 | 114 | // get the orchestration log to check the results 115 | List log = await LogReader.GetOrchLog(microflow.workflowName); 116 | 117 | // check that the orchestraion id is logged 118 | Assert.IsTrue(log.FindIndex(i => i.OrchestrationId.Equals(instanceId)) >= 0); 119 | 120 | List sortedSteps = steps.OrderBy(e => e.EndDate).ToList(); 121 | 122 | Assert.IsTrue(sortedSteps[0].StepNumber == 1); 123 | 124 | var arr = sortedSteps[0].PartitionKey.Split("__"); 125 | 126 | var blobHttpRosponseTask = HttpBlobDataManager.GetHttpBlob(false, microflow.workflowName, sortedSteps[0].StepNumber, sortedSteps[0].RunId, sortedSteps[0].SubOrchestrationId); 127 | var blobHttpRequestTask = HttpBlobDataManager.GetHttpBlob(true, microflow.workflowName, sortedSteps[0].StepNumber, sortedSteps[0].RunId, sortedSteps[0].SubOrchestrationId); 128 | 129 | await Task.WhenAll(blobHttpRequestTask, blobHttpRosponseTask); 130 | 131 | var blobHttpRequest = blobHttpRequestTask.Result; 132 | var blobHttpRosponse = blobHttpRosponseTask.Result; 133 | 134 | Assert.IsTrue(blobHttpRequest.Equals("{\"WorkflowName\":\"" 135 | + microflow.workflowName 136 | + "\",\"MainOrchestrationId\":\"" 137 | + arr[1] 138 | + "\",\"SubOrchestrationId\":\"" 139 | + sortedSteps[0].SubOrchestrationId 140 | + "\",\"Webhook\":null," 141 | + "\"RunId\":\"" 142 | + sortedSteps[0].RunId 143 | + "\",\"StepNumber\":1,\"StepId\":\"myStep 1\",\"GlobalKey\":\"" 144 | + sortedSteps[0].GlobalKey 145 | + "\",\"PostData\":\"" 146 | + input 147 | + "\"}")); 148 | 149 | Assert.IsTrue(blobHttpRosponse.Equals("{\"success\":\"true\"}\n")); 150 | 151 | // Microflow will not replace the querystring with meta data if it is a post 152 | //Assert.IsTrue(sortedSteps[0].CalloutUrl.Equals("https://reqbin.com/echo/post/json")); 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /MicroflowTest/Test7_RunFromSteps.cs: -------------------------------------------------------------------------------- 1 | using Microflow.MicroflowTableModels; 2 | using MicroflowModels; 3 | using MicroflowSDK; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Newtonsoft.Json; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Net.Http; 10 | using System.Net.Http.Json; 11 | using System.Threading.Tasks; 12 | 13 | namespace MicroflowTest 14 | { 15 | [TestClass] 16 | public class Test7_RunFromSteps 17 | { 18 | [TestMethod] 19 | public async Task RunFromSteps() 20 | { 21 | string workflowName = await RunBasicWorkflow(); 22 | 23 | HttpResponseMessage runcall = await TestWorkflowHelper.HttpClient.PostAsJsonAsync>($"{TestWorkflowHelper.BaseUrl}/RunFromSteps/{workflowName}", new() { 2, 3 }); 24 | 25 | //// CHECK RESULTS //// stepnumber 1 is now not in the log 26 | string instanceId = await WorkflowManager.WaitForWorkflowCompleted(runcall); 27 | 28 | // get the orchestration log to check the results 29 | List log = await LogReader.GetOrchLog(workflowName); 30 | 31 | // check that the orchestraion id is logged 32 | Assert.IsTrue(log.Single(i => i.OrchestrationId.Equals(instanceId) && i.RowKey.Contains("RunFromSteps")) != null); 33 | 34 | // get the steps log to check the results 35 | List steps = await LogReader.GetStepsLog(workflowName, instanceId); 36 | 37 | List sortedSteps = steps.OrderBy(e => e.EndDate).ToList(); 38 | 39 | if (sortedSteps[0].StepNumber == 2) 40 | Assert.IsTrue(sortedSteps[1].StepNumber == 3); 41 | else 42 | { 43 | Assert.IsTrue(sortedSteps[0].StepNumber == 3); 44 | Assert.IsTrue(sortedSteps[1].StepNumber == 2); 45 | } 46 | 47 | Assert.IsTrue(sortedSteps[2].StepNumber == 4); 48 | 49 | Assert.IsTrue(sortedSteps.Count == 3); 50 | } 51 | 52 | private static async Task RunBasicWorkflow() 53 | { 54 | // create a simple workflow with parent step 1, subling children step 2 and 3, and child of 2 and 3 step 4 55 | // siblings steps 2 and 3 runs in parallel 56 | List stepsList = TestWorkflowHelper.CreateTestWorkflow_SimpleSteps(); 57 | 58 | // create Microflow with the created workflow 59 | (MicroflowModels.Microflow workflow, string workflowName) microflow = TestWorkflowHelper.CreateMicroflow(stepsList, passThroughParams: false); 60 | 61 | // set loop to 1, how many time the workflow will execute 62 | int loop = 1; 63 | 64 | // set the global key if needed 65 | // the global key is needed to group related workflows and control the group 66 | string globalKey = Guid.NewGuid().ToString(); 67 | 68 | // upsert Microflow json 69 | //string json = JsonSerializer.Serialize(microflow.workflow); 70 | bool successUpsert = await WorkflowManager.UpsertWorkFlow(microflow.workflow, TestWorkflowHelper.BaseUrl); 71 | 72 | Assert.IsTrue(successUpsert); 73 | 74 | // start the upserted Microflow 75 | HttpResponseMessage startResult = await TestWorkflowHelper.StartMicroflow(microflow, loop, globalKey); 76 | 77 | string instanceId = await WorkflowManager.WaitForWorkflowCompleted(startResult); 78 | 79 | //// CHECK RESULTS //// 80 | 81 | // get the orchestration log to check the results 82 | List log = await LogReader.GetOrchLog(microflow.workflowName); 83 | 84 | // check that the orchestraion id is logged 85 | Assert.IsTrue(log.FindIndex(i => i.OrchestrationId.Equals(instanceId)) >= 0); 86 | 87 | // get the steps log to check the results 88 | List steps = await LogReader.GetStepsLog(microflow.workflowName, instanceId); 89 | 90 | List sortedSteps = steps.OrderBy(e => e.EndDate).ToList(); 91 | 92 | Assert.IsTrue(sortedSteps[0].StepNumber == 1); 93 | 94 | if (sortedSteps[1].StepNumber == 2) 95 | Assert.IsTrue(sortedSteps[2].StepNumber == 3); 96 | else 97 | { 98 | Assert.IsTrue(sortedSteps[1].StepNumber == 3); 99 | Assert.IsTrue(sortedSteps[2].StepNumber == 2); 100 | } 101 | 102 | Assert.IsTrue(sortedSteps[3].StepNumber == 4); 103 | 104 | Assert.IsTrue(sortedSteps.Count == 4); 105 | 106 | return microflow.workflowName; 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /MicroflowTest/Test8_2TopStepsPostDataStart.cs: -------------------------------------------------------------------------------- 1 | using MicroflowModels; 2 | using MicroflowSDK; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Newtonsoft.Json; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net.Http; 9 | using System.Threading.Tasks; 10 | 11 | namespace MicroflowTest 12 | { 13 | public class PostTestObject 14 | { 15 | public string Name { get; set; } = "Andre"; 16 | public int Id { get; set; } = 1; 17 | } 18 | 19 | [TestClass] 20 | public class Test8_2TopStepsPostDataStart 21 | { 22 | [TestMethod] 23 | public async Task TwoTopStepsPostDataStart() 24 | { 25 | // create a simple workflow top steps 1 and 2, with a child step 3, and step 3 with child step 4 26 | List stepsList = TestWorkflowHelper.CreateTestWorkflow_2TopSteps(); 27 | stepsList.ForEach(x=>x.IsHttpGet = false); 28 | 29 | // create Microflow with the created workflow 30 | (MicroflowModels.Microflow workflow, string workflowName) microflow = TestWorkflowHelper.CreateMicroflow(stepsList, isHttpGet: false); 31 | 32 | // set loop to 1, how many time the workflow will execute 33 | int loop = 1; 34 | 35 | // set the global key if needed 36 | // the global key is needed to group related workflows and control the group 37 | string globalKey = Guid.NewGuid().ToString(); 38 | 39 | // upsert Microflow json 40 | //string json = JsonSerializer.Serialize(microflow.workflow); 41 | bool successUpsert = await WorkflowManager.UpsertWorkFlow(microflow.workflow, TestWorkflowHelper.BaseUrl); 42 | 43 | Assert.IsTrue(successUpsert); 44 | 45 | string input = JsonConvert.SerializeObject(new PostTestObject()); 46 | 47 | // start the upserted Microflow 48 | HttpResponseMessage startResult = await TestWorkflowHelper.StartMicroflow(microflow, loop, globalKey, input); 49 | 50 | string instanceId = await WorkflowManager.WaitForWorkflowCompleted(startResult); 51 | 52 | //// CHECK RESULTS //// 53 | /// 54 | // get the steps log to check the results 55 | List steps = await LogReader.GetStepsLog(microflow.workflowName, instanceId); 56 | 57 | // get the orchestration log to check the results 58 | List log = await LogReader.GetOrchLog(microflow.workflowName); 59 | 60 | // check that the orchestraion id is logged 61 | Assert.IsTrue(log.FindIndex(i => i.OrchestrationId.Equals(instanceId)) >= 0); 62 | 63 | List sortedSteps = steps.OrderBy(e => e.EndDate).ToList(); 64 | 65 | bool step0is1 = false; 66 | 67 | if (sortedSteps[0].StepNumber == 1) 68 | { 69 | Assert.IsTrue(sortedSteps[1].StepNumber == 2); 70 | step0is1 = true; 71 | } 72 | else 73 | { 74 | Assert.IsTrue(sortedSteps[0].StepNumber == 2); 75 | Assert.IsTrue(sortedSteps[1].StepNumber == 1); 76 | } 77 | 78 | Assert.IsTrue(sortedSteps[2].StepNumber == 3); 79 | Assert.IsTrue(sortedSteps[3].StepNumber == 4); 80 | 81 | var arr = sortedSteps[0].PartitionKey.Split("__"); 82 | 83 | var blobHttpRequest1Task = HttpBlobDataManager.GetHttpBlob(true, microflow.workflowName, sortedSteps[0].StepNumber, sortedSteps[0].RunId, sortedSteps[0].SubOrchestrationId); 84 | var blobHttpRequest2Task = HttpBlobDataManager.GetHttpBlob(true, microflow.workflowName, sortedSteps[1].StepNumber, sortedSteps[1].RunId, sortedSteps[1].SubOrchestrationId); 85 | 86 | await Task.WhenAll(blobHttpRequest1Task, blobHttpRequest2Task); 87 | 88 | var blobHttpRosponse1 = blobHttpRequest1Task.Result; 89 | var blobHttpRosponse2 = blobHttpRequest2Task.Result; 90 | 91 | if (step0is1) 92 | { 93 | string s1 = "{\"WorkflowName\":\"Unit_test_workflow@1.0\",\"MainOrchestrationId\":\"" + 94 | arr[1] + "\"," + 95 | "\"SubOrchestrationId\":\"" + 96 | sortedSteps[0].SubOrchestrationId + "\"," + 97 | "\"Webhook\":null," + 98 | "\"RunId\":\"" + 99 | sortedSteps[0].RunId + "\"," + 100 | "\"StepNumber\":1,\"StepId\":\"myStep 1\",\"GlobalKey\":\"" + 101 | sortedSteps[0].GlobalKey + "\"," + 102 | "\"PostData\":\"{\\u0022Name\\u0022:\\u0022Andre\\u0022,\\u0022Id\\u0022:1}\"}"; 103 | 104 | string s2 = "{\"WorkflowName\":\"Unit_test_workflow@1.0\",\"MainOrchestrationId\":\"" + 105 | arr[1] + "\"," + 106 | "\"SubOrchestrationId\":\"" + 107 | sortedSteps[1].SubOrchestrationId + "\"," + 108 | "\"Webhook\":null," + 109 | "\"RunId\":\"" + 110 | sortedSteps[1].RunId + "\"," + 111 | "\"StepNumber\":2,\"StepId\":\"myStep 2\",\"GlobalKey\":\"" + 112 | sortedSteps[1].GlobalKey + "\"," + 113 | "\"PostData\":\"{\\u0022Name\\u0022:\\u0022Andre\\u0022,\\u0022Id\\u0022:1}\"}"; 114 | 115 | Assert.IsTrue(blobHttpRosponse1.Equals(s1)); 116 | Assert.IsTrue(blobHttpRosponse2.Equals(s2)); 117 | } 118 | else 119 | { 120 | string s1 = "{\"WorkflowName\":\"Unit_test_workflow@1.0\",\"MainOrchestrationId\":\"" + 121 | arr[1] + "\"," + 122 | "\"SubOrchestrationId\":\"" + 123 | sortedSteps[1].SubOrchestrationId + "\"," + 124 | "\"Webhook\":null," + 125 | "\"RunId\":\"" + 126 | sortedSteps[1].RunId + "\"," + 127 | "\"StepNumber\":1,\"StepId\":\"myStep 1\",\"GlobalKey\":\"" + 128 | sortedSteps[1].GlobalKey + "\"," + 129 | "\"PostData\":\"{\\u0022Name\\u0022:\\u0022Andre\\u0022,\\u0022Id\\u0022:1}\"}"; 130 | 131 | string s2 = "{\"WorkflowName\":\"Unit_test_workflow@1.0\",\"MainOrchestrationId\":\"" + 132 | arr[1] + "\"," + 133 | "\"SubOrchestrationId\":\"" + 134 | sortedSteps[0].SubOrchestrationId + "\"," + 135 | "\"Webhook\":null," + 136 | "\"RunId\":\"" + 137 | sortedSteps[0].RunId + "\"," + 138 | "\"StepNumber\":2,\"StepId\":\"myStep 2\",\"GlobalKey\":\"" + 139 | sortedSteps[0].GlobalKey + "\"," + 140 | "\"PostData\":\"{\\u0022Name\\u0022:\\u0022Andre\\u0022,\\u0022Id\\u0022:1}\"}"; 141 | 142 | Assert.IsTrue(blobHttpRosponse1.Equals(s2)); 143 | Assert.IsTrue(blobHttpRosponse2.Equals(s1)); 144 | } 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /MicroflowTest/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "MicroflowStorage": "UseDevelopmentStorage=true", 3 | "BaseUrl": "http://localhost:7071/microflow/v1", 4 | "CallOut_GET_Url": "https://reqbin.com/echo/get/json", 5 | "CallOut_POST_Url": "https://reqbin.com/echo/post/json", 6 | "UseEmulator": "false", 7 | "EmulatorCallOut_GET_Url": "http://localhost:7072/api/SleepTestOrchestrator_HttpStart", 8 | "EmulatorCallOut_POST_Url": "http://localhost:7072/api/SleepTestOrchestrator_HttpStart" 9 | } -------------------------------------------------------------------------------- /MicroserviceEmulator/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /MicroserviceEmulator/Function1.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using Microsoft.Azure.WebJobs.Extensions.Http; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace MicroserviceEmulator 10 | { 11 | public static class Function1 12 | { 13 | [FunctionName("Function1")] 14 | public static async Task> RunOrchestrator( 15 | [OrchestrationTrigger] IDurableOrchestrationContext context) 16 | { 17 | List outputs = new(); 18 | 19 | // Replace "hello" with the name of your Durable Activity Function. 20 | outputs.Add(await context.CallActivityAsync("Function1_Hello", "Tokyo")); 21 | outputs.Add(await context.CallActivityAsync("Function1_Hello", "Seattle")); 22 | outputs.Add(await context.CallActivityAsync("Function1_Hello", "London")); 23 | 24 | // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"] 25 | return outputs; 26 | } 27 | 28 | [FunctionName("Function1_Hello")] 29 | public static string SayHello([ActivityTrigger] string name, ILogger log) 30 | { 31 | log.LogInformation($"Saying hello to {name}."); 32 | return $"Hello {name}!"; 33 | } 34 | 35 | [FunctionName("Function1_HttpStart")] 36 | public static async Task HttpStart( 37 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestMessage req, 38 | [DurableClient] IDurableOrchestrationClient starter, 39 | ILogger log) 40 | { 41 | // Function input comes from the request content. 42 | string instanceId = await starter.StartNewAsync("Function1", null); 43 | 44 | log.LogInformation($"Started orchestration with ID = '{instanceId}'."); 45 | 46 | return starter.CreateCheckStatusResponse(req, instanceId); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /MicroserviceEmulator/HttpClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace Microflow.API 4 | { 5 | public class EmulatorShared 6 | { 7 | // NB! To prevent port exaustion, use 1 static HttpClient for as much as possible 8 | // This instance of the HttpClient is also used in the ResponseProxyInlineDemoFunction 9 | // This client will be removed and is only included for the SleepTestOrchestrator 10 | public static readonly HttpClient HttpClient = new(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MicroserviceEmulator/MicroflowExternalAPI.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Text.Json; 3 | using System.Threading.Tasks; 4 | using MicroflowModels; 5 | using MicroflowModels.Helpers; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 8 | using Microsoft.Azure.WebJobs.Extensions.Http; 9 | using static MicroflowModels.Constants; 10 | 11 | namespace Microflow.API.External 12 | { 13 | public static class MicroflowExternalApi 14 | { 15 | 16 | /// 17 | /// Called from Microflow.ExecuteStep to get the current step table config 18 | /// 19 | [FunctionName(CallNames.GetStepInternal)] 20 | public static async Task GetStep([ActivityTrigger] MicroflowRun workflowRun) => await workflowRun.GetStep(); 21 | 22 | /// 23 | /// use this to test some things like causing an exception 24 | /// 25 | [FunctionName("testpost")] 26 | public static async Task TestPost( 27 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "testpost")] HttpRequestMessage req) 28 | { 29 | await Task.Delay(1000); 30 | 31 | //if (req.Method == HttpMethod.Post) 32 | //{ 33 | string r = await req.Content.ReadAsStringAsync(); 34 | 35 | MicroflowPostData result = JsonSerializer.Deserialize(r); 36 | 37 | if (result.StepNumber == 6 || result.StepNumber == 8 || result.StepNumber == 10)// && result.workflowName.Equals("xxx")) 38 | { 39 | //HttpResponseMessage result2 = await MicroflowHttpClient.HttpClient.GetAsync($"{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/start/"); 40 | //var kgkg = 0; 41 | await Task.Delay(5000); 42 | } 43 | //} 44 | //else 45 | //{ 46 | // NameValueCollection data = req.RequestUri.ParseQueryString(); 47 | // MicroflowPostData postData = new MicroflowPostData() 48 | // { 49 | // Webhook = data["Webhook"], 50 | // MainOrchestrationId = data["MainOrchestrationId"], 51 | // workflowName = data["workflowName"], 52 | // RunId = data["RunId"], 53 | // StepNumber = Convert.ToInt32(data["StepNumber"]), 54 | // StepId = data["StepId"], 55 | // SubOrchestrationId = data["SubOrchestrationId"], 56 | // GlobalKey = data["GlobalKey"] 57 | // }; 58 | // await Task.Delay(10000); 59 | 60 | //} 61 | 62 | 63 | HttpResponseMessage resp = new(System.Net.HttpStatusCode.OK); 64 | // resp.Headers.Location = new Uri("http://localhost:7071/api/testpost"); 65 | //resp.Content = new StringContent("wappa"); 66 | return resp; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /MicroserviceEmulator/MicroserviceEmulator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | v4 5 | Debug;Release;DEBUG_NOUPSERT_NOFLOWCONTROL_NOSCALEGROUPS 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | PreserveNewest 20 | Never 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /MicroserviceEmulator/MicroserviceEmulator/MicroserviceEmulator.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32929.385 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroserviceEmulator", "..\MicroserviceEmulator.csproj", "{277EF938-ECB5-4208-AD74-0BD8530C6D1F}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | DEBUG_NOUPSERT_NOFLOWCONTROL_NOSCALEGROUPS|Any CPU = DEBUG_NOUPSERT_NOFLOWCONTROL_NOSCALEGROUPS|Any CPU 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {277EF938-ECB5-4208-AD74-0BD8530C6D1F}.DEBUG_NOUPSERT_NOFLOWCONTROL_NOSCALEGROUPS|Any CPU.ActiveCfg = DEBUG_NOUPSERT_NOFLOWCONTROL_NOSCALEGROUPS|Any CPU 16 | {277EF938-ECB5-4208-AD74-0BD8530C6D1F}.DEBUG_NOUPSERT_NOFLOWCONTROL_NOSCALEGROUPS|Any CPU.Build.0 = DEBUG_NOUPSERT_NOFLOWCONTROL_NOSCALEGROUPS|Any CPU 17 | {277EF938-ECB5-4208-AD74-0BD8530C6D1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {277EF938-ECB5-4208-AD74-0BD8530C6D1F}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {277EF938-ECB5-4208-AD74-0BD8530C6D1F}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {277EF938-ECB5-4208-AD74-0BD8530C6D1F}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(SolutionProperties) = preSolution 23 | HideSolutionNode = FALSE 24 | EndGlobalSection 25 | GlobalSection(ExtensibilityGlobals) = postSolution 26 | SolutionGuid = {F710C39D-3540-4702-8042-5E45420F7B71} 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /MicroserviceEmulator/OrchestrationStateSimulations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.IO; 5 | using System.Net.Http; 6 | using System.Text.Json; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using MicroflowModels; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Azure.WebJobs; 12 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 13 | using Microsoft.Azure.WebJobs.Extensions.Http; 14 | using Microsoft.Extensions.Logging; 15 | 16 | namespace Microflow.API.Internal 17 | { 18 | public static class OrchestrationStateSimulations 19 | { 20 | [FunctionName("RunningOrchestration")] 21 | public static async Task> RunningOrchestrator( 22 | [OrchestrationTrigger] IDurableOrchestrationContext context) 23 | { 24 | List outputs = new(); 25 | 26 | outputs.Add(await context.CallActivityAsync("Arb_Activity", "Tokyo")); 27 | outputs.Add(await context.CallActivityAsync("Arb_Activity", "Seattle")); 28 | outputs.Add(await context.CallActivityAsync("Arb_Activity", "London")); 29 | 30 | //int o = 0; 31 | //int t = 5 / o; 32 | 33 | return outputs; 34 | } 35 | 36 | [FunctionName("Arb_Activity")] 37 | public static async Task Arb_Activity([ActivityTrigger] string name, ILogger log) 38 | { 39 | await Task.Delay(30000); 40 | 41 | log.LogInformation($"Saying hello to {name}."); 42 | return $"Hello {name}!"; 43 | } 44 | 45 | [FunctionName("EmulateRunningOrchestration")] 46 | public static async Task EmulateRunningOrchestration( 47 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "EmulateRunningOrchestration")] HttpRequestMessage req, 48 | [DurableClient] IDurableOrchestrationClient starter, 49 | ILogger log) 50 | { 51 | string instanceId = await starter.StartNewAsync("RunningOrchestration", instanceId: "timer_qwerty"); 52 | 53 | log.LogInformation($"Started orchestration with ID = '{instanceId}'."); 54 | 55 | return starter.CreateCheckStatusResponse(req, instanceId); 56 | } 57 | 58 | [FunctionName("EmulateCompletedOrchestration")] 59 | public static async Task EmulateCompletedOrchestration( 60 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "EmulateCompletedOrchestration")] HttpRequestMessage req, 61 | [DurableClient] IDurableOrchestrationClient starter, 62 | ILogger log) 63 | { 64 | string instanceId = await starter.StartNewAsync("RunningOrchestration", null); 65 | 66 | log.LogInformation($"Started orchestration with ID = '{instanceId}'."); 67 | 68 | return await starter.WaitForCompletionOrCreateCheckStatusResponseAsync(req, instanceId, timeout:TimeSpan.FromMinutes(100)); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /MicroserviceEmulator/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "MicroserviceEmulator": { 4 | "commandName": "Project", 5 | "commandLineArgs": "host start --pause-on-error --port 7072" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /MicroserviceEmulator/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /MicroserviceEmulator/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /MicroserviceEmulator/ResponseProxies/Callbacks/ResponseProxyCallbackDemoFunction.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading.Tasks; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.Http; 5 | using System.Net.Http; 6 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 7 | 8 | namespace Microflow.ResponseProxies 9 | { 10 | public static class ResponseProxyCallbackDemoFunction 11 | { 12 | /// 13 | /// These client functions can be refactored into separate function apps, 14 | /// this being custom code that might change more frequently than the workflow engine core, 15 | /// and will then also scale on its own. 16 | /// 17 | [FunctionName("webhook")] 18 | public static async Task RaiseEvent( 19 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "webhook/{action}/{orchestratorId}/{stepId:int?}")] HttpRequestMessage req, 20 | [DurableClient] IDurableOrchestrationClient client, int stepId, string action, string orchestratorId) 21 | { 22 | HttpResponseMessage resp = new(HttpStatusCode.OK); 23 | 24 | await client.RaiseEventAsync(orchestratorId, action, resp); 25 | 26 | return resp; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MicroserviceEmulator/ResponseProxies/Inline/ResponseProxyInlineDemoFunction.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Azure.WebJobs; 3 | using System.Net.Http; 4 | using Microflow.API; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using System.Threading; 7 | using MicroflowModels; 8 | 9 | namespace Microflow 10 | { 11 | public static class ResponseProxyInlineDemoFunction 12 | { 13 | /// 14 | /// This simulates an activity executing, replace with real call like an API call 15 | /// 16 | [FunctionName("httpcall")] 17 | public static async Task HttpCall([ActivityTrigger] HttpCall httpCall) 18 | { 19 | using (CancellationTokenSource cts = new(httpCall.WebhookTimeoutSeconds * 1000)) 20 | { 21 | try 22 | { 23 | HttpResponseMessage result = await EmulatorShared.HttpClient.PostAsJsonAsync(httpCall.CalloutUrl, (ProcessId: httpCall.PartitionKey, StepId: httpCall.RowKey), cts.Token); 24 | 25 | if (result.IsSuccessStatusCode) 26 | { 27 | return new HttpResponseMessage(System.Net.HttpStatusCode.OK); 28 | } 29 | 30 | return new HttpResponseMessage(result.StatusCode); 31 | } 32 | catch (TaskCanceledException) 33 | { 34 | return new HttpResponseMessage(System.Net.HttpStatusCode.RequestTimeout); 35 | } 36 | finally 37 | { 38 | cts.Dispose(); 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MicroserviceEmulator/SleepTestOrchestrator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Specialized; 3 | using System.IO; 4 | using System.Net.Http; 5 | using System.Text.Json; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using MicroflowModels; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.Azure.WebJobs; 11 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 12 | using Microsoft.Azure.WebJobs.Extensions.Http; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace Microflow.API.Internal 16 | { 17 | /// 18 | /// Use this to mock an orchestration micro-service for testing purposes, 19 | /// this will be moved to its own function app in the future, 20 | /// because it`s better to split the action/micro-service workers from the Microflow app for scalability 21 | /// 22 | public static class SleepTestOrchestrator 23 | { 24 | /// 25 | /// Sleep for between random min and max seconds 26 | /// 27 | [Deterministic] 28 | [FunctionName("SleepTestOrchestrator")] 29 | public static async Task SleepTestMethod( 30 | [OrchestrationTrigger] IDurableOrchestrationContext context, 31 | ILogger inLog) 32 | { 33 | ILogger log = context.CreateReplaySafeLogger(inLog); 34 | 35 | (MicroflowPostData postData, string action) = context.GetInput<(MicroflowPostData, string)>(); 36 | 37 | Random random = new(); 38 | TimeSpan ts = TimeSpan.FromSeconds(random.Next(1, 5)); 39 | DateTime deadline = context.CurrentUtcDateTime.Add(ts); 40 | 41 | using (CancellationTokenSource cts = new()) 42 | { 43 | try 44 | { 45 | //cts.CancelAfter(60000); 46 | await context.CreateTimer(deadline, cts.Token); 47 | } 48 | catch (TaskCanceledException) 49 | { 50 | log.LogCritical("========================TaskCanceledException=========================="); 51 | } 52 | finally 53 | { 54 | cts.Dispose(); 55 | } 56 | } 57 | // test if the webhook is done, do the call back if there is 1 58 | if (!string.IsNullOrWhiteSpace(postData.Webhook)) 59 | { 60 | postData.Webhook += string.IsNullOrEmpty(action) ? "" : "/" + action; 61 | 62 | DurableHttpRequest req = new(HttpMethod.Get, new Uri(postData.Webhook)); 63 | 64 | await context.CallHttpAsync(req); 65 | } 66 | 67 | return; 68 | } 69 | 70 | /// 71 | /// Called by the normal function and creates a new SleepTestOrchestrator 72 | /// 73 | [FunctionName("SleepTestOrchestrator_HttpStart")] 74 | public static async Task SleepTestOrchestrator_HttpStart( 75 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "SleepTestOrchestrator_HttpStart/{webhookAction?}/{isAsync:bool?}")] HttpRequestMessage req, 76 | [DurableClient] IDurableOrchestrationClient client, string webhookAction, bool? isAsync) 77 | { 78 | if (!isAsync.HasValue) 79 | { 80 | isAsync = false; 81 | } 82 | 83 | MicroflowPostData postData; 84 | 85 | string instanceId = Guid.NewGuid().ToString(); 86 | 87 | if (req.Method == HttpMethod.Post) 88 | { 89 | string data = await req.Content.ReadAsStringAsync(); 90 | 91 | postData = JsonSerializer.Deserialize(data); 92 | 93 | await client.StartNewAsync("SleepTestOrchestrator", instanceId, (postData, webhookAction)); 94 | } 95 | else 96 | { 97 | NameValueCollection data = req.RequestUri.ParseQueryString(); 98 | 99 | postData = new() 100 | { 101 | Webhook = data["Webhook"], 102 | MainOrchestrationId = data["MainOrchestrationId"], 103 | WorkflowName = data["WorkflowName"], 104 | RunId = data["RunId"], 105 | StepNumber = Convert.ToInt32(data["StepNumber"]), 106 | StepId = data["StepId"], 107 | SubOrchestrationId = data["SubOrchestrationId"], 108 | GlobalKey = data["GlobalKey"] 109 | }; 110 | 111 | await client.StartNewAsync("SleepTestOrchestrator", instanceId, (postData, webhookAction)); 112 | } 113 | 114 | if(isAsync.Value) 115 | { 116 | return client.CreateCheckStatusResponse(req, instanceId); 117 | } 118 | else if (!string.IsNullOrEmpty(postData.Webhook)) 119 | { 120 | return new HttpResponseMessage(System.Net.HttpStatusCode.OK) 121 | { 122 | Content = new StringContent("{\"success\":\"true\"}\n") 123 | }; 124 | } 125 | else 126 | { 127 | await client.WaitForCompletionOrCreateCheckStatusResponseAsync(req, instanceId, TimeSpan.FromSeconds(1000)); 128 | 129 | return new HttpResponseMessage(System.Net.HttpStatusCode.OK) 130 | { 131 | Content = new StringContent("{\"success\":\"true\"}\n") 132 | }; 133 | } 134 | } 135 | 136 | /// 137 | /// Http entry point as a normal function calling a client function 138 | /// 139 | [FunctionName("SleepTestOrchestrator_Function")] 140 | public static async Task Run( 141 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req) 142 | { 143 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 144 | 145 | MicroflowPostData data = JsonSerializer.Deserialize(requestBody); 146 | 147 | await EmulatorShared.HttpClient.PostAsJsonAsync($"{Environment.GetEnvironmentVariable("BaseUrl")}SleepTestOrchestrator_HttpStart/", data); 148 | 149 | HttpResponseMessage resp = new(); 150 | 151 | // test the returned status codes here and also the effect if Microflows step setting StopOnWebhookTimeout 152 | //resp.StatusCode = System.Net.HttpStatusCode.NotFound; 153 | resp.StatusCode = System.Net.HttpStatusCode.OK; 154 | // set the location and check in the stpe log if its saved when 201 created 155 | //resp.Headers.Location = new Uri("http://localhost:7071/api/SleepTestOrchestrator_HttpStart/"); 156 | 157 | return resp; 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /MicroserviceEmulator/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "durableTask": { 4 | "logReplayEvents": "false", 5 | "hubName": "MicroserviceEmulatorTaskHub" 6 | }, 7 | "logging": { 8 | "applicationInsights": { 9 | "samplingSettings": { 10 | "isEnabled": true, 11 | "excludedTypes": "Request" 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /MicroserviceEmulator/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet" 6 | } 7 | } -------------------------------------------------------------------------------- /local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet" 6 | } 7 | } --------------------------------------------------------------------------------