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);
+ }
+}