diff --git a/all.sln b/all.sln
index ac3f669c1..326be2274 100644
--- a/all.sln
+++ b/all.sln
@@ -241,6 +241,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Versioning",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Workflow.Versioning", "test\Dapr.IntegrationTest.Workflow.Versioning\Dapr.IntegrationTest.Workflow.Versioning.csproj", "{1AD32297-630E-4DFB-B3E4-CAFCE993F27F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Versioning.Runtime.Test", "test\Dapr.Workflow.Versioning.Runtime.Test\Dapr.Workflow.Versioning.Runtime.Test.csproj", "{4FF7F075-2818-41E4-A88F-743417EA0A99}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -639,6 +641,10 @@ Global
{1AD32297-630E-4DFB-B3E4-CAFCE993F27F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1AD32297-630E-4DFB-B3E4-CAFCE993F27F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1AD32297-630E-4DFB-B3E4-CAFCE993F27F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4FF7F075-2818-41E4-A88F-743417EA0A99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4FF7F075-2818-41E4-A88F-743417EA0A99}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4FF7F075-2818-41E4-A88F-743417EA0A99}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4FF7F075-2818-41E4-A88F-743417EA0A99}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -756,6 +762,7 @@ Global
{BD1FA767-AC6D-429D-8BC0-3C0B52AA11FF} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{CB619F1E-B90C-4BCB-9DDA-A5A4F5967661} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{1AD32297-630E-4DFB-B3E4-CAFCE993F27F} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
+ {4FF7F075-2818-41E4-A88F-743417EA0A99} = {0AF0FE8D-C234-4F04-8514-32206ACE01BD}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
diff --git a/src/Dapr.Workflow.Versioning.Abstractions/WorkflowVersionAttribute.cs b/src/Dapr.Workflow.Versioning.Abstractions/WorkflowVersionAttribute.cs
index 1861eaf18..832f0f4fa 100644
--- a/src/Dapr.Workflow.Versioning.Abstractions/WorkflowVersionAttribute.cs
+++ b/src/Dapr.Workflow.Versioning.Abstractions/WorkflowVersionAttribute.cs
@@ -40,7 +40,12 @@ public sealed class WorkflowVersionAttribute : Attribute
///
/// Gets or sets an optional strategy type to use for this workflow, overriding the globally configured strategy.
- /// The type must implement and expose a public parameterless constructor.
+ /// The type must implement and be constructible by the active
///
public Type? StrategyType { get; init; }
+
+ ///
+ /// Gets or sets an optional named options scope to use when configuring the strategy for this workflow.
+ ///
+ public string? OptionsName { get; init; }
}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/DefaultWorkflowVersionStrategyFactory.cs b/src/Dapr.Workflow.Versioning.Runtime/DefaultWorkflowVersionStrategyFactory.cs
index 722eb2eda..94bbb83a7 100644
--- a/src/Dapr.Workflow.Versioning.Runtime/DefaultWorkflowVersionStrategyFactory.cs
+++ b/src/Dapr.Workflow.Versioning.Runtime/DefaultWorkflowVersionStrategyFactory.cs
@@ -42,6 +42,11 @@ public IWorkflowVersionStrategy Create(
throw new InvalidOperationException(
$"Could not construct strategy of type '{strategyType.FullName}'.");
+ if (instance is IWorkflowVersionStrategyContextConsumer contextConsumer)
+ {
+ contextConsumer.Configure(new WorkflowVersionStrategyContext(canonicalName, optionsName));
+ }
+
return instance;
}
}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/IWorkflowVersionStrategyContextConsumer.cs b/src/Dapr.Workflow.Versioning.Runtime/IWorkflowVersionStrategyContextConsumer.cs
new file mode 100644
index 000000000..269791822
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/IWorkflowVersionStrategyContextConsumer.cs
@@ -0,0 +1,26 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Optional interface for strategies that want per-family context (canonical name and options scope).
+///
+public interface IWorkflowVersionStrategyContextConsumer
+{
+ ///
+ /// Configures the strategy with the canonical name and optional options scope.
+ ///
+ /// The strategy context.
+ void Configure(WorkflowVersionStrategyContext context);
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/MaxVersionSelector.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionSelectors/MaxVersionSelector.cs
similarity index 100%
rename from src/Dapr.Workflow.Versioning.Runtime/MaxVersionSelector.cs
rename to src/Dapr.Workflow.Versioning.Runtime/VersionSelectors/MaxVersionSelector.cs
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DateSuffixVersionStrategy.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DateSuffixVersionStrategy.cs
new file mode 100644
index 000000000..5dc6d9eb3
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DateSuffixVersionStrategy.cs
@@ -0,0 +1,112 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Globalization;
+using Microsoft.Extensions.Options;
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Strategy that derives a date-based version from a trailing suffix
+/// (for example, MyWorkflow20260212 with format yyyyMMdd).
+///
+public sealed class DateSuffixVersionStrategy(IOptionsMonitor? optionsMonitor = null)
+ : IWorkflowVersionStrategy, IWorkflowVersionStrategyContextConsumer
+{
+ private DateSuffixVersionStrategyOptions _options = new();
+
+ ///
+ public void Configure(WorkflowVersionStrategyContext context)
+ {
+ var optionsName = string.IsNullOrWhiteSpace(context.OptionsName)
+ ? Options.DefaultName
+ : context.OptionsName;
+
+ if (optionsMonitor is not null)
+ {
+ _options = optionsMonitor.Get(optionsName);
+ }
+ }
+
+ ///
+ public bool TryParse(string typeName, out string canonicalName, out string version)
+ {
+ canonicalName = string.Empty;
+ version = string.Empty;
+
+ if (string.IsNullOrWhiteSpace(typeName))
+ return false;
+
+ var format = string.IsNullOrWhiteSpace(_options.DateFormat) ? "yyyyMMdd" : _options.DateFormat;
+ if (typeName.Length <= format.Length)
+ return ApplyNoSuffix(typeName, out canonicalName, out version);
+
+ var suffix = typeName.Substring(typeName.Length - format.Length);
+ if (!TryParseDate(suffix, format, out _))
+ return ApplyNoSuffix(typeName, out canonicalName, out version);
+
+ canonicalName = typeName.Substring(0, typeName.Length - format.Length);
+ if (string.IsNullOrEmpty(canonicalName))
+ return false;
+
+ version = suffix;
+ return true;
+ }
+
+ ///
+ public int Compare(string? v1, string? v2)
+ {
+ if (ReferenceEquals(v1, v2)) return 0;
+ if (v1 is null) return -1;
+ if (v2 is null) return 1;
+
+ var format = string.IsNullOrWhiteSpace(_options.DateFormat) ? "yyyyMMdd" : _options.DateFormat;
+ var ok1 = TryParseDate(v1.Trim(), format, out var d1);
+ var ok2 = TryParseDate(v2.Trim(), format, out var d2);
+
+ switch (ok1)
+ {
+ case true when ok2:
+ return d1.CompareTo(d2);
+ case true:
+ return 1;
+ }
+
+ if (ok2) return -1;
+
+ return StringComparer.Ordinal.Compare(v1, v2);
+ }
+
+ private bool ApplyNoSuffix(string typeName, out string canonicalName, out string version)
+ {
+ canonicalName = string.Empty;
+ version = string.Empty;
+
+ if (!_options.AllowNoSuffix)
+ return false;
+
+ canonicalName = typeName;
+ version = string.IsNullOrWhiteSpace(_options.DefaultVersion) ? "0" : _options.DefaultVersion;
+ return true;
+ }
+
+ private static bool TryParseDate(string value, string format, out DateTime date)
+ {
+ return DateTime.TryParseExact(
+ value,
+ format,
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.None,
+ out date);
+ }
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DateSuffixVersionStrategyOptions.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DateSuffixVersionStrategyOptions.cs
new file mode 100644
index 000000000..454206779
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DateSuffixVersionStrategyOptions.cs
@@ -0,0 +1,37 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Options for .
+///
+public sealed class DateSuffixVersionStrategyOptions
+{
+ ///
+ /// Gets or sets the date format expected at the end of the workflow type name.
+ /// Defaults to yyyyMMdd.
+ ///
+ public string DateFormat { get; set; } = "yyyyMMdd";
+
+ ///
+ /// Gets or sets a value indicating whether names without a date suffix are allowed.
+ /// When enabled, the default version is applied.
+ ///
+ public bool AllowNoSuffix { get; set; }
+
+ ///
+ /// Gets or sets the default version used when no suffix is present.
+ ///
+ public string DefaultVersion { get; set; } = "0";
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DelimitedSuffixVersionStrategy.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DelimitedSuffixVersionStrategy.cs
new file mode 100644
index 000000000..02dbd17a2
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DelimitedSuffixVersionStrategy.cs
@@ -0,0 +1,104 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Globalization;
+using Microsoft.Extensions.Options;
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Strategy that derives the version from a delimiter-separated suffix (for example, MyWorkflow-1).
+///
+public sealed class DelimitedSuffixVersionStrategy(
+ IOptionsMonitor? optionsMonitor = null)
+ : IWorkflowVersionStrategy, IWorkflowVersionStrategyContextConsumer
+{
+ private DelimitedSuffixVersionStrategyOptions _options = new();
+
+ ///
+ public void Configure(WorkflowVersionStrategyContext context)
+ {
+ var optionsName = string.IsNullOrWhiteSpace(context.OptionsName)
+ ? Options.DefaultName
+ : context.OptionsName;
+
+ if (optionsMonitor is not null)
+ {
+ _options = optionsMonitor.Get(optionsName);
+ }
+ }
+
+ ///
+ public bool TryParse(string typeName, out string canonicalName, out string version)
+ {
+ canonicalName = string.Empty;
+ version = string.Empty;
+
+ if (string.IsNullOrWhiteSpace(typeName))
+ return false;
+
+ var delimiter = _options.Delimiter ?? string.Empty;
+ if (delimiter.Length == 0)
+ return false;
+
+ var comparison = _options.IgnoreDelimiterCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
+ var delimiterIndex = typeName.LastIndexOf(delimiter, comparison);
+
+ if (delimiterIndex < 0)
+ {
+ if (_options.AllowNoSuffix)
+ {
+ canonicalName = typeName;
+ version = string.IsNullOrWhiteSpace(_options.DefaultVersion) ? "0" : _options.DefaultVersion;
+ return true;
+ }
+
+ return false;
+ }
+
+ var versionStart = delimiterIndex + delimiter.Length;
+ if (delimiterIndex == 0 || versionStart >= typeName.Length)
+ return false;
+
+ canonicalName = typeName[..delimiterIndex];
+ version = typeName[versionStart..];
+
+ return !string.IsNullOrEmpty(canonicalName) && !string.IsNullOrEmpty(version);
+ }
+
+ ///
+ public int Compare(string? v1, string? v2)
+ {
+ if (ReferenceEquals(v1, v2)) return 0;
+ if (v1 is null) return -1;
+ if (v2 is null) return 1;
+
+ var s1 = v1.Trim();
+ var s2 = v2.Trim();
+
+ var ok1 = long.TryParse(s1, NumberStyles.None, CultureInfo.InvariantCulture, out var n1);
+ var ok2 = long.TryParse(s2, NumberStyles.None, CultureInfo.InvariantCulture, out var n2);
+
+ switch (ok1)
+ {
+ case true when ok2:
+ return n1.CompareTo(n2);
+ case true:
+ return 1;
+ }
+
+ if (ok2) return -1;
+
+ return StringComparer.Ordinal.Compare(s1, s2);
+ }
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DelimitedSuffixVersionStrategyOptions.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DelimitedSuffixVersionStrategyOptions.cs
new file mode 100644
index 000000000..713ac8778
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/DelimitedSuffixVersionStrategyOptions.cs
@@ -0,0 +1,41 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Options for .
+///
+public sealed class DelimitedSuffixVersionStrategyOptions
+{
+ ///
+ /// Gets or sets the delimiter separating the canonical name from the version.
+ ///
+ public string Delimiter { get; set; } = "-";
+
+ ///
+ /// Gets or sets a value indicating whether delimiter matching ignores case.
+ ///
+ public bool IgnoreDelimiterCase { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether names without a delimiter are allowed.
+ /// When enabled, the default version is applied.
+ ///
+ public bool AllowNoSuffix { get; set; }
+
+ ///
+ /// Gets or sets the default version used when no suffix is present.
+ ///
+ public string DefaultVersion { get; set; } = "0";
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ExplicitVersionStrategy.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ExplicitVersionStrategy.cs
new file mode 100644
index 000000000..d3a899db9
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ExplicitVersionStrategy.cs
@@ -0,0 +1,63 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.Options;
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Strategy that requires versions to be supplied explicitly via .
+///
+public sealed class ExplicitVersionStrategy(IOptionsMonitor? optionsMonitor = null)
+ : IWorkflowVersionStrategy, IWorkflowVersionStrategyContextConsumer
+{
+ private ExplicitVersionStrategyOptions _options = new();
+
+ ///
+ public void Configure(WorkflowVersionStrategyContext context)
+ {
+ var optionsName = string.IsNullOrWhiteSpace(context.OptionsName)
+ ? Options.DefaultName
+ : context.OptionsName;
+
+ if (optionsMonitor is not null)
+ {
+ _options = optionsMonitor.Get(optionsName);
+ }
+ }
+
+ ///
+ public bool TryParse(string typeName, out string canonicalName, out string version)
+ {
+ canonicalName = string.Empty;
+ version = string.Empty;
+
+ if (!_options.AllowMissingVersion || string.IsNullOrWhiteSpace(typeName))
+ return false;
+
+ canonicalName = typeName;
+ version = string.IsNullOrWhiteSpace(_options.DefaultVersion) ? "0" : _options.DefaultVersion;
+ return true;
+ }
+
+ ///
+ public int Compare(string? v1, string? v2)
+ {
+ if (ReferenceEquals(v1, v2)) return 0;
+ if (v1 is null) return -1;
+ if (v2 is null) return 1;
+
+ var comparer = _options.IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
+ return comparer.Compare(v1, v2);
+ }
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ExplicitVersionStrategyOptions.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ExplicitVersionStrategyOptions.cs
new file mode 100644
index 000000000..57dd04f82
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ExplicitVersionStrategyOptions.cs
@@ -0,0 +1,35 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Options for .
+///
+public sealed class ExplicitVersionStrategyOptions
+{
+ ///
+ /// Gets or sets a value indicating whether parsing should be allowed when no explicit version is supplied.
+ ///
+ public bool AllowMissingVersion { get; set; }
+
+ ///
+ /// Gets or sets the default version used when no explicit version is supplied and parsing is allowed.
+ ///
+ public string DefaultVersion { get; set; } = "0";
+
+ ///
+ /// Gets or sets a value indicating whether version comparisons ignore case.
+ ///
+ public bool IgnoreCase { get; set; }
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/NumericVersionStrategy.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/NumericVersionStrategy.cs
new file mode 100644
index 000000000..d7e71e864
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/NumericVersionStrategy.cs
@@ -0,0 +1,137 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Globalization;
+using Microsoft.Extensions.Options;
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Strategy that derives a numeric version from a trailing suffix with an optional prefix
+/// (for example, MyWorkflowV1 with prefix V).
+///
+public sealed class NumericVersionStrategy : IWorkflowVersionStrategy, IWorkflowVersionStrategyContextConsumer
+{
+ private readonly IOptionsMonitor? _optionsMonitor;
+ private NumericVersionStrategyOptions _options = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Optional options monitor for named configuration.
+ public NumericVersionStrategy(IOptionsMonitor? optionsMonitor = null)
+ {
+ _optionsMonitor = optionsMonitor;
+ }
+
+ ///
+ public void Configure(WorkflowVersionStrategyContext context)
+ {
+ var optionsName = string.IsNullOrWhiteSpace(context.OptionsName)
+ ? Options.DefaultName
+ : context.OptionsName;
+
+ if (_optionsMonitor is not null)
+ {
+ _options = _optionsMonitor.Get(optionsName);
+ }
+ }
+
+ ///
+ public bool TryParse(string typeName, out string canonicalName, out string version)
+ {
+ canonicalName = string.Empty;
+ version = string.Empty;
+
+ if (string.IsNullOrWhiteSpace(typeName))
+ return false;
+
+ var prefix = _options.SuffixPrefix ?? string.Empty;
+ var comparison = _options.IgnorePrefixCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
+
+ var digitsStart = FindTrailingDigits(typeName);
+ if (digitsStart < 0)
+ {
+ if (_options.AllowNoSuffix && !EndsWithPrefix(typeName, prefix, comparison))
+ {
+ canonicalName = typeName;
+ version = string.IsNullOrWhiteSpace(_options.DefaultVersion) ? "0" : _options.DefaultVersion;
+ return true;
+ }
+
+ return false;
+ }
+
+ if (!string.IsNullOrEmpty(prefix))
+ {
+ var prefixStart = digitsStart - prefix.Length;
+ if (prefixStart < 1)
+ return false;
+
+ var candidatePrefix = typeName.Substring(prefixStart, prefix.Length);
+ if (!string.Equals(candidatePrefix, prefix, comparison))
+ return false;
+
+ canonicalName = typeName.Substring(0, prefixStart);
+ }
+ else
+ {
+ if (digitsStart < 1)
+ return false;
+
+ canonicalName = typeName.Substring(0, digitsStart);
+ }
+
+ version = typeName.Substring(digitsStart);
+ return !string.IsNullOrEmpty(canonicalName) && !string.IsNullOrEmpty(version);
+ }
+
+ ///
+ public int Compare(string? v1, string? v2)
+ {
+ if (ReferenceEquals(v1, v2)) return 0;
+ if (v1 is null) return -1;
+ if (v2 is null) return 1;
+
+ var s1 = v1.Trim();
+ var s2 = v2.Trim();
+
+ var ok1 = long.TryParse(s1, NumberStyles.None, CultureInfo.InvariantCulture, out var n1);
+ var ok2 = long.TryParse(s2, NumberStyles.None, CultureInfo.InvariantCulture, out var n2);
+
+ if (ok1 && ok2) return n1.CompareTo(n2);
+ if (ok1) return 1;
+ if (ok2) return -1;
+
+ return StringComparer.Ordinal.Compare(s1, s2);
+ }
+
+ private static int FindTrailingDigits(string value)
+ {
+ var i = value.Length - 1;
+ while (i >= 0 && value[i] >= '0' && value[i] <= '9')
+ {
+ i--;
+ }
+
+ return i == value.Length - 1 ? -1 : i + 1;
+ }
+
+ private static bool EndsWithPrefix(string value, string prefix, StringComparison comparison)
+ {
+ if (string.IsNullOrEmpty(prefix))
+ return false;
+
+ return value.EndsWith(prefix, comparison);
+ }
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/NumericVersionStrategyOptions.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/NumericVersionStrategyOptions.cs
new file mode 100644
index 000000000..ca5ed60c5
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/NumericVersionStrategyOptions.cs
@@ -0,0 +1,42 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Options for .
+///
+public sealed class NumericVersionStrategyOptions
+{
+ ///
+ /// Gets or sets the prefix used before the numeric suffix (for example, "V" in MyWorkflowV1).
+ /// Set to an empty string to allow a raw numeric suffix (for example, MyWorkflow1).
+ ///
+ public string SuffixPrefix { get; set; } = "V";
+
+ ///
+ /// Gets or sets a value indicating whether prefix matching ignores case.
+ ///
+ public bool IgnorePrefixCase { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether names without a numeric suffix are allowed.
+ /// When enabled, the default version is applied.
+ ///
+ public bool AllowNoSuffix { get; set; } = true;
+
+ ///
+ /// Gets or sets the default version used when no suffix is present.
+ ///
+ public string DefaultVersion { get; set; } = "0";
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/SemVerVersionStrategy.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/SemVerVersionStrategy.cs
new file mode 100644
index 000000000..89b41e661
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/SemVerVersionStrategy.cs
@@ -0,0 +1,249 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.Options;
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Strategy that derives a SemVer version from a trailing suffix (for example, MyWorkflowv1.2.3).
+///
+public sealed class SemVerVersionStrategy(IOptionsMonitor? optionsMonitor = null)
+ : IWorkflowVersionStrategy, IWorkflowVersionStrategyContextConsumer
+{
+ private SemVerVersionStrategyOptions _options = new();
+
+ ///
+ public void Configure(WorkflowVersionStrategyContext context)
+ {
+ var optionsName = string.IsNullOrWhiteSpace(context.OptionsName)
+ ? Options.DefaultName
+ : context.OptionsName;
+
+ if (optionsMonitor is not null)
+ {
+ _options = optionsMonitor.Get(optionsName);
+ }
+ }
+
+ ///
+ public bool TryParse(string typeName, out string canonicalName, out string version)
+ {
+ canonicalName = string.Empty;
+ version = string.Empty;
+
+ if (string.IsNullOrWhiteSpace(typeName))
+ return false;
+
+ var prefix = _options.Prefix ?? string.Empty;
+ if (prefix.Length > 0)
+ {
+ var comparison = _options.IgnorePrefixCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
+ var prefixIndex = typeName.LastIndexOf(prefix, comparison);
+ if (prefixIndex < 0)
+ {
+ return ApplyNoSuffix(typeName, out canonicalName, out version);
+ }
+
+ var versionStart = prefixIndex + prefix.Length;
+ if (prefixIndex == 0 || versionStart >= typeName.Length)
+ return false;
+
+ var candidate = typeName.Substring(versionStart);
+ if (!TryParseSemVer(candidate, _options, out _))
+ return false;
+
+ canonicalName = typeName.Substring(0, prefixIndex);
+ version = candidate;
+ return !string.IsNullOrEmpty(canonicalName);
+ }
+
+ var candidateStart = FindSemVerSuffixStart(typeName);
+ if (candidateStart < 0)
+ return ApplyNoSuffix(typeName, out canonicalName, out version);
+
+ var suffix = typeName.Substring(candidateStart);
+ if (!TryParseSemVer(suffix, _options, out _))
+ return ApplyNoSuffix(typeName, out canonicalName, out version);
+
+ canonicalName = typeName.Substring(0, candidateStart);
+ if (string.IsNullOrEmpty(canonicalName))
+ return false;
+
+ version = suffix;
+ return true;
+ }
+
+ ///
+ public int Compare(string? v1, string? v2)
+ {
+ if (ReferenceEquals(v1, v2)) return 0;
+ if (v1 is null) return -1;
+ if (v2 is null) return 1;
+
+ var ok1 = TryParseSemVer(v1.Trim(), _options, out var s1);
+ var ok2 = TryParseSemVer(v2.Trim(), _options, out var s2);
+
+ switch (ok1)
+ {
+ case true when ok2:
+ return s1.CompareTo(s2);
+ case true:
+ return 1;
+ }
+
+ if (ok2) return -1;
+
+ return StringComparer.Ordinal.Compare(v1, v2);
+ }
+
+ private bool ApplyNoSuffix(string typeName, out string canonicalName, out string version)
+ {
+ canonicalName = string.Empty;
+ version = string.Empty;
+
+ if (!_options.AllowNoSuffix)
+ return false;
+
+ canonicalName = typeName;
+ version = string.IsNullOrWhiteSpace(_options.DefaultVersion) ? "0.0.0" : _options.DefaultVersion;
+ return true;
+ }
+
+ private static int FindSemVerSuffixStart(string value)
+ {
+ var i = value.Length - 1;
+ while (i >= 0 && IsSemVerChar(value[i]))
+ {
+ i--;
+ }
+
+ return i == value.Length - 1 ? -1 : i + 1;
+ }
+
+ private static bool IsSemVerChar(char c) =>
+ c is >= '0' and <= '9' or >= 'A' and <= 'Z' or >= 'a' and <= 'z' or '.' or '-' or '+';
+
+ private static bool TryParseSemVer(string value, SemVerVersionStrategyOptions options, out SemVer semVer)
+ {
+ semVer = default;
+
+ if (string.IsNullOrWhiteSpace(value))
+ return false;
+
+ var buildSplit = value.Split('+', 2);
+ if (buildSplit.Length == 2 && !options.AllowBuildMetadata)
+ return false;
+
+ var withoutBuild = buildSplit[0];
+ var preSplit = withoutBuild.Split('-', 2);
+ var core = preSplit[0];
+
+ if (preSplit.Length == 2 && !options.AllowPrerelease)
+ return false;
+
+ var coreParts = core.Split('.');
+ if (coreParts.Length != 3)
+ return false;
+
+ if (!int.TryParse(coreParts[0], out var major) ||
+ !int.TryParse(coreParts[1], out var minor) ||
+ !int.TryParse(coreParts[2], out var patch))
+ {
+ return false;
+ }
+
+ var prerelease = preSplit.Length == 2 ? preSplit[1] : null;
+ var build = buildSplit.Length == 2 ? buildSplit[1] : null;
+
+ semVer = new SemVer(major, minor, patch, prerelease, build);
+ return true;
+ }
+
+ private readonly struct SemVer : IComparable
+ {
+ public SemVer(int major, int minor, int patch, string? prerelease, string? build)
+ {
+ Major = major;
+ Minor = minor;
+ Patch = patch;
+ Prerelease = prerelease;
+ Build = build;
+ }
+
+ public int Major { get; }
+ public int Minor { get; }
+ public int Patch { get; }
+ public string? Prerelease { get; }
+ public string? Build { get; }
+
+ public int CompareTo(SemVer other)
+ {
+ var major = Major.CompareTo(other.Major);
+ if (major != 0) return major;
+
+ var minor = Minor.CompareTo(other.Minor);
+ if (minor != 0) return minor;
+
+ var patch = Patch.CompareTo(other.Patch);
+ if (patch != 0) return patch;
+
+ var thisPre = Prerelease;
+ var otherPre = other.Prerelease;
+
+ if (string.IsNullOrEmpty(thisPre) && string.IsNullOrEmpty(otherPre)) return 0;
+ if (string.IsNullOrEmpty(thisPre)) return 1;
+ if (string.IsNullOrEmpty(otherPre)) return -1;
+
+ return ComparePrerelease(thisPre, otherPre);
+ }
+
+ private static int ComparePrerelease(string left, string right)
+ {
+ var leftParts = left.Split('.');
+ var rightParts = right.Split('.');
+ var length = Math.Max(leftParts.Length, rightParts.Length);
+
+ for (var i = 0; i < length; i++)
+ {
+ if (i >= leftParts.Length) return -1;
+ if (i >= rightParts.Length) return 1;
+
+ var l = leftParts[i];
+ var r = rightParts[i];
+
+ var lIsNum = int.TryParse(l, out var lNum);
+ var rIsNum = int.TryParse(r, out var rNum);
+
+ switch (lIsNum)
+ {
+ case true when rIsNum:
+ {
+ var cmp = lNum.CompareTo(rNum);
+ if (cmp != 0) return cmp;
+ continue;
+ }
+ case true:
+ return -1;
+ }
+
+ if (rIsNum) return 1;
+
+ var cmpStr = StringComparer.Ordinal.Compare(l, r);
+ if (cmpStr != 0) return cmpStr;
+ }
+
+ return 0;
+ }
+ }
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/SemVerVersionStrategyOptions.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/SemVerVersionStrategyOptions.cs
new file mode 100644
index 000000000..3f893169e
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/SemVerVersionStrategyOptions.cs
@@ -0,0 +1,52 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Options for .
+///
+public sealed class SemVerVersionStrategyOptions
+{
+ ///
+ /// Gets or sets the prefix expected before the SemVer suffix (for example, "v" in MyWorkflowv1.2.3).
+ /// Set to an empty string to require no prefix.
+ ///
+ public string Prefix { get; set; } = "v";
+
+ ///
+ /// Gets or sets a value indicating whether prefix matching ignores case.
+ ///
+ public bool IgnorePrefixCase { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether pre-release labels (for example, -alpha.1) are allowed.
+ ///
+ public bool AllowPrerelease { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether build metadata (for example, +build.5) is allowed.
+ ///
+ public bool AllowBuildMetadata { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether names without a SemVer suffix are allowed.
+ /// When enabled, the default version is applied.
+ ///
+ public bool AllowNoSuffix { get; set; }
+
+ ///
+ /// Gets or sets the default version used when no suffix is present.
+ ///
+ public string DefaultVersion { get; set; } = "0.0.0";
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ZeroPaddedNumericVersionStrategy.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ZeroPaddedNumericVersionStrategy.cs
new file mode 100644
index 000000000..363b84fd9
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ZeroPaddedNumericVersionStrategy.cs
@@ -0,0 +1,133 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Globalization;
+using Microsoft.Extensions.Options;
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Strategy that derives a numeric version from a zero-padded trailing suffix (for example, MyWorkflow0001).
+///
+public sealed class ZeroPaddedNumericVersionStrategy(
+ IOptionsMonitor? optionsMonitor = null)
+ : IWorkflowVersionStrategy, IWorkflowVersionStrategyContextConsumer
+{
+ private ZeroPaddedNumericVersionStrategyOptions _options = new();
+
+ ///
+ public void Configure(WorkflowVersionStrategyContext context)
+ {
+ var optionsName = string.IsNullOrWhiteSpace(context.OptionsName)
+ ? Options.DefaultName
+ : context.OptionsName;
+
+ if (optionsMonitor is not null)
+ {
+ _options = optionsMonitor.Get(optionsName);
+ }
+ }
+
+ ///
+ public bool TryParse(string typeName, out string canonicalName, out string version)
+ {
+ canonicalName = string.Empty;
+ version = string.Empty;
+
+ if (string.IsNullOrWhiteSpace(typeName))
+ return false;
+
+ var prefix = _options.SuffixPrefix ?? string.Empty;
+ var comparison = _options.IgnorePrefixCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
+
+ var digitsStart = FindTrailingDigits(typeName);
+ if (digitsStart < 0)
+ {
+ if (_options.AllowNoSuffix && !EndsWithPrefix(typeName, prefix, comparison))
+ {
+ canonicalName = typeName;
+ version = string.IsNullOrWhiteSpace(_options.DefaultVersion) ? "0" : _options.DefaultVersion;
+ return true;
+ }
+
+ return false;
+ }
+
+ var digitsLength = typeName.Length - digitsStart;
+ if (_options.Width > 0 && digitsLength != _options.Width)
+ return false;
+
+ if (!string.IsNullOrEmpty(prefix))
+ {
+ var prefixStart = digitsStart - prefix.Length;
+ if (prefixStart < 1)
+ return false;
+
+ var candidatePrefix = typeName.Substring(prefixStart, prefix.Length);
+ if (!string.Equals(candidatePrefix, prefix, comparison))
+ return false;
+
+ canonicalName = typeName.Substring(0, prefixStart);
+ }
+ else
+ {
+ if (digitsStart < 1)
+ return false;
+
+ canonicalName = typeName.Substring(0, digitsStart);
+ }
+
+ version = typeName.Substring(digitsStart);
+ return !string.IsNullOrEmpty(canonicalName) && !string.IsNullOrEmpty(version);
+ }
+
+ ///
+ public int Compare(string? v1, string? v2)
+ {
+ if (ReferenceEquals(v1, v2)) return 0;
+ if (v1 is null) return -1;
+ if (v2 is null) return 1;
+
+ var s1 = v1.Trim();
+ var s2 = v2.Trim();
+
+ var ok1 = long.TryParse(s1, NumberStyles.None, CultureInfo.InvariantCulture, out var n1);
+ var ok2 = long.TryParse(s2, NumberStyles.None, CultureInfo.InvariantCulture, out var n2);
+
+ switch (ok1)
+ {
+ case true when ok2:
+ return n1.CompareTo(n2);
+ case true:
+ return 1;
+ }
+
+ if (ok2) return -1;
+
+ return StringComparer.Ordinal.Compare(s1, s2);
+ }
+
+ private static int FindTrailingDigits(string value)
+ {
+ var i = value.Length - 1;
+ while (i >= 0 && value[i] >= '0' && value[i] <= '9')
+ {
+ i--;
+ }
+
+ return i == value.Length - 1 ? -1 : i + 1;
+ }
+
+ private static bool EndsWithPrefix(string value, string prefix, StringComparison comparison) =>
+ !string.IsNullOrEmpty(prefix) && value.EndsWith(prefix, comparison);
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ZeroPaddedNumericVersionStrategyOptions.cs b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ZeroPaddedNumericVersionStrategyOptions.cs
new file mode 100644
index 000000000..c64ca9bdb
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/VersionStrategies/ZeroPaddedNumericVersionStrategyOptions.cs
@@ -0,0 +1,47 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Options for .
+///
+public sealed class ZeroPaddedNumericVersionStrategyOptions
+{
+ ///
+ /// Gets or sets the prefix used before the numeric suffix (for example, "V" in MyWorkflowV0001).
+ /// Set to an empty string to allow a raw numeric suffix.
+ ///
+ public string SuffixPrefix { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets a value indicating whether prefix matching ignores case.
+ ///
+ public bool IgnorePrefixCase { get; set; }
+
+ ///
+ /// Gets or sets the required width for the numeric suffix. Set to 0 to allow any width.
+ ///
+ public int Width { get; set; } = 4;
+
+ ///
+ /// Gets or sets a value indicating whether names without a numeric suffix are allowed.
+ /// When enabled, the default version is applied.
+ ///
+ public bool AllowNoSuffix { get; set; }
+
+ ///
+ /// Gets or sets the default version used when no suffix is present.
+ ///
+ public string DefaultVersion { get; set; } = "0";
+}
diff --git a/src/Dapr.Workflow.Versioning.Runtime/WorkflowVersionStrategyContext.cs b/src/Dapr.Workflow.Versioning.Runtime/WorkflowVersionStrategyContext.cs
new file mode 100644
index 000000000..524c72f7e
--- /dev/null
+++ b/src/Dapr.Workflow.Versioning.Runtime/WorkflowVersionStrategyContext.cs
@@ -0,0 +1,19 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning;
+
+///
+/// Context information provided to version strategies when they are constructed by the factory.
+///
+public readonly record struct WorkflowVersionStrategyContext(string CanonicalName, string? OptionsName);
diff --git a/src/Dapr.Workflow.Versioning.Runtime/WorkflowVersioningServiceCollectionExtensions.cs b/src/Dapr.Workflow.Versioning.Runtime/WorkflowVersioningServiceCollectionExtensions.cs
index 822f7c4a7..ad7403919 100644
--- a/src/Dapr.Workflow.Versioning.Runtime/WorkflowVersioningServiceCollectionExtensions.cs
+++ b/src/Dapr.Workflow.Versioning.Runtime/WorkflowVersioningServiceCollectionExtensions.cs
@@ -36,6 +36,24 @@ public static IServiceCollection AddDaprWorkflowVersioning(
// Options container for defaults
var opts = new WorkflowVersioningOptions();
configure?.Invoke(opts);
+
+ if (opts.DefaultStrategy is null)
+ {
+ opts.DefaultStrategy = sp =>
+ {
+ var factory = sp.GetRequiredService();
+ return factory.Create(typeof(NumericVersionStrategy), canonicalName: "DEFAULT", optionsName: null, services: sp);
+ };
+ }
+
+ if (opts.DefaultSelector is null)
+ {
+ opts.DefaultSelector = sp =>
+ {
+ var factory = sp.GetRequiredService();
+ return factory.Create(typeof(MaxVersionSelector), canonicalName: "DEFAULT", optionsName: null, services: sp);
+ };
+ }
// Register singletons for options, diagnostics, factories and resolver
services.AddSingleton(opts);
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/Dapr.Workflow.Versioning.Runtime.Test.csproj b/test/Dapr.Workflow.Versioning.Runtime.Test/Dapr.Workflow.Versioning.Runtime.Test.csproj
new file mode 100644
index 000000000..1e29f1a8e
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/Dapr.Workflow.Versioning.Runtime.Test.csproj
@@ -0,0 +1,25 @@
+
+
+
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/DateSuffixVersionStrategyOptionsTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/DateSuffixVersionStrategyOptionsTests.cs
new file mode 100644
index 000000000..e86bdb9de
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/DateSuffixVersionStrategyOptionsTests.cs
@@ -0,0 +1,27 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class DateSuffixVersionStrategyOptionsTests
+{
+ [Fact]
+ public void Defaults_ShouldMatchExpectedValues()
+ {
+ var options = new DateSuffixVersionStrategyOptions();
+
+ Assert.Equal("yyyyMMdd", options.DateFormat);
+ Assert.False(options.AllowNoSuffix);
+ Assert.Equal("0", options.DefaultVersion);
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/DateSuffixVersionStrategyTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/DateSuffixVersionStrategyTests.cs
new file mode 100644
index 000000000..2b2e517ea
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/DateSuffixVersionStrategyTests.cs
@@ -0,0 +1,98 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class DateSuffixVersionStrategyTests
+{
+ [Fact]
+ public void TryParse_ShouldParseDefaultFormat()
+ {
+ var strategy = new DateSuffixVersionStrategy();
+
+ var parsed = strategy.TryParse("MyWorkflow20260212", out var canonical, out var version);
+
+ Assert.True(parsed);
+ Assert.Equal("MyWorkflow", canonical);
+ Assert.Equal("20260212", version);
+ }
+
+ [Fact]
+ public void TryParse_ShouldRejectNoSuffix_ByDefault()
+ {
+ var strategy = new DateSuffixVersionStrategy();
+
+ Assert.False(strategy.TryParse("MyWorkflow", out _, out _));
+ }
+
+ [Fact]
+ public void TryParse_ShouldAllowNoSuffix_WhenEnabled()
+ {
+ var services = new ServiceCollection();
+ services.AddOptions(Options.DefaultName)
+ .Configure(o => o.AllowNoSuffix = true);
+
+ using var provider = services.BuildServiceProvider();
+ var factory = new DefaultWorkflowVersionStrategyFactory();
+ var strategy = (DateSuffixVersionStrategy)factory.Create(
+ typeof(DateSuffixVersionStrategy),
+ canonicalName: "Orders",
+ optionsName: null,
+ services: provider);
+
+ Assert.True(strategy.TryParse("Orders", out var canonical, out var version));
+ Assert.Equal("Orders", canonical);
+ Assert.Equal("0", version);
+ }
+
+ [Fact]
+ public void TryParse_ShouldUseNamedFormatFromFactory()
+ {
+ var services = new ServiceCollection();
+ services.AddOptions("custom")
+ .Configure(o => o.DateFormat = "yyyy-MM-dd");
+
+ using var provider = services.BuildServiceProvider();
+ var factory = new DefaultWorkflowVersionStrategyFactory();
+ var strategy = (DateSuffixVersionStrategy)factory.Create(
+ typeof(DateSuffixVersionStrategy),
+ canonicalName: "Orders",
+ optionsName: "custom",
+ services: provider);
+
+ Assert.True(strategy.TryParse("Orders2026-02-12", out var canonical, out var version));
+ Assert.Equal("Orders", canonical);
+ Assert.Equal("2026-02-12", version);
+ }
+
+ [Fact]
+ public void Compare_ShouldOrderByDate()
+ {
+ var strategy = new DateSuffixVersionStrategy();
+
+ Assert.True(strategy.Compare("20260101", "20261231") < 0);
+ Assert.True(strategy.Compare("20261231", "20260101") > 0);
+ }
+
+ [Fact]
+ public void Compare_ShouldPreferValidDateOverInvalid()
+ {
+ var strategy = new DateSuffixVersionStrategy();
+
+ Assert.True(strategy.Compare("20261231", "not-a-date") > 0);
+ Assert.True(strategy.Compare("bad", "20260101") < 0);
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/DefaultWorkflowVersionStrategyFactoryTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/DefaultWorkflowVersionStrategyFactoryTests.cs
new file mode 100644
index 000000000..e0e07b4c3
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/DefaultWorkflowVersionStrategyFactoryTests.cs
@@ -0,0 +1,51 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class DefaultWorkflowVersionStrategyFactoryTests
+{
+ [Fact]
+ public void Create_ShouldConfigureContextConsumer()
+ {
+ using var services = new ServiceCollection().BuildServiceProvider();
+ var factory = new DefaultWorkflowVersionStrategyFactory();
+
+ var strategy = factory.Create(typeof(TestContextStrategy), "Orders", "orders-options", services);
+ var typed = Assert.IsType(strategy);
+
+ Assert.Equal("Orders", typed.Context.CanonicalName);
+ Assert.Equal("orders-options", typed.Context.OptionsName);
+ }
+
+ private sealed class TestContextStrategy : IWorkflowVersionStrategy, IWorkflowVersionStrategyContextConsumer
+ {
+ public WorkflowVersionStrategyContext Context { get; private set; }
+
+ public void Configure(WorkflowVersionStrategyContext context)
+ {
+ Context = context;
+ }
+
+ public bool TryParse(string typeName, out string canonicalName, out string version)
+ {
+ canonicalName = typeName;
+ version = "0";
+ return true;
+ }
+
+ public int Compare(string? v1, string? v2) => 0;
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/DelimitedSuffixVersionStrategyOptionsTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/DelimitedSuffixVersionStrategyOptionsTests.cs
new file mode 100644
index 000000000..b922779b7
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/DelimitedSuffixVersionStrategyOptionsTests.cs
@@ -0,0 +1,28 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class DelimitedSuffixVersionStrategyOptionsTests
+{
+ [Fact]
+ public void Defaults_ShouldMatchExpectedValues()
+ {
+ var options = new DelimitedSuffixVersionStrategyOptions();
+
+ Assert.Equal("-", options.Delimiter);
+ Assert.False(options.IgnoreDelimiterCase);
+ Assert.False(options.AllowNoSuffix);
+ Assert.Equal("0", options.DefaultVersion);
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/DelimitedSuffixVersionStrategyTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/DelimitedSuffixVersionStrategyTests.cs
new file mode 100644
index 000000000..c008a2b5f
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/DelimitedSuffixVersionStrategyTests.cs
@@ -0,0 +1,60 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class DelimitedSuffixVersionStrategyTests
+{
+ [Fact]
+ public void TryParse_ShouldParseDelimitedSuffix()
+ {
+ var strategy = new DelimitedSuffixVersionStrategy();
+
+ var parsed = strategy.TryParse("Orders-2", out var canonical, out var version);
+
+ Assert.True(parsed);
+ Assert.Equal("Orders", canonical);
+ Assert.Equal("2", version);
+ }
+
+ [Fact]
+ public void TryParse_ShouldRejectNoSuffix_ByDefault()
+ {
+ var strategy = new DelimitedSuffixVersionStrategy();
+
+ Assert.False(strategy.TryParse("Orders", out _, out _));
+ }
+
+ [Fact]
+ public void TryParse_ShouldAllowNoSuffix_WhenConfigured()
+ {
+ var services = new ServiceCollection();
+ services.AddOptions(Options.DefaultName)
+ .Configure(o => o.AllowNoSuffix = true);
+
+ using var provider = services.BuildServiceProvider();
+ var factory = new DefaultWorkflowVersionStrategyFactory();
+ var strategy = (DelimitedSuffixVersionStrategy)factory.Create(
+ typeof(DelimitedSuffixVersionStrategy),
+ canonicalName: "Orders",
+ optionsName: null,
+ services: provider);
+
+ Assert.True(strategy.TryParse("Orders", out var canonical, out var version));
+ Assert.Equal("Orders", canonical);
+ Assert.Equal("0", version);
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/ExplicitVersionStrategyOptionsTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/ExplicitVersionStrategyOptionsTests.cs
new file mode 100644
index 000000000..e6677762e
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/ExplicitVersionStrategyOptionsTests.cs
@@ -0,0 +1,27 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class ExplicitVersionStrategyOptionsTests
+{
+ [Fact]
+ public void Defaults_ShouldMatchExpectedValues()
+ {
+ var options = new ExplicitVersionStrategyOptions();
+
+ Assert.False(options.AllowMissingVersion);
+ Assert.Equal("0", options.DefaultVersion);
+ Assert.False(options.IgnoreCase);
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/ExplicitVersionStrategyTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/ExplicitVersionStrategyTests.cs
new file mode 100644
index 000000000..6a9662e97
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/ExplicitVersionStrategyTests.cs
@@ -0,0 +1,48 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class ExplicitVersionStrategyTests
+{
+ [Fact]
+ public void TryParse_ShouldRejectMissingVersion_ByDefault()
+ {
+ var strategy = new ExplicitVersionStrategy();
+
+ Assert.False(strategy.TryParse("Orders", out _, out _));
+ }
+
+ [Fact]
+ public void TryParse_ShouldAllowMissingVersion_WhenConfigured()
+ {
+ var services = new ServiceCollection();
+ services.AddOptions(Options.DefaultName)
+ .Configure(o => o.AllowMissingVersion = true);
+
+ using var provider = services.BuildServiceProvider();
+ var factory = new DefaultWorkflowVersionStrategyFactory();
+ var strategy = (ExplicitVersionStrategy)factory.Create(
+ typeof(ExplicitVersionStrategy),
+ canonicalName: "Orders",
+ optionsName: null,
+ services: provider);
+
+ Assert.True(strategy.TryParse("Orders", out var canonical, out var version));
+ Assert.Equal("Orders", canonical);
+ Assert.Equal("0", version);
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/NumericVersionStrategyOptionsTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/NumericVersionStrategyOptionsTests.cs
new file mode 100644
index 000000000..cb7cedd73
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/NumericVersionStrategyOptionsTests.cs
@@ -0,0 +1,28 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class NumericVersionStrategyOptionsTests
+{
+ [Fact]
+ public void Defaults_ShouldMatchExpectedValues()
+ {
+ var options = new NumericVersionStrategyOptions();
+
+ Assert.Equal("V", options.SuffixPrefix);
+ Assert.False(options.IgnorePrefixCase);
+ Assert.True(options.AllowNoSuffix);
+ Assert.Equal("0", options.DefaultVersion);
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/NumericVersionStrategyTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/NumericVersionStrategyTests.cs
new file mode 100644
index 000000000..67725c2e5
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/NumericVersionStrategyTests.cs
@@ -0,0 +1,99 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class NumericVersionStrategyTests
+{
+ [Fact]
+ public void TryParse_ShouldParseWithDefaultPrefix()
+ {
+ var strategy = new NumericVersionStrategy();
+
+ var parsed = strategy.TryParse("MyWorkflowV1", out var canonical, out var version);
+
+ Assert.True(parsed);
+ Assert.Equal("MyWorkflow", canonical);
+ Assert.Equal("1", version);
+ }
+
+ [Fact]
+ public void TryParse_ShouldParseDefaultVersion_WhenNoSuffix()
+ {
+ var strategy = new NumericVersionStrategy();
+
+ var parsed = strategy.TryParse("MyWorkflow", out var canonical, out var version);
+
+ Assert.True(parsed);
+ Assert.Equal("MyWorkflow", canonical);
+ Assert.Equal("0", version);
+ }
+
+ [Fact]
+ public void TryParse_ShouldRejectMissingPrefix_WhenDigitsPresent()
+ {
+ var strategy = new NumericVersionStrategy();
+
+ var parsed = strategy.TryParse("MyWorkflow1", out _, out _);
+
+ Assert.False(parsed);
+ }
+
+ [Theory]
+ [InlineData("1", "2")]
+ [InlineData("9", "10")]
+ public void Compare_ShouldOrderNumerically(string older, string newer)
+ {
+ var strategy = new NumericVersionStrategy();
+
+ Assert.True(strategy.Compare(older, newer) < 0);
+ Assert.True(strategy.Compare(newer, older) > 0);
+ }
+
+ [Fact]
+ public void Compare_ShouldPreferNumericOverNonNumeric()
+ {
+ var strategy = new NumericVersionStrategy();
+
+ Assert.True(strategy.Compare("2", "beta") > 0);
+ Assert.True(strategy.Compare("alpha", "3") < 0);
+ }
+
+ [Fact]
+ public void TryParse_ShouldUseNamedOptionsFromFactory()
+ {
+ var services = new ServiceCollection();
+ services.AddOptions("custom")
+ .Configure(o =>
+ {
+ o.SuffixPrefix = "v";
+ o.IgnorePrefixCase = true;
+ o.AllowNoSuffix = false;
+ });
+
+ using var provider = services.BuildServiceProvider();
+ var factory = new DefaultWorkflowVersionStrategyFactory();
+ var strategy = (NumericVersionStrategy)factory.Create(
+ typeof(NumericVersionStrategy),
+ canonicalName: "Orders",
+ optionsName: "custom",
+ services: provider);
+
+ Assert.True(strategy.TryParse("Ordersv2", out var canonical, out var version));
+ Assert.Equal("Orders", canonical);
+ Assert.Equal("2", version);
+ Assert.False(strategy.TryParse("Orders2", out _, out _));
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/SemVerVersionStrategyOptionsTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/SemVerVersionStrategyOptionsTests.cs
new file mode 100644
index 000000000..7569a3a6c
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/SemVerVersionStrategyOptionsTests.cs
@@ -0,0 +1,30 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class SemVerVersionStrategyOptionsTests
+{
+ [Fact]
+ public void Defaults_ShouldMatchExpectedValues()
+ {
+ var options = new SemVerVersionStrategyOptions();
+
+ Assert.Equal("v", options.Prefix);
+ Assert.False(options.IgnorePrefixCase);
+ Assert.True(options.AllowPrerelease);
+ Assert.True(options.AllowBuildMetadata);
+ Assert.False(options.AllowNoSuffix);
+ Assert.Equal("0.0.0", options.DefaultVersion);
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/SemVerVersionStrategyTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/SemVerVersionStrategyTests.cs
new file mode 100644
index 000000000..0170cbb10
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/SemVerVersionStrategyTests.cs
@@ -0,0 +1,62 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class SemVerVersionStrategyTests
+{
+ [Fact]
+ public void TryParse_ShouldParseWithDefaultPrefix()
+ {
+ var strategy = new SemVerVersionStrategy();
+
+ var parsed = strategy.TryParse("Ordersv1.2.3", out var canonical, out var version);
+
+ Assert.True(parsed);
+ Assert.Equal("Orders", canonical);
+ Assert.Equal("1.2.3", version);
+ }
+
+ [Fact]
+ public void Compare_ShouldRespectSemVerRules()
+ {
+ var strategy = new SemVerVersionStrategy();
+
+ Assert.True(strategy.Compare("1.2.3", "1.3.0") < 0);
+ Assert.True(strategy.Compare("1.2.3-alpha", "1.2.3") < 0);
+ Assert.Equal(0, strategy.Compare("1.0.0+build1", "1.0.0+build2"));
+ }
+
+ [Fact]
+ public void TryParse_ShouldAllowNoSuffix_WhenConfigured()
+ {
+ var services = new ServiceCollection();
+ services.AddOptions(Options.DefaultName)
+ .Configure(o => o.AllowNoSuffix = true);
+
+ using var provider = services.BuildServiceProvider();
+ var factory = new DefaultWorkflowVersionStrategyFactory();
+ var strategy = (SemVerVersionStrategy)factory.Create(
+ typeof(SemVerVersionStrategy),
+ canonicalName: "Orders",
+ optionsName: null,
+ services: provider);
+
+ Assert.True(strategy.TryParse("Orders", out var canonical, out var version));
+ Assert.Equal("Orders", canonical);
+ Assert.Equal("0.0.0", version);
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/WorkflowVersionStrategyContextTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/WorkflowVersionStrategyContextTests.cs
new file mode 100644
index 000000000..7e36d16c2
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/WorkflowVersionStrategyContextTests.cs
@@ -0,0 +1,26 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class WorkflowVersionStrategyContextTests
+{
+ [Fact]
+ public void Constructor_ShouldStoreValues()
+ {
+ var context = new WorkflowVersionStrategyContext("Orders", "options");
+
+ Assert.Equal("Orders", context.CanonicalName);
+ Assert.Equal("options", context.OptionsName);
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/ZeroPaddedNumericVersionStrategyOptionsTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/ZeroPaddedNumericVersionStrategyOptionsTests.cs
new file mode 100644
index 000000000..233514c07
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/ZeroPaddedNumericVersionStrategyOptionsTests.cs
@@ -0,0 +1,29 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class ZeroPaddedNumericVersionStrategyOptionsTests
+{
+ [Fact]
+ public void Defaults_ShouldMatchExpectedValues()
+ {
+ var options = new ZeroPaddedNumericVersionStrategyOptions();
+
+ Assert.Equal(string.Empty, options.SuffixPrefix);
+ Assert.False(options.IgnorePrefixCase);
+ Assert.Equal(4, options.Width);
+ Assert.False(options.AllowNoSuffix);
+ Assert.Equal("0", options.DefaultVersion);
+ }
+}
diff --git a/test/Dapr.Workflow.Versioning.Runtime.Test/ZeroPaddedNumericVersionStrategyTests.cs b/test/Dapr.Workflow.Versioning.Runtime.Test/ZeroPaddedNumericVersionStrategyTests.cs
new file mode 100644
index 000000000..9f3af941e
--- /dev/null
+++ b/test/Dapr.Workflow.Versioning.Runtime.Test/ZeroPaddedNumericVersionStrategyTests.cs
@@ -0,0 +1,58 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace Dapr.Workflow.Versioning.Runtime.Test;
+
+public class ZeroPaddedNumericVersionStrategyTests
+{
+ [Fact]
+ public void TryParse_ShouldParseWithWidth()
+ {
+ var strategy = new ZeroPaddedNumericVersionStrategy();
+
+ Assert.True(strategy.TryParse("Orders0007", out var canonical, out var version));
+ Assert.Equal("Orders", canonical);
+ Assert.Equal("0007", version);
+ }
+
+ [Fact]
+ public void TryParse_ShouldRejectWrongWidth()
+ {
+ var strategy = new ZeroPaddedNumericVersionStrategy();
+
+ Assert.False(strategy.TryParse("Orders007", out _, out _));
+ }
+
+ [Fact]
+ public void TryParse_ShouldAllowNoSuffix_WhenConfigured()
+ {
+ var services = new ServiceCollection();
+ services.AddOptions(Options.DefaultName)
+ .Configure(o => o.AllowNoSuffix = true);
+
+ using var provider = services.BuildServiceProvider();
+ var factory = new DefaultWorkflowVersionStrategyFactory();
+ var strategy = (ZeroPaddedNumericVersionStrategy)factory.Create(
+ typeof(ZeroPaddedNumericVersionStrategy),
+ canonicalName: "Orders",
+ optionsName: null,
+ services: provider);
+
+ Assert.True(strategy.TryParse("Orders", out var canonical, out var version));
+ Assert.Equal("Orders", canonical);
+ Assert.Equal("0", version);
+ }
+}