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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Changelog

## Unreleased

- Add support for multiple versions in DurableTask attribute ([#751](https://github.com/microsoft/durabletask-dotnet/pull/751))

## v1.25.0-preview.2
- On-demand sandbox ([#736](https://github.com/microsoft/durabletask-dotnet/pull/736))
Expand Down
9 changes: 8 additions & 1 deletion src/Abstractions/DurableTaskAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public DurableTaskAttribute(string? name = null)
public TaskName Name { get; }

/// <summary>
/// Gets or sets the version of the durable task. Multiple classes may declare the same
/// Gets or sets the version(s) of the durable task. Multiple classes may declare the same
/// <see cref="Name"/> as long as each declares a unique <see cref="Version"/>.
/// </summary>
/// <remarks>
Expand All @@ -44,6 +44,13 @@ public DurableTaskAttribute(string? name = null)
/// <c>DURABLE3005</c> and at registration time by the <see cref="TaskVersion"/> constructor.
/// </para>
/// <para>
/// A single class may declare multiple versions by supplying a comma-separated list (for example
/// <c>"v1,v2"</c>). Each listed version is plumbed through exactly as a single version is: the type is
/// registered under every declared version, and the source generator emits version-aware call helpers
/// for them. Empty entries (such as a trailing comma) are ignored, whitespace-only entries are rejected,
/// and duplicate entries are coalesced (case-insensitive).
/// </para>
/// <para>
/// Entities ignore this property.
/// </para>
/// </remarks>
Expand Down
16 changes: 8 additions & 8 deletions src/Abstractions/DurableTaskRegistry.Activities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ public DurableTaskRegistry AddActivity(TaskName name, TaskVersion version, Func<
public DurableTaskRegistry AddActivity(TaskName name, Type type)
{
Check.ConcreteType<ITaskActivity>(type);
return this.AddActivity(
return this.AddActivityAllVersions(
name,
type.GetDurableTaskVersion(),
type.GetDurableTaskVersions(),
sp => (ITaskActivity)ActivatorUtilities.GetServiceOrCreateInstance(sp, type));
}

Expand All @@ -77,9 +77,9 @@ public DurableTaskRegistry AddActivity(TaskName name, Type type)
public DurableTaskRegistry AddActivity(Type type)
{
Check.ConcreteType<ITaskActivity>(type);
return this.AddActivity(
return this.AddActivityAllVersions(
type.GetTaskName(),
type.GetDurableTaskVersion(),
type.GetDurableTaskVersions(),
sp => (ITaskActivity)ActivatorUtilities.GetServiceOrCreateInstance(sp, type));
}

Expand Down Expand Up @@ -112,7 +112,7 @@ public DurableTaskRegistry AddActivity<TActivity>()
public DurableTaskRegistry AddActivity(TaskName name, ITaskActivity activity)
{
Check.NotNull(activity);
return this.AddActivity(name, activity.GetType().GetDurableTaskVersion(), () => activity);
return this.AddActivityAllVersions(name, activity.GetType().GetDurableTaskVersions(), _ => activity);
}

/// <summary>
Expand All @@ -123,10 +123,10 @@ public DurableTaskRegistry AddActivity(TaskName name, ITaskActivity activity)
public DurableTaskRegistry AddActivity(ITaskActivity activity)
{
Check.NotNull(activity);
return this.AddActivity(
return this.AddActivityAllVersions(
activity.GetType().GetTaskName(),
activity.GetType().GetDurableTaskVersion(),
() => activity);
activity.GetType().GetDurableTaskVersions(),
_ => activity);
}

/// <summary>
Expand Down
29 changes: 23 additions & 6 deletions src/Abstractions/DurableTaskRegistry.Orchestrators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ public DurableTaskRegistry AddOrchestrator(TaskName name, Type type)
{
// TODO: Compile a constructor expression for performance.
Check.ConcreteType<ITaskOrchestrator>(type);
return this.AddOrchestrator(
return this.AddOrchestratorAllVersions(
name,
type.GetDurableTaskVersion(),
type.GetDurableTaskVersions(),
() => (ITaskOrchestrator)Activator.CreateInstance(type));
}

Expand All @@ -109,7 +109,8 @@ public DurableTaskRegistry AddOrchestrator(TaskName name, Type type)
public DurableTaskRegistry AddOrchestrator(Type type)
{
Check.ConcreteType<ITaskOrchestrator>(type);
return this.AddOrchestrator(type.GetTaskName(), type.GetDurableTaskVersion(), () => (ITaskOrchestrator)Activator.CreateInstance(type));
return this.AddOrchestratorAllVersions(
type.GetTaskName(), type.GetDurableTaskVersions(), () => (ITaskOrchestrator)Activator.CreateInstance(type));
}

/// <summary>
Expand Down Expand Up @@ -140,7 +141,7 @@ public DurableTaskRegistry AddOrchestrator<TOrchestrator>()
public DurableTaskRegistry AddOrchestrator(TaskName name, ITaskOrchestrator orchestrator)
{
Check.NotNull(orchestrator);
return this.AddOrchestrator(name, orchestrator.GetType().GetDurableTaskVersion(), () => orchestrator);
return this.AddOrchestratorAllVersions(name, orchestrator.GetType().GetDurableTaskVersions(), () => orchestrator);
}

/// <summary>
Expand All @@ -151,9 +152,9 @@ public DurableTaskRegistry AddOrchestrator(TaskName name, ITaskOrchestrator orch
public DurableTaskRegistry AddOrchestrator(ITaskOrchestrator orchestrator)
{
Check.NotNull(orchestrator);
return this.AddOrchestrator(
return this.AddOrchestratorAllVersions(
orchestrator.GetType().GetTaskName(),
orchestrator.GetType().GetDurableTaskVersion(),
orchestrator.GetType().GetDurableTaskVersions(),
() => orchestrator);
}

Expand Down Expand Up @@ -302,4 +303,20 @@ public DurableTaskRegistry AddOrchestratorFunc(TaskName name, Action<TaskOrchest
return CompletedNullTask;
});
}

/// <summary>
/// Registers an orchestrator factory under every supplied version. This is the shared fan-out used by the
/// type- and singleton-based registration overloads so that a class declaring multiple versions via
/// <see cref="DurableTaskAttribute.Version"/> is registered once per declared version.
/// </summary>
DurableTaskRegistry AddOrchestratorAllVersions(
TaskName name, IReadOnlyList<TaskVersion> versions, Func<ITaskOrchestrator> factory)
{
foreach (TaskVersion version in versions)
{
this.AddOrchestrator(name, version, factory);
}

return this;
}
}
16 changes: 16 additions & 0 deletions src/Abstractions/DurableTaskRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,20 @@ DurableTaskRegistry AddActivity(TaskName name, TaskVersion version, Func<IServic
this.ActivitiesByVersion.Add(key, factory);
return this;
}

/// <summary>
/// Registers an activity factory under every supplied version. This is the shared fan-out used by the
/// type- and singleton-based registration overloads so that a class declaring multiple versions via
/// <see cref="DurableTaskAttribute.Version"/> is registered once per declared version.
/// </summary>
DurableTaskRegistry AddActivityAllVersions(
TaskName name, IReadOnlyList<TaskVersion> versions, Func<IServiceProvider, ITaskActivity> factory)
{
foreach (TaskVersion version in versions)
{
this.AddActivity(name, version, factory);
}

return this;
}
}
64 changes: 60 additions & 4 deletions src/Abstractions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,70 @@ public static TaskName GetTaskName(this Type type)
/// </summary>
/// <param name="type">The type to get the durable task version for.</param>
/// <returns>The durable task version.</returns>
/// <remarks>
/// When the <see cref="DurableTaskAttribute.Version"/> declares multiple comma-separated versions, this
/// returns only the first declared version. Prefer <see cref="GetDurableTaskVersions"/> when all declared
/// versions are needed (for example, when registering a type under every version it supports).
/// </remarks>
internal static TaskVersion GetDurableTaskVersion(this Type type)
{
// IMPORTANT: This logic needs to be kept consistent with the source generator logic.
Check.NotNull(type);
return Attribute.GetCustomAttribute(type, typeof(DurableTaskAttribute)) switch
IReadOnlyList<TaskVersion> versions = type.GetDurableTaskVersions();
return versions.Count > 0 ? versions[0] : default;
}

/// <summary>
/// Gets every durable task version declared for a type via <see cref="DurableTaskAttribute.Version"/>.
/// </summary>
/// <param name="type">The type to get the durable task versions for.</param>
/// <returns>
/// The distinct (case-insensitive) set of declared versions in declaration order. An unversioned type
/// (no attribute, or an empty/unset <see cref="DurableTaskAttribute.Version"/>) yields a single
/// <see cref="TaskVersion.Unversioned"/> entry so callers can always register at least once.
/// </returns>
/// <exception cref="ArgumentException">
/// Thrown when any comma-separated entry is whitespace-only. This mirrors the source generator's
/// <c>DURABLE3005</c> diagnostic so the reflection-based registration path fails closed for types whose
/// attribute the generator did not see.
/// </exception>
internal static IReadOnlyList<TaskVersion> GetDurableTaskVersions(this Type type)
{
// IMPORTANT: This logic needs to be kept consistent with the source generator logic.
Check.NotNull(type);
if (Attribute.GetCustomAttribute(type, typeof(DurableTaskAttribute)) is not DurableTaskAttribute attr
|| string.IsNullOrEmpty(attr.Version))
{
DurableTaskAttribute { Version: not null and not "" } attr => new TaskVersion(attr.Version),
_ => default,
};
return new[] { TaskVersion.Unversioned };
}

List<TaskVersion> versions = new();
foreach (string segment in attr.Version!.Split(','))
{
if (segment.Length == 0)
{
// Truly-empty entry (e.g. a trailing or doubled comma). Skip silently.
continue;
}

string trimmed = segment.Trim();
if (trimmed.Length == 0)
{
// Whitespace-only entry. Fail closed, consistent with the TaskVersion constructor and the
// source generator's DURABLE3005 diagnostic.
throw new ArgumentException(
"A [DurableTask] Version entry must not be whitespace-only. Provide non-empty version " +
"values or omit the Version argument to declare an unversioned task.",
nameof(type));
}

TaskVersion version = new(trimmed);
if (!versions.Contains(version))
{
versions.Add(version);
}
}

return versions.Count > 0 ? versions : new[] { TaskVersion.Unversioned };
}
}
Loading
Loading