Skip to content

Commit

Permalink
Optional DI lifecycle change (#1408)
Browse files Browse the repository at this point in the history
* Added mechanism to allow the service lifetime to be overridden from a singleton (default) to another lifetime

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added unit tests - updated dependencies accordingly

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added service lifetime to DaprClient as well

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added update to DaprClient to pass service lifetime through

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added documentation indicating how to register DaprWorkflowClient with different lifecycle options.

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Removed unnecessary line from csproj

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Simplified registrations

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Called out an important point about registrations

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

---------

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
  • Loading branch information
WhitWaldo authored Nov 21, 2024
1 parent f769eb1 commit ef04cad
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 16 deletions.
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
<PackageVersion Include="Microsoft.DurableTask.Worker.Grpc" Version="1.3.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
Expand Down
7 changes: 7 additions & 0 deletions all.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<YourWorkflow>();
options.RegisterActivity<YourActivity>();
});

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<YourWorkflow>();
options.RegisterActivity<YourActivity>();
}, 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<YourWorkflow>();
options.RegisterActivity<YourActivity>();
}, ServiceLifecycle.Transient);

var app = builder.Build();
await app.RunAsync();
```
41 changes: 36 additions & 5 deletions src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,32 @@ public static class DaprServiceCollectionExtensions
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" />.</param>
/// <param name="configure"></param>
public static void AddDaprClient(this IServiceCollection services, Action<DaprClientBuilder>? configure = null)
/// <param name="lifetime">The lifetime of the registered services.</param>
public static void AddDaprClient(this IServiceCollection services, Action<DaprClientBuilder>? configure = null,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
ArgumentNullException.ThrowIfNull(services, nameof(services));

services.TryAddSingleton(serviceProvider =>
var registration = new Func<IServiceProvider, DaprClient>((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;
}
}

/// <summary>
Expand All @@ -50,17 +66,32 @@ public static void AddDaprClient(this IServiceCollection services, Action<DaprCl
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="configure"></param>
/// <param name="lifetime">The lifetime of the registered services.</param>
public static void AddDaprClient(this IServiceCollection services,
Action<IServiceProvider, DaprClientBuilder> configure)
Action<IServiceProvider, DaprClientBuilder> configure, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
ArgumentNullException.ThrowIfNull(services, nameof(services));

services.TryAddSingleton(serviceProvider =>
var registration = new Func<IServiceProvider, DaprClient>((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)
Expand Down
1 change: 0 additions & 1 deletion src/Dapr.Workflow/Dapr.Workflow.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<!-- NuGet configuration -->
<PropertyGroup>
<!-- NOTE: Workflows targeted .NET 7 (whereas other packages did not, so we must continue until .NET 7 EOL). -->
<TargetFrameworks>net6;net7;net8</TargetFrameworks>
<Nullable>enable</Nullable>
<PackageId>Dapr.Workflow</PackageId>
<Title>Dapr Workflow Authoring SDK</Title>
Expand Down
39 changes: 31 additions & 8 deletions src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,48 @@ public static class WorkflowServiceCollectionExtensions
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
/// <param name="configure">A delegate used to configure actor options and register workflow functions.</param>
/// <param name="lifetime">The lifetime of the registered services.</param>
public static IServiceCollection AddDaprWorkflow(
this IServiceCollection serviceCollection,
Action<WorkflowRuntimeOptions> configure)
Action<WorkflowRuntimeOptions> configure,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
if (serviceCollection == null)
{
throw new ArgumentNullException(nameof(serviceCollection));
}

serviceCollection.TryAddSingleton<WorkflowRuntimeOptions>();
serviceCollection.AddDaprClient(lifetime: lifetime);
serviceCollection.AddHttpClient();

serviceCollection.AddHostedService<WorkflowLoggingService>();

switch (lifetime)
{
case ServiceLifetime.Singleton:
#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient
serviceCollection.TryAddSingleton<WorkflowEngineClient>();
serviceCollection.TryAddSingleton<WorkflowEngineClient>();
#pragma warning restore CS0618 // Type or member is obsolete
serviceCollection.AddHostedService<WorkflowLoggingService>();
serviceCollection.TryAddSingleton<DaprWorkflowClient>();
serviceCollection.AddDaprClient();

serviceCollection.TryAddSingleton<DaprWorkflowClient>();
serviceCollection.TryAddSingleton<WorkflowRuntimeOptions>();
break;
case ServiceLifetime.Scoped:
#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient
serviceCollection.TryAddScoped<WorkflowEngineClient>();
#pragma warning restore CS0618 // Type or member is obsolete
serviceCollection.TryAddScoped<DaprWorkflowClient>();
serviceCollection.TryAddScoped<WorkflowRuntimeOptions>();
break;
case ServiceLifetime.Transient:
#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient
serviceCollection.TryAddTransient<WorkflowEngineClient>();
#pragma warning restore CS0618 // Type or member is obsolete
serviceCollection.TryAddTransient<DaprWorkflowClient>();
serviceCollection.TryAddTransient<WorkflowRuntimeOptions>();
break;
default:
throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null);
}

serviceCollection.AddOptions<WorkflowRuntimeOptions>().Configure(configure);

//Register the factory and force resolution so the Durable Task client and worker can be registered
Expand Down
27 changes: 27 additions & 0 deletions test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.extensibility.core" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Dapr.Workflow\Dapr.Workflow.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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<DaprWorkflowClient>();
var daprWorkflowClient2 = serviceProvider.GetService<DaprWorkflowClient>();

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<DaprWorkflowClient>();

await using var scope2 = serviceProvider.CreateAsyncScope();
var daprWorkflowClient2 = scope2.ServiceProvider.GetService<DaprWorkflowClient>();

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<DaprWorkflowClient>();
var daprWorkflowClient2 = serviceProvider.GetService<DaprWorkflowClient>();

Assert.NotNull(daprWorkflowClient1);
Assert.NotNull(daprWorkflowClient2);
Assert.NotSame(daprWorkflowClient1, daprWorkflowClient2);
}
}

0 comments on commit ef04cad

Please sign in to comment.