Skip to content

Commit 3c4271a

Browse files
WhitWaldodivzi-p
authored andcommitted
Optional DI lifecycle change (dapr#1408)
* Added mechanism to allow the service lifetime to be overridden from a singleton (default) to another lifetime Signed-off-by: Whit Waldo <[email protected]> * Added unit tests - updated dependencies accordingly Signed-off-by: Whit Waldo <[email protected]> * Added service lifetime to DaprClient as well Signed-off-by: Whit Waldo <[email protected]> * Added update to DaprClient to pass service lifetime through Signed-off-by: Whit Waldo <[email protected]> * Added documentation indicating how to register DaprWorkflowClient with different lifecycle options. Signed-off-by: Whit Waldo <[email protected]> * Removed unnecessary line from csproj Signed-off-by: Whit Waldo <[email protected]> * Simplified registrations Signed-off-by: Whit Waldo <[email protected]> * Called out an important point about registrations Signed-off-by: Whit Waldo <[email protected]> --------- Signed-off-by: Whit Waldo <[email protected]> Signed-off-by: Divya Perumal <[email protected]>
1 parent aa4f494 commit 3c4271a

File tree

8 files changed

+238
-16
lines changed

8 files changed

+238
-16
lines changed

Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
<PackageVersion Include="Microsoft.DurableTask.Worker.Grpc" Version="1.3.0" />
2929
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
3030
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
31-
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
32-
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
31+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
32+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
3333
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
3434
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0" />
3535
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />

all.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jobs", "Jobs", "{D9697361-2
145145
EndProject
146146
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs\JobsSample\JobsSample.csproj", "{9CAF360E-5AD3-4C4F-89A0-327EEB70D673}"
147147
EndProject
148+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}"
149+
EndProject
148150
Global
149151
GlobalSection(SolutionConfigurationPlatforms) = preSolution
150152
Debug|Any CPU = Debug|Any CPU
@@ -377,6 +379,10 @@ Global
377379
{9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Debug|Any CPU.Build.0 = Debug|Any CPU
378380
{9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Release|Any CPU.ActiveCfg = Release|Any CPU
379381
{9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Release|Any CPU.Build.0 = Release|Any CPU
382+
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
383+
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
384+
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
385+
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU
380386
EndGlobalSection
381387
GlobalSection(SolutionProperties) = preSolution
382388
HideSolutionNode = FALSE
@@ -446,6 +452,7 @@ Global
446452
{BF9828E9-5597-4D42-AA6E-6E6C12214204} = {DD020B34-460F-455F-8D17-CF4A949F100B}
447453
{D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
448454
{9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488}
455+
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B}
449456
EndGlobalSection
450457
GlobalSection(ExtensibilityGlobals) = postSolution
451458
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
type: docs
3+
title: "DaprWorkflowClient usage"
4+
linkTitle: "DaprWorkflowClient usage"
5+
weight: 100000
6+
description: Essential tips and advice for using DaprWorkflowClient
7+
---
8+
9+
## Lifetime management
10+
11+
A `DaprWorkflowClient` holds access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar as well
12+
as other types used in the management and operation of Workflows. `DaprWorkflowClient` implements `IAsyncDisposable` to support eager
13+
cleanup of resources.
14+
15+
## Dependency Injection
16+
17+
The `AddDaprWorkflow()` method will register the Dapr workflow services with ASP.NET Core dependency injection. This method
18+
requires an options delegate that defines each of the workflows and activities you wish to register and use in your application.
19+
20+
{{% alert title="Note" color="primary" %}}
21+
22+
This method will attempt to register a `DaprClient` instance, but this will only work if it hasn't already been registered with another
23+
lifetime. For example, an earlier call to `AddDaprClient()` with a singleton lifetime will always use a singleton regardless of the
24+
lifetime chose for the workflow client. The `DaprClient` instance will be used to communicate with the Dapr sidecar and if it's not
25+
yet registered, the lifetime provided during the `AddDaprWorkflow()` registration will be used to register the `DaprWorkflowClient`
26+
as well as its own dependencies.
27+
28+
{{% /alert %}}
29+
30+
### Singleton Registration
31+
By default, the `AddDaprWorkflow` method will register the `DaprWorkflowClient` and associated services using a singleton lifetime. This means
32+
that the services will be instantiated only a single time.
33+
34+
The following is an example of how registration of the `DaprWorkflowClient` as it would appear in a typical `Program.cs` file:
35+
36+
```csharp
37+
builder.Services.AddDaprWorkflow(options => {
38+
options.RegisterWorkflow<YourWorkflow>();
39+
options.RegisterActivity<YourActivity>();
40+
});
41+
42+
var app = builder.Build();
43+
await app.RunAsync();
44+
```
45+
46+
### Scoped Registration
47+
48+
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`
49+
argument in `AddDaprWorkflow`. For example, you may wish to inject another scoped service into your ASP.NET Core processing pipeline
50+
that needs context used by the `DaprClient` that wouldn't be available if the former service were registered as a singleton.
51+
52+
This is demonstrated in the following example:
53+
54+
```csharp
55+
builder.Services.AddDaprWorkflow(options => {
56+
options.RegisterWorkflow<YourWorkflow>();
57+
options.RegisterActivity<YourActivity>();
58+
}, ServiceLifecycle.Scoped);
59+
60+
var app = builder.Build();
61+
await app.RunAsync();
62+
```
63+
64+
### Transient Registration
65+
66+
Finally, Dapr services can also be registered using a transient lifetime meaning that they will be initialized every time they're injected. This
67+
is demonstrated in the following example:
68+
69+
```csharp
70+
builder.Services.AddDaprWorkflow(options => {
71+
options.RegisterWorkflow<YourWorkflow>();
72+
options.RegisterActivity<YourActivity>();
73+
}, ServiceLifecycle.Transient);
74+
75+
var app = builder.Build();
76+
await app.RunAsync();
77+
```

src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,32 @@ public static class DaprServiceCollectionExtensions
3232
/// </summary>
3333
/// <param name="services">The <see cref="IServiceCollection" />.</param>
3434
/// <param name="configure"></param>
35-
public static void AddDaprClient(this IServiceCollection services, Action<DaprClientBuilder>? configure = null)
35+
/// <param name="lifetime">The lifetime of the registered services.</param>
36+
public static void AddDaprClient(this IServiceCollection services, Action<DaprClientBuilder>? configure = null,
37+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
3638
{
3739
ArgumentNullException.ThrowIfNull(services, nameof(services));
3840

39-
services.TryAddSingleton(serviceProvider =>
41+
var registration = new Func<IServiceProvider, DaprClient>((serviceProvider) =>
4042
{
4143
var builder = CreateDaprClientBuilder(serviceProvider);
4244
configure?.Invoke(builder);
4345
return builder.Build();
4446
});
47+
48+
switch (lifetime)
49+
{
50+
case ServiceLifetime.Scoped:
51+
services.TryAddScoped(registration);
52+
break;
53+
case ServiceLifetime.Transient:
54+
services.TryAddTransient(registration);
55+
break;
56+
case ServiceLifetime.Singleton:
57+
default:
58+
services.TryAddSingleton(registration);
59+
break;
60+
}
4561
}
4662

4763
/// <summary>
@@ -50,17 +66,32 @@ public static void AddDaprClient(this IServiceCollection services, Action<DaprCl
5066
/// </summary>
5167
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
5268
/// <param name="configure"></param>
69+
/// <param name="lifetime">The lifetime of the registered services.</param>
5370
public static void AddDaprClient(this IServiceCollection services,
54-
Action<IServiceProvider, DaprClientBuilder> configure)
71+
Action<IServiceProvider, DaprClientBuilder> configure, ServiceLifetime lifetime = ServiceLifetime.Singleton)
5572
{
5673
ArgumentNullException.ThrowIfNull(services, nameof(services));
57-
58-
services.TryAddSingleton(serviceProvider =>
74+
75+
var registration = new Func<IServiceProvider, DaprClient>((serviceProvider) =>
5976
{
6077
var builder = CreateDaprClientBuilder(serviceProvider);
6178
configure?.Invoke(serviceProvider, builder);
6279
return builder.Build();
6380
});
81+
82+
switch (lifetime)
83+
{
84+
case ServiceLifetime.Singleton:
85+
services.TryAddSingleton(registration);
86+
break;
87+
case ServiceLifetime.Scoped:
88+
services.TryAddScoped(registration);
89+
break;
90+
case ServiceLifetime.Transient:
91+
default:
92+
services.TryAddTransient(registration);
93+
break;
94+
}
6495
}
6596

6697
private static DaprClientBuilder CreateDaprClientBuilder(IServiceProvider serviceProvider)

src/Dapr.Workflow/Dapr.Workflow.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
<!-- NuGet configuration -->
44
<PropertyGroup>
55
<!-- NOTE: Workflows targeted .NET 7 (whereas other packages did not, so we must continue until .NET 7 EOL). -->
6-
<TargetFrameworks>net6;net7;net8</TargetFrameworks>
76
<Nullable>enable</Nullable>
87
<PackageId>Dapr.Workflow</PackageId>
98
<Title>Dapr Workflow Authoring SDK</Title>

src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,48 @@ public static class WorkflowServiceCollectionExtensions
2929
/// </summary>
3030
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
3131
/// <param name="configure">A delegate used to configure actor options and register workflow functions.</param>
32+
/// <param name="lifetime">The lifetime of the registered services.</param>
3233
public static IServiceCollection AddDaprWorkflow(
3334
this IServiceCollection serviceCollection,
34-
Action<WorkflowRuntimeOptions> configure)
35+
Action<WorkflowRuntimeOptions> configure,
36+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
3537
{
3638
if (serviceCollection == null)
3739
{
3840
throw new ArgumentNullException(nameof(serviceCollection));
3941
}
4042

41-
serviceCollection.TryAddSingleton<WorkflowRuntimeOptions>();
43+
serviceCollection.AddDaprClient(lifetime: lifetime);
4244
serviceCollection.AddHttpClient();
43-
45+
serviceCollection.AddHostedService<WorkflowLoggingService>();
46+
47+
switch (lifetime)
48+
{
49+
case ServiceLifetime.Singleton:
4450
#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient
45-
serviceCollection.TryAddSingleton<WorkflowEngineClient>();
51+
serviceCollection.TryAddSingleton<WorkflowEngineClient>();
4652
#pragma warning restore CS0618 // Type or member is obsolete
47-
serviceCollection.AddHostedService<WorkflowLoggingService>();
48-
serviceCollection.TryAddSingleton<DaprWorkflowClient>();
49-
serviceCollection.AddDaprClient();
50-
53+
serviceCollection.TryAddSingleton<DaprWorkflowClient>();
54+
serviceCollection.TryAddSingleton<WorkflowRuntimeOptions>();
55+
break;
56+
case ServiceLifetime.Scoped:
57+
#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient
58+
serviceCollection.TryAddScoped<WorkflowEngineClient>();
59+
#pragma warning restore CS0618 // Type or member is obsolete
60+
serviceCollection.TryAddScoped<DaprWorkflowClient>();
61+
serviceCollection.TryAddScoped<WorkflowRuntimeOptions>();
62+
break;
63+
case ServiceLifetime.Transient:
64+
#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient
65+
serviceCollection.TryAddTransient<WorkflowEngineClient>();
66+
#pragma warning restore CS0618 // Type or member is obsolete
67+
serviceCollection.TryAddTransient<DaprWorkflowClient>();
68+
serviceCollection.TryAddTransient<WorkflowRuntimeOptions>();
69+
break;
70+
default:
71+
throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null);
72+
}
73+
5174
serviceCollection.AddOptions<WorkflowRuntimeOptions>().Configure(configure);
5275

5376
//Register the factory and force resolution so the Durable Task client and worker can be registered
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<ImplicitUsings>enable</ImplicitUsings>
5+
<Nullable>enable</Nullable>
6+
<IsPackable>false</IsPackable>
7+
<IsTestProject>true</IsTestProject>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" />
12+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
14+
<PackageReference Include="xunit" />
15+
<PackageReference Include="xunit.extensibility.core" />
16+
<PackageReference Include="xunit.runner.visualstudio" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<Using Include="Xunit"/>
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<ProjectReference Include="..\..\src\Dapr.Workflow\Dapr.Workflow.csproj" />
25+
</ItemGroup>
26+
27+
</Project>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
3+
namespace Dapr.Workflow.Test;
4+
5+
public class WorkflowServiceCollectionExtensionsTests
6+
{
7+
[Fact]
8+
public void RegisterWorkflowClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton()
9+
{
10+
var services = new ServiceCollection();
11+
12+
services.AddDaprWorkflow(options => { }, ServiceLifetime.Singleton);
13+
var serviceProvider = services.BuildServiceProvider();
14+
15+
var daprWorkflowClient1 = serviceProvider.GetService<DaprWorkflowClient>();
16+
var daprWorkflowClient2 = serviceProvider.GetService<DaprWorkflowClient>();
17+
18+
Assert.NotNull(daprWorkflowClient1);
19+
Assert.NotNull(daprWorkflowClient2);
20+
21+
Assert.Same(daprWorkflowClient1, daprWorkflowClient2);
22+
}
23+
24+
[Fact]
25+
public async Task RegisterWorkflowClient_ShouldRegisterScoped_WhenLifetimeIsScoped()
26+
{
27+
var services = new ServiceCollection();
28+
29+
services.AddDaprWorkflow(options => { }, ServiceLifetime.Scoped);
30+
var serviceProvider = services.BuildServiceProvider();
31+
32+
await using var scope1 = serviceProvider.CreateAsyncScope();
33+
var daprWorkflowClient1 = scope1.ServiceProvider.GetService<DaprWorkflowClient>();
34+
35+
await using var scope2 = serviceProvider.CreateAsyncScope();
36+
var daprWorkflowClient2 = scope2.ServiceProvider.GetService<DaprWorkflowClient>();
37+
38+
Assert.NotNull(daprWorkflowClient1);
39+
Assert.NotNull(daprWorkflowClient2);
40+
Assert.NotSame(daprWorkflowClient1, daprWorkflowClient2);
41+
}
42+
43+
[Fact]
44+
public void RegisterWorkflowClient_ShouldRegisterTransient_WhenLifetimeIsTransient()
45+
{
46+
var services = new ServiceCollection();
47+
48+
services.AddDaprWorkflow(options => { }, ServiceLifetime.Transient);
49+
var serviceProvider = services.BuildServiceProvider();
50+
51+
var daprWorkflowClient1 = serviceProvider.GetService<DaprWorkflowClient>();
52+
var daprWorkflowClient2 = serviceProvider.GetService<DaprWorkflowClient>();
53+
54+
Assert.NotNull(daprWorkflowClient1);
55+
Assert.NotNull(daprWorkflowClient2);
56+
Assert.NotSame(daprWorkflowClient1, daprWorkflowClient2);
57+
}
58+
}

0 commit comments

Comments
 (0)