Skip to content
Merged
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
7 changes: 7 additions & 0 deletions all.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ public sealed class WorkflowVersionAttribute : Attribute

/// <summary>
/// Gets or sets an optional strategy type to use for this workflow, overriding the globally configured strategy.
/// The type must implement <see cref="IWorkflowVersionStrategy"/> and expose a public parameterless constructor.
/// The type must implement <see cref="IWorkflowVersionStrategy"/> and be constructible by the active
/// </summary>
public Type? StrategyType { get; init; }

/// <summary>
/// Gets or sets an optional named options scope to use when configuring the strategy for this workflow.
/// </summary>
public string? OptionsName { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Optional interface for strategies that want per-family context (canonical name and options scope).
/// </summary>
public interface IWorkflowVersionStrategyContextConsumer
{
/// <summary>
/// Configures the strategy with the canonical name and optional options scope.
/// </summary>
/// <param name="context">The strategy context.</param>
void Configure(WorkflowVersionStrategyContext context);
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Strategy that derives a date-based version from a trailing suffix
/// (for example, <c>MyWorkflow20260212</c> with format <c>yyyyMMdd</c>).
/// </summary>
public sealed class DateSuffixVersionStrategy(IOptionsMonitor<DateSuffixVersionStrategyOptions>? optionsMonitor = null)
: IWorkflowVersionStrategy, IWorkflowVersionStrategyContextConsumer
{
private DateSuffixVersionStrategyOptions _options = new();

/// <inheritdoc />
public void Configure(WorkflowVersionStrategyContext context)
{
var optionsName = string.IsNullOrWhiteSpace(context.OptionsName)
? Options.DefaultName
: context.OptionsName;

if (optionsMonitor is not null)
{
_options = optionsMonitor.Get(optionsName);
}
}

/// <inheritdoc />
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;
}

/// <inheritdoc />
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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Options for <see cref="DateSuffixVersionStrategy"/>.
/// </summary>
public sealed class DateSuffixVersionStrategyOptions
{
/// <summary>
/// Gets or sets the date format expected at the end of the workflow type name.
/// Defaults to <c>yyyyMMdd</c>.
/// </summary>
public string DateFormat { get; set; } = "yyyyMMdd";

/// <summary>
/// Gets or sets a value indicating whether names without a date suffix are allowed.
/// When enabled, the default version is applied.
/// </summary>
public bool AllowNoSuffix { get; set; }

/// <summary>
/// Gets or sets the default version used when no suffix is present.
/// </summary>
public string DefaultVersion { get; set; } = "0";
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Strategy that derives the version from a delimiter-separated suffix (for example, <c>MyWorkflow-1</c>).
/// </summary>
public sealed class DelimitedSuffixVersionStrategy(
IOptionsMonitor<DelimitedSuffixVersionStrategyOptions>? optionsMonitor = null)
: IWorkflowVersionStrategy, IWorkflowVersionStrategyContextConsumer
{
private DelimitedSuffixVersionStrategyOptions _options = new();

/// <inheritdoc />
public void Configure(WorkflowVersionStrategyContext context)
{
var optionsName = string.IsNullOrWhiteSpace(context.OptionsName)
? Options.DefaultName
: context.OptionsName;

if (optionsMonitor is not null)
{
_options = optionsMonitor.Get(optionsName);
}
}

/// <inheritdoc />
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);
}

/// <inheritdoc />
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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Options for <see cref="DelimitedSuffixVersionStrategy"/>.
/// </summary>
public sealed class DelimitedSuffixVersionStrategyOptions
{
/// <summary>
/// Gets or sets the delimiter separating the canonical name from the version.
/// </summary>
public string Delimiter { get; set; } = "-";

/// <summary>
/// Gets or sets a value indicating whether delimiter matching ignores case.
/// </summary>
public bool IgnoreDelimiterCase { get; set; }

/// <summary>
/// Gets or sets a value indicating whether names without a delimiter are allowed.
/// When enabled, the default version is applied.
/// </summary>
public bool AllowNoSuffix { get; set; }

/// <summary>
/// Gets or sets the default version used when no suffix is present.
/// </summary>
public string DefaultVersion { get; set; } = "0";
}
Loading
Loading