diff --git a/Directory.Packages.props b/Directory.Packages.props index 772dd7c6..a98e9db5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,8 +28,8 @@ - - + + diff --git a/all.sln b/all.sln index 8a6eb2ff..bb44a3bd 100644 --- a/all.sln +++ b/all.sln @@ -145,6 +145,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jobs", "Jobs", "{D9697361-2 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs\JobsSample\JobsSample.csproj", "{9CAF360E-5AD3-4C4F-89A0-327EEB70D673}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -377,6 +379,10 @@ Global {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Debug|Any CPU.Build.0 = Debug|Any CPU {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Release|Any CPU.ActiveCfg = Release|Any CPU {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Release|Any CPU.Build.0 = Release|Any CPU + {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -446,6 +452,7 @@ Global {BF9828E9-5597-4D42-AA6E-6E6C12214204} = {DD020B34-460F-455F-8D17-CF4A949F100B} {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} + {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md new file mode 100644 index 00000000..ac6a0f18 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md @@ -0,0 +1,77 @@ +--- +type: docs +title: "DaprWorkflowClient usage" +linkTitle: "DaprWorkflowClient usage" +weight: 100000 +description: Essential tips and advice for using DaprWorkflowClient +--- + +## Lifetime management + +A `DaprWorkflowClient` holds access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar as well +as other types used in the management and operation of Workflows. `DaprWorkflowClient` implements `IAsyncDisposable` to support eager +cleanup of resources. + +## Dependency Injection + +The `AddDaprWorkflow()` method will register the Dapr workflow services with ASP.NET Core dependency injection. This method +requires an options delegate that defines each of the workflows and activities you wish to register and use in your application. + +{{% alert title="Note" color="primary" %}} + +This method will attempt to register a `DaprClient` instance, but this will only work if it hasn't already been registered with another +lifetime. For example, an earlier call to `AddDaprClient()` with a singleton lifetime will always use a singleton regardless of the +lifetime chose for the workflow client. The `DaprClient` instance will be used to communicate with the Dapr sidecar and if it's not +yet registered, the lifetime provided during the `AddDaprWorkflow()` registration will be used to register the `DaprWorkflowClient` +as well as its own dependencies. + +{{% /alert %}} + +### Singleton Registration +By default, the `AddDaprWorkflow` method will register the `DaprWorkflowClient` and associated services using a singleton lifetime. This means +that the services will be instantiated only a single time. + +The following is an example of how registration of the `DaprWorkflowClient` as it would appear in a typical `Program.cs` file: + +```csharp +builder.Services.AddDaprWorkflow(options => { + options.RegisterWorkflow(); + options.RegisterActivity(); +}); + +var app = builder.Build(); +await app.RunAsync(); +``` + +### Scoped Registration + +While this may generally be acceptable in your use case, you may instead wish to override the lifetime specified. This is done by passing a `ServiceLifetime` +argument in `AddDaprWorkflow`. For example, you may wish to inject another scoped service into your ASP.NET Core processing pipeline +that needs context used by the `DaprClient` that wouldn't be available if the former service were registered as a singleton. + +This is demonstrated in the following example: + +```csharp +builder.Services.AddDaprWorkflow(options => { + options.RegisterWorkflow(); + options.RegisterActivity(); +}, ServiceLifecycle.Scoped); + +var app = builder.Build(); +await app.RunAsync(); +``` + +### Transient Registration + +Finally, Dapr services can also be registered using a transient lifetime meaning that they will be initialized every time they're injected. This +is demonstrated in the following example: + +```csharp +builder.Services.AddDaprWorkflow(options => { + options.RegisterWorkflow(); + options.RegisterActivity(); +}, ServiceLifecycle.Transient); + +var app = builder.Build(); +await app.RunAsync(); +``` \ No newline at end of file diff --git a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs index 52e9110b..ea6fb520 100644 --- a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs +++ b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs @@ -32,16 +32,32 @@ public static class DaprServiceCollectionExtensions /// /// The . /// - public static void AddDaprClient(this IServiceCollection services, Action? configure = null) + /// The lifetime of the registered services. + public static void AddDaprClient(this IServiceCollection services, Action? configure = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(services, nameof(services)); - services.TryAddSingleton(serviceProvider => + var registration = new Func((serviceProvider) => { var builder = CreateDaprClientBuilder(serviceProvider); configure?.Invoke(builder); return builder.Build(); }); + + switch (lifetime) + { + case ServiceLifetime.Scoped: + services.TryAddScoped(registration); + break; + case ServiceLifetime.Transient: + services.TryAddTransient(registration); + break; + case ServiceLifetime.Singleton: + default: + services.TryAddSingleton(registration); + break; + } } /// @@ -50,17 +66,32 @@ public static class DaprServiceCollectionExtensions /// /// The . /// + /// The lifetime of the registered services. public static void AddDaprClient(this IServiceCollection services, - Action configure) + Action configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(services, nameof(services)); - - services.TryAddSingleton(serviceProvider => + + var registration = new Func((serviceProvider) => { var builder = CreateDaprClientBuilder(serviceProvider); configure?.Invoke(serviceProvider, builder); return builder.Build(); }); + + switch (lifetime) + { + case ServiceLifetime.Singleton: + services.TryAddSingleton(registration); + break; + case ServiceLifetime.Scoped: + services.TryAddScoped(registration); + break; + case ServiceLifetime.Transient: + default: + services.TryAddTransient(registration); + break; + } } private static DaprClientBuilder CreateDaprClientBuilder(IServiceProvider serviceProvider) diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 992baee7..360d121e 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -3,7 +3,6 @@ - net6;net7;net8 enable Dapr.Workflow Dapr Workflow Authoring SDK diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 5c10a776..209e4edc 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -29,25 +29,48 @@ public static class WorkflowServiceCollectionExtensions /// /// The . /// A delegate used to configure actor options and register workflow functions. + /// The lifetime of the registered services. public static IServiceCollection AddDaprWorkflow( this IServiceCollection serviceCollection, - Action configure) + Action configure, + ServiceLifetime lifetime = ServiceLifetime.Singleton) { if (serviceCollection == null) { throw new ArgumentNullException(nameof(serviceCollection)); } - serviceCollection.TryAddSingleton(); + serviceCollection.AddDaprClient(lifetime: lifetime); serviceCollection.AddHttpClient(); - -#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient - serviceCollection.TryAddSingleton(); -#pragma warning restore CS0618 // Type or member is obsolete serviceCollection.AddHostedService(); - serviceCollection.TryAddSingleton(); - serviceCollection.AddDaprClient(); - + + switch (lifetime) + { + case ServiceLifetime.Singleton: +#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient + serviceCollection.TryAddSingleton(); +#pragma warning restore CS0618 // Type or member is obsolete + serviceCollection.TryAddSingleton(); + serviceCollection.TryAddSingleton(); + break; + case ServiceLifetime.Scoped: +#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient + serviceCollection.TryAddScoped(); +#pragma warning restore CS0618 // Type or member is obsolete + serviceCollection.TryAddScoped(); + serviceCollection.TryAddScoped(); + break; + case ServiceLifetime.Transient: +#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient + serviceCollection.TryAddTransient(); +#pragma warning restore CS0618 // Type or member is obsolete + serviceCollection.TryAddTransient(); + serviceCollection.TryAddTransient(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null); + } + serviceCollection.AddOptions().Configure(configure); //Register the factory and force resolution so the Durable Task client and worker can be registered diff --git a/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj new file mode 100644 index 00000000..531a0b1f --- /dev/null +++ b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj @@ -0,0 +1,27 @@ + + + + enable + enable + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs b/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..2206d939 --- /dev/null +++ b/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs @@ -0,0 +1,58 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Workflow.Test; + +public class WorkflowServiceCollectionExtensionsTests +{ + [Fact] + public void RegisterWorkflowClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprWorkflowClient1 = serviceProvider.GetService(); + var daprWorkflowClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprWorkflowClient1); + Assert.NotNull(daprWorkflowClient2); + + Assert.Same(daprWorkflowClient1, daprWorkflowClient2); + } + + [Fact] + public async Task RegisterWorkflowClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprWorkflowClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprWorkflowClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprWorkflowClient1); + Assert.NotNull(daprWorkflowClient2); + Assert.NotSame(daprWorkflowClient1, daprWorkflowClient2); + } + + [Fact] + public void RegisterWorkflowClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprWorkflowClient1 = serviceProvider.GetService(); + var daprWorkflowClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprWorkflowClient1); + Assert.NotNull(daprWorkflowClient2); + Assert.NotSame(daprWorkflowClient1, daprWorkflowClient2); + } +}