Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions misc/misc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<None Include="$(RepoRoot).gitignore" />
<None Include="$(RepoRoot)azure-pipelines-release.yml" />
<None Include="$(RepoRoot)Directory.Build.*" />
<None Include="$(RepoRoot)Directory.Packages.*" />
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change to include Directory.Packages.* appears unrelated to the PR's stated purpose of adding simplified func-based activity and orchestrator registration. This should be removed or moved to a separate PR if it's intended to be a general maintenance improvement.

Suggested change
<None Include="$(RepoRoot)Directory.Packages.*" />

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to keep this in here since it's not worth creating a separate PR for.

<None Include="$(RepoRoot)*.md" />
<None Include="$(RepoRoot)nuget.config" />
<None Include="$(EngRoot)**" LinkBase="eng" />
Expand Down
6 changes: 4 additions & 2 deletions src/Abstractions/DurableTaskAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
namespace Microsoft.DurableTask;

/// <summary>
/// Indicates that the attributed class represents a durable task.
/// Indicates that the attributed class or method represents a durable task.
/// </summary>
/// <remarks>
/// This attribute is meant to be used on class definitions that derive from
/// <see cref="TaskOrchestrator{TInput, TOutput}"/>, <see cref="TaskActivity{TInput, TOutput}"/>,
/// or TaskEntity{TState} from the Microsoft.DurableTask.Entities namespace.
/// It can also be applied to methods used with <see cref="DurableTaskRegistry.AddOrchestratorFunc{TInput, TOutput}(System.Func{TaskOrchestrationContext, TInput, System.Threading.Tasks.Task{TOutput}})"/>
/// or similar overloads to specify a custom name for the orchestrator.
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation mentions "orchestrator" specifically but this attribute can now also be applied to activity methods. The documentation should mention both orchestrators and activities to be more accurate and helpful.

Suggested change
/// or similar overloads to specify a custom name for the orchestrator.
/// or similar overloads to specify a custom name for the orchestrator or activity.

Copilot uses AI. Check for mistakes.
/// It is used specifically by build-time source generators to generate type-safe methods for invoking
/// orchestrations, activities, or registering entities.
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class DurableTaskAttribute : Attribute
{
/// <summary>
Expand Down
176 changes: 175 additions & 1 deletion src/Abstractions/DurableTaskRegistry.Activities.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Reflection;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.DurableTask;
Expand All @@ -21,7 +22,17 @@ TaskName and TActivity generic parameter
ITaskActivity singleton
TaskName ITaskActivity singleton

by func/action:
by func/action (with explicit name):
Func{Context, Input, Task{Output}}
Func{Context, Input, Task}
Func{Context, Input, Output}
Func{Context, Task{Output}}
Func{Context, Task}
Func{Context, Output}
Action{Context, TInput}
Action{Context}

by func/action (name inferred from method or [DurableTask] attribute):
Func{Context, Input, Task{Output}}
Func{Context, Input, Task}
Func{Context, Input, Output}
Expand Down Expand Up @@ -219,4 +230,167 @@ public DurableTaskRegistry AddActivityFunc(TaskName name, Action<TaskActivityCon
return CompletedNullTask;
});
}

/// <summary>
/// Registers an activity factory, where the implementation is <paramref name="activity" />.
/// The name is inferred from a <see cref="DurableTaskAttribute"/> on the method, or the method name.
/// </summary>
/// <typeparam name="TInput">The activity input type.</typeparam>
/// <typeparam name="TOutput">The activity output type.</typeparam>
/// <param name="activity">The activity implementation.</param>
/// <returns>The same registry, for call chaining.</returns>
/// <exception cref="ArgumentException">
/// Thrown if the name cannot be inferred from the delegate.
/// </exception>
public DurableTaskRegistry AddActivityFunc<TInput, TOutput>(
Func<TaskActivityContext, TInput, Task<TOutput>> activity)
{
Check.NotNull(activity);
return this.AddActivityFunc(GetActivityNameFromDelegate(activity), activity);
}

/// <summary>
/// Registers an activity factory, where the implementation is <paramref name="activity" />.
/// The name is inferred from a <see cref="DurableTaskAttribute"/> on the method, or the method name.
/// </summary>
/// <typeparam name="TInput">The activity input type.</typeparam>
/// <typeparam name="TOutput">The activity output type.</typeparam>
/// <param name="activity">The activity implementation.</param>
/// <returns>The same registry, for call chaining.</returns>
/// <exception cref="ArgumentException">
/// Thrown if the name cannot be inferred from the delegate.
/// </exception>
public DurableTaskRegistry AddActivityFunc<TInput, TOutput>(
Func<TaskActivityContext, TInput, TOutput> activity)
{
Check.NotNull(activity);
return this.AddActivityFunc(GetActivityNameFromDelegate(activity), activity);
}

/// <summary>
/// Registers an activity factory, where the implementation is <paramref name="activity" />.
/// The name is inferred from a <see cref="DurableTaskAttribute"/> on the method, or the method name.
/// </summary>
/// <typeparam name="TInput">The activity input type.</typeparam>
/// <param name="activity">The activity implementation.</param>
/// <returns>The same registry, for call chaining.</returns>
/// <exception cref="ArgumentException">
/// Thrown if the name cannot be inferred from the delegate.
/// </exception>
public DurableTaskRegistry AddActivityFunc<TInput>(Func<TaskActivityContext, TInput, Task> activity)
{
Check.NotNull(activity);
return this.AddActivityFunc(GetActivityNameFromDelegate(activity), activity);
}

/// <summary>
/// Registers an activity factory, where the implementation is <paramref name="activity" />.
/// The name is inferred from a <see cref="DurableTaskAttribute"/> on the method, or the method name.
/// </summary>
/// <typeparam name="TOutput">The activity output type.</typeparam>
/// <param name="activity">The activity implementation.</param>
/// <returns>The same registry, for call chaining.</returns>
/// <exception cref="ArgumentException">
/// Thrown if the name cannot be inferred from the delegate.
/// </exception>
public DurableTaskRegistry AddActivityFunc<TOutput>(Func<TaskActivityContext, Task<TOutput>> activity)
{
Check.NotNull(activity);
return this.AddActivityFunc(GetActivityNameFromDelegate(activity), activity);
}

/// <summary>
/// Registers an activity factory, where the implementation is <paramref name="activity" />.
/// The name is inferred from a <see cref="DurableTaskAttribute"/> on the method, or the method name.
/// </summary>
/// <param name="activity">The activity implementation.</param>
/// <returns>The same registry, for call chaining.</returns>
/// <exception cref="ArgumentException">
/// Thrown if the name cannot be inferred from the delegate.
/// </exception>
public DurableTaskRegistry AddActivityFunc(Func<TaskActivityContext, Task> activity)
{
Check.NotNull(activity);
return this.AddActivityFunc(GetActivityNameFromDelegate(activity), activity);
}

/// <summary>
/// Registers an activity factory, where the implementation is <paramref name="activity" />.
/// The name is inferred from a <see cref="DurableTaskAttribute"/> on the method, or the method name.
/// </summary>
/// <typeparam name="TOutput">The activity output type.</typeparam>
/// <param name="activity">The activity implementation.</param>
/// <returns>The same registry, for call chaining.</returns>
/// <exception cref="ArgumentException">
/// Thrown if the name cannot be inferred from the delegate.
/// </exception>
public DurableTaskRegistry AddActivityFunc<TOutput>(Func<TaskActivityContext, TOutput> activity)
{
Check.NotNull(activity);
return this.AddActivityFunc(GetActivityNameFromDelegate(activity), activity);
}

/// <summary>
/// Registers an activity factory, where the implementation is <paramref name="activity" />.
/// The name is inferred from a <see cref="DurableTaskAttribute"/> on the method, or the method name.
/// </summary>
/// <typeparam name="TInput">The activity input type.</typeparam>
/// <param name="activity">The activity implementation.</param>
/// <returns>The same registry, for call chaining.</returns>
/// <exception cref="ArgumentException">
/// Thrown if the name cannot be inferred from the delegate.
/// </exception>
public DurableTaskRegistry AddActivityFunc<TInput>(Action<TaskActivityContext, TInput> activity)
{
Check.NotNull(activity);
return this.AddActivityFunc(GetActivityNameFromDelegate(activity), activity);
}

/// <summary>
/// Registers an activity factory, where the implementation is <paramref name="activity" />.
/// The name is inferred from a <see cref="DurableTaskAttribute"/> on the method, or the method name.
/// </summary>
/// <param name="activity">The activity implementation.</param>
/// <returns>The same registry, for call chaining.</returns>
/// <exception cref="ArgumentException">
/// Thrown if the name cannot be inferred from the delegate.
/// </exception>
public DurableTaskRegistry AddActivityFunc(Action<TaskActivityContext> activity)
{
Check.NotNull(activity);
return this.AddActivityFunc(GetActivityNameFromDelegate(activity), activity);
}

/// <summary>
/// Gets the task name from a delegate by checking for a <see cref="DurableTaskAttribute"/>
/// or falling back to the method name.
/// </summary>
/// <param name="delegate">The delegate to extract the name from.</param>
/// <returns>The task name.</returns>
/// <exception cref="ArgumentException">
/// Thrown if the name cannot be inferred from the delegate.
/// </exception>
static TaskName GetActivityNameFromDelegate(Delegate @delegate)
{
MethodInfo method = @delegate.Method;

// Check for DurableTaskAttribute on the method
DurableTaskAttribute? attribute = method.GetCustomAttribute<DurableTaskAttribute>();
if (attribute?.Name.Name is not null and not "")
{
return attribute.Name;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really see how it's possible for attribute to ever be null here, assuming it's not shared state that can be accessed (and mutated) by multiple threads concurrently (something we're not doing).

}

// Fall back to method name
string? methodName = method.Name;
if (string.IsNullOrEmpty(methodName) || methodName.StartsWith("<", StringComparison.Ordinal))
{
throw new ArgumentException(
"Cannot infer activity name from the delegate. The delegate must either have a " +
"[DurableTask] attribute with a name, or be a named method (not a lambda or anonymous delegate).",
nameof(@delegate));
}

return new TaskName(methodName);
}
}
Loading
Loading