-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Dependency injection hosting extension (#92)
- Loading branch information
Showing
17 changed files
with
968 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
src/Temporalio.Extensions.Hosting/ITemporalWorkerServiceOptionsBuilder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Temporalio.Extensions.Hosting | ||
{ | ||
/// <summary> | ||
/// Interface for configuring <see cref="TemporalWorkerServiceOptions" /> for | ||
/// <see cref="TemporalWorkerService" />. Methods for using this are as extensions in this | ||
/// namespace. | ||
/// </summary> | ||
public interface ITemporalWorkerServiceOptionsBuilder | ||
{ | ||
/// <summary> | ||
/// Gets the task queue for this worker service. | ||
/// </summary> | ||
string TaskQueue { get; } | ||
|
||
/// <summary> | ||
/// Gets the service collection being configured. | ||
/// </summary> | ||
IServiceCollection Services { get; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# Hosting and Dependency Injection Support | ||
|
||
This extension adds support for worker | ||
[Generic Host](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host)s and activity | ||
[Dependency Injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) to the | ||
[Temporal .NET SDK](https://github.com/temporalio/sdk-dotnet). | ||
|
||
⚠️ UNDER ACTIVE DEVELOPMENT | ||
|
||
This SDK is under active development and has not released a stable version yet. APIs may change in incompatible ways | ||
until the SDK is marked stable. | ||
|
||
## Quick Start | ||
|
||
Add the `Temporalio.Extensions.Hosting` package from | ||
[NuGet](https://www.nuget.org/packages/Temporalio.Extensions.Hosting). For example, using the `dotnet` CLI: | ||
|
||
dotnet add package Temporalio.Extensions.Hosting --prerelease | ||
|
||
To create a worker service, you can do the following: | ||
|
||
```csharp | ||
using Temporalio.Extensions.Hosting; | ||
|
||
var builder = Host.CreateApplicationBuilder(args); | ||
// Add a hosted Temporal worker which returns a builder to add activities and workflows | ||
builder.Services. | ||
AddHostedTemporalWorker( | ||
"my-temporal-host:7233", | ||
"my-namespace", | ||
"my-task-queue"). | ||
AddScopedActivities<MyActivityClass>(). | ||
AddWorkflow<MyWorkflow>(); | ||
|
||
// Run | ||
var host = builder.Build(); | ||
host.Run(); | ||
``` | ||
|
||
This creates a hosted Temporal worker which returns a builder. Then `MyActivityClass` is added to the service collection | ||
as scoped (via | ||
[`TryAddScoped`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.extensions.servicecollectiondescriptorextensions.tryaddscoped)) | ||
and registered on the worker. Also `MyWorkflow` is registered as a workflow on the worker. | ||
|
||
This means that for every activity invocation, a new scope is created and the the activity is obtained via the service | ||
provider. So if it is registered as scoped the activity is created each time but if it registered as singleton it is | ||
created only once and reused. The activity's constructor can be used to accept injected dependencies. | ||
|
||
Workflows are inherently self-contained, deterministic units of work and therefore should never call anything external. | ||
Therefore, there is no such thing as dependency injection for workflows, their construction and lifetime is managed by | ||
Temporal. | ||
|
||
## Worker Services | ||
|
||
When this extension is depended upon, two overloads for `AddHostedTemporalWorker` are added as extension methods on | ||
`IServiceCollection` in the `Microsoft.Extensions.DependencyInjection`. | ||
|
||
One overload of `AddHostedTemporalWorker`, used in the quick start sample above, accepts the client target host, the | ||
client namespace, and the worker task queue. This form will connect to a client for the worker. The other overload of | ||
`AddHostedTemporalWorker` only accepts the worker task queue. In the latter, an `ITemporalClient` can either be set on | ||
the services container and therefore reused across workers, or the resulting builder can have client options set. | ||
|
||
When called, these register a `TemporalWorkerServiceOptions` options class with the container and return a | ||
`ITemporalWorkerServiceOptionsBuilder` for configuring the worker service options. | ||
|
||
For adding activity classes to the service collection and registering activities with the worker, the following | ||
extension methods exist on the builder each accepting activity type: | ||
|
||
* `AddSingletonActivities` - `TryAddSingleton` + register activities on worker | ||
* `AddScopedActivities` - `TryAddScoped` + register activities on worker | ||
* `AddTransientActivities` - `TryAddTransient` + register activities on worker | ||
* `AddStaticActivities` - register activities on worker that all must be static | ||
* `AddActivitiesInstance` - register activities on worker via an existing instance | ||
|
||
These all expect the activity methods to have the `[Activity]` attribute on them. If an activity type is already added | ||
to the service collection, `ApplyTemporalActivities` can be used to just register the activities on the worker. | ||
|
||
For registering workflows on the worker, `AddWorkflow` extension method is available. This does nothing to the service | ||
collection because the construction and lifecycle of workflows is managed by Temporal. Dependency injection for | ||
workflows is intentionally not supported. | ||
|
||
Other worker and client options can be configured on the builder via the `ConfigureOptions` extension method. With no | ||
parameters, this returns an `OptionsBuilder<TemporalWorkerServiceOptions>` to use. When provided an action, the options | ||
are available as parameters that can be configured. `TemporalWorkerServiceOptions` simply extends | ||
`TemporalWorkerOptions` with an added property for optional client options that can be set to connect a client on worker | ||
start instead of expecting a `ITemporalClient` to be present on the service collection. | ||
|
||
## Activity Dependency Injection without Worker Services | ||
|
||
Some users may prefer to manually create the `TemporalWorker` without using host support, but still make their | ||
activities created via the service provider. `CreateTemporalActivityDefinitions` extension methods are present on | ||
`IServiceProvider` that will return a collection of `ActivityDefinition` instances for each activity on the type. These | ||
can be added to the `TemporalWorkerOptions` directly. |
113 changes: 113 additions & 0 deletions
113
src/Temporalio.Extensions.Hosting/ServiceProviderExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Runtime.ExceptionServices; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Temporalio.Activities; | ||
|
||
namespace Temporalio.Extensions.Hosting | ||
{ | ||
/// <summary> | ||
/// Temporal extension methods for <see cref="IServiceProvider" />. | ||
/// </summary> | ||
public static class ServiceProviderExtensions | ||
{ | ||
/// <summary> | ||
/// Create activity definitions for every activity-attributed method on the given type. For | ||
/// non-static methods, this will use the service provider to get the instance to call the | ||
/// method on. | ||
/// </summary> | ||
/// <typeparam name="T">Type to create activity definitions from.</typeparam> | ||
/// <param name="provider">Service provider for creating the instance for non-static | ||
/// activities.</param> | ||
/// <returns>Collection of activity definitions.</returns> | ||
public static IReadOnlyCollection<ActivityDefinition> CreateTemporalActivityDefinitions<T>( | ||
this IServiceProvider provider) => | ||
provider.CreateTemporalActivityDefinitions(typeof(T)); | ||
|
||
/// <summary> | ||
/// Create activity definitions for every activity-attributed method on the given type. For | ||
/// non-static methods, this will use the service provider to get the instance to call the | ||
/// method on. | ||
/// </summary> | ||
/// <param name="provider">Service provider for creating the instance for non-static | ||
/// activities.</param> | ||
/// <param name="type">Type to create activity definitions from.</param> | ||
/// <returns>Collection of activity definitions.</returns> | ||
public static IReadOnlyCollection<ActivityDefinition> CreateTemporalActivityDefinitions( | ||
this IServiceProvider provider, Type type) => | ||
type. | ||
GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance). | ||
Where(method => method.IsDefined(typeof(ActivityAttribute))). | ||
Select(method => provider.CreateTemporalActivityDefinition(type, method)). | ||
ToList(); | ||
|
||
/// <summary> | ||
/// Create activity definition for the given activity-attributed method on the given | ||
/// instance type. If the method is non-static, this will use the service provider to get | ||
/// the instance to call the method on. | ||
/// </summary> | ||
/// <param name="provider">Service provider for creating the instance if the method is | ||
/// non-static.</param> | ||
/// <param name="instanceType">Type of the instance.</param> | ||
/// <param name="method">Method to create activity definition from.</param> | ||
/// <returns>Created definition.</returns> | ||
public static ActivityDefinition CreateTemporalActivityDefinition( | ||
this IServiceProvider provider, Type instanceType, MethodInfo method) | ||
{ | ||
// Invoker can be async (i.e. returns Task<object?>) | ||
Func<object?[], Task<object>> invoker = async args => | ||
{ | ||
// If static, just invoke and unwrap exception | ||
if (method.IsStatic) | ||
{ | ||
try | ||
{ | ||
return method.Invoke(null, args); | ||
} | ||
catch (TargetInvocationException e) | ||
{ | ||
ExceptionDispatchInfo.Capture(e.InnerException!).Throw(); | ||
// Unreachable | ||
throw new InvalidOperationException("Unreachable"); | ||
} | ||
} | ||
// Wrap in a scope | ||
using (var scope = provider.CreateScope()) | ||
{ | ||
object? result; | ||
try | ||
{ | ||
result = method.Invoke(scope.ServiceProvider.GetRequiredService(instanceType), args); | ||
} | ||
catch (TargetInvocationException e) | ||
{ | ||
ExceptionDispatchInfo.Capture(e.InnerException!).Throw(); | ||
// Unreachable | ||
throw new InvalidOperationException("Unreachable"); | ||
} | ||
// In order to make sure the scope lasts the life of the activity, we need to | ||
// wait on the task if it's a task | ||
if (result is Task resultTask) | ||
{ | ||
await resultTask.ConfigureAwait(false); | ||
// We have to use reflection to extract value if it's a Task<> | ||
var resultTaskType = resultTask.GetType(); | ||
if (resultTaskType.IsGenericType) | ||
{ | ||
result = resultTaskType.GetProperty("Result")!.GetValue(resultTask); | ||
} | ||
else | ||
{ | ||
result = ValueTuple.Create(); | ||
} | ||
} | ||
return result; | ||
} | ||
}; | ||
return ActivityDefinition.Create(method, invoker); | ||
} | ||
} | ||
} |
Oops, something went wrong.