├── .config └── dotnet-tools.json ├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .husky ├── pre-commit └── task-runner.json ├── .vscode └── settings.json ├── LICENSE ├── LLL.DurableTask.sln ├── README.md ├── benchmark └── StoragesBenchmark │ ├── AzureStorageBenchmark.cs │ ├── DurableTaskSqlServerBenchmark.cs │ ├── InMemoryEFCoreBenchmark.cs │ ├── MySqlEFCoreBenchmark.cs │ ├── OrchestrationBenchmark.cs │ ├── PostgresEFCoreBenchmark.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── SqlServerEFCoreBenchmark.cs │ ├── StoragesBenchmark.csproj │ └── appsettings.json ├── docker-compose.yml ├── global.json ├── readme ├── architecture.md ├── diagrams │ ├── architecture_1.drawio │ ├── architecture_1.png │ ├── architecture_2.drawio │ ├── architecture_2.png │ ├── architecture_3.drawio │ ├── architecture_3.png │ ├── architecture_4.drawio │ └── architecture_4.png ├── screenshots.md └── screenshots │ ├── create-orchestration.png │ ├── orchestration-history-parallel.png │ ├── orchestration-history-sequential.png │ ├── orchestration-purge.png │ ├── orchestration-raise-event.png │ ├── orchestration-state.png │ ├── orchestration-terminate.png │ └── orchestrations.png ├── samples ├── Api │ ├── Api.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json ├── AppHost │ ├── AppHost.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json ├── BpmnWorker │ ├── Activities │ │ ├── EmptyActivity.cs │ │ ├── HttpRequestActivity.cs │ │ └── ScriptActivity.cs │ ├── BpmnWorker.csproj │ ├── Orchestrations │ │ └── BPMNOrchestrator.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Providers │ │ ├── IBPMNProvider.cs │ │ └── LocalFileBPMNProvider.cs │ ├── Scripting │ │ ├── CSharpScriptEngine.cs │ │ ├── IScriptEngine.cs │ │ ├── IScriptEngineFactory.cs │ │ ├── IScriptExecutor.cs │ │ ├── JavaScriptScriptEngine.cs │ │ ├── ScriptExecutor.cs │ │ └── ServiceProviderScriptEngineFactory.cs │ ├── Spec │ │ ├── BPMN20.xsd │ │ ├── BPMNDI.xsd │ │ ├── DC.xsd │ │ ├── DI.xsd │ │ ├── Generate.txt │ │ ├── Semantic.xsd │ │ ├── Spec.BPMN.DI.cs │ │ ├── Spec.BPMN.MODEL.cs │ │ ├── Spec.DD.DC.cs │ │ └── Spec.DD.DI.cs │ ├── Workflows │ │ ├── Bonus.bpmn │ │ ├── BookParallel.bpmn │ │ └── BookSequential.bpmn │ ├── appsettings.Development.json │ └── appsettings.json ├── CarWorker │ ├── Activities │ │ ├── BookCarActivity.cs │ │ └── CancelCarActivity.cs │ ├── CarWorker.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json ├── FlightWorker │ ├── Activities │ │ ├── BookFlightActivity.cs │ │ └── CancelFlightActivity.cs │ ├── FlightWorker.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json ├── HotelWorker │ ├── Activities │ │ ├── BookHotelActivity.cs │ │ └── CancelHotelActivity.cs │ ├── HotelWorker.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json ├── OrchestrationWorker │ ├── OrchestrationWorker.csproj │ ├── Orchestrations │ │ ├── BookParallelOrchestration.cs │ │ └── BookSequentialOrchestration.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json ├── README.md ├── Server │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Server.csproj │ ├── appsettings.Development.json │ └── appsettings.json └── Ui │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Ui.csproj │ ├── appsettings.Development.json │ └── appsettings.json ├── src ├── LLL.DurableTask.Api │ ├── Builder │ │ ├── DurableTaskApiEndpointConventionBuilder.cs │ │ └── EndpointRouteBuilderExtensions.cs │ ├── DependencyInjection │ │ ├── DurableTaskApiOptions.cs │ │ └── DurableTaskApiServiceCollectionExtensions.cs │ ├── DurableTaskPolicy.cs │ ├── Endpoints │ │ ├── EntrypointEndpoint.cs │ │ └── OrchestrationsEndpoints.cs │ ├── Extensions │ │ └── HttpContextExtensions.cs │ ├── LLL.DurableTask.Api.csproj │ ├── Metadata │ │ └── DurableTaskEndpointMetadata.cs │ ├── Models │ │ ├── CreateOrchestrationRequest.cs │ │ ├── EndpointInfo.cs │ │ ├── EntrypointResponse.cs │ │ ├── RewindRequest.cs │ │ └── TerminateRequest.cs │ └── README.md ├── LLL.DurableTask.AzureStorage │ ├── AzureStorageOrchestrationServiceFeaturesClient.cs │ ├── AzureStorageOrchestrationServiceRewindClient.cs │ ├── DependencyInjection │ │ └── AzureStorageServiceCollectionExtensions.cs │ ├── LLL.DurableTask.AzureStorage.csproj │ └── README.md ├── LLL.DurableTask.Client │ ├── DependencyInjection │ │ └── DurableTaskClientServiceCollectionExtensions.cs │ ├── LLL.DurableTask.Client.csproj │ └── README.md ├── LLL.DurableTask.Core │ ├── IDistributedOrchestrationService.cs │ ├── IOrchestrationServiceFeaturesClient.cs │ ├── IOrchestrationServiceRewindClient.cs │ ├── LLL.DurableTask.Core.csproj │ ├── OrchestrationFeature.cs │ ├── OrchestrationQueryExtended.cs │ ├── PurgeInstanceFilterExtended.cs │ ├── README.md │ └── Serializing │ │ ├── HistoryEventConverter.cs │ │ └── TypelessJsonDataConverter.cs ├── LLL.DurableTask.EFCore.InMemory │ ├── DependencyInjection │ │ └── InMemoryEFCoreOrchestrationBuilderExtensions.cs │ ├── InMemoryOrchestrationDbContextExtensions.cs │ ├── LLL.DurableTask.EFCore.InMemory.csproj │ └── README.md ├── LLL.DurableTask.EFCore.MySql │ ├── DependencyInjection │ │ └── MySqlEFCoreOrchestrationBuilderExtensions.cs │ ├── LLL.DurableTask.EFCore.MySql.csproj │ ├── Migrations │ │ ├── 20220925023618_Initial.Designer.cs │ │ ├── 20220925023618_Initial.cs │ │ ├── 20221124092416_SearchIndexes.Designer.cs │ │ ├── 20221124092416_SearchIndexes.cs │ │ ├── 20221203122137_FailureDetails.Designer.cs │ │ ├── 20221203122137_FailureDetails.cs │ │ ├── 20230205130925_AdjustIndexesForLock.Designer.cs │ │ ├── 20230205130925_AdjustIndexesForLock.cs │ │ ├── 20230305134736_IndexTags.Designer.cs │ │ ├── 20230305134736_IndexTags.cs │ │ ├── 20230809064638_IndexCompletedTime.Designer.cs │ │ ├── 20230809064638_IndexCompletedTime.cs │ │ ├── 20240602074455_EFCore8.Designer.cs │ │ ├── 20240602074455_EFCore8.cs │ │ └── OrchestrationDbContextModelSnapshot.cs │ ├── MySqlOrchestrationDbContextExtensions.cs │ ├── MySqlQueryableExtensions.cs │ ├── OrchestrationDesignTimeDbContextFactory.cs │ ├── README.md │ └── StraightJoinCommandInterceptor.cs ├── LLL.DurableTask.EFCore.PostgreSQL │ ├── DependencyInjection │ │ └── PostgreSqlEFCoreOrchestrationBuilderExtensions.cs │ ├── LLL.DurableTask.EFCore.PostgreSQL.csproj │ ├── Migrations │ │ ├── 20220925023621_Initial.Designer.cs │ │ ├── 20220925023621_Initial.cs │ │ ├── 20221124092430_SearchIndexes.Designer.cs │ │ ├── 20221124092430_SearchIndexes.cs │ │ ├── 20221203122138_FailureDetails.Designer.cs │ │ ├── 20221203122138_FailureDetails.cs │ │ ├── 20230205130948_AdjustIndexesForLock.Designer.cs │ │ ├── 20230205130948_AdjustIndexesForLock.cs │ │ ├── 20230305134745_IndexTags.Designer.cs │ │ ├── 20230305134745_IndexTags.cs │ │ ├── 20230809064704_IndexCompletedTime.Designer.cs │ │ ├── 20230809064704_IndexCompletedTime.cs │ │ ├── 20240602074528_EFCore8.Designer.cs │ │ ├── 20240602074528_EFCore8.cs │ │ └── OrchestrationDbContextModelSnapshot.cs │ ├── OrchestrationDesignTimeDbContextFactory.cs │ ├── PostgreOrchestrationDbContextExtensions.cs │ └── README.md ├── LLL.DurableTask.EFCore.SqlServer │ ├── DependencyInjection │ │ └── SqlServerEFCoreOrchestrationBuilderExtensions.cs │ ├── LLL.DurableTask.EFCore.SqlServer.csproj │ ├── Migrations │ │ ├── 20220925023624_Initial.Designer.cs │ │ ├── 20220925023624_Initial.cs │ │ ├── 20221124092441_SearchIndexes.Designer.cs │ │ ├── 20221124092441_SearchIndexes.cs │ │ ├── 20221203122146_FailureDetails.Designer.cs │ │ ├── 20221203122146_FailureDetails.cs │ │ ├── 20230205130939_AdjustIndexesForLock.Designer.cs │ │ ├── 20230205130939_AdjustIndexesForLock.cs │ │ ├── 20230305134753_IndexTags.Designer.cs │ │ ├── 20230305134753_IndexTags.cs │ │ ├── 20230809064650_IndexCompletedTime.Designer.cs │ │ ├── 20230809064650_IndexCompletedTime.cs │ │ ├── 20240602074541_EFCore8.Designer.cs │ │ ├── 20240602074541_EFCore8.cs │ │ └── OrchestrationDbContextModelSnapshot.cs │ ├── OrchestrationDesignTimeDbContextFactory.cs │ ├── README.md │ └── SqlServerOrchestrationDbContextExtensions.cs ├── LLL.DurableTask.EFCore │ ├── Configuration │ │ ├── ActivityMessageConfiguration.cs │ │ ├── Converters │ │ │ └── UtcDateTimeConverter.cs │ │ ├── EventConfiguration.cs │ │ ├── ExecutionConfiguration.cs │ │ ├── InstanceConfiguration.cs │ │ └── OrchestrationMessageConfiguration.cs │ ├── DependencyInjection │ │ ├── DurableTaskEFCoreServiceCollectionExtensions.cs │ │ ├── EFCoreOrchestrationBuilder.cs │ │ └── IEFCoreOrchestrationBuilder.cs │ ├── EFCoreContinuationToken.cs │ ├── EFCoreOrchestrationOptions.cs │ ├── EFCoreOrchestrationService.cs │ ├── EFCoreOrchestrationServiceClient.cs │ ├── EFCoreOrchestrationSession.cs │ ├── Entities │ │ ├── ActivityMessage.cs │ │ ├── Event.cs │ │ ├── Execution.cs │ │ ├── Instance.cs │ │ ├── OrchestrationMessage.cs │ │ └── Tag.cs │ ├── Extensions │ │ ├── HistoryEventExtensions.cs │ │ └── TaskExtensions.cs │ ├── Helpers │ │ └── ParametersCollection.cs │ ├── LLL.DurableTask.EFCore.csproj │ ├── Mappers │ │ ├── ActivityMessageMapper.cs │ │ ├── ExecutionMapper.cs │ │ ├── InstanceMapper.cs │ │ ├── OrchestrationMessageMapper.cs │ │ └── QueueMapper.cs │ ├── OrchestrationDbContext.cs │ ├── OrchestrationDbContextExtensions.cs │ ├── Polling │ │ ├── BackoffPollingHelper.cs │ │ └── PollingIntervalOptions.cs │ └── README.md ├── LLL.DurableTask.Server.Grpc.Client │ ├── DependencyInjection │ │ └── DurableTaskGrpcServiceCollectionExtensions.cs │ ├── GrpcClientOrchestrationService.cs │ ├── GrpcClientOrchestrationServiceClient.cs │ ├── GrpcClientOrchestrationServiceOptions.cs │ ├── GrpcClientOrchestrationSession.cs │ ├── LLL.DurableTask.Server.Grpc.Client.csproj │ └── README.md ├── LLL.DurableTask.Server.Grpc │ ├── DependencyInjection │ │ ├── EndpointRouteBuilderExtensions.cs │ │ └── TaskHubServerBuilderExtensions.cs │ ├── GrpcServerOrchestrationService.cs │ ├── GrpcServerOrchestrationServiceOptions.cs │ ├── LLL.DurableTask.Server.Grpc.csproj │ ├── NameVersion.cs │ ├── Protos │ │ └── OrchestrationService.proto │ └── README.md ├── LLL.DurableTask.Server │ ├── DependencyInjection │ │ ├── DurableTaskServerServiceCollectionExtensions.cs │ │ ├── ITaskHubServerBuilder.cs │ │ └── TaskHubServerBuilder.cs │ ├── LLL.DurableTask.Server.csproj │ ├── README.md │ └── Services │ │ └── ServerHostedService.cs ├── LLL.DurableTask.Ui │ ├── .gitignore │ ├── Builder │ │ └── DurableTaskUiApplicationBuilderExtensions.cs │ ├── DependencyInjection │ │ ├── DurableTaskUiOptions.cs │ │ ├── DurableTaskUiServiceCollectionExtensions.cs │ │ └── OidcOptions.cs │ ├── Extensions │ │ └── HttpContextExtensions.cs │ ├── LLL.DurableTask.Ui.csproj │ ├── README.md │ └── app │ │ ├── .vscode │ │ └── settings.json │ │ ├── package.json │ │ ├── public │ │ ├── configuration.json │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ │ ├── src │ │ ├── App.tsx │ │ ├── ConfigurationProvider.tsx │ │ ├── CustomTheme.ts │ │ ├── clients │ │ │ └── ApiClient.ts │ │ ├── components │ │ │ ├── AuthorizedGuard.tsx │ │ │ ├── ErrorAlert.tsx │ │ │ ├── MuiCodeEditor.tsx │ │ │ ├── Pagination.tsx │ │ │ ├── RefreshButton.tsx │ │ │ └── TopNav.tsx │ │ ├── form │ │ │ ├── CodeEditor.tsx │ │ │ ├── TextField.tsx │ │ │ └── useForm.tsx │ │ ├── hooks │ │ │ ├── useApiClient.tsx │ │ │ ├── useAuth.tsx │ │ │ ├── useDynamicRefs.ts │ │ │ ├── useLocationState.ts │ │ │ ├── useOrchestrationsFilter.ts │ │ │ ├── usePageSize.ts │ │ │ ├── useQueryState.ts │ │ │ └── useRefreshInterval.ts │ │ ├── index.tsx │ │ ├── models │ │ │ ├── ApiModels.ts │ │ │ └── Configuration.ts │ │ ├── react-app-env.d.ts │ │ ├── serviceWorker.ts │ │ ├── utils │ │ │ ├── date-utils.ts │ │ │ └── yup-utils.ts │ │ └── views │ │ │ ├── create │ │ │ ├── Create.tsx │ │ │ └── index.ts │ │ │ ├── home │ │ │ ├── Home.tsx │ │ │ └── index.ts │ │ │ ├── not_found │ │ │ ├── NotFound.tsx │ │ │ └── index.ts │ │ │ ├── orchestration │ │ │ ├── HistoryTable.tsx │ │ │ ├── LineBuilder.tsx │ │ │ ├── Orchestration.tsx │ │ │ ├── RaiseEvent.tsx │ │ │ ├── Rewind.tsx │ │ │ ├── State.tsx │ │ │ ├── Terminate.tsx │ │ │ └── index.ts │ │ │ └── orchestrations │ │ │ ├── Orchestrations.tsx │ │ │ ├── OrchestrationsSearch.tsx │ │ │ ├── OrchestrationsTable.tsx │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── yarn.lock └── LLL.DurableTask.Worker │ ├── Activities │ ├── MethodTaskActivity.cs │ └── ServiceProviderTaskActivity.cs │ ├── ActivityBase.cs │ ├── Attributes │ ├── ActivityAttribute.cs │ └── OrchestrationAttribute.cs │ ├── Builder │ ├── DurableTaskWorkerBuilder.cs │ ├── DurableTaskWorkerBuilderExtensions.cs │ └── IDurableTaskWorkerBuilder.cs │ ├── DependencyInjection │ └── DurableTaskWorkerServiceCollectionExtensions.cs │ ├── ExtendedOrchestrationContext.cs │ ├── LLL.DurableTask.Worker.csproj │ ├── Middlewares │ ├── ServiceProviderActivityMiddleware.cs │ └── ServiceProviderOrchestrationMiddleware.cs │ ├── ObjectCreators │ └── FactoryObjectCreator.cs │ ├── OrchestrationBase.cs │ ├── OrchestrationContextEventExtensions.cs │ ├── OrchestrationContextStatusExtensions.cs │ ├── Orchestrations │ ├── MethodTaskOrchestration.cs │ └── ServiceProviderTaskOrchestration.cs │ ├── README.md │ ├── Services │ ├── WorkerHostedService.cs │ └── WorkerOrchestrationService.cs │ └── Utils │ └── DeterministicGuid.cs └── test └── LLL.DurableTask.Tests ├── Api ├── ApiTestBase.cs ├── CreateOrchestrationTests.cs ├── GetOrchestrationTests.cs ├── ListOrchestrationsTests.cs ├── PurgeOrchestrationTests.cs ├── RaiseOrchestrationEventTests.cs ├── RewindOrchestrationTests.cs ├── RootEndpointTests.cs └── TerminateOrchestrationTests.cs ├── LLL.DurableTask.Tests.csproj ├── Storages ├── Activities │ ├── MeasuredDelayActivity.cs │ ├── SubtractActivity.cs │ └── SumActivity.cs ├── AzureStorageServerTests.cs ├── AzureStorageTests.cs ├── EFCoreTestBase.cs ├── InMemoryEFCoreServerTests.cs ├── InMemoryEFCoreTests.cs ├── MySqlEFCoreTests.cs ├── Orchestrations │ ├── ContinueAsNewEmptyOrchestration.cs │ ├── ContinueAsNewOrchestration.cs │ ├── EmptyOrchestration.cs │ ├── FibonacciRecursiveOrchestration.cs │ ├── ParallelTasksOrchestration.cs │ ├── ParentOrchestration.cs │ ├── SendEventOrchestration.cs │ ├── TimerOrchestration.cs │ └── WaitForEventOrchestration.cs ├── PostgresEFCoreTests.cs ├── ServerStorageTestBase.cs ├── SqlServerEFCoreTests.cs └── StorageTestBase.cs ├── Utils ├── DbContextDispenser.cs └── ResponseVersionHandler.cs ├── Worker ├── ActivityClass │ ├── ConstructorInjectionTests.cs │ ├── FailureDetailsTests.cs │ └── SerializeResultTests.cs ├── ActivityMethod │ ├── ConstructorInjectionTests.cs │ ├── FailureDetailsTests.cs │ ├── FromInterfaceTests.cs │ └── SerializeResultTests.cs ├── OrchestrationClass │ ├── ConstructorInjectionTests.cs │ ├── EventTests.cs │ ├── FailureDetailsTests.cs │ ├── GuidTests.cs │ └── SerializeResultTests.cs ├── OrchestrationMethod │ ├── ConstructorInjectionTests.cs │ ├── CustomStatusTests.cs │ ├── EventTests.cs │ ├── FailureDetailsTests.cs │ ├── GuidTests.cs │ └── SerializeResultTests.cs ├── TestHelpers │ └── InvokeActivityOrchestration.cs └── WorkerTestBase.cs └── appsettings.json /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "husky": { 6 | "version": "0.5.4", 7 | "commands": [ 8 | "husky" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | ## husky task runner examples ------------------- 5 | ## Note : for local installation use 'dotnet' prefix. e.g. 'dotnet husky' 6 | 7 | ## run all tasks 8 | #husky run 9 | 10 | ### run all tasks with group: 'group-name' 11 | #husky run --group group-name 12 | 13 | ## run task with name: 'task-name' 14 | #husky run --name task-name 15 | 16 | ## pass hook arguments to task 17 | #husky run --args "$1" "$2" 18 | 19 | ## or put your custom commands ------------------- 20 | #echo 'Husky.Net is awesome!' 21 | 22 | dotnet husky run 23 | -------------------------------------------------------------------------------- /.husky/task-runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "name": "Run dotnet format", 5 | "command": "dotnet", 6 | "args": [ 7 | "dotnet", 8 | "format", 9 | "--severity", 10 | "warn", 11 | "--include", 12 | "${staged}" 13 | ], 14 | "include": ["**/*.cs"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "LLL.DurableTask.sln" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lucas Lorentz 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 | -------------------------------------------------------------------------------- /benchmark/StoragesBenchmark/AzureStorageBenchmark.cs: -------------------------------------------------------------------------------- 1 | using DurableTask.AzureStorage; 2 | using LLL.DurableTask.Worker.Builder; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace StoragesBenchmark; 7 | 8 | public class AzureStorageBenchmark : OrchestrationBenchmark 9 | { 10 | protected override void ConfigureStorage(IServiceCollection services) 11 | { 12 | var connectionString = _configuration.GetConnectionString("AzureStorageAccount"); 13 | 14 | services.AddDurableTaskAzureStorage(options => 15 | { 16 | options.TaskHubName = "test"; 17 | options.StorageAccountClientProvider = new StorageAccountClientProvider(connectionString); 18 | }); 19 | } 20 | 21 | protected override void ConfigureWorker(IDurableTaskWorkerBuilder builder) 22 | { 23 | base.ConfigureWorker(builder); 24 | builder.HasAllOrchestrations = true; 25 | builder.HasAllActivities = true; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /benchmark/StoragesBenchmark/DurableTaskSqlServerBenchmark.cs: -------------------------------------------------------------------------------- 1 | using DurableTask.Core; 2 | using DurableTask.SqlServer; 3 | using LLL.DurableTask.Worker.Builder; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace StoragesBenchmark; 8 | 9 | public class DurableTaskSqlServerBenchmark : OrchestrationBenchmark 10 | { 11 | protected override void ConfigureStorage(IServiceCollection services) 12 | { 13 | var connectionString = _configuration.GetConnectionString("SqlServer"); 14 | 15 | var settings = new SqlOrchestrationServiceSettings(connectionString); 16 | 17 | var provider = new SqlOrchestrationService(settings); 18 | provider.CreateIfNotExistsAsync().GetAwaiter().GetResult(); 19 | 20 | services.AddSingleton(provider); 21 | services.AddSingleton(p => p.GetRequiredService()); 22 | services.AddSingleton(p => p.GetRequiredService()); 23 | } 24 | 25 | protected override void ConfigureWorker(IDurableTaskWorkerBuilder builder) 26 | { 27 | base.ConfigureWorker(builder); 28 | builder.HasAllOrchestrations = true; 29 | builder.HasAllActivities = true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /benchmark/StoragesBenchmark/InMemoryEFCoreBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace StoragesBenchmark; 5 | 6 | public class InMemoryEFCoreBenchmark : OrchestrationBenchmark 7 | { 8 | protected override void ConfigureStorage(IServiceCollection services) 9 | { 10 | services.AddDurableTaskEFCoreStorage() 11 | .UseInMemoryDatabase(Guid.NewGuid().ToString()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /benchmark/StoragesBenchmark/MySqlEFCoreBenchmark.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace StoragesBenchmark; 6 | 7 | public class MySqlEFCoreBenchmark : OrchestrationBenchmark 8 | { 9 | protected override void ConfigureStorage(IServiceCollection services) 10 | { 11 | var connectionString = _configuration.GetConnectionString("MySql"); 12 | 13 | services.AddDurableTaskEFCoreStorage() 14 | .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /benchmark/StoragesBenchmark/PostgresEFCoreBenchmark.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace StoragesBenchmark; 5 | 6 | public class PostgresEFCoreBenchmark : OrchestrationBenchmark 7 | { 8 | protected override void ConfigureStorage(IServiceCollection services) 9 | { 10 | var connectionString = _configuration.GetConnectionString("Postgres"); 11 | 12 | services.AddDurableTaskEFCoreStorage() 13 | .UseNpgsql(connectionString); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /benchmark/StoragesBenchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Configs; 2 | using BenchmarkDotNet.Running; 3 | 4 | namespace StoragesBenchmark; 5 | 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | _ = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly) 11 | .Run(args, ManualConfig 12 | .Create(DefaultConfig.Instance) 13 | .WithOption(ConfigOptions.DisableOptimizationsValidator, true)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /benchmark/StoragesBenchmark/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "CarWorker": { 4 | "commandName": "Project", 5 | "launchBrowser": false, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /benchmark/StoragesBenchmark/SqlServerEFCoreBenchmark.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace StoragesBenchmark; 5 | 6 | public class SqlServerEFCoreBenchmark : OrchestrationBenchmark 7 | { 8 | protected override void ConfigureStorage(IServiceCollection services) 9 | { 10 | var connectionString = _configuration.GetConnectionString("SqlServer"); 11 | 12 | services.AddDurableTaskEFCoreStorage() 13 | .UseSqlServer(connectionString); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /benchmark/StoragesBenchmark/StoragesBenchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /benchmark/StoragesBenchmark/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "ConnectionStrings": { 8 | "AzureStorageAccount": "UseDevelopmentStorage=true", 9 | "MySql": "server=localhost;database=durabletask;user=root;password=root", 10 | "Postgres": "Server=localhost;Port=5432;Database=durabletask;User Id=postgres;Password=root", 11 | "SqlServer": "server=localhost;database=durabletask;user=sa;password=P1ssw0rd;Encrypt=False" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | wait_for_healthcecks: 3 | image: busybox 4 | depends_on: 5 | mysql: 6 | condition: service_healthy 7 | postgres: 8 | condition: service_healthy 9 | 10 | mysql: 11 | image: mysql:8.0.31 12 | ports: 13 | - 3306:3306 14 | command: --default-authentication-plugin=mysql_native_password 15 | environment: 16 | - MYSQL_ROOT_PASSWORD=root 17 | volumes: 18 | - mysql:/var/lib/mysql:delegated 19 | healthcheck: 20 | test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] 21 | timeout: 20s 22 | retries: 10 23 | 24 | sqlserver: 25 | image: mcr.microsoft.com/mssql/server:2022-CU15-ubuntu-22.04 26 | platform: linux/amd64 27 | ports: 28 | - 1433:1433 29 | environment: 30 | - ACCEPT_EULA=1 31 | - SA_PASSWORD=P1ssw0rd 32 | volumes: 33 | - sqlserver:/var/opt/mssql:delegated 34 | cap_add: 35 | - SYS_PTRACE 36 | healthcheck: 37 | test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$${SA_PASSWORD}" -Q "SELECT 1" -b -o /dev/null 38 | interval: 10s 39 | timeout: 5s 40 | retries: 5 41 | 42 | postgres: 43 | image: postgres:15 44 | ports: 45 | - 5432:5432 46 | environment: 47 | - POSTGRES_PASSWORD=root 48 | volumes: 49 | - postgres:/var/lib/postgresql/data:delegated 50 | healthcheck: 51 | test: ["CMD-SHELL", "pg_isready -U postgres"] 52 | interval: 10s 53 | timeout: 5s 54 | retries: 5 55 | 56 | azurite: 57 | image: mcr.microsoft.com/azure-storage/azurite 58 | command: azurite --loose -l /data --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 59 | ports: 60 | - 10000:10000 61 | - 10001:10001 62 | - 10002:10002 63 | 64 | volumes: 65 | mysql: 66 | sqlserver: 67 | postgres: 68 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.100", 4 | "rollForward": "latestFeature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /readme/architecture.md: -------------------------------------------------------------------------------- 1 | 2 | ## Compose components to build your own architecture 3 | 4 | ### Microservices with server 5 | 6 | ![Diagram](readme/diagrams/architecture_1.png) 7 | 8 | ### Microservices with direct storage connection 9 | 10 | ![Diagram](readme/diagrams/architecture_2.png) 11 | 12 | ### Single service 13 | 14 | ![Diagram](readme/diagrams/architecture_3.png) 15 | 16 | ### UI for Durable Functions 17 | 18 | ![Diagram](readme/diagrams/architecture_4.png) -------------------------------------------------------------------------------- /readme/diagrams/architecture_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/diagrams/architecture_1.png -------------------------------------------------------------------------------- /readme/diagrams/architecture_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/diagrams/architecture_2.png -------------------------------------------------------------------------------- /readme/diagrams/architecture_3.drawio: -------------------------------------------------------------------------------- 1 | 1Zdbb9owFMc/DVL3QOXc4bFc1m1qpa6s2rqXySQniYuJg2MK9NPPTpyGkICgt2kvyP4f33L8y/mTjjWcry85TuNrFgDtmChYd6xRxzT7tit/lbApBKfnFULESVBIRiVMyBNoEWl1SQLIagMFY1SQtC76LEnAFzUNc85W9WEho/VdUxxBQ5j4mDbVnyQQcaH2TK/SvwCJ4nJnw+0XkTkuB+snyWIcsNWWZI071pAzJorWfD0EqnJX5qWY93lP9PlgHBJxzARz4sD9w93Tn9HvnrgZflvMLhddvUomNuUDQyCfX3cZFzGLWILpuFIHnC2TANSqSPaqMVeMpVI0pPgAQmz0ZeKlYFKKxZzqaPPk5THYkvtw4LiWJgDzCMSBcRo69SxbG+i8XAKbg+AbOYADxYI81u8aa2Si53HPU28YkUc2kabb9vQUDbfpoPoSxUH1rOpuZGPrGJWU39gJt2cWuz1iutSPcI0TBbSJVozPQqrwN10qUzWYctmKVOt2mewbsAWCu1gqMgchS0Q3y6/yQg4wULquguWSZ5lgXG78qRWnKzyVRaGGAKYkSmTblwgAl8IjcEHka3ehA3MSBAVtIDfH03w9xVuqkpmn1xl0nJE+oWbNQK14HUJfbQzrtlqiN629rjV69Cx0bjo1DMqlX0lX13Bbly1XYGGYwbtwZTW4mgB/JH4OFlF17aKJVVdljhKV9NaYAg54ewynpD2wJA2iqvKjQFrFRMAkxXnZWEkLqnMWEkqHjDKez7XCEFzfl3omOJvBViTw+lOk6MlmIPx4B1MKoTpRJvchSaSC5qE61mBqLzuma9UrCNIVZFWZjWFos4m3jQah/TzVeDj18u1/YQkyg3zzS8/PO/eqc+6U3dF6Ozja6N7LrcQ70kqsj7ESo28etpLigd7NSpwTreTFTnE7nvz4n2zCfiub6KJzZDn1172k66XwlMvYH+UMbhOTzWRB1T6nwWFJOHIqdvi4YZmIOEy+X73dmvKAyr+U+exgJ/+Tp6oZYIHVXxg4wlG2MLJ2/WLXbno+tNvNtOfYzsECdryH2Dulw+i7TQ9BdpuH2PtJe5WHeA1M7r42Df5sqr7PgDfLwWs8HozAAa8t6X3Xs7C7e2dvcAOuV6/dVtPEe6iZf/t0C5fd6ouxeKmrz25r/Bc= -------------------------------------------------------------------------------- /readme/diagrams/architecture_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/diagrams/architecture_3.png -------------------------------------------------------------------------------- /readme/diagrams/architecture_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/diagrams/architecture_4.png -------------------------------------------------------------------------------- /readme/screenshots.md: -------------------------------------------------------------------------------- 1 | # Durable Task UI Screenshots 2 | 3 | ## List and search orchestrations 4 | 5 | ![Screenshot](screenshots/orchestrations.png) 6 | 7 | ### Create orchestration 8 | 9 | ![Screenshot](screenshots/create-orchestration.png) 10 | 11 | ## View orchestration 12 | 13 | ![Screenshot](screenshots/orchestration-state.png) 14 | 15 | ## Orchestration history visualization 16 | 17 | ![Screenshot](screenshots/orchestration-history-parallel.png) 18 | ![Screenshot](screenshots/orchestration-history-sequential.png) 19 | 20 | ## Raise orchestration event 21 | 22 | ![Screenshot](screenshots/orchestration-raise-event.png) 23 | 24 | ## Terminate orchestration 25 | 26 | ![Screenshot](screenshots/orchestration-terminate.png) 27 | 28 | ## Purge orchestration 29 | 30 | ![Screenshot](screenshots/orchestration-purge.png) 31 | -------------------------------------------------------------------------------- /readme/screenshots/create-orchestration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/screenshots/create-orchestration.png -------------------------------------------------------------------------------- /readme/screenshots/orchestration-history-parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/screenshots/orchestration-history-parallel.png -------------------------------------------------------------------------------- /readme/screenshots/orchestration-history-sequential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/screenshots/orchestration-history-sequential.png -------------------------------------------------------------------------------- /readme/screenshots/orchestration-purge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/screenshots/orchestration-purge.png -------------------------------------------------------------------------------- /readme/screenshots/orchestration-raise-event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/screenshots/orchestration-raise-event.png -------------------------------------------------------------------------------- /readme/screenshots/orchestration-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/screenshots/orchestration-state.png -------------------------------------------------------------------------------- /readme/screenshots/orchestration-terminate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/screenshots/orchestration-terminate.png -------------------------------------------------------------------------------- /readme/screenshots/orchestrations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/readme/screenshots/orchestrations.png -------------------------------------------------------------------------------- /samples/Api/Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/Api/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | 3 | var services = builder.Services; 4 | services.AddDurableTaskServerStorageGrpc(options => 5 | { 6 | options.BaseAddress = new Uri("http://localhost:5000"); 7 | }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler 8 | { 9 | ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator 10 | }); 11 | 12 | services.AddDurableTaskClient(); 13 | 14 | services.AddDurableTaskApi(options => 15 | { 16 | options.DisableAuthorization = true; 17 | }); 18 | 19 | services.AddCors(c => c.AddDefaultPolicy(p => p 20 | .AllowAnyOrigin() 21 | .AllowAnyHeader() 22 | .WithMethods("GET", "POST", "DELETE"))); 23 | 24 | var app = builder.Build(); 25 | 26 | app.UseRouting(); 27 | 28 | app.UseCors(); 29 | 30 | app.MapDurableTaskApi(); 31 | 32 | app.Run(); 33 | -------------------------------------------------------------------------------- /samples/Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Api": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:5003" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /samples/Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /samples/AppHost/AppHost.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Exe 6 | net9.0 7 | enable 8 | enable 9 | true 10 | 5fdc07d4-9390-44d1-80b0-9f54717a0f64 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/AppHost/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = DistributedApplication.CreateBuilder(args); 2 | 3 | var server = builder.AddProject("server"); 4 | 5 | builder.AddProject("api") 6 | .WithReference(server); 7 | 8 | builder.AddProject("bpmnworker") 9 | .WithReference(server); 10 | 11 | builder.AddProject("carworker") 12 | .WithReference(server); 13 | 14 | builder.AddProject("flightworker") 15 | .WithReference(server); 16 | 17 | builder.AddProject("hotelworker") 18 | .WithReference(server); 19 | 20 | builder.AddProject("orchestrationworker") 21 | .WithReference(server); 22 | 23 | builder.AddProject("ui") 24 | .WithReference(server); 25 | 26 | builder.Build().Run(); 27 | -------------------------------------------------------------------------------- /samples/AppHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "https://localhost:17198;http://localhost:15070", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development", 11 | "DOTNET_ENVIRONMENT": "Development", 12 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21105", 13 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22276" 14 | } 15 | }, 16 | "http": { 17 | "commandName": "Project", 18 | "dotnetRunMessages": true, 19 | "launchBrowser": true, 20 | "applicationUrl": "http://localhost:15070", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development", 23 | "DOTNET_ENVIRONMENT": "Development", 24 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19293", 25 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20163" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/AppHost/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/AppHost/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "Aspire.Hosting.Dcp": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Activities/EmptyActivity.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using LLL.DurableTask.Worker; 3 | using LLL.DurableTask.Worker.Attributes; 4 | 5 | namespace BpmnWorker.Activities; 6 | 7 | [Activity(Name = "Empty")] 8 | public class EmptyActivity : ActivityBase 9 | { 10 | public override Task ExecuteAsync(object input) 11 | { 12 | return Task.FromResult(default(object)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Activities/ScriptActivity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using LLL.DurableTask.Worker; 4 | using LLL.DurableTask.Worker.Attributes; 5 | using Microsoft.Extensions.Logging; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace BpmnWorker.Activities; 9 | 10 | [Activity(Name = "Script")] 11 | public class ScriptActivity : ActivityBase 12 | { 13 | private readonly IScriptExecutor _scriptExecutor; 14 | private readonly ILogger _logger; 15 | 16 | public ScriptActivity( 17 | IScriptExecutor scriptExecutor, 18 | ILogger logger) 19 | { 20 | _scriptExecutor = scriptExecutor; 21 | _logger = logger; 22 | } 23 | 24 | public override async Task ExecuteAsync(Input input) 25 | { 26 | _logger.LogWarning("Executing script {format}", input.ScriptFormat); 27 | 28 | return await _scriptExecutor.Execute(input.ScriptFormat, input.Script, input.Variables 29 | .ToObject>()); 30 | } 31 | 32 | public class Input 33 | { 34 | public string Name { get; set; } 35 | public string ScriptFormat { get; set; } 36 | public string Script { get; set; } 37 | public JObject Variables { get; set; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /samples/BpmnWorker/BpmnWorker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Designer 24 | 25 | 26 | Designer 27 | 28 | 29 | Designer 30 | 31 | 32 | Designer 33 | 34 | 35 | Designer 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using BpmnWorker.Activities; 3 | using BpmnWorker.Providers; 4 | using BpmnWorker.Scripting; 5 | 6 | var builder = Host.CreateApplicationBuilder(args); 7 | 8 | var services = builder.Services; 9 | services.AddDurableTaskServerStorageGrpc(options => 10 | { 11 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 12 | { 13 | AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); 14 | options.BaseAddress = new Uri("http://localhost:5000"); 15 | } 16 | else 17 | { 18 | options.BaseAddress = new Uri("https://localhost:5001"); 19 | } 20 | }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler 21 | { 22 | ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator 23 | }); 24 | 25 | services.AddDurableTaskWorker(builder => 26 | { 27 | builder.AddAnnotatedFrom(typeof(Program).Assembly); 28 | }); 29 | 30 | services.AddSingleton(); 31 | 32 | services.AddSingleton(); 33 | services.AddScoped(p => new ServiceProviderScriptEngineFactory(p, "c#")); 34 | services.AddSingleton(); 35 | services.AddScoped(p => new ServiceProviderScriptEngineFactory(p, "javascript")); 36 | 37 | services.AddScoped(); 38 | 39 | var app = builder.Build(); 40 | 41 | app.Run(); 42 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "BpmnWorker": { 4 | "commandName": "Project", 5 | "launchBrowser": false, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Providers/IBPMNProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BpmnWorker.Providers; 4 | 5 | public interface IBPMNProvider 6 | { 7 | public Task GetBPMN(string name); 8 | } 9 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Providers/LocalFileBPMNProvider.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | 4 | namespace BpmnWorker.Providers; 5 | 6 | public class LocalFileBPMNProvider : IBPMNProvider 7 | { 8 | public Task GetBPMN(string name) 9 | { 10 | var bpmnPath = $"Workflows/{name}.bpmn"; 11 | 12 | var bytes = File.ReadAllBytes(bpmnPath); 13 | 14 | return Task.FromResult(bytes); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Scripting/CSharpScriptEngine.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Microsoft.CodeAnalysis.CSharp.Scripting; 4 | using Newtonsoft.Json; 5 | 6 | namespace BpmnWorker.Scripting; 7 | 8 | public class CSharpScriptEngine : IScriptEngine 9 | { 10 | public async Task Execute(string script, IDictionary variables) 11 | { 12 | var output = await CSharpScript.EvaluateAsync(script); 13 | if (output == null) 14 | return default; 15 | 16 | if (output is T t) 17 | return t; 18 | 19 | var serialized = JsonConvert.SerializeObject(output); 20 | return JsonConvert.DeserializeObject(serialized); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Scripting/IScriptEngine.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace BpmnWorker.Scripting; 5 | 6 | public interface IScriptEngine 7 | { 8 | Task Execute(string script, IDictionary variables); 9 | } 10 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Scripting/IScriptEngineFactory.cs: -------------------------------------------------------------------------------- 1 | namespace BpmnWorker.Scripting; 2 | 3 | public interface IScriptEngineFactory 4 | { 5 | string Language { get; } 6 | 7 | IScriptEngine Create(); 8 | } 9 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Scripting/IScriptExecutor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace BpmnWorker.Activities; 5 | 6 | public interface IScriptExecutor 7 | { 8 | Task Execute(string language, string script, IDictionary variables = null); 9 | } 10 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Scripting/JavaScriptScriptEngine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.ClearScript.V8; 5 | using Newtonsoft.Json; 6 | 7 | namespace BpmnWorker.Scripting; 8 | 9 | public class JavaScriptScriptEngine : IScriptEngine 10 | { 11 | public Task Execute(string script, IDictionary variables) 12 | { 13 | using var engine = new V8ScriptEngine(); 14 | if (variables != null) 15 | { 16 | foreach (var kv in variables) 17 | { 18 | if (kv.Value != null) 19 | { 20 | var inputJson = JsonConvert.SerializeObject(kv.Value); 21 | var inputJs = engine.Script.JSON.parse(inputJson); 22 | engine.Script[kv.Key] = inputJs; 23 | } 24 | } 25 | } 26 | 27 | engine.AddHostType("Console", typeof(Console)); 28 | 29 | var outputJs = engine.Evaluate(script); 30 | if (outputJs == null) 31 | return Task.FromResult(default(T)); 32 | 33 | if (engine.Script.JSON.stringify(outputJs) is not string outputJson) 34 | return Task.FromResult(default(T)); 35 | 36 | var output = JsonConvert.DeserializeObject(outputJson); 37 | return Task.FromResult(output); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Scripting/ScriptExecutor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using BpmnWorker.Scripting; 6 | 7 | namespace BpmnWorker.Activities; 8 | 9 | public class ScriptExecutor : IScriptExecutor 10 | { 11 | private readonly ILookup _scriptEngineFactories; 12 | 13 | public ScriptExecutor(IEnumerable scriptEngineFactories) 14 | { 15 | _scriptEngineFactories = scriptEngineFactories 16 | .ToLookup(f => f.Language, StringComparer.OrdinalIgnoreCase); 17 | } 18 | 19 | public async Task Execute(string language, string script, IDictionary variables = null) 20 | { 21 | var scriptEngineFactory = _scriptEngineFactories[language].FirstOrDefault() 22 | ?? throw new NotSupportedException($"Script language {language} is not supported"); 23 | var scriptEngine = scriptEngineFactory.Create(); 24 | 25 | return await scriptEngine.Execute(script, variables); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Scripting/ServiceProviderScriptEngineFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace BpmnWorker.Scripting; 5 | 6 | public class ServiceProviderScriptEngineFactory : ResolveTypeScriptEngineFactory 7 | { 8 | public ServiceProviderScriptEngineFactory(IServiceProvider serviceProvider, 9 | string language) 10 | : base(serviceProvider, language, typeof(T)) 11 | { 12 | } 13 | } 14 | 15 | public class ResolveTypeScriptEngineFactory : IScriptEngineFactory 16 | { 17 | private readonly IServiceProvider _serviceProvider; 18 | private readonly Type _type; 19 | 20 | public ResolveTypeScriptEngineFactory( 21 | IServiceProvider serviceProvider, 22 | string language, 23 | Type type) 24 | { 25 | _serviceProvider = serviceProvider; 26 | Language = language; 27 | _type = type; 28 | } 29 | 30 | public string Language { get; } 31 | 32 | public IScriptEngine Create() 33 | { 34 | return _serviceProvider.GetRequiredService(_type) as IScriptEngine; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Spec/DC.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /samples/BpmnWorker/Spec/Generate.txt: -------------------------------------------------------------------------------- 1 | dotnet tool install --global dotnet-xscgen --version 2.0.370 2 | 3 | xscgen --nullable *.xsd -------------------------------------------------------------------------------- /samples/BpmnWorker/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/BpmnWorker/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /samples/CarWorker/Activities/BookCarActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using LLL.DurableTask.Worker; 4 | using LLL.DurableTask.Worker.Attributes; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace CarWorker.Activities; 8 | 9 | [Activity(Name = "BookCar", Version = "v1")] 10 | public class BookCarActivity : ActivityBase 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public BookCarActivity(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public override Task ExecuteAsync(BookCarInput input) 20 | { 21 | var bookingId = Guid.NewGuid(); 22 | 23 | _logger.LogInformation("Booking car {bookingId}", bookingId); 24 | 25 | return Task.FromResult(new BookCarResult 26 | { 27 | BookingId = bookingId 28 | }); 29 | } 30 | } 31 | 32 | public class BookCarInput 33 | { 34 | } 35 | 36 | public class BookCarResult 37 | { 38 | public Guid BookingId { get; set; } 39 | } 40 | -------------------------------------------------------------------------------- /samples/CarWorker/Activities/CancelCarActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using LLL.DurableTask.Worker; 4 | using LLL.DurableTask.Worker.Attributes; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace CarWorker.Activities; 8 | 9 | [Activity(Name = "CancelCar", Version = "v1")] 10 | public class CancelCarActivity : ActivityBase 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public CancelCarActivity(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public override Task ExecuteAsync(CancelCarInput input) 20 | { 21 | _logger.LogInformation("Canceling car {bookingId}", input.BookingId); 22 | 23 | return Task.FromResult(new CancelCarResult()); 24 | } 25 | } 26 | 27 | public class CancelCarInput 28 | { 29 | public Guid BookingId { get; set; } 30 | } 31 | 32 | public class CancelCarResult 33 | { 34 | } 35 | -------------------------------------------------------------------------------- /samples/CarWorker/CarWorker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/CarWorker/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | var builder = Host.CreateApplicationBuilder(); 4 | 5 | var services = builder.Services; 6 | services.AddDurableTaskServerStorageGrpc(options => 7 | { 8 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 9 | { 10 | AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); 11 | options.BaseAddress = new Uri("http://localhost:5000"); 12 | } 13 | else 14 | { 15 | options.BaseAddress = new Uri("https://localhost:5001"); 16 | } 17 | }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler 18 | { 19 | ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator 20 | }); 21 | 22 | services.AddDurableTaskWorker(builder => 23 | { 24 | builder.AddAnnotatedFrom(typeof(Program).Assembly); 25 | }); 26 | 27 | var app = builder.Build(); 28 | 29 | app.Run(); 30 | -------------------------------------------------------------------------------- /samples/CarWorker/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "CarWorker": { 4 | "commandName": "Project", 5 | "launchBrowser": false, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/CarWorker/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/CarWorker/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/FlightWorker/Activities/BookFlightActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using LLL.DurableTask.Worker; 4 | using LLL.DurableTask.Worker.Attributes; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace FlightWorker.Activities; 8 | 9 | [Activity(Name = "BookFlight", Version = "v1")] 10 | public class BookFlightActivity : ActivityBase 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public BookFlightActivity(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public override Task ExecuteAsync(BookFlightInput input) 20 | { 21 | var bookingId = Guid.NewGuid(); 22 | 23 | _logger.LogInformation("Booking Flight {bookingId}", bookingId); 24 | 25 | return Task.FromResult(new BookFlightResult 26 | { 27 | BookingId = bookingId 28 | }); 29 | } 30 | } 31 | 32 | public class BookFlightInput 33 | { 34 | } 35 | 36 | public class BookFlightResult 37 | { 38 | public Guid BookingId { get; set; } 39 | } 40 | -------------------------------------------------------------------------------- /samples/FlightWorker/Activities/CancelFlightActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using LLL.DurableTask.Worker; 4 | using LLL.DurableTask.Worker.Attributes; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace FlightWorker.Activities; 8 | 9 | [Activity(Name = "CancelFlight", Version = "v1")] 10 | public class CancelFlightActivity : ActivityBase 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public CancelFlightActivity(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public override Task ExecuteAsync(CancelFlightInput input) 20 | { 21 | _logger.LogInformation("Canceling Flight {bookingId}", input.BookingId); 22 | 23 | return Task.FromResult(new CancelFlightResult()); 24 | } 25 | } 26 | 27 | public class CancelFlightInput 28 | { 29 | public Guid BookingId { get; set; } 30 | } 31 | 32 | public class CancelFlightResult 33 | { 34 | } 35 | -------------------------------------------------------------------------------- /samples/FlightWorker/FlightWorker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/FlightWorker/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | var builder = Host.CreateApplicationBuilder(); 4 | 5 | var services = builder.Services; 6 | services.AddDurableTaskServerStorageGrpc(options => 7 | { 8 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 9 | { 10 | AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); 11 | options.BaseAddress = new Uri("http://localhost:5000"); 12 | } 13 | else 14 | { 15 | options.BaseAddress = new Uri("https://localhost:5001"); 16 | } 17 | }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler 18 | { 19 | ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator 20 | }); 21 | 22 | services.AddDurableTaskWorker(builder => 23 | { 24 | builder.AddAnnotatedFrom(typeof(Program).Assembly); 25 | }); 26 | 27 | var app = builder.Build(); 28 | 29 | app.Run(); 30 | -------------------------------------------------------------------------------- /samples/FlightWorker/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "FlightWorker": { 4 | "commandName": "Project", 5 | "launchBrowser": false, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/FlightWorker/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/FlightWorker/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/HotelWorker/Activities/BookHotelActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using LLL.DurableTask.Worker; 4 | using LLL.DurableTask.Worker.Attributes; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace HotelWorker.Activities; 8 | 9 | [Activity(Name = "BookHotel", Version = "v1")] 10 | public class BookHotelActivity : ActivityBase 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public BookHotelActivity(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public override Task ExecuteAsync(BookHotelInput input) 20 | { 21 | var bookingId = Guid.NewGuid(); 22 | 23 | _logger.LogInformation("Booking Hotel {bookingId}", bookingId); 24 | 25 | return Task.FromResult(new BookHotelResult 26 | { 27 | BookingId = bookingId 28 | }); 29 | } 30 | } 31 | 32 | public class BookHotelInput 33 | { 34 | } 35 | 36 | public class BookHotelResult 37 | { 38 | public Guid BookingId { get; set; } 39 | } 40 | -------------------------------------------------------------------------------- /samples/HotelWorker/Activities/CancelHotelActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using LLL.DurableTask.Worker; 4 | using LLL.DurableTask.Worker.Attributes; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace HotelWorker.Activities; 8 | 9 | [Activity(Name = "CancelHotel", Version = "v1")] 10 | public class CancelHotelActivity : ActivityBase 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public CancelHotelActivity(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public override Task ExecuteAsync(CancelHotelInput input) 20 | { 21 | _logger.LogInformation("Canceling Hotel {bookingId}", input.BookingId); 22 | 23 | return Task.FromResult(new CancelHotelResult()); 24 | } 25 | } 26 | 27 | public class CancelHotelInput 28 | { 29 | public Guid BookingId { get; set; } 30 | } 31 | 32 | public class CancelHotelResult 33 | { 34 | } 35 | -------------------------------------------------------------------------------- /samples/HotelWorker/HotelWorker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/HotelWorker/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | var builder = Host.CreateApplicationBuilder(); 4 | 5 | var services = builder.Services; 6 | services.AddDurableTaskServerStorageGrpc(options => 7 | { 8 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 9 | { 10 | AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); 11 | options.BaseAddress = new Uri("http://localhost:5000"); 12 | } 13 | else 14 | { 15 | options.BaseAddress = new Uri("https://localhost:5001"); 16 | } 17 | }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler 18 | { 19 | ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator 20 | }); 21 | 22 | services.AddDurableTaskWorker(builder => 23 | { 24 | builder.AddAnnotatedFrom(typeof(Program).Assembly); 25 | }); 26 | 27 | var app = builder.Build(); 28 | 29 | app.Run(); 30 | -------------------------------------------------------------------------------- /samples/HotelWorker/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "HotelWorker": { 4 | "commandName": "Project", 5 | "launchBrowser": false, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/HotelWorker/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/HotelWorker/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/OrchestrationWorker/OrchestrationWorker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/OrchestrationWorker/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | var builder = Host.CreateApplicationBuilder(); 4 | 5 | var services = builder.Services; 6 | services.AddDurableTaskServerStorageGrpc(options => 7 | { 8 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 9 | { 10 | AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); 11 | options.BaseAddress = new Uri("http://localhost:5000"); 12 | } 13 | else 14 | { 15 | options.BaseAddress = new Uri("https://localhost:5001"); 16 | } 17 | }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler 18 | { 19 | ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator 20 | }); 21 | 22 | services.AddDurableTaskWorker(builder => 23 | { 24 | builder.AddAnnotatedFrom(typeof(Program).Assembly); 25 | }); 26 | 27 | var app = builder.Build(); 28 | 29 | app.Run(); 30 | -------------------------------------------------------------------------------- /samples/OrchestrationWorker/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "OrchestrationWorker": { 4 | "commandName": "Project", 5 | "launchBrowser": false, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/OrchestrationWorker/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/OrchestrationWorker/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/Server/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | 3 | var services = builder.Services; 4 | services.AddGrpc(); 5 | 6 | // var mysqlConnectionString = "server=localhost;database=durabletask;user=root;password=root"; 7 | 8 | services.AddDurableTaskEFCoreStorage() 9 | .UseInMemoryDatabase("Sample"); 10 | // .UseNpgsql("Server=localhost;Port=5432;Database=durabletask;User Id=postgres;Password=root"); 11 | // .UseMySql(mysqlConnectionString, ServerVersion.AutoDetect(mysqlConnectionString)); 12 | // .UseSqlServer("server=localhost;database=durabletask;user=sa;password=P1ssw0rd"); 13 | 14 | services.AddDurableTaskServer(builder => 15 | { 16 | builder.AddGrpcEndpoints(); 17 | }); 18 | 19 | var app = builder.Build(); 20 | 21 | app.UseRouting(); 22 | 23 | app.MapDurableTaskServerGrpcService(); 24 | 25 | app.Run(); 26 | 27 | -------------------------------------------------------------------------------- /samples/Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Server": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /samples/Server/Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "Kestrel": { 10 | "EndpointDefaults": { 11 | "Protocols": "Http2" 12 | } 13 | }, 14 | "AllowedHosts": "*" 15 | } 16 | -------------------------------------------------------------------------------- /samples/Ui/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(); 2 | 3 | var services = builder.Services; 4 | services.AddDurableTaskUi(options => 5 | { 6 | options.ApiBaseUrl = "https://localhost:5003/api"; 7 | }); 8 | 9 | var app = builder.Build(); 10 | 11 | app.UseDurableTaskUi(); 12 | 13 | app.Run(); 14 | -------------------------------------------------------------------------------- /samples/Ui/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Ui": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:5002" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /samples/Ui/Ui.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/Ui/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/Ui/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/Builder/DurableTaskApiEndpointConventionBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.AspNetCore.Builder; 4 | 5 | namespace LLL.DurableTask.Api; 6 | 7 | public class DurableTaskApiEndpointConventionBuilder : IEndpointConventionBuilder 8 | { 9 | private readonly List _endpoints; 10 | 11 | public DurableTaskApiEndpointConventionBuilder() 12 | { 13 | _endpoints = new List(); 14 | } 15 | 16 | public void AddEndpoints(IEnumerable endpoints) 17 | { 18 | _endpoints.AddRange(endpoints); 19 | } 20 | 21 | public void Add(Action convention) 22 | { 23 | foreach (var endpoint in _endpoints) 24 | { 25 | endpoint.Add(convention); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/Builder/EndpointRouteBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using LLL.DurableTask.Api; 3 | using LLL.DurableTask.Api.DependencyInjection; 4 | using LLL.DurableTask.Api.Endpoints; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace Microsoft.AspNetCore.Builder; 12 | 13 | public static class EndpointRouteBuilderExtensions 14 | { 15 | public static IEndpointConventionBuilder MapDurableTaskApi(this IEndpointRouteBuilder builder) 16 | { 17 | return builder.MapDurableTaskApi("/api"); 18 | } 19 | 20 | public static IEndpointConventionBuilder MapDurableTaskApi(this IEndpointRouteBuilder builder, 21 | PathString prefix) 22 | { 23 | var options = builder.ServiceProvider.GetRequiredService>().Value; 24 | 25 | var durableTaskEndpointConventionBuilder = new DurableTaskApiEndpointConventionBuilder(); 26 | 27 | durableTaskEndpointConventionBuilder.AddEndpoints(builder.MapEntrypointEndpoint(prefix)); 28 | durableTaskEndpointConventionBuilder.AddEndpoints(builder.MapOrchestrationEndpoints(prefix)); 29 | 30 | if (options.DisableAuthorization) 31 | { 32 | durableTaskEndpointConventionBuilder.Add(builder => 33 | { 34 | foreach (var authorizeAttribute in builder.Metadata.OfType().ToArray()) 35 | { 36 | builder.Metadata.Remove(authorizeAttribute); 37 | } 38 | }); 39 | } 40 | 41 | return durableTaskEndpointConventionBuilder; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/DependencyInjection/DurableTaskApiOptions.cs: -------------------------------------------------------------------------------- 1 | namespace LLL.DurableTask.Api.DependencyInjection; 2 | 3 | public class DurableTaskApiOptions 4 | { 5 | public bool DisableAuthorization { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/DependencyInjection/DurableTaskApiServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LLL.DurableTask.Api.DependencyInjection; 3 | 4 | namespace Microsoft.Extensions.DependencyInjection; 5 | 6 | public static class DurableTaskApiServiceCollectionExtensions 7 | { 8 | public static IServiceCollection AddDurableTaskApi( 9 | this IServiceCollection services, 10 | Action configure = null) 11 | { 12 | services.AddOptions(); 13 | 14 | if (configure != null) 15 | services.Configure(configure); 16 | 17 | return services; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/DurableTaskPolicy.cs: -------------------------------------------------------------------------------- 1 | namespace LLL.DurableTask.Api; 2 | 3 | public static class DurableTaskPolicy 4 | { 5 | public const string Entrypoint = "DurableTaskEntrypoint"; 6 | public const string Read = "DurableTaskRead"; 7 | public const string ReadHistory = "DurableTaskReadHistory"; 8 | public const string Create = "DurableTaskCreate"; 9 | public const string Terminate = "DurableTaskTerminate"; 10 | public const string Rewind = "DurableTaskRewind"; 11 | public const string RaiseEvent = "DurableTaskRaiseEvent"; 12 | public const string Purge = "DurableTaskPurge"; 13 | } 14 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/LLL.DurableTask.Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0;net8.0 5 | 6 | 7 | 8 | Exposes Durable Task management operations as a REST API 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/Metadata/DurableTaskEndpointMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace LLL.DurableTask.Api.Metadata; 2 | 3 | public class DurableTaskEndpointMetadata 4 | { 5 | public string Id { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/Models/CreateOrchestrationRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace LLL.DurableTask.Server.Api.Models; 6 | 7 | public class CreateOrchestrationRequest 8 | { 9 | [Required] 10 | public string Name { get; set; } 11 | 12 | public string Version { get; set; } 13 | 14 | public string InstanceId { get; set; } 15 | 16 | public JToken Input { get; set; } 17 | 18 | public Dictionary Tags { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/Models/EndpointInfo.cs: -------------------------------------------------------------------------------- 1 | namespace LLL.DurableTask.Api.Models; 2 | 3 | public class EndpointInfo 4 | { 5 | public string Href { get; set; } 6 | public string Method { get; set; } 7 | public bool Authorized { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/Models/EntrypointResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using LLL.DurableTask.Core; 3 | 4 | namespace LLL.DurableTask.Api.Models; 5 | 6 | public class EntrypointResponse 7 | { 8 | public Dictionary Endpoints { get; set; } = new Dictionary(); 9 | public OrchestrationFeature[] Features { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/Models/RewindRequest.cs: -------------------------------------------------------------------------------- 1 | namespace LLL.DurableTask.Api.Models; 2 | 3 | public class RewindRequest 4 | { 5 | public string Reason { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Api/Models/TerminateRequest.cs: -------------------------------------------------------------------------------- 1 | namespace LLL.DurableTask.Api.Models; 2 | 3 | public class TerminateRequest 4 | { 5 | public string Reason { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.AzureStorage/AzureStorageOrchestrationServiceFeaturesClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableTask.AzureStorage; 3 | using LLL.DurableTask.Core; 4 | 5 | namespace LLL.DurableTaskExtensions.AzureStorage; 6 | 7 | public class AzureStorageOrchestrationServiceFeaturesClient : IOrchestrationServiceFeaturesClient 8 | { 9 | private readonly AzureStorageOrchestrationService _azureStorageOrchestrationService; 10 | 11 | public AzureStorageOrchestrationServiceFeaturesClient( 12 | AzureStorageOrchestrationService azureStorageOrchestrationService) 13 | { 14 | _azureStorageOrchestrationService = azureStorageOrchestrationService; 15 | } 16 | 17 | public Task GetFeatures() 18 | { 19 | return Task.FromResult(new OrchestrationFeature[] 20 | { 21 | OrchestrationFeature.SearchByInstanceId, 22 | OrchestrationFeature.SearchByCreatedTime, 23 | OrchestrationFeature.SearchByStatus, 24 | OrchestrationFeature.Rewind 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.AzureStorage/AzureStorageOrchestrationServiceRewindClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableTask.AzureStorage; 3 | using LLL.DurableTask.Core; 4 | 5 | namespace LLL.DurableTaskExtensions.AzureStorage; 6 | 7 | public class AzureStorageOrchestrationServiceRewindClient : IOrchestrationServiceRewindClient 8 | { 9 | private readonly AzureStorageOrchestrationService _azureStorageOrchestrationService; 10 | 11 | public AzureStorageOrchestrationServiceRewindClient( 12 | AzureStorageOrchestrationService azureStorageOrchestrationService) 13 | { 14 | _azureStorageOrchestrationService = azureStorageOrchestrationService; 15 | } 16 | 17 | public async Task RewindTaskOrchestrationAsync(string instanceId, string reason) 18 | { 19 | await _azureStorageOrchestrationService.RewindTaskOrchestrationAsync(instanceId, reason); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.AzureStorage/DependencyInjection/AzureStorageServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DurableTask.AzureStorage; 3 | using DurableTask.Core; 4 | using DurableTask.Core.Query; 5 | using LLL.DurableTask.Core; 6 | using LLL.DurableTaskExtensions.AzureStorage; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace Microsoft.Extensions.DependencyInjection; 10 | 11 | public static class AzureStorageServiceCollectionExtensions 12 | { 13 | public static IServiceCollection AddDurableTaskAzureStorage( 14 | this IServiceCollection services, 15 | Action configure) 16 | { 17 | services.AddOptions() 18 | .Configure(configure); 19 | 20 | services.AddTransient(p => p.GetRequiredService>().Value); 21 | 22 | services.AddSingleton(); 23 | services.AddSingleton(); 24 | 25 | services.AddSingleton(p => p.GetService()); 26 | services.AddSingleton(p => p.GetService()); 27 | services.AddSingleton(p => p.GetService()); 28 | services.AddSingleton(p => p.GetService()); 29 | services.AddSingleton(p => p.GetService()); 30 | services.AddSingleton(p => p.GetService()); 31 | 32 | return services; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.AzureStorage/LLL.DurableTask.AzureStorage.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | 6 | 7 | 8 | Extends Durable Task Azure Storage 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.AzureStorage/README.md: -------------------------------------------------------------------------------- 1 | # LLL.DurableTask.AzureStorage [![Nuget](https://img.shields.io/nuget/v/LLL.DurableTask.AzureStorage)](https://www.nuget.org/packages/LLL.DurableTask.AzureStorage/) 2 | 3 | Dependency injection extensions to configure the official Azure Storage. 4 | 5 | ## Configuration 6 | 7 | ```C# 8 | services.AddDurableTaskAzureStorage(options => 9 | { 10 | options.TaskHubName = "Test"; 11 | options.StorageConnectionString = "UseDevelopmentStorage=true"; 12 | }); 13 | ``` 14 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Client/DependencyInjection/DurableTaskClientServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using DurableTask.Core; 2 | using LLL.DurableTask.Core.Serializing; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection; 7 | 8 | public static class DurableTaskClientServiceCollectionExtensions 9 | { 10 | public static IServiceCollection AddDurableTaskClient( 11 | this IServiceCollection services) 12 | { 13 | services.TryAddSingleton(serviceProvider => 14 | { 15 | var orchestrationServiceClient = serviceProvider.GetRequiredService(); 16 | var jsonDataConverter = new TypelessJsonDataConverter(); 17 | var loggerFactory = serviceProvider.GetService(); 18 | return new TaskHubClient(orchestrationServiceClient, jsonDataConverter, loggerFactory); 19 | }); 20 | 21 | return services; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Client/LLL.DurableTask.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | 6 | 7 | 8 | Configure Durable Task client 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Client/README.md: -------------------------------------------------------------------------------- 1 | # LLL.DurableTask.Client [![Nuget](https://img.shields.io/nuget/v/LLL.DurableTask.Client)](https://www.nuget.org/packages/LLL.DurableTask.Client/) 2 | 3 | Dependency injection extensions to configure TaskHubClient. 4 | 5 | Allows management of orchestrations via code. 6 | 7 | ## Depends on 8 | 9 | - Storage 10 | 11 | ## Configuration 12 | 13 | ```C# 14 | services.AddDurableTaskClient(); 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```C# 20 | public IActionResult BookPackage([FromService] TaskHubClient taskHubClient) { 21 | await taskHubClient.CreateOrchestrationInstanceAsync("BookParallel", "v1", new { 22 | bookFlight: true, 23 | bookHotel: true, 24 | bookCar: true 25 | }); 26 | ... 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Core/IDistributedOrchestrationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using DurableTask.Core; 5 | 6 | namespace LLL.DurableTask.Core; 7 | 8 | public interface IDistributedOrchestrationService 9 | { 10 | Task LockNextTaskOrchestrationWorkItemAsync( 11 | TimeSpan receiveTimeout, 12 | INameVersionInfo[] orchestrations, 13 | CancellationToken cancellationToken); 14 | 15 | Task LockNextTaskActivityWorkItem( 16 | TimeSpan receiveTimeout, 17 | INameVersionInfo[] activities, 18 | CancellationToken cancellationToken); 19 | } 20 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Core/IOrchestrationServiceFeaturesClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace LLL.DurableTask.Core; 4 | 5 | public interface IOrchestrationServiceFeaturesClient 6 | { 7 | Task GetFeatures(); 8 | } 9 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Core/IOrchestrationServiceRewindClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace LLL.DurableTask.Core; 4 | 5 | public interface IOrchestrationServiceRewindClient 6 | { 7 | Task RewindTaskOrchestrationAsync(string instanceId, string reason); 8 | } 9 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Core/LLL.DurableTask.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | 6 | 7 | 8 | Extends Durable Task Core contracts 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Core/OrchestrationFeature.cs: -------------------------------------------------------------------------------- 1 | namespace LLL.DurableTask.Core; 2 | 3 | public enum OrchestrationFeature 4 | { 5 | SearchByInstanceId = 1, 6 | SearchByName = 2, 7 | SearchByCreatedTime = 3, 8 | SearchByStatus = 4, 9 | Rewind = 5, 10 | Tags = 6, 11 | StatePerExecution = 7 12 | } 13 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Core/OrchestrationQueryExtended.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using DurableTask.Core.Query; 4 | 5 | namespace LLL.DurableTask.Core; 6 | 7 | public class OrchestrationQueryExtended : OrchestrationQuery 8 | { 9 | public string NamePrefix { get; set; } 10 | public DateTime? CompletedTimeFrom { get; set; } 11 | public DateTime? CompletedTimeTo { get; set; } 12 | public bool IncludePreviousExecutions { get; set; } 13 | public Dictionary Tags { get; } = new Dictionary(); 14 | } 15 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Core/PurgeInstanceFilterExtended.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using DurableTask.Core; 4 | 5 | namespace LLL.DurableTask.Core; 6 | 7 | public class PurgeInstanceFilterExtended : PurgeInstanceFilter 8 | { 9 | public PurgeInstanceFilterExtended( 10 | DateTime createdTimeFrom, 11 | DateTime? createdTimeTo, 12 | IEnumerable runtimeStatus) 13 | : base(createdTimeFrom, createdTimeTo, runtimeStatus) 14 | { 15 | } 16 | 17 | public int? Limit { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Core/README.md: -------------------------------------------------------------------------------- 1 | # LLL.DurableTask.Core [![Nuget](https://img.shields.io/nuget/v/LLL.DurableTask.Core)](https://www.nuget.org/packages/LLL.DurableTask.Core/) 2 | 3 | Extends Durable Task Core with more interfaces and features. 4 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Core/Serializing/TypelessJsonDataConverter.cs: -------------------------------------------------------------------------------- 1 | using DurableTask.Core.Serializing; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | using Newtonsoft.Json.Serialization; 5 | 6 | namespace LLL.DurableTask.Core.Serializing; 7 | 8 | public class TypelessJsonDataConverter : JsonDataConverter 9 | { 10 | public TypelessJsonDataConverter() 11 | : base(new JsonSerializerSettings 12 | { 13 | ContractResolver = new DefaultContractResolver 14 | { 15 | NamingStrategy = new CamelCaseNamingStrategy() 16 | }, 17 | Converters = { 18 | new HistoryEventConverter(), 19 | new StringEnumConverter() 20 | }, 21 | DateTimeZoneHandling = DateTimeZoneHandling.Utc, 22 | NullValueHandling = NullValueHandling.Ignore 23 | }) 24 | { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.InMemory/DependencyInjection/InMemoryEFCoreOrchestrationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LLL.DurableTask.EFCore; 3 | using LLL.DurableTask.EFCore.DependencyInjection; 4 | using LLL.DurableTask.EFCore.InMemory; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Infrastructure; 7 | using Microsoft.EntityFrameworkCore.Storage; 8 | 9 | namespace Microsoft.Extensions.DependencyInjection; 10 | 11 | public static class InMemoryEFCoreOrchestrationBuilderExtensions 12 | { 13 | public static IEFCoreOrchestrationBuilder UseInMemoryDatabase( 14 | this IEFCoreOrchestrationBuilder builder, 15 | string databaseName, 16 | Action inMemoryOptionsAction = null) 17 | { 18 | return builder.UseInMemoryDatabase(databaseName, null, inMemoryOptionsAction); 19 | } 20 | 21 | public static IEFCoreOrchestrationBuilder UseInMemoryDatabase( 22 | this IEFCoreOrchestrationBuilder builder, 23 | string databaseName, 24 | InMemoryDatabaseRoot databaseRoot, 25 | Action inMemoryOptionsAction = null) 26 | { 27 | builder.Services.AddSingleton(); 28 | 29 | return builder.ConfigureDbContext(options => 30 | { 31 | options.UseInMemoryDatabase(databaseName, databaseRoot, inMemoryOptions => 32 | { 33 | inMemoryOptionsAction?.Invoke(inMemoryOptions); 34 | }); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.InMemory/LLL.DurableTask.EFCore.InMemory.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | latest 6 | 7 | 8 | 9 | InMemory extensions to EFCore Durable Task Storage 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.InMemory/README.md: -------------------------------------------------------------------------------- 1 | # LLL.DurableTask.EFCore.InMemory [![Nuget](https://img.shields.io/nuget/v/LLL.DurableTask.EFCore.InMemory)](https://www.nuget.org/packages/LLL.DurableTask.EFCore.InMemory/) 2 | 3 | Extension to EFCore storage with queries specific to InMemory database. 4 | 5 | ## Configuration 6 | 7 | ```C# 8 | services.AddDurableTaskEFCoreStorage() 9 | .UseInMemoryDatabase("DatabaseName"); 10 | ``` 11 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.MySql/DependencyInjection/MySqlEFCoreOrchestrationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LLL.DurableTask.EFCore; 3 | using LLL.DurableTask.EFCore.DependencyInjection; 4 | using LLL.DurableTask.EFCore.MySql; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Infrastructure; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace Microsoft.Extensions.DependencyInjection; 10 | 11 | public static class MySqlEFCoreOrchestrationBuilderExtensions 12 | { 13 | public static IEFCoreOrchestrationBuilder UseMySql( 14 | this IEFCoreOrchestrationBuilder builder, 15 | string connectionString, 16 | ServerVersion serverVersion, 17 | Action mysqlOptionsAction = null) 18 | { 19 | builder.Services.AddSingleton(); 20 | 21 | return builder.ConfigureDbContext(options => 22 | { 23 | options.AddInterceptors(new StraightJoinCommandInterceptor()); 24 | options.UseMySql(connectionString, serverVersion, mysqlOptions => 25 | { 26 | mysqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); 27 | var assemblyName = typeof(MySqlEFCoreOrchestrationBuilderExtensions).Assembly.GetName().Name; 28 | mysqlOptions.MigrationsAssembly(assemblyName); 29 | mysqlOptionsAction?.Invoke(mysqlOptions); 30 | }); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.MySql/LLL.DurableTask.EFCore.MySql.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | 6 | $(Version)-preview 7 | 8 | 9 | 10 | MySql extensions to EFCore Durable Task Storage 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | all 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.MySql/Migrations/20221203122137_FailureDetails.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace LLL.DurableTask.EFCore.MySql.Migrations; 6 | 7 | /// 8 | public partial class FailureDetails : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "FailureDetails", 15 | table: "Executions", 16 | type: "longtext", 17 | maxLength: 2147483647, 18 | nullable: true) 19 | .Annotation("MySql:CharSet", "utf8mb4"); 20 | } 21 | 22 | /// 23 | protected override void Down(MigrationBuilder migrationBuilder) 24 | { 25 | migrationBuilder.DropColumn( 26 | name: "FailureDetails", 27 | table: "Executions"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.MySql/Migrations/20230205130925_AdjustIndexesForLock.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace LLL.DurableTask.EFCore.MySql.Migrations; 6 | 7 | /// 8 | public partial class AdjustIndexesForLock : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.DropIndex( 14 | name: "IX_ActivityMessages_LockedUntil", 15 | table: "ActivityMessages"); 16 | 17 | migrationBuilder.CreateIndex( 18 | name: "IX_Instances_InstanceId_LockedUntil", 19 | table: "Instances", 20 | columns: new[] { "InstanceId", "LockedUntil" }); 21 | } 22 | 23 | /// 24 | protected override void Down(MigrationBuilder migrationBuilder) 25 | { 26 | migrationBuilder.DropIndex( 27 | name: "IX_Instances_InstanceId_LockedUntil", 28 | table: "Instances"); 29 | 30 | migrationBuilder.CreateIndex( 31 | name: "IX_ActivityMessages_LockedUntil", 32 | table: "ActivityMessages", 33 | column: "LockedUntil"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.MySql/Migrations/20230809064638_IndexCompletedTime.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace LLL.DurableTask.EFCore.MySql.Migrations; 6 | 7 | /// 8 | public partial class IndexCompletedTime : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateIndex( 14 | name: "IX_Executions_CompletedTime", 15 | table: "Executions", 16 | column: "CompletedTime"); 17 | } 18 | 19 | /// 20 | protected override void Down(MigrationBuilder migrationBuilder) 21 | { 22 | migrationBuilder.DropIndex( 23 | name: "IX_Executions_CompletedTime", 24 | table: "Executions"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.MySql/Migrations/20240602074455_EFCore8.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace LLL.DurableTask.EFCore.MySql.Migrations; 7 | 8 | /// 9 | public partial class EFCore8 : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AlterColumn( 15 | name: "Id", 16 | table: "ExecutionTags", 17 | type: "int", 18 | nullable: false, 19 | oldClrType: typeof(int), 20 | oldType: "int") 21 | .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); 22 | } 23 | 24 | /// 25 | protected override void Down(MigrationBuilder migrationBuilder) 26 | { 27 | migrationBuilder.AlterColumn( 28 | name: "Id", 29 | table: "ExecutionTags", 30 | type: "int", 31 | nullable: false, 32 | oldClrType: typeof(int), 33 | oldType: "int") 34 | .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.MySql/MySqlQueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace LLL.DurableTask.EFCore.MySql; 5 | 6 | public static class QueryableExtesions 7 | { 8 | public static IQueryable WithStraightJoin(this IQueryable queryable) 9 | { 10 | return queryable.TagWith("STRAIGHT_JOIN"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.MySql/OrchestrationDesignTimeDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Design; 4 | 5 | namespace LLL.DurableTask.EFCore.MySql; 6 | 7 | [ExcludeFromCodeCoverage] 8 | public class OrchestrationDesignTimeDbContextFactory : IDesignTimeDbContextFactory 9 | { 10 | public OrchestrationDbContext CreateDbContext(string[] args) 11 | { 12 | var builder = new DbContextOptionsBuilder(); 13 | var connectionString = "server=localhost;database=durabletask;user=root;password=root"; 14 | builder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), mysqlOptions => 15 | { 16 | var assemblyName = typeof(OrchestrationDesignTimeDbContextFactory).Assembly.GetName().Name; 17 | mysqlOptions.MigrationsAssembly(assemblyName); 18 | }); 19 | return new OrchestrationDbContext(builder.Options); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.MySql/README.md: -------------------------------------------------------------------------------- 1 | # LLL.DurableTask.EFCore.MySql [![Nuget](https://img.shields.io/nuget/v/LLL.DurableTask.EFCore.MySql)](https://www.nuget.org/packages/LLL.DurableTask.EFCore.MySql/) 2 | 3 | Extension to EFCore storage with migrations and queries specific to MySql. 4 | 5 | ## Configuration 6 | 7 | ```C# 8 | services.AddDurableTaskEFCoreStorage() 9 | .UseMySql(ServerVersion.AutoDetect("YOUR_CONNECTION_STRING")); 10 | ``` 11 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.MySql/StraightJoinCommandInterceptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore.Diagnostics; 6 | 7 | namespace LLL.DurableTask.EFCore.MySql; 8 | 9 | public class StraightJoinCommandInterceptor : DbCommandInterceptor 10 | { 11 | public override InterceptionResult ReaderExecuting( 12 | DbCommand command, 13 | CommandEventData eventData, 14 | InterceptionResult result) 15 | { 16 | ManipulateCommand(command); 17 | 18 | return result; 19 | } 20 | 21 | public override ValueTask> ReaderExecutingAsync( 22 | DbCommand command, 23 | CommandEventData eventData, 24 | InterceptionResult result, 25 | CancellationToken cancellationToken = default) 26 | { 27 | ManipulateCommand(command); 28 | 29 | return new ValueTask>(result); 30 | } 31 | 32 | private static void ManipulateCommand(DbCommand command) 33 | { 34 | if (command.CommandText.Contains("-- STRAIGHT_JOIN", StringComparison.Ordinal)) 35 | { 36 | command.CommandText = command.CommandText.Replace("SELECT", "SELECT STRAIGHT_JOIN", StringComparison.OrdinalIgnoreCase); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.PostgreSQL/DependencyInjection/PostgreSqlEFCoreOrchestrationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LLL.DurableTask.EFCore; 3 | using LLL.DurableTask.EFCore.DependencyInjection; 4 | using LLL.DurableTask.EFCore.PostgreSQL; 5 | using Microsoft.EntityFrameworkCore; 6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; 7 | 8 | namespace Microsoft.Extensions.DependencyInjection; 9 | 10 | public static class PostgreSqlEFCoreOrchestrationBuilderExtensions 11 | { 12 | public static IEFCoreOrchestrationBuilder UseNpgsql( 13 | this IEFCoreOrchestrationBuilder builder, 14 | string connectionString, 15 | Action mysqlOptionsAction = null) 16 | { 17 | builder.Services.AddSingleton(); 18 | 19 | return builder.ConfigureDbContext(options => 20 | { 21 | options.UseNpgsql(connectionString, npgsqlOptions => 22 | { 23 | npgsqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); 24 | var assemblyName = typeof(PostgreSqlEFCoreOrchestrationBuilderExtensions).Assembly.GetName().Name; 25 | npgsqlOptions.MigrationsAssembly(assemblyName); 26 | mysqlOptionsAction?.Invoke(npgsqlOptions); 27 | }); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.PostgreSQL/LLL.DurableTask.EFCore.PostgreSQL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | 6 | 7 | 8 | PostgreSQL extensions to EFCore Durable Task Storage 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.PostgreSQL/Migrations/20221203122138_FailureDetails.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace LLL.DurableTask.EFCore.PostgreSQL.Migrations; 6 | 7 | /// 8 | public partial class FailureDetails : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "FailureDetails", 15 | table: "Executions", 16 | type: "text", 17 | maxLength: 2147483647, 18 | nullable: true); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "FailureDetails", 26 | table: "Executions"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.PostgreSQL/Migrations/20230205130948_AdjustIndexesForLock.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace LLL.DurableTask.EFCore.PostgreSQL.Migrations; 6 | 7 | /// 8 | public partial class AdjustIndexesForLock : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.DropIndex( 14 | name: "IX_ActivityMessages_LockedUntil", 15 | table: "ActivityMessages"); 16 | 17 | migrationBuilder.CreateIndex( 18 | name: "IX_Instances_InstanceId_LockedUntil", 19 | table: "Instances", 20 | columns: new[] { "InstanceId", "LockedUntil" }); 21 | } 22 | 23 | /// 24 | protected override void Down(MigrationBuilder migrationBuilder) 25 | { 26 | migrationBuilder.DropIndex( 27 | name: "IX_Instances_InstanceId_LockedUntil", 28 | table: "Instances"); 29 | 30 | migrationBuilder.CreateIndex( 31 | name: "IX_ActivityMessages_LockedUntil", 32 | table: "ActivityMessages", 33 | column: "LockedUntil"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.PostgreSQL/Migrations/20230809064704_IndexCompletedTime.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace LLL.DurableTask.EFCore.PostgreSQL.Migrations; 6 | 7 | /// 8 | public partial class IndexCompletedTime : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateIndex( 14 | name: "IX_Executions_CompletedTime", 15 | table: "Executions", 16 | column: "CompletedTime"); 17 | } 18 | 19 | /// 20 | protected override void Down(MigrationBuilder migrationBuilder) 21 | { 22 | migrationBuilder.DropIndex( 23 | name: "IX_Executions_CompletedTime", 24 | table: "Executions"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.PostgreSQL/Migrations/20240602074528_EFCore8.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace LLL.DurableTask.EFCore.PostgreSQL.Migrations; 6 | 7 | /// 8 | public partial class EFCore8 : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | 14 | } 15 | 16 | /// 17 | protected override void Down(MigrationBuilder migrationBuilder) 18 | { 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.PostgreSQL/OrchestrationDesignTimeDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Design; 4 | 5 | namespace LLL.DurableTask.EFCore.PostgreSQL; 6 | 7 | [ExcludeFromCodeCoverage] 8 | public class OrchestrationDesignTimeDbContextFactory : IDesignTimeDbContextFactory 9 | { 10 | public OrchestrationDbContext CreateDbContext(string[] args) 11 | { 12 | var builder = new DbContextOptionsBuilder(); 13 | builder.UseNpgsql("Server=localhost;Port=5432;Database=durabletask;User Id=postgres;Password=root", mysqlOptions => 14 | { 15 | var assemblyName = typeof(OrchestrationDesignTimeDbContextFactory).Assembly.GetName().Name; 16 | mysqlOptions.MigrationsAssembly(assemblyName); 17 | }); 18 | return new OrchestrationDbContext(builder.Options); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.PostgreSQL/README.md: -------------------------------------------------------------------------------- 1 | # LLL.DurableTask.EFCore.PostgreSQL [![Nuget](https://img.shields.io/nuget/v/LLL.DurableTask.EFCore.PostgreSQL)](https://www.nuget.org/packages/LLL.DurableTask.EFCore.PostgreSQL/) 2 | 3 | Extension to EFCore storage with migrations and queries specific to PostgreSQL. 4 | 5 | ## Configuration 6 | 7 | ```C# 8 | services.AddDurableTaskEFCoreStorage() 9 | .UseNpgsql("YOUR_CONNECTION_STRING"); 10 | ``` 11 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.SqlServer/DependencyInjection/SqlServerEFCoreOrchestrationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LLL.DurableTask.EFCore; 3 | using LLL.DurableTask.EFCore.DependencyInjection; 4 | using LLL.DurableTask.EFCore.SqlServer; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Infrastructure; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace Microsoft.Extensions.DependencyInjection; 10 | 11 | public static class SqlServerEFCoreOrchestrationBuilderExtensions 12 | { 13 | public static IEFCoreOrchestrationBuilder UseSqlServer( 14 | this IEFCoreOrchestrationBuilder builder, 15 | string connectionString, 16 | Action mysqlOptionsAction = null) 17 | { 18 | builder.Services.AddSingleton(); 19 | 20 | return builder.ConfigureDbContext(options => 21 | { 22 | options.UseSqlServer(connectionString, sqlServerOptions => 23 | { 24 | sqlServerOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); 25 | var assemblyName = typeof(SqlServerEFCoreOrchestrationBuilderExtensions).Assembly.GetName().Name; 26 | sqlServerOptions.MigrationsAssembly(assemblyName); 27 | mysqlOptionsAction?.Invoke(sqlServerOptions); 28 | }); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.SqlServer/LLL.DurableTask.EFCore.SqlServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | 6 | 7 | 8 | SqlServer extensions to EFCore Durable Task Storage 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.SqlServer/Migrations/20221203122146_FailureDetails.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace LLL.DurableTask.EFCore.SqlServer.Migrations; 6 | 7 | /// 8 | public partial class FailureDetails : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "FailureDetails", 15 | table: "Executions", 16 | type: "nvarchar(max)", 17 | maxLength: 2147483647, 18 | nullable: true); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "FailureDetails", 26 | table: "Executions"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.SqlServer/Migrations/20230205130939_AdjustIndexesForLock.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace LLL.DurableTask.EFCore.SqlServer.Migrations; 6 | 7 | /// 8 | public partial class AdjustIndexesForLock : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.DropIndex( 14 | name: "IX_ActivityMessages_LockedUntil", 15 | table: "ActivityMessages"); 16 | 17 | migrationBuilder.CreateIndex( 18 | name: "IX_Instances_InstanceId_LockedUntil", 19 | table: "Instances", 20 | columns: new[] { "InstanceId", "LockedUntil" }); 21 | } 22 | 23 | /// 24 | protected override void Down(MigrationBuilder migrationBuilder) 25 | { 26 | migrationBuilder.DropIndex( 27 | name: "IX_Instances_InstanceId_LockedUntil", 28 | table: "Instances"); 29 | 30 | migrationBuilder.CreateIndex( 31 | name: "IX_ActivityMessages_LockedUntil", 32 | table: "ActivityMessages", 33 | column: "LockedUntil"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.SqlServer/Migrations/20230809064650_IndexCompletedTime.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace LLL.DurableTask.EFCore.SqlServer.Migrations; 6 | 7 | /// 8 | public partial class IndexCompletedTime : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateIndex( 14 | name: "IX_Executions_CompletedTime", 15 | table: "Executions", 16 | column: "CompletedTime"); 17 | } 18 | 19 | /// 20 | protected override void Down(MigrationBuilder migrationBuilder) 21 | { 22 | migrationBuilder.DropIndex( 23 | name: "IX_Executions_CompletedTime", 24 | table: "Executions"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.SqlServer/Migrations/20240602074541_EFCore8.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace LLL.DurableTask.EFCore.SqlServer.Migrations; 6 | 7 | /// 8 | public partial class EFCore8 : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | 14 | } 15 | 16 | /// 17 | protected override void Down(MigrationBuilder migrationBuilder) 18 | { 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.SqlServer/OrchestrationDesignTimeDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Design; 4 | 5 | namespace LLL.DurableTask.EFCore.SqlServer; 6 | 7 | [ExcludeFromCodeCoverage] 8 | public class OrchestrationDesignTimeDbContextFactory : IDesignTimeDbContextFactory 9 | { 10 | public OrchestrationDbContext CreateDbContext(string[] args) 11 | { 12 | var builder = new DbContextOptionsBuilder(); 13 | builder.UseSqlServer("server=localhost;database=durabletask;user=sa;password=P1ssw0rd;Encrypt=false", sqlServerOptions => 14 | { 15 | var assemblyName = typeof(OrchestrationDesignTimeDbContextFactory).Assembly.GetName().Name; 16 | sqlServerOptions.MigrationsAssembly(assemblyName); 17 | }); 18 | return new OrchestrationDbContext(builder.Options); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore.SqlServer/README.md: -------------------------------------------------------------------------------- 1 | # LLL.DurableTask.EFCore.SqlServer [![Nuget](https://img.shields.io/nuget/v/LLL.DurableTask.EFCore.SqlServer)](https://www.nuget.org/packages/LLL.DurableTask.EFCore.SqlServer/) 2 | 3 | Extension to EFCore storage with migrations and queries specific to Sql Server. 4 | 5 | ## Configuration 6 | 7 | ```C# 8 | services.AddDurableTaskEFCoreStorage() 9 | .UseSqlServer("YOUR_CONNECTION_STRING"); 10 | ``` 11 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Configuration/ActivityMessageConfiguration.cs: -------------------------------------------------------------------------------- 1 | using LLL.DurableTask.EFCore.Configuration.Converters; 2 | using LLL.DurableTask.EFCore.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace LLL.DurableTask.EFCore.Configuration; 7 | 8 | public class ActivityMessageConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.HasKey(x => x.Id); 13 | builder.Property(x => x.Id).IsRequired(); 14 | 15 | builder.Property(x => x.InstanceId).HasMaxLength(250).IsRequired(); 16 | builder.HasOne(x => x.Instance) 17 | .WithMany() 18 | .HasForeignKey(x => x.InstanceId) 19 | .IsRequired() 20 | .OnDelete(DeleteBehavior.Cascade); 21 | 22 | builder.Property(x => x.Queue).HasMaxLength(250).IsRequired(); 23 | builder.Property(x => x.ReplyQueue).HasMaxLength(250).IsRequired(); 24 | 25 | builder.Property(x => x.Message).HasMaxLength(int.MaxValue); 26 | 27 | builder.Property(x => x.CreatedAt).IsRequired().HasConversion(new UtcDateTimeConverter()); 28 | builder.Property(x => x.LockedUntil).HasConversion(new UtcDateTimeConverter()); 29 | builder.Property(x => x.LockId).HasMaxLength(100).IsConcurrencyToken(); 30 | 31 | builder.HasIndex(x => new { x.LockedUntil, x.Queue }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Configuration/Converters/UtcDateTimeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 4 | 5 | namespace LLL.DurableTask.EFCore.Configuration.Converters; 6 | 7 | public class UtcDateTimeConverter : ValueConverter 8 | { 9 | public UtcDateTimeConverter() 10 | : base(To, From) 11 | { 12 | } 13 | 14 | static readonly Expression> To = x => x.Kind == DateTimeKind.Unspecified 15 | ? DateTime.SpecifyKind(x, DateTimeKind.Utc) 16 | : x.ToUniversalTime(); 17 | 18 | static readonly Expression> From = x => DateTime.SpecifyKind(x, DateTimeKind.Utc); 19 | } 20 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Configuration/EventConfiguration.cs: -------------------------------------------------------------------------------- 1 | using LLL.DurableTask.EFCore.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace LLL.DurableTask.EFCore.Configuration; 6 | 7 | public class EventConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasKey(x => x.Id); 12 | builder.Property(x => x.Id).IsRequired(); 13 | 14 | builder.Property(x => x.InstanceId).HasMaxLength(250).IsRequired(); 15 | builder.Property(x => x.ExecutionId).HasMaxLength(100).IsRequired(); 16 | builder.HasOne(x => x.Execution) 17 | .WithMany(x => x.Events) 18 | .IsRequired() 19 | .HasForeignKey(x => x.ExecutionId) 20 | .OnDelete(DeleteBehavior.Cascade); 21 | 22 | builder.Property(x => x.SequenceNumber).IsRequired(); 23 | 24 | builder.Property(x => x.Content).HasMaxLength(int.MaxValue).IsRequired(); 25 | 26 | builder.HasIndex(x => new { x.InstanceId, x.ExecutionId, x.SequenceNumber }).IsUnique(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Configuration/InstanceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using LLL.DurableTask.EFCore.Configuration.Converters; 2 | using LLL.DurableTask.EFCore.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace LLL.DurableTask.EFCore.Configuration; 7 | 8 | public class InstanceConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.HasKey(x => x.InstanceId); 13 | builder.Property(x => x.InstanceId).HasMaxLength(250).IsRequired(); 14 | 15 | builder.Property(x => x.LastExecutionId).HasMaxLength(100).IsRequired(); 16 | builder.HasOne(x => x.LastExecution) 17 | .WithOne(x => x.LastExecutionInstance) 18 | .HasForeignKey(x => x.LastExecutionId) 19 | .OnDelete(DeleteBehavior.Cascade); 20 | 21 | builder.Property(x => x.LastQueue).HasMaxLength(250).IsRequired(); 22 | 23 | builder.Property(x => x.LockedUntil).IsRequired().HasConversion(new UtcDateTimeConverter()); 24 | builder.Property(x => x.LockId).HasMaxLength(100).IsConcurrencyToken(); 25 | 26 | builder.HasIndex(x => new { x.InstanceId, x.LockedUntil }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Configuration/OrchestrationMessageConfiguration.cs: -------------------------------------------------------------------------------- 1 | using LLL.DurableTask.EFCore.Configuration.Converters; 2 | using LLL.DurableTask.EFCore.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace LLL.DurableTask.EFCore.Configuration; 7 | 8 | public class OrchestrationMessageConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.HasKey(x => x.Id); 13 | builder.Property(x => x.Id).HasMaxLength(36).IsRequired(); 14 | 15 | builder.Property(x => x.InstanceId).HasMaxLength(250).IsRequired(); 16 | builder.HasOne(x => x.Instance) 17 | .WithMany() 18 | .HasForeignKey(x => x.InstanceId) 19 | .IsRequired(); 20 | 21 | builder.Property(x => x.ExecutionId).HasMaxLength(100); 22 | 23 | builder.Property(x => x.AvailableAt).IsRequired().HasConversion(new UtcDateTimeConverter()); 24 | 25 | builder.Property(x => x.SequenceNumber).IsRequired(); 26 | 27 | builder.Property(x => x.Message).HasMaxLength(int.MaxValue); 28 | 29 | // This allows locking next message instance via index scan, not locking extra rows 30 | builder.HasIndex(x => new { x.AvailableAt, x.Queue, x.InstanceId }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/DependencyInjection/EFCoreOrchestrationBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace LLL.DurableTask.EFCore.DependencyInjection; 7 | 8 | public class EFCoreOrchestrationBuilder : IEFCoreOrchestrationBuilder 9 | { 10 | public EFCoreOrchestrationBuilder(IServiceCollection services) 11 | { 12 | Services = services; 13 | DbContextConfigurations = new List>(); 14 | } 15 | 16 | public IServiceCollection Services { get; } 17 | 18 | public List> DbContextConfigurations { get; } 19 | 20 | public IEFCoreOrchestrationBuilder ConfigureDbContext(Action configuration) 21 | { 22 | DbContextConfigurations.Add(configuration); 23 | return this; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/DependencyInjection/IEFCoreOrchestrationBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace LLL.DurableTask.EFCore.DependencyInjection; 6 | 7 | public interface IEFCoreOrchestrationBuilder 8 | { 9 | IServiceCollection Services { get; } 10 | 11 | IEFCoreOrchestrationBuilder ConfigureDbContext(Action options); 12 | } 13 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/EFCoreContinuationToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Newtonsoft.Json; 4 | 5 | namespace LLL.DurableTask.EFCore; 6 | 7 | public class EFCoreContinuationToken 8 | { 9 | public DateTime CreatedTime { get; set; } 10 | public string InstanceId { get; set; } 11 | 12 | public static EFCoreContinuationToken Parse(string value) 13 | { 14 | if (string.IsNullOrEmpty(value)) 15 | return null; 16 | 17 | return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(value))); 18 | } 19 | 20 | public string Serialize() 21 | { 22 | return Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(this))); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/EFCoreOrchestrationOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DurableTask.Core.Serializing; 3 | using LLL.DurableTask.Core.Serializing; 4 | using LLL.DurableTask.EFCore.Polling; 5 | 6 | namespace LLL.DurableTask.EFCore; 7 | 8 | public class EFCoreOrchestrationOptions 9 | { 10 | public DataConverter DataConverter { get; set; } = new TypelessJsonDataConverter(); 11 | public int TaskOrchestrationDispatcherCount { get; set; } = 1; 12 | public int TaskActivityDispatcherCount { get; set; } = 1; 13 | public int MaxConcurrentTaskOrchestrationWorkItems { get; set; } = 20; 14 | public int MaxConcurrentTaskActivityWorkItems { get; set; } = 10; 15 | public PollingIntervalOptions PollingInterval { get; } = new PollingIntervalOptions(100, 2, 1000); 16 | public TimeSpan OrchestrationLockTimeout { get; set; } = TimeSpan.FromMinutes(1); 17 | public TimeSpan ActivtyLockTimeout { get; set; } = TimeSpan.FromMinutes(1); 18 | public TimeSpan FetchNewMessagesPollingTimeout { get; set; } = TimeSpan.FromSeconds(10); 19 | public int DelayInSecondsAfterFailure { get; set; } = 5; 20 | } 21 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Entities/ActivityMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LLL.DurableTask.EFCore.Entities; 4 | 5 | public class ActivityMessage 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string InstanceId { get; set; } 10 | // This relationship ensures messages are deleted when instance is deleted 11 | public Instance Instance { get; set; } 12 | 13 | public string Queue { get; set; } 14 | public string ReplyQueue { get; set; } 15 | 16 | public string Message { get; set; } 17 | 18 | public DateTime CreatedAt { get; set; } 19 | public DateTime LockedUntil { get; set; } 20 | public string LockId { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Entities/Event.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LLL.DurableTask.EFCore.Entities; 4 | 5 | public class Event 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string InstanceId { get; set; } 10 | 11 | public string ExecutionId { get; set; } 12 | // This relationship ensures events are deleted when it's execution is deleted 13 | public Execution Execution { get; set; } 14 | 15 | public int SequenceNumber { get; set; } 16 | 17 | public string Content { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Entities/Execution.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using DurableTask.Core; 4 | 5 | namespace LLL.DurableTask.EFCore.Entities; 6 | 7 | public class Execution 8 | { 9 | public string ExecutionId { get; set; } 10 | 11 | public string InstanceId { get; set; } 12 | 13 | public string Name { get; set; } 14 | public string Version { get; set; } 15 | 16 | public DateTime CreatedTime { get; set; } 17 | public DateTime CompletedTime { get; set; } 18 | public DateTime LastUpdatedTime { get; set; } 19 | 20 | public long CompressedSize { get; set; } 21 | public long Size { get; set; } 22 | 23 | public OrchestrationStatus Status { get; set; } 24 | 25 | public string FailureDetails { get; set; } 26 | 27 | public string CustomStatus { get; set; } 28 | 29 | public string ParentInstance { get; set; } 30 | 31 | public HashSet Tags { get; } = new HashSet(); 32 | 33 | public string Input { get; set; } 34 | 35 | public string Output { get; set; } 36 | 37 | public IList Events { get; } = new List(); 38 | 39 | public Instance LastExecutionInstance { get; set; } 40 | } 41 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Entities/Instance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LLL.DurableTask.EFCore.Entities; 4 | 5 | public class Instance 6 | { 7 | public string InstanceId { get; set; } 8 | public string LastExecutionId { get; set; } 9 | // This relationship ensures instance is deleted when last execution is deleted 10 | public Execution LastExecution { get; set; } 11 | public string LastQueue { get; set; } 12 | public DateTime LockedUntil { get; set; } 13 | public string LockId { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Entities/OrchestrationMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LLL.DurableTask.EFCore.Entities; 4 | 5 | public class OrchestrationMessage 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string InstanceId { get; set; } 10 | // This relationship ensures messages are deleted when instance is deleted 11 | public Instance Instance { get; set; } 12 | 13 | public string ExecutionId { get; set; } 14 | 15 | public string Queue { get; set; } 16 | 17 | public DateTime AvailableAt { get; set; } 18 | 19 | // Used to order messages fired at the same moment 20 | public int SequenceNumber { get; set; } 21 | 22 | public string Message { get; set; } 23 | } 24 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Entities/Tag.cs: -------------------------------------------------------------------------------- 1 | namespace LLL.DurableTask.EFCore.Entities; 2 | 3 | public class Tag 4 | { 5 | public string Name { get; set; } 6 | public string Value { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Extensions/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace LLL.DurableTask.EFCore.Extensions; 5 | 6 | public static class TaskExtensions 7 | { 8 | public static async Task> WhenAllSerial(this IEnumerable> tasks) 9 | { 10 | var result = new List(); 11 | foreach (var task in tasks) 12 | { 13 | result.Add(await task); 14 | } 15 | return result; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Helpers/ParametersCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace LLL.DurableTask.EFCore; 5 | 6 | public class ParametersCollection : IEnumerable 7 | { 8 | private readonly List _values = new(); 9 | 10 | public string Add(object value) 11 | { 12 | _values.Add(value); 13 | return $"{{{_values.Count - 1}}}"; 14 | } 15 | 16 | public object[] ToArray() 17 | { 18 | return _values.ToArray(); 19 | } 20 | 21 | public IEnumerator GetEnumerator() 22 | { 23 | return _values.GetEnumerator(); 24 | } 25 | 26 | IEnumerator IEnumerable.GetEnumerator() 27 | { 28 | return _values.GetEnumerator(); 29 | } 30 | } -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/LLL.DurableTask.EFCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | latest 6 | 7 | 8 | 9 | EFCore implementation of Durable Task Storage 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Mappers/ActivityMessageMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DurableTask.Core; 3 | using DurableTask.Core.History; 4 | using LLL.DurableTask.EFCore.Entities; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace LLL.DurableTask.EFCore.Mappers; 8 | 9 | public class ActivityMessageMapper 10 | { 11 | private readonly EFCoreOrchestrationOptions _options; 12 | 13 | public ActivityMessageMapper(IOptions options) 14 | { 15 | _options = options.Value; 16 | } 17 | 18 | public ActivityMessage CreateActivityMessage( 19 | TaskMessage message, 20 | string replyQueue) 21 | { 22 | var taskScheduledEvent = message.Event as TaskScheduledEvent; 23 | 24 | return new ActivityMessage 25 | { 26 | Id = Guid.NewGuid(), 27 | CreatedAt = DateTime.UtcNow, 28 | Queue = QueueMapper.ToQueue(taskScheduledEvent.Name, taskScheduledEvent.Version), 29 | ReplyQueue = replyQueue, 30 | InstanceId = message.OrchestrationInstance.InstanceId, 31 | Message = _options.DataConverter.Serialize(message), 32 | LockedUntil = DateTime.UtcNow 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Mappers/InstanceMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DurableTask.Core; 3 | using DurableTask.Core.History; 4 | using LLL.DurableTask.EFCore.Entities; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace LLL.DurableTask.EFCore.Mappers; 8 | 9 | public class InstanceMapper 10 | { 11 | private readonly EFCoreOrchestrationOptions _options; 12 | 13 | public InstanceMapper(IOptions options) 14 | { 15 | _options = options.Value; 16 | } 17 | 18 | public Instance CreateInstance(ExecutionStartedEvent executionStartedEvent) 19 | { 20 | var instance = new Instance 21 | { 22 | InstanceId = executionStartedEvent.OrchestrationInstance.InstanceId, 23 | LastExecutionId = executionStartedEvent.OrchestrationInstance.ExecutionId, 24 | LastQueue = QueueMapper.ToQueue(executionStartedEvent.Name, executionStartedEvent.Version), 25 | LockedUntil = DateTime.UtcNow 26 | }; 27 | return instance; 28 | } 29 | 30 | public void UpdateInstance( 31 | Instance instance, 32 | OrchestrationRuntimeState runtimeState) 33 | { 34 | instance.LastExecutionId = runtimeState.OrchestrationInstance.ExecutionId; 35 | instance.LastQueue = QueueMapper.ToQueue(runtimeState.Name, runtimeState.Version); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Mappers/OrchestrationMessageMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DurableTask.Core; 3 | using DurableTask.Core.History; 4 | using LLL.DurableTask.EFCore.Entities; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace LLL.DurableTask.EFCore.Mappers; 8 | 9 | public class OrchestrationMessageMapper 10 | { 11 | private readonly EFCoreOrchestrationOptions _options; 12 | 13 | public OrchestrationMessageMapper(IOptions options) 14 | { 15 | _options = options.Value; 16 | } 17 | 18 | public OrchestrationMessage CreateOrchestrationMessageAsync( 19 | TaskMessage message, 20 | int sequence, 21 | string queue) 22 | { 23 | return new OrchestrationMessage 24 | { 25 | Id = Guid.NewGuid(), 26 | InstanceId = message.OrchestrationInstance.InstanceId, 27 | ExecutionId = message.OrchestrationInstance.ExecutionId, 28 | SequenceNumber = sequence, 29 | AvailableAt = message.Event is TimerFiredEvent timerFiredEvent 30 | ? timerFiredEvent.FireAt.ToUniversalTime() 31 | : DateTime.UtcNow, 32 | Queue = queue, 33 | Message = _options.DataConverter.Serialize(message), 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Mappers/QueueMapper.cs: -------------------------------------------------------------------------------- 1 | using DurableTask.Core; 2 | 3 | namespace LLL.DurableTask.EFCore.Mappers; 4 | 5 | public static class QueueMapper 6 | { 7 | public static string ToQueue(INameVersionInfo nameVersion) 8 | { 9 | return ToQueue(nameVersion.Name, nameVersion.Version); 10 | } 11 | 12 | public static string ToQueue(string name, string version) 13 | { 14 | if (string.IsNullOrEmpty(version)) 15 | return name; 16 | 17 | return $"{name}_{version}"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/OrchestrationDbContext.cs: -------------------------------------------------------------------------------- 1 | using LLL.DurableTask.EFCore.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace LLL.DurableTask.EFCore; 5 | 6 | public class OrchestrationDbContext : DbContext 7 | { 8 | public OrchestrationDbContext(DbContextOptions options) : base(options) 9 | { 10 | } 11 | 12 | public DbSet Instances { get; set; } 13 | public DbSet Executions { get; set; } 14 | public DbSet Events { get; set; } 15 | public DbSet OrchestrationMessages { get; set; } 16 | public DbSet ActivityMessages { get; set; } 17 | 18 | protected override void OnModelCreating(ModelBuilder modelBuilder) 19 | { 20 | modelBuilder.ApplyConfigurationsFromAssembly(typeof(OrchestrationDbContext).Assembly); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Polling/BackoffPollingHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace LLL.DurableTask.EFCore.Polling; 7 | 8 | public static class BackoffPollingHelper 9 | { 10 | public static async Task PollAsync( 11 | Func> valueProvider, 12 | Func shouldAcceptValue, 13 | TimeSpan timeout, 14 | PollingIntervalOptions interval, 15 | CancellationToken cancellationToken) 16 | { 17 | T value; 18 | 19 | var stopwatch = Stopwatch.StartNew(); 20 | var count = 0; 21 | do 22 | { 23 | cancellationToken.ThrowIfCancellationRequested(); 24 | 25 | value = await valueProvider(); 26 | 27 | if (shouldAcceptValue(value) 28 | || stopwatch.Elapsed >= timeout) 29 | break; 30 | 31 | await Task.Delay(CalculateDelay(interval, count++)); 32 | } while (stopwatch.Elapsed < timeout); 33 | 34 | return value; 35 | } 36 | 37 | private static int CalculateDelay(PollingIntervalOptions interval, int count) 38 | { 39 | return (int)Math.Min(interval.Initial * Math.Pow(interval.Factor, count), interval.Max); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.EFCore/Polling/PollingIntervalOptions.cs: -------------------------------------------------------------------------------- 1 | namespace LLL.DurableTask.EFCore.Polling; 2 | 3 | public class PollingIntervalOptions 4 | { 5 | public PollingIntervalOptions(double initial, double factor, double max) 6 | { 7 | Initial = initial; 8 | Factor = factor; 9 | Max = max; 10 | } 11 | 12 | public double Initial { get; set; } 13 | public double Factor { get; set; } 14 | public double Max { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server.Grpc.Client/GrpcClientOrchestrationServiceOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DurableTask.Core.Serializing; 3 | using LLL.DurableTask.Core.Serializing; 4 | 5 | namespace LLL.DurableTask.Server.Client; 6 | 7 | public class GrpcClientOrchestrationServiceOptions 8 | { 9 | public Uri BaseAddress { get; set; } 10 | public DataConverter DataConverter { get; set; } = new TypelessJsonDataConverter(); 11 | public int TaskOrchestrationDispatcherCount { get; set; } = 1; 12 | public int TaskActivityDispatcherCount { get; set; } = 1; 13 | public int MaxConcurrentTaskOrchestrationWorkItems { get; set; } = 20; 14 | public int MaxConcurrentTaskActivityWorkItems { get; set; } = 10; 15 | public int DelayInSecondsAfterFailure { get; set; } = 5; 16 | } 17 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server.Grpc.Client/README.md: -------------------------------------------------------------------------------- 1 | # LLL.DurableTask.Server.Grpc.Client [![Nuget](https://img.shields.io/nuget/v/LLL.DurableTask.Server.Grpc.Client)](https://www.nuget.org/packages/LLL.DurableTask.Server.Grpc.Client/) 2 | 3 | Durable Task storage implementation using server GRPC endpoints. 4 | 5 | Supports same features as the storage configured in the server. 6 | 7 | ## Configuration 8 | 9 | ```C# 10 | services.AddDurableTaskServerGrpcStorage(options => 11 | { 12 | options.BaseAddress = new Uri("YOUR_SERVER_ADDRESS"); 13 | }); 14 | ``` 15 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server.Grpc/DependencyInjection/EndpointRouteBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using LLL.DurableTask.Server.Grpc.Server; 2 | using Microsoft.AspNetCore.Routing; 3 | 4 | namespace Microsoft.AspNetCore.Builder; 5 | 6 | public static class EndpointRouteBuilderExtensions 7 | { 8 | public static GrpcServiceEndpointConventionBuilder MapDurableTaskServerGrpcService( 9 | this IEndpointRouteBuilder endpoints) 10 | { 11 | return endpoints.MapGrpcService(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server.Grpc/DependencyInjection/TaskHubServerBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LLL.DurableTask.Server.Configuration; 3 | using LLL.DurableTask.Server.Grpc; 4 | using LLL.DurableTask.Server.Grpc.Server; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection; 7 | 8 | public static class TaskHubServerBuilderExtensions 9 | { 10 | public static ITaskHubServerBuilder AddGrpcEndpoints( 11 | this ITaskHubServerBuilder builder, 12 | Action configure = null 13 | ) 14 | { 15 | builder.Services.AddOptions(); 16 | 17 | if (configure != null) 18 | builder.Services.Configure(configure); 19 | 20 | builder.Services.AddSingleton(); 21 | return builder; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server.Grpc/GrpcServerOrchestrationServiceOptions.cs: -------------------------------------------------------------------------------- 1 | using DurableTask.Core.Serializing; 2 | using LLL.DurableTask.Core.Serializing; 3 | 4 | namespace LLL.DurableTask.Server.Grpc; 5 | 6 | public class GrpcServerOrchestrationServiceOptions 7 | { 8 | public DataConverter DataConverter { get; set; } = new TypelessJsonDataConverter(); 9 | } 10 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server.Grpc/LLL.DurableTask.Server.Grpc.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0;net8.0 5 | 6 | 7 | 8 | Exposes a Durable Task storage implementation as GRPC 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server.Grpc/NameVersion.cs: -------------------------------------------------------------------------------- 1 | using DurableTask.Core; 2 | 3 | namespace LLL.DurableTask.Server.Grpc.Server; 4 | 5 | class NameVersion : INameVersionInfo 6 | { 7 | public NameVersion() 8 | { 9 | } 10 | 11 | public NameVersion(string name, string version) 12 | { 13 | Name = name; 14 | Version = version; 15 | } 16 | 17 | public string Name { get; set; } 18 | public string Version { get; set; } 19 | 20 | public override string ToString() 21 | { 22 | return $"{Name}_{Version}"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server.Grpc/README.md: -------------------------------------------------------------------------------- 1 | # LLL.DurableTask.Server.Grpc [![Nuget](https://img.shields.io/nuget/v/LLL.DurableTask.Server.Grpc)](https://www.nuget.org/packages/LLL.DurableTask.Server.Grpc/) 2 | 3 | GRPC endpoints for server. 4 | 5 | The chatty orchestration execution communication is done with bidirectional streaming, maintaining the orchestration session alive in the server side. 6 | 7 | Activity execution and all remaining communication is done with non streamed rpc. 8 | 9 | ## Configuration 10 | 11 | ```C# 12 | services.AddDurableTaskServer(builder => 13 | { 14 | builder.AddGrpcEndpoints(); 15 | }); 16 | ... 17 | app.UseEndpoints(endpoints => 18 | { 19 | endpoints.MapDurableTaskServerGrpcService(); 20 | }); 21 | ``` 22 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server/DependencyInjection/DurableTaskServerServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LLL.DurableTask.Server.Configuration; 3 | using LLL.DurableTask.Server.Services; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection; 6 | 7 | public static class DurableTaskServerServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddDurableTaskServer( 10 | this IServiceCollection services, 11 | Action config = null) 12 | { 13 | services.AddHostedService(); 14 | 15 | var builder = new TaskHubServerBuilder(services); 16 | 17 | config?.Invoke(builder); 18 | 19 | return services; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server/DependencyInjection/ITaskHubServerBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace LLL.DurableTask.Server.Configuration; 4 | 5 | public interface ITaskHubServerBuilder 6 | { 7 | IServiceCollection Services { get; } 8 | } 9 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server/DependencyInjection/TaskHubServerBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace LLL.DurableTask.Server.Configuration; 4 | 5 | public class TaskHubServerBuilder : ITaskHubServerBuilder 6 | { 7 | public IServiceCollection Services { get; } 8 | 9 | public TaskHubServerBuilder(IServiceCollection services) 10 | { 11 | Services = services; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server/LLL.DurableTask.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | 6 | 7 | 8 | Exposes a Durable Task storage implementation 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server/README.md: -------------------------------------------------------------------------------- 1 | # LLL.DurableTask.Server [![Nuget](https://img.shields.io/nuget/v/LLL.DurableTask.Server)](https://www.nuget.org/packages/LLL.DurableTask.Server/) 2 | 3 | Expose any storage implementation as API. 4 | 5 | Allow microservices to connect to an API instead of directly to storage. 6 | 7 | ## Depends on 8 | 9 | - Storage 10 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Server/Services/ServerHostedService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using DurableTask.Core; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace LLL.DurableTask.Server.Services; 7 | 8 | public class ServerHostedService : IHostedService 9 | { 10 | private readonly IOrchestrationService _orchestrationService; 11 | 12 | public ServerHostedService(IOrchestrationService orchestrationService) 13 | { 14 | _orchestrationService = orchestrationService; 15 | } 16 | 17 | public async Task StartAsync(CancellationToken cancellationToken) 18 | { 19 | await _orchestrationService.CreateIfNotExistsAsync(); 20 | await _orchestrationService.StartAsync(); 21 | } 22 | 23 | public Task StopAsync(CancellationToken cancellationToken) 24 | { 25 | return _orchestrationService.StopAsync(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/.gitignore: -------------------------------------------------------------------------------- 1 | app/build 2 | wwwroot -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/DependencyInjection/DurableTaskUiOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.DependencyInjection; 2 | 3 | public class DurableTaskUiOptions 4 | { 5 | public string ApiBaseUrl { get; set; } = "/api"; 6 | public OidcOptions Oidc { get; set; } 7 | public string[] UserNameClaims { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/DependencyInjection/DurableTaskUiServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.Extensions.DependencyInjection; 4 | 5 | public static class DurableTaskServerServiceCollectionExtensions 6 | { 7 | public static IServiceCollection AddDurableTaskUi( 8 | this IServiceCollection services, 9 | Action configure = null) 10 | { 11 | services.AddOptions(); 12 | 13 | if (configure != null) 14 | services.Configure(configure); 15 | 16 | return services; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/DependencyInjection/OidcOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.Extensions.DependencyInjection; 4 | 5 | public class OidcOptions 6 | { 7 | [JsonPropertyName("authority")] 8 | public string Authority { get; set; } 9 | 10 | [JsonPropertyName("client_id")] 11 | public string ClientId { get; set; } 12 | 13 | [JsonPropertyName("response_type")] 14 | public string ResponseType { get; set; } 15 | 16 | [JsonPropertyName("scope")] 17 | public string Scope { get; set; } 18 | 19 | [JsonPropertyName("prompt")] 20 | public string Prompt { get; set; } 21 | 22 | [JsonPropertyName("display")] 23 | public string Display { get; set; } 24 | 25 | [JsonPropertyName("loadUserInfo")] 26 | public bool? LoadUserInfo { get; set; } 27 | 28 | [JsonPropertyName("automaticSilentRenew")] 29 | public bool AutomaticSilentRenew { get; set; } 30 | } 31 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/Extensions/HttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace LLL.DurableTask.Ui.Extensions; 7 | 8 | static class HttpContextExtensions 9 | { 10 | private static readonly JsonSerializerOptions _serializerOptions = new() 11 | { 12 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 13 | Converters = { new JsonStringEnumConverter() } 14 | }; 15 | 16 | public static async Task RespondJson( 17 | this HttpContext context, 18 | object data, 19 | int statusCode = 200) 20 | { 21 | var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(data, _serializerOptions); 22 | context.Response.StatusCode = statusCode; 23 | context.Response.ContentType = "application/json; charset=utf-8"; 24 | await context.Response.Body.WriteAsync(jsonBytes); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/public/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiBaseUrl": "/api" 3 | } 4 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/src/LLL.DurableTask.Ui/app/public/favicon.ico -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/src/LLL.DurableTask.Ui/app/public/logo192.png -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaslorentz/durabletask-extensions/6a866f3117b4a98bbdf718931836a45168286217/src/LLL.DurableTask.Ui/app/public/logo512.png -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/ConfigurationProvider.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import React, { useContext } from "react"; 3 | import { useAsync } from "react-use"; 4 | import { ErrorAlert } from "./components/ErrorAlert"; 5 | import { Configuration } from "./models/Configuration"; 6 | 7 | type Props = { 8 | children: React.ReactNode; 9 | }; 10 | 11 | const configurationContext = React.createContext( 12 | undefined 13 | ); 14 | 15 | export function useConfiguration(): Configuration { 16 | return useContext(configurationContext) as Configuration; 17 | } 18 | 19 | export function ConfigurationProvider(props: Props) { 20 | const { children } = props; 21 | 22 | const configAsync = useAsync(async () => { 23 | const response = await axios.get("configuration.json"); 24 | return response.data; 25 | }, []); 26 | 27 | if (configAsync.error) { 28 | return ; 29 | } 30 | 31 | if (configAsync.loading) return null; 32 | 33 | return ( 34 | 35 | {children} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/CustomTheme.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from "@mui/material/styles"; 2 | 3 | const { breakpoints, spacing } = createTheme(); 4 | 5 | export const customTheme = createTheme({ 6 | spacing: spacing, 7 | breakpoints: breakpoints, 8 | typography: { 9 | fontSize: 13, 10 | h1: { 11 | fontSize: "2rem", 12 | }, 13 | h2: { 14 | fontSize: "1.75rem", 15 | }, 16 | h3: { 17 | fontSize: "1.5rem", 18 | }, 19 | h4: { 20 | fontSize: "1.25rem", 21 | }, 22 | h5: { 23 | fontSize: "1rem", 24 | }, 25 | h6: { 26 | fontSize: "0.875rem", 27 | }, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/components/AuthorizedGuard.tsx: -------------------------------------------------------------------------------- 1 | import Alert from "@mui/material/Alert"; 2 | import React from "react"; 3 | import { useApiClient } from "../hooks/useApiClient"; 4 | import { Endpoint } from "../models/ApiModels"; 5 | 6 | type Props = { 7 | requiredEndpoints: Endpoint[]; 8 | children?: React.ReactNode; 9 | }; 10 | 11 | export function AuthorizedGuard(props: Props) { 12 | const apiClient = useApiClient(); 13 | 14 | const { requiredEndpoints, children } = props; 15 | 16 | const authorizedEndpoints = requiredEndpoints.filter((e) => 17 | apiClient.isAuthorized(e) 18 | ); 19 | 20 | const authorized = requiredEndpoints.length === authorizedEndpoints.length; 21 | 22 | if (!authorized) { 23 | return Unauthorized; 24 | } 25 | 26 | return <>{children}; 27 | } 28 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/components/ErrorAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Alert from '@mui/material/Alert'; 3 | 4 | type Props = { 5 | error: unknown; 6 | }; 7 | 8 | export function ErrorAlert(props: Props) { 9 | const { error } = props; 10 | 11 | if (!error) return null; 12 | 13 | return {String(error)}; 14 | } 15 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/components/MuiCodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import Editor from "@monaco-editor/react"; 2 | import { TextField as MuiTextField } from "@mui/material"; 3 | import { TextFieldProps } from "@mui/material/TextField"; 4 | import React from "react"; 5 | 6 | type OmitTextFieldProps = "multiline" | "onChange"; 7 | 8 | type EditorProps = React.ComponentProps; 9 | 10 | type Props = Omit & { 11 | editorProps?: EditorProps; 12 | }; 13 | 14 | const wrappedEditor = React.forwardRef((props: EditorProps, _ref) => { 15 | return ; 16 | }); 17 | 18 | export function MuiCodeEditor(props: Props) { 19 | const { disabled, editorProps, ...other } = props; 20 | 21 | return ( 22 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/form/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react-lite"; 2 | import React from "react"; 3 | import { MuiCodeEditor } from "../components/MuiCodeEditor"; 4 | import { Field } from "./useForm"; 5 | 6 | type PropsDerivedFromField = 7 | | "required" 8 | | "label" 9 | | "value" 10 | | "onChange" 11 | | "error" 12 | | "helperText"; 13 | 14 | type Props = { field: Field } & Omit< 15 | React.ComponentProps, 16 | PropsDerivedFromField 17 | >; 18 | 19 | export const CodeEditor = observer((props: Props) => { 20 | const { field, editorProps, ...other } = props; 21 | return ( 22 | (field.value = value), 32 | }} 33 | error={field.hasError} 34 | helperText={field.errorMessage} 35 | /> 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/form/TextField.tsx: -------------------------------------------------------------------------------- 1 | import { TextField as MuiTextField } from "@mui/material"; 2 | import { TextFieldProps } from "@mui/material/TextField"; 3 | import { observer } from "mobx-react-lite"; 4 | import React from "react"; 5 | import { Field } from "./useForm"; 6 | 7 | export const TextField = observer( 8 | (props: { field: Field } & TextFieldProps) => { 9 | const { field, ...other } = props; 10 | return ( 11 | (field.value = e.target.value)} 19 | error={field.hasError} 20 | helperText={field.errorMessage} 21 | /> 22 | ); 23 | }, 24 | ); 25 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/hooks/useApiClient.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { useAsync } from "react-use"; 3 | import { useAuth } from "./useAuth"; 4 | import { ApiClient } from "../clients/ApiClient"; 5 | import { ErrorAlert } from "../components/ErrorAlert"; 6 | import { useConfiguration } from "../ConfigurationProvider"; 7 | 8 | type Props = { 9 | children: React.ReactNode; 10 | }; 11 | 12 | const apiClientContext = React.createContext(undefined); 13 | 14 | export function useApiClient(): ApiClient { 15 | return useContext(apiClientContext) as ApiClient; 16 | } 17 | 18 | export function ApiClientProvider(props: Props) { 19 | const { children } = props; 20 | 21 | const configuration = useConfiguration(); 22 | const auth = useAuth(); 23 | 24 | const apiClientAsync = useAsync(async () => { 25 | try { 26 | const apiClient = new ApiClient(); 27 | apiClient.setToken(auth.user?.access_token); 28 | await apiClient.initialize(configuration.apiBaseUrl); 29 | return apiClient; 30 | } catch (error: any) { 31 | if (error?.response?.status === 401) { 32 | await auth.signIn?.(); 33 | } 34 | throw error; 35 | } 36 | }, [auth.user, configuration]); 37 | 38 | if (apiClientAsync.error) { 39 | return ; 40 | } 41 | 42 | if (apiClientAsync.loading) return null; 43 | 44 | return ( 45 | 46 | {children} 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/hooks/useDynamicRefs.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react"; 2 | 3 | type DynamicRefsResult = [ 4 | (name: string | number) => T, 5 | (name: string | number) => (value: T) => void 6 | ]; 7 | 8 | export function useDynamicRefs( 9 | updateOnChange: boolean = false 10 | ): DynamicRefsResult { 11 | const ref = useRef(); 12 | const [, setState] = useState(0); 13 | if (ref.current == null) { 14 | const values: Record = {}; 15 | const callbacks: Record void> = {}; 16 | ref.current = [ 17 | (name: string | number) => values[name], 18 | (name: string | number) => { 19 | if (name in callbacks) return callbacks[name]; 20 | return (callbacks[name] = (value) => { 21 | values[name] = value; 22 | if (updateOnChange) { 23 | setState((v) => v + 1); 24 | } 25 | }); 26 | }, 27 | ]; 28 | } 29 | return ref.current!; 30 | } 31 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/hooks/useLocationState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from "react"; 2 | import { useLocation, useNavigate } from "react-router-dom"; 3 | 4 | export function useLocationState( 5 | name: string, 6 | initialValue: T 7 | ): [T, (v: T) => void] { 8 | const location = useLocation(); 9 | const navigate = useNavigate(); 10 | 11 | const stateValue = useMemo(() => { 12 | const state = location.state as Record; 13 | return state && name in state ? state[name] : initialValue; 14 | }, [initialValue, location.state, name]); 15 | 16 | const setValue = useCallback( 17 | (newValue: T) => { 18 | navigate( 19 | { 20 | ...location, 21 | }, 22 | { 23 | replace: true, 24 | state: { 25 | ...(location.state as any), 26 | [name]: newValue, 27 | }, 28 | } 29 | ); 30 | }, 31 | [location, name, navigate] 32 | ); 33 | 34 | return [stateValue, setValue]; 35 | } 36 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/hooks/usePageSize.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from "react"; 2 | import { useLocalStorage } from "react-use"; 3 | 4 | export function usePageSize(): [number, Dispatch] { 5 | const [value, setValue] = useLocalStorage("pageSize"); 6 | return [value ?? 10, setValue]; 7 | } 8 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/hooks/useRefreshInterval.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, useCallback } from "react"; 2 | import { useLocalStorage } from "react-use"; 3 | 4 | export function useRefreshInterval( 5 | name: string 6 | ): [number | undefined, Dispatch] { 7 | const [value, setValue, remove] = useLocalStorage( 8 | `refresh-interval/${name}` 9 | ); 10 | 11 | const customSetValue = useCallback( 12 | (newValue: number | undefined) => { 13 | if (newValue) { 14 | setValue(newValue); 15 | } else { 16 | remove(); 17 | } 18 | }, 19 | [remove, setValue] 20 | ); 21 | 22 | return [value, customSetValue]; 23 | } 24 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/models/Configuration.ts: -------------------------------------------------------------------------------- 1 | import { UserManagerSettings } from "oidc-client"; 2 | 3 | export interface Configuration { 4 | apiBaseUrl: string; 5 | oidc?: UserManagerSettings; 6 | userNameClaims?: string[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/utils/date-utils.ts: -------------------------------------------------------------------------------- 1 | export function toLocalISO(value: string) { 2 | if (!value) return ""; 3 | 4 | let date = new Date(value); 5 | 6 | const year = String(date.getFullYear()).padStart(4, "0"), 7 | month = String(date.getMonth() + 1).padStart(2, "0"), 8 | day = String(date.getDate()).padStart(2, "0"), 9 | hours = String(date.getHours()).padStart(2, "0"), 10 | minutes = String(date.getMinutes()).padStart(2, "0"), 11 | seconds = String(date.getSeconds()).padStart(2, "0"), 12 | milliseconds = String(date.getMilliseconds()).padStart(3, "0"); 13 | 14 | return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}`; 15 | } 16 | 17 | export function toUtcISO(value: string) { 18 | if (!value) return ""; 19 | 20 | return new Date(value).toISOString(); 21 | } 22 | 23 | export function formatDateTime(value: string) { 24 | if (!value) return ""; 25 | 26 | let date = new Date(value); 27 | 28 | if (date.getFullYear() <= 1 || date.getFullYear() >= 9999) { 29 | return ""; 30 | } 31 | 32 | return date.toLocaleString(); 33 | } 34 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/utils/yup-utils.ts: -------------------------------------------------------------------------------- 1 | import { TestContext } from "yup"; 2 | 3 | export function validateJson(value: string, testContext: TestContext) { 4 | if (!value) return true; 5 | try { 6 | JSON.parse(value); 7 | return true; 8 | } catch (e) { 9 | return testContext.createError({ 10 | path: testContext.path, 11 | message: `Invalid JSON: ${e}`, 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/views/create/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Create"; 2 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/views/home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@mui/material"; 2 | import React from "react"; 3 | 4 | export function Home() { 5 | return ( 6 |
7 | 8 | Welcome to Durable Task UI 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/views/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Home"; 2 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/views/not_found/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@mui/material"; 2 | import React from "react"; 3 | 4 | export function NotFound() { 5 | return ( 6 |
7 | 8 | Page not found 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/views/not_found/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./NotFound"; 2 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/views/orchestration/LineBuilder.tsx: -------------------------------------------------------------------------------- 1 | export class LineBuilder { 2 | private commands: string[] = []; 3 | public left: number = 0; 4 | public top: number = 0; 5 | public color: string; 6 | 7 | constructor(color: string) { 8 | this.color = color; 9 | } 10 | 11 | lineTo(left: number, top: number) { 12 | var controlPoint1 = { 13 | left: this.left, 14 | top: (this.top + top) / 2, 15 | }; 16 | 17 | var midPoint = { 18 | left: (this.left + left) / 2, 19 | top: (this.top + top) / 2, 20 | }; 21 | 22 | var controlPoint2 = { 23 | left: left, 24 | top: (this.top + top) / 2, 25 | }; 26 | 27 | this.commands.push( 28 | `Q ${controlPoint1.left} ${controlPoint1.top} ${midPoint.left} ${midPoint.top}`, 29 | `Q ${controlPoint2.left} ${controlPoint2.top} ${left} ${top}` 30 | ); 31 | 32 | this.left = left; 33 | this.top = top; 34 | } 35 | 36 | moveTo(left: number, top: number) { 37 | this.commands.push(`M ${left} ${top}`); 38 | this.left = left; 39 | this.top = top; 40 | } 41 | 42 | public toPath(): string { 43 | return this.commands.join(" "); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/views/orchestration/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Orchestration"; 2 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/src/views/orchestrations/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Orchestrations"; 2 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Ui/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react", 21 | "experimentalDecorators": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/Activities/ServiceProviderTaskActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DurableTask.Core; 4 | 5 | namespace LLL.DurableTask.Worker.Activities; 6 | 7 | public class ServiceProviderTaskActivity : TaskActivity 8 | { 9 | public ServiceProviderTaskActivity(Func factory) 10 | { 11 | Factory = factory; 12 | } 13 | 14 | public Func Factory { get; } 15 | public TaskActivity Instance { get; private set; } 16 | 17 | public void Initialize(IServiceProvider serviceProvider) 18 | { 19 | if (Instance == null) 20 | Instance = Factory(serviceProvider); 21 | } 22 | 23 | public override string Run(TaskContext context, string input) 24 | { 25 | return Instance.Run(context, input); 26 | } 27 | 28 | public override async Task RunAsync(TaskContext context, string input) 29 | { 30 | return await Instance.RunAsync(context, input); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/ActivityBase.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableTask.Core; 3 | using LLL.DurableTask.Core.Serializing; 4 | 5 | namespace LLL.DurableTask.Worker; 6 | 7 | public abstract class ActivityBase : AsyncTaskActivity 8 | { 9 | public TaskContext Context { get; private set; } 10 | 11 | public ActivityBase() 12 | { 13 | DataConverter = new TypelessJsonDataConverter(); 14 | } 15 | 16 | protected sealed override async Task ExecuteAsync(TaskContext context, TInput input) 17 | { 18 | Context = context; 19 | return await ExecuteAsync(input); 20 | } 21 | 22 | public abstract Task ExecuteAsync(TInput input); 23 | } 24 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/Attributes/ActivityAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LLL.DurableTask.Worker.Attributes; 4 | 5 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 6 | public class ActivityAttribute : Attribute 7 | { 8 | public string Name { get; set; } 9 | public string Version { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/Attributes/OrchestrationAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LLL.DurableTask.Worker.Attributes; 4 | 5 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 6 | public class OrchestrationAttribute : Attribute 7 | { 8 | public string Name { get; set; } 9 | public string Version { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/Builder/IDurableTaskWorkerBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DurableTask.Core; 4 | using DurableTask.Core.Middleware; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace LLL.DurableTask.Worker.Builder; 8 | 9 | public interface IDurableTaskWorkerBuilder 10 | { 11 | IServiceCollection Services { get; } 12 | 13 | bool HasAllOrchestrations { get; set; } 14 | bool HasAllActivities { get; set; } 15 | 16 | DurableTaskWorkerBuilder AddOrchestration(Func factory, string name = null, string version = null); 17 | 18 | DurableTaskWorkerBuilder AddActivity(Func factory, string name = null, string version = null); 19 | 20 | DurableTaskWorkerBuilder AddOrchestrationDispatcherMiddleware(Func, Task>> factory); 21 | 22 | DurableTaskWorkerBuilder AddActivityDispatcherMiddleware(Func, Task>> factory); 23 | } 24 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/DependencyInjection/DurableTaskWorkerServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DurableTask.Core; 3 | using LLL.DurableTask.Worker.Builder; 4 | using LLL.DurableTask.Worker.Middlewares; 5 | using LLL.DurableTask.Worker.Services; 6 | using Microsoft.Extensions.DependencyInjection.Extensions; 7 | 8 | namespace Microsoft.Extensions.DependencyInjection; 9 | 10 | public static class DurableTaskWorkerServiceCollectionExtensions 11 | { 12 | public static IServiceCollection AddDurableTaskWorker( 13 | this IServiceCollection services, 14 | Action config = null) 15 | { 16 | services.AddHostedService(); 17 | 18 | services.TryAddSingleton(); 19 | services.TryAddSingleton(); 20 | 21 | var builder = new DurableTaskWorkerBuilder(services); 22 | 23 | config?.Invoke(builder); 24 | 25 | services.TryAddSingleton(provider => builder.Build(provider)); 26 | 27 | return services; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/LLL.DurableTask.Worker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | 6 | 7 | 8 | Configure Durable Task workers 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/Middlewares/ServiceProviderActivityMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DurableTask.Core; 4 | using DurableTask.Core.Middleware; 5 | using LLL.DurableTask.Worker.Activities; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace LLL.DurableTask.Worker.Middlewares; 9 | 10 | public class ServiceProviderActivityMiddleware 11 | { 12 | private readonly IServiceScopeFactory _serviceScopeFactory; 13 | 14 | public ServiceProviderActivityMiddleware(IServiceScopeFactory serviceScopeFactory) 15 | { 16 | _serviceScopeFactory = serviceScopeFactory; 17 | } 18 | 19 | public async Task Execute(DispatchMiddlewareContext context, Func next) 20 | { 21 | using var serviceScope = _serviceScopeFactory.CreateScope(); 22 | context.SetProperty(serviceScope.ServiceProvider); 23 | 24 | if (context.GetProperty() is ServiceProviderTaskActivity initializable) 25 | { 26 | initializable.Initialize(serviceScope.ServiceProvider); 27 | } 28 | 29 | await next(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/Middlewares/ServiceProviderOrchestrationMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DurableTask.Core; 4 | using DurableTask.Core.Middleware; 5 | using LLL.DurableTask.Worker.Orchestrations; 6 | using LLL.DurableTask.Worker.Services; 7 | 8 | namespace LLL.DurableTask.Worker.Middlewares; 9 | 10 | public class ServiceProviderOrchestrationMiddleware 11 | { 12 | public async Task Execute(DispatchMiddlewareContext context, Func next) 13 | { 14 | var orchestrationInstance = context.GetProperty(); 15 | 16 | var serviceScope = WorkerOrchestrationService.OrchestrationsServiceScopes[orchestrationInstance.InstanceId]; 17 | 18 | context.SetProperty(serviceScope.ServiceProvider); 19 | 20 | if (context.GetProperty() is ServiceProviderTaskOrchestration initializable) 21 | { 22 | initializable.Initialize(serviceScope.ServiceProvider); 23 | } 24 | 25 | await next(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/ObjectCreators/FactoryObjectCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DurableTask.Core; 3 | 4 | namespace LLL.DurableTask.Worker.ObjectCreators; 5 | 6 | public class FactoryObjectCreator : ObjectCreator 7 | { 8 | private readonly Func _factory; 9 | 10 | public FactoryObjectCreator( 11 | string name, 12 | string version, 13 | Func factory) 14 | { 15 | Name = name; 16 | Version = version; 17 | _factory = factory; 18 | } 19 | 20 | public override T Create() 21 | { 22 | return _factory(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/OrchestrationBase.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableTask.Core; 3 | using LLL.DurableTask.Core.Serializing; 4 | 5 | namespace LLL.DurableTask.Worker; 6 | 7 | public abstract class OrchestrationBase : TaskOrchestration 8 | { 9 | public ExtendedOrchestrationContext Context { get; private set; } 10 | 11 | public sealed override async Task Execute(OrchestrationContext context, string input) 12 | { 13 | context.MessageDataConverter = new TypelessJsonDataConverter(); 14 | context.ErrorDataConverter = new TypelessJsonDataConverter(); 15 | Context = new ExtendedOrchestrationContext(context); 16 | 17 | var parameter = context.MessageDataConverter.Deserialize(input); 18 | 19 | var result = await Execute(parameter); 20 | return context.MessageDataConverter.Serialize(result); 21 | } 22 | 23 | public sealed override string GetStatus() 24 | { 25 | if (Context.StatusProvider != null) 26 | return Context.StatusProvider(); 27 | 28 | return Context.MessageDataConverter.Serialize(OnGetStatus()); 29 | } 30 | 31 | public sealed override void RaiseEvent(OrchestrationContext context, string name, string input) 32 | { 33 | Context.RaiseEvent(name, input); 34 | } 35 | 36 | public abstract Task Execute(TInput input); 37 | 38 | public virtual object OnGetStatus() 39 | { 40 | return null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/OrchestrationContextStatusExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LLL.DurableTask.Worker; 4 | 5 | public static class OrchestrationContextStatusExtensions 6 | { 7 | public static void SetStatusProvider( 8 | this ExtendedOrchestrationContext context, 9 | Func statusProvider) 10 | { 11 | if (statusProvider == null) 12 | { 13 | context.StatusProvider = null; 14 | return; 15 | } 16 | 17 | context.StatusProvider = () => context.MessageDataConverter.Serialize(statusProvider()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/Orchestrations/ServiceProviderTaskOrchestration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DurableTask.Core; 4 | 5 | namespace LLL.DurableTask.Worker.Orchestrations; 6 | 7 | public class ServiceProviderTaskOrchestration : TaskOrchestration 8 | { 9 | public ServiceProviderTaskOrchestration(Func factory) 10 | { 11 | Factory = factory; 12 | } 13 | 14 | public Func Factory { get; } 15 | public TaskOrchestration Instance { get; private set; } 16 | 17 | public void Initialize(IServiceProvider serviceProvider) 18 | { 19 | if (Instance == null) 20 | Instance = Factory(serviceProvider); 21 | } 22 | 23 | public override Task Execute(OrchestrationContext context, string input) 24 | { 25 | return Instance.Execute(context, input); 26 | } 27 | 28 | public override string GetStatus() 29 | { 30 | return Instance.GetStatus(); 31 | } 32 | 33 | public override void RaiseEvent(OrchestrationContext context, string name, string input) 34 | { 35 | Instance.RaiseEvent(context, name, input); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/LLL.DurableTask.Worker/Services/WorkerHostedService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using DurableTask.Core; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace LLL.DurableTask.Worker.Services; 7 | 8 | public class WorkerHostedService : IHostedService 9 | { 10 | private readonly IOrchestrationService _orchestrationService; 11 | private readonly TaskHubWorker _taskHubWorker; 12 | 13 | public WorkerHostedService( 14 | IOrchestrationService orchestrationService, 15 | TaskHubWorker taskHubWorker) 16 | { 17 | _orchestrationService = orchestrationService; 18 | _taskHubWorker = taskHubWorker; 19 | } 20 | 21 | public async Task StartAsync(CancellationToken cancellationToken) 22 | { 23 | await _orchestrationService.CreateIfNotExistsAsync(); 24 | 25 | await _taskHubWorker.StartAsync(); 26 | } 27 | 28 | public async Task StopAsync(CancellationToken cancellationToken) 29 | { 30 | await _taskHubWorker.StopAsync(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Api/PurgeOrchestrationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading.Tasks; 3 | using AwesomeAssertions; 4 | using DurableTask.Core; 5 | using Microsoft.AspNetCore.TestHost; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Xunit; 8 | using Xunit.Abstractions; 9 | 10 | namespace LLL.DurableTask.Tests.Api; 11 | 12 | public class PurgeOrchestrationTests : ApiTestBase 13 | { 14 | public PurgeOrchestrationTests(ITestOutputHelper output) : base(output) 15 | { 16 | } 17 | 18 | [Trait("Category", "Integration")] 19 | [Fact] 20 | public async Task PurgeOrchestration_ShouldReturnSuccess() 21 | { 22 | var taskHubClient = _host.Services.GetRequiredService(); 23 | 24 | var orchestrationInstance = await taskHubClient.CreateOrchestrationInstanceAsync("Test", "v1", null); 25 | 26 | var stateBeforePurge = await taskHubClient.GetOrchestrationStateAsync(orchestrationInstance.InstanceId); 27 | stateBeforePurge.Should().NotBeNull(); 28 | 29 | using var httpClient = _host.GetTestClient(); 30 | 31 | var httpResponse = await httpClient.DeleteAsync($"/api/v1/orchestrations/{orchestrationInstance.InstanceId}"); 32 | httpResponse.StatusCode.Should().Be(HttpStatusCode.OK); 33 | httpResponse.Content.Headers.ContentType.MediaType.Should().Be("application/json"); 34 | 35 | var content = await httpResponse.Content.ReadAsStringAsync(); 36 | content.Should().Be("{}"); 37 | 38 | var stateAfterPurge = await taskHubClient.GetOrchestrationStateAsync(orchestrationInstance.InstanceId); 39 | stateAfterPurge.Should().BeNull(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Api/RewindOrchestrationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using AwesomeAssertions; 6 | using DurableTask.Core; 7 | using LLL.DurableTask.Api.Models; 8 | using Microsoft.AspNetCore.TestHost; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Newtonsoft.Json; 11 | using Xunit; 12 | using Xunit.Abstractions; 13 | 14 | namespace LLL.DurableTask.Tests.Api; 15 | 16 | public class RewindOrchestrationTests : ApiTestBase 17 | { 18 | public RewindOrchestrationTests(ITestOutputHelper output) : base(output) 19 | { 20 | } 21 | 22 | [Trait("Category", "Integration")] 23 | [Fact] 24 | public async Task RewindOrchestration_ShouldReturnSuccess() 25 | { 26 | var taskHubClient = _host.Services.GetRequiredService(); 27 | 28 | var orchestrationInstance = await taskHubClient.CreateOrchestrationInstanceAsync("Test", "v1", null); 29 | 30 | using var httpClient = _host.GetTestClient(); 31 | 32 | var request = new RewindRequest 33 | { 34 | Reason = "Some reason" 35 | }; 36 | var requestJson = JsonConvert.SerializeObject(request); 37 | var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); 38 | 39 | var httpResponse = await httpClient.PostAsync($"/api/v1/orchestrations/{orchestrationInstance.InstanceId}/rewind", requestContent); 40 | httpResponse.StatusCode.Should().Be(HttpStatusCode.OK); 41 | httpResponse.Content.Headers.ContentType.MediaType.Should().Be("application/json"); 42 | 43 | var content = await httpResponse.Content.ReadAsStringAsync(); 44 | content.Should().Be("{}"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/Activities/MeasuredDelayActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DurableTask.Core; 4 | 5 | namespace LLL.DurableTask.Tests.Storage.Activities; 6 | 7 | public class MeasuredDelayActivity : AsyncTaskActivity 8 | { 9 | public const string Name = "MeasuredDelay"; 10 | public const string Version = "v1"; 11 | 12 | protected override async Task ExecuteAsync(TaskContext context, int milliseconds) 13 | { 14 | var start = DateTime.UtcNow; 15 | await Task.Delay(milliseconds); 16 | return new Output 17 | { 18 | Start = start, 19 | End = DateTime.UtcNow 20 | }; 21 | } 22 | 23 | public class Output 24 | { 25 | public DateTime Start { get; set; } 26 | public DateTime End { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/Activities/SubtractActivity.cs: -------------------------------------------------------------------------------- 1 | using DurableTask.Core; 2 | 3 | namespace LLL.DurableTask.Tests.Storage.Activities; 4 | 5 | public class SubtractActivity : TaskActivity 6 | { 7 | public const string Name = "SubtractActivity"; 8 | public const string Version = "v1"; 9 | 10 | protected override int Execute(TaskContext context, Input input) 11 | { 12 | return input.LeftValue - input.RightValue; 13 | } 14 | 15 | public class Input 16 | { 17 | public int LeftValue { get; set; } 18 | public int RightValue { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/Activities/SumActivity.cs: -------------------------------------------------------------------------------- 1 | using DurableTask.Core; 2 | 3 | namespace LLL.DurableTask.Tests.Storage.Activities; 4 | 5 | public class SumActivity : TaskActivity 6 | { 7 | public const string Name = "Sum"; 8 | public const string Version = "v1"; 9 | 10 | protected override int Execute(TaskContext context, Input input) 11 | { 12 | return input.LeftValue + input.RightValue; 13 | } 14 | 15 | public class Input 16 | { 17 | public int LeftValue { get; set; } 18 | public int RightValue { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/AzureStorageServerTests.cs: -------------------------------------------------------------------------------- 1 | using DurableTask.AzureStorage; 2 | using LLL.DurableTask.Worker.Builder; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace LLL.DurableTask.Tests.Storages; 9 | 10 | [Collection("AzureStorage")] 11 | public class AzureStorageServerTests : ServerStorageTestBase 12 | { 13 | public AzureStorageServerTests(ITestOutputHelper output) : base(output) 14 | { 15 | FastWaitTimeout *= 2; 16 | SlowWaitTimeout *= 2; 17 | SupportsMultipleExecutionStorage = false; 18 | SupportsTags = false; 19 | SupportsEventsAfterCompletion = false; 20 | } 21 | 22 | protected override void ConfigureServerStorage(IServiceCollection services) 23 | { 24 | var connectionString = Configuration.GetConnectionString("AzureStorageAccount"); 25 | 26 | Skip.If(string.IsNullOrWhiteSpace(connectionString), "AzureStorageAccount connection string not configured"); 27 | 28 | services.AddDurableTaskAzureStorage(options => 29 | { 30 | options.TaskHubName = "test"; 31 | options.StorageAccountClientProvider = new StorageAccountClientProvider(connectionString); 32 | }); 33 | } 34 | 35 | protected override void ConigureWorker(IDurableTaskWorkerBuilder builder) 36 | { 37 | base.ConigureWorker(builder); 38 | 39 | builder.HasAllOrchestrations = true; 40 | builder.HasAllActivities = true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/AzureStorageTests.cs: -------------------------------------------------------------------------------- 1 | using DurableTask.AzureStorage; 2 | using LLL.DurableTask.Worker.Builder; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace LLL.DurableTask.Tests.Storages; 9 | 10 | [Collection("AzureStorage")] 11 | public class AzureStorageTests : StorageTestBase 12 | { 13 | public AzureStorageTests(ITestOutputHelper output) : base(output) 14 | { 15 | FastWaitTimeout *= 2; 16 | SlowWaitTimeout *= 2; 17 | SupportsMultipleExecutionStorage = false; 18 | SupportsTags = false; 19 | SupportsEventsAfterCompletion = false; 20 | } 21 | 22 | protected override void ConfigureStorage(IServiceCollection services) 23 | { 24 | var connectionString = Configuration.GetConnectionString("AzureStorageAccount"); 25 | 26 | Skip.If(string.IsNullOrWhiteSpace(connectionString), "AzureStorageAccount connection string not configured"); 27 | 28 | services.AddDurableTaskAzureStorage(options => 29 | { 30 | options.TaskHubName = "test"; 31 | options.StorageAccountClientProvider = new StorageAccountClientProvider(connectionString); 32 | }); 33 | } 34 | 35 | protected override void ConigureWorker(IDurableTaskWorkerBuilder builder) 36 | { 37 | base.ConigureWorker(builder); 38 | 39 | builder.HasAllOrchestrations = true; 40 | builder.HasAllActivities = true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/InMemoryEFCoreServerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit.Abstractions; 5 | 6 | namespace LLL.DurableTask.Tests.Storages; 7 | 8 | public class InMemoryEFCoreServerTests : ServerStorageTestBase 9 | { 10 | private readonly string _databaseId; 11 | 12 | public InMemoryEFCoreServerTests(ITestOutputHelper output) : base(output) 13 | { 14 | _databaseId = Guid.NewGuid().ToString(); 15 | } 16 | 17 | protected override void ConfigureServerStorage(IServiceCollection services) 18 | { 19 | services.AddDurableTaskEFCoreStorage() 20 | .UseInMemoryDatabase(_databaseId); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/InMemoryEFCoreTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Xunit.Abstractions; 4 | 5 | namespace LLL.DurableTask.Tests.Storages; 6 | 7 | public class InMemoryEFCoreTests : EFCoreTestBase 8 | { 9 | private readonly string _databaseId; 10 | 11 | public InMemoryEFCoreTests(ITestOutputHelper output) : base(output) 12 | { 13 | _databaseId = Guid.NewGuid().ToString(); 14 | } 15 | 16 | protected override void ConfigureStorage(IServiceCollection services) 17 | { 18 | services.AddDurableTaskEFCoreStorage() 19 | .UseInMemoryDatabase(_databaseId); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/MySqlEFCoreTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace LLL.DurableTask.Tests.Storages; 8 | 9 | [Collection("MySql")] 10 | public class MySqlEFCoreTests : EFCoreTestBase 11 | { 12 | public MySqlEFCoreTests(ITestOutputHelper output) : base(output) 13 | { 14 | } 15 | 16 | protected override void ConfigureStorage(IServiceCollection services) 17 | { 18 | var connectionString = Configuration.GetConnectionString("MySql"); 19 | 20 | Skip.If(string.IsNullOrWhiteSpace(connectionString), "MySql connection string not configured"); 21 | 22 | services.AddDurableTaskEFCoreStorage() 23 | .UseMySql(connectionString, MySqlServerVersion.AutoDetect(connectionString)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/Orchestrations/ContinueAsNewEmptyOrchestration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableTask.Core; 3 | 4 | namespace LLL.DurableTask.Tests.Storage.Orchestrations; 5 | 6 | public class ContinueAsNewEmptyOrchestration : TaskOrchestration 7 | { 8 | public const string Name = "ContinueAsNewEmpty"; 9 | public const string Version = "v1"; 10 | 11 | public override Task RunTask(OrchestrationContext context, int input) 12 | { 13 | if (input > 0) 14 | { 15 | context.ContinueAsNew(input - 1); 16 | } 17 | 18 | return Task.FromResult(input); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/Orchestrations/ContinueAsNewOrchestration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableTask.Core; 3 | using LLL.DurableTask.Tests.Storage.Activities; 4 | 5 | namespace LLL.DurableTask.Tests.Storage.Orchestrations; 6 | 7 | public class ContinueAsNewOrchestration : TaskOrchestration 8 | { 9 | public const string Name = "ContinueAsNew"; 10 | public const string Version = "v1"; 11 | 12 | public override async Task RunTask(OrchestrationContext context, int input) 13 | { 14 | if (input > 0) 15 | { 16 | var nextIteration = await context.ScheduleTask(SubtractActivity.Name, SubtractActivity.Version, new SubtractActivity.Input 17 | { 18 | LeftValue = input, 19 | RightValue = 1 20 | }); 21 | 22 | context.ContinueAsNew(nextIteration); 23 | } 24 | 25 | return input; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/Orchestrations/EmptyOrchestration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableTask.Core; 3 | 4 | namespace LLL.DurableTask.Tests.Storage.Orchestrations; 5 | 6 | public class EmptyOrchestration : TaskOrchestration 7 | { 8 | public const string Name = "Empty"; 9 | public const string Version = "v1"; 10 | 11 | public override Task RunTask(OrchestrationContext context, object input) 12 | { 13 | return Task.FromResult(input); 14 | } 15 | 16 | public override void OnEvent(OrchestrationContext context, string name, string input) 17 | { 18 | base.OnEvent(context, name, input); 19 | context.ContinueAsNew(input); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/Orchestrations/ParallelTasksOrchestration.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using DurableTask.Core; 4 | using LLL.DurableTask.Tests.Storage.Activities; 5 | 6 | namespace LLL.DurableTask.Tests.Storage.Orchestrations; 7 | 8 | public class ParallelTasksOrchestration : TaskOrchestration 9 | { 10 | public const string Name = "ParallelTasks"; 11 | public const string Version = "v1"; 12 | 13 | public override async Task RunTask(OrchestrationContext context, int numberOfTasks) 14 | { 15 | var tasks = Enumerable.Range(0, numberOfTasks).Select(_ => 16 | context.ScheduleTask( 17 | MeasuredDelayActivity.Name, 18 | MeasuredDelayActivity.Version, 19 | 2000) 20 | ); 21 | 22 | var measurements = await Task.WhenAll(tasks); 23 | 24 | var modifications = measurements.Select(m => new { Moment = m.Start, Change = 1 }) 25 | .Concat(measurements.Select(m => new { Moment = m.End, Change = -1 })) 26 | .OrderBy(m => m.Moment) 27 | .ToArray(); 28 | 29 | var parallelTasks = 0; 30 | var degreeOfParallelism = 0; 31 | foreach (var modification in modifications) 32 | { 33 | parallelTasks += modification.Change; 34 | if (parallelTasks > degreeOfParallelism) 35 | degreeOfParallelism = parallelTasks; 36 | } 37 | 38 | return degreeOfParallelism; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/Orchestrations/ParentOrchestration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableTask.Core; 3 | 4 | namespace LLL.DurableTask.Tests.Storage.Orchestrations; 5 | 6 | public class ParentOrchestration : TaskOrchestration 7 | { 8 | public const string Name = "ParentOrchestration"; 9 | public const string Version = "v1"; 10 | 11 | public override async Task RunTask(OrchestrationContext context, int input) 12 | { 13 | var subOrchestrationOutput = await context.CreateSubOrchestrationInstance(EmptyOrchestration.Name, EmptyOrchestration.Version, input); 14 | 15 | return subOrchestrationOutput; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/Orchestrations/SendEventOrchestration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableTask.Core; 3 | 4 | namespace LLL.DurableTask.Tests.Storage.Orchestrations; 5 | 6 | public class SendEventOrchestration : TaskOrchestration 7 | { 8 | public class Input 9 | { 10 | public string TargetInstanceId { get; set; } 11 | public string EventName { get; set; } 12 | public object EventInput { get; set; } 13 | } 14 | 15 | public const string Name = "SendEvent"; 16 | public const string Version = "v1"; 17 | 18 | public override Task RunTask(OrchestrationContext context, Input input) 19 | { 20 | var orchestrationInstance = new OrchestrationInstance 21 | { 22 | InstanceId = input.TargetInstanceId 23 | }; 24 | 25 | context.SendEvent(orchestrationInstance, input.EventName, input.EventInput); 26 | 27 | return Task.FromResult(default(object)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/Orchestrations/TimerOrchestration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DurableTask.Core; 4 | 5 | namespace LLL.DurableTask.Tests.Storage.Orchestrations; 6 | 7 | public class TimerOrchestration : TaskOrchestration 8 | { 9 | public const string Name = "Timer"; 10 | public const string Version = "v1"; 11 | 12 | public override async Task RunTask(OrchestrationContext context, object input) 13 | { 14 | var output = await context.CreateTimer(DateTime.UtcNow.AddSeconds(2), input); 15 | return output; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/Orchestrations/WaitForEventOrchestration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableTask.Core; 3 | 4 | namespace LLL.DurableTask.Tests.Storage.Orchestrations; 5 | 6 | public class WaitForEventOrchestration : TaskOrchestration 7 | { 8 | public const string Name = "WaitForEvent"; 9 | public const string Version = "v1"; 10 | 11 | private TaskCompletionSource _eventTaskCompletionSource; 12 | 13 | public override async Task RunTask(OrchestrationContext context, object _) 14 | { 15 | _eventTaskCompletionSource = new TaskCompletionSource(); 16 | var eventInput = await _eventTaskCompletionSource.Task; 17 | return eventInput; 18 | } 19 | 20 | public override void OnEvent(OrchestrationContext context, string name, object input) 21 | { 22 | if (name == "SetResult") 23 | _eventTaskCompletionSource?.TrySetResult(input); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/PostgresEFCoreTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace LLL.DurableTask.Tests.Storages; 7 | 8 | [Collection("Postgres")] 9 | public class PostgresEFCoreTests : EFCoreTestBase 10 | { 11 | public PostgresEFCoreTests(ITestOutputHelper output) : base(output) 12 | { 13 | } 14 | 15 | protected override void ConfigureStorage(IServiceCollection services) 16 | { 17 | var connectionString = Configuration.GetConnectionString("Postgres"); 18 | 19 | Skip.If(string.IsNullOrWhiteSpace(connectionString), "Postgres connection string not configured"); 20 | 21 | services.AddDurableTaskEFCoreStorage() 22 | .UseNpgsql(connectionString); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Storages/SqlServerEFCoreTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace LLL.DurableTask.Tests.Storages; 7 | 8 | [Collection("SqlServer")] 9 | public class SqlServerEFCoreTests : EFCoreTestBase 10 | { 11 | public SqlServerEFCoreTests(ITestOutputHelper output) : base(output) 12 | { 13 | } 14 | 15 | protected override void ConfigureStorage(IServiceCollection services) 16 | { 17 | var connectionString = Configuration.GetConnectionString("SqlServer"); 18 | 19 | Skip.If(string.IsNullOrWhiteSpace(connectionString), "SqlServer connection string not configured"); 20 | 21 | services.AddDurableTaskEFCoreStorage() 22 | .UseSqlServer(connectionString); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Utils/DbContextDispenser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using LLL.DurableTask.EFCore; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace LLL.DurableTask.Tests.Utils; 7 | 8 | public class DbContextDispenser : IDisposable 9 | { 10 | private readonly IDbContextFactory _factory; 11 | private readonly Stack _disposables; 12 | 13 | public DbContextDispenser(IDbContextFactory factory) 14 | { 15 | _factory = factory; 16 | _disposables = new Stack(); 17 | } 18 | 19 | public OrchestrationDbContext Get() 20 | { 21 | return _factory.CreateDbContext(); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | while (_disposables.TryPop(out var disposable)) 27 | disposable.Dispose(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Utils/ResponseVersionHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace LLL.DurableTask.Tests.Utils; 6 | 7 | class ResponseVersionHandler : DelegatingHandler 8 | { 9 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 10 | { 11 | var response = await base.SendAsync(request, cancellationToken); 12 | response.Version = request.Version; 13 | 14 | return response; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/Worker/TestHelpers/InvokeActivityOrchestration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using LLL.DurableTask.Worker; 3 | using LLL.DurableTask.Worker.Attributes; 4 | 5 | namespace LLL.DurableTask.Tests.Worker.TestHelpers; 6 | 7 | [Orchestration(Name = "InvokeActivity")] 8 | public class InvokeActivityOrchestration : OrchestrationBase 9 | { 10 | public class Input 11 | { 12 | public string Name { get; set; } 13 | public string Version { get; set; } 14 | public object[] Parameters { get; set; } = System.Array.Empty(); 15 | } 16 | 17 | public override async Task Execute(Input input) 18 | { 19 | return await Context.ScheduleTask(input.Name, input.Version, input.Parameters); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/LLL.DurableTask.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "AzureStorageAccount": "UseDevelopmentStorage=true", 4 | "MySql": "server=localhost;database=durabletask;user=root;password=root", 5 | "Postgres": "Server=localhost;Port=5432;Database=durabletask;User Id=postgres;Password=root", 6 | "SqlServer": "server=localhost;database=durabletask;user=sa;password=P1ssw0rd;Encrypt=False" 7 | } 8 | } 9 | --------------------------------------------------------------------------------