Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
46b3174
Tentatively added workflow versioning via patches with unit tests
WhitWaldo Jan 16, 2026
5060bff
Added unit tests to achieve 100% test coverage of WorkflowVersionTracker
WhitWaldo Jan 16, 2026
d0ac891
Applied anticipatory bugfix for properly retrieving the app ID from t…
WhitWaldo Jan 16, 2026
605251e
Converted DaprHarnessBuilder to use a builder pattern to avoid having…
WhitWaldo Jan 16, 2026
65fb8eb
Simplified version tracker to abstract away ordered set to avoid inco…
WhitWaldo Jan 17, 2026
43cd0db
Building out e2e tests for patch workflows
WhitWaldo Jan 17, 2026
c3d0e2c
Merge remote-tracking branch 'origin/master' into wf-patching
WhitWaldo Jan 18, 2026
f6bc5ec
Merge branch 'master' into wf-patching
WhitWaldo Jan 26, 2026
e461103
Enabled harness for Dapr.DistributedLock
WhitWaldo Jan 27, 2026
2dd8cbe
Updated patch-based workflow versioning
WhitWaldo Jan 27, 2026
2797287
Fixed usings for Dapr.Testcontainers
WhitWaldo Jan 27, 2026
2d973f5
Added some additional integration tests to prove that a bidirectional…
WhitWaldo Jan 27, 2026
77f2295
Removing unused using
WhitWaldo Jan 27, 2026
2cbd642
Disabled unused startup method since it's been replaecd by alternativ…
WhitWaldo Jan 27, 2026
647a455
Working to test wf patching
WhitWaldo Jan 27, 2026
255f912
Merge branch 'master' into wf-patching
WhitWaldo Jan 27, 2026
d430142
Initial patch test is passing
WhitWaldo Jan 27, 2026
c1ee4ff
Removed duplicate call to process past events + unused field
WhitWaldo Jan 28, 2026
a0bcaf9
Modified to include whole orchestrator history
WhitWaldo Jan 28, 2026
4eb0155
Updated unit test to reflect full history
WhitWaldo Jan 28, 2026
05cb055
Refactored WorkflowWorker to improve event processing by separating r…
WhitWaldo Jan 28, 2026
c2462f9
First working pass at patched versioning in workflows
WhitWaldo Jan 29, 2026
4c46a94
Disabled unit test for now while vlaidating correct semantics
WhitWaldo Jan 29, 2026
a4bee39
Added missing `Dapr.Workflow.Abstractions.Test` project to solution a…
WhitWaldo Jan 29, 2026
fdb3b7a
Merge branch 'master' into wf-patching
WhitWaldo Feb 5, 2026
c528b50
Merged master into branch & fixed build errors
WhitWaldo Feb 5, 2026
f480d03
Merge branch 'master' into wf-patching
WhitWaldo Feb 5, 2026
f77f3ad
Made some tweaks to the patch versioning implementation, added anothe…
WhitWaldo Feb 5, 2026
cfd2791
Fixed duplicate versioning issues and all e2e tests pass now
WhitWaldo Feb 5, 2026
0b74575
Added project to contain XUnit-specific functionality used for integr…
WhitWaldo Feb 6, 2026
d79b7c8
Updated to use the gated fact in the new project so the patch version…
WhitWaldo Feb 6, 2026
00c10a4
Reverting condition setting 'Stalled' as a terminal status with regar…
WhitWaldo Feb 6, 2026
10b9c8d
Added extension method that accommodates stalls as a terminal state w…
WhitWaldo Feb 6, 2026
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
14 changes: 14 additions & 0 deletions all.sln
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Testcontainers.Test",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.DistributedLock", "test\Dapr.IntegrationTest.DistributedLock\Dapr.IntegrationTest.DistributedLock.csproj", "{E958E875-8DDE-4B25-BE3A-C0760EC89376}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Abstractions.Test", "test\Dapr.Workflow.Abstractions.Test\Dapr.Workflow.Abstractions.Test.csproj", "{38AAD849-B59C-4011-B309-3E9F291E9B9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Testcontainers.Xunit", "src\Dapr.Testcontainers.Xunit\Dapr.Testcontainers.Xunit.csproj", "{2D6EB9E0-C5BF-4BA4-B69F-0D2B5A0E36D5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -597,6 +601,14 @@ Global
{E958E875-8DDE-4B25-BE3A-C0760EC89376}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E958E875-8DDE-4B25-BE3A-C0760EC89376}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E958E875-8DDE-4B25-BE3A-C0760EC89376}.Release|Any CPU.Build.0 = Release|Any CPU
{38AAD849-B59C-4011-B309-3E9F291E9B9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{38AAD849-B59C-4011-B309-3E9F291E9B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{38AAD849-B59C-4011-B309-3E9F291E9B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{38AAD849-B59C-4011-B309-3E9F291E9B9F}.Release|Any CPU.Build.0 = Release|Any CPU
{2D6EB9E0-C5BF-4BA4-B69F-0D2B5A0E36D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D6EB9E0-C5BF-4BA4-B69F-0D2B5A0E36D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D6EB9E0-C5BF-4BA4-B69F-0D2B5A0E36D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D6EB9E0-C5BF-4BA4-B69F-0D2B5A0E36D5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -707,6 +719,8 @@ Global
{A05D1519-6A82-498F-B7C9-3D14E08D35CA} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{5A93F96B-4D0E-479D-B540-29678A0998FA} = {0AF0FE8D-C234-4F04-8514-32206ACE01BD}
{E958E875-8DDE-4B25-BE3A-C0760EC89376} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
{38AAD849-B59C-4011-B309-3E9F291E9B9F} = {0AF0FE8D-C234-4F04-8514-32206ACE01BD}
{2D6EB9E0-C5BF-4BA4-B69F-0D2B5A0E36D5} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using Xunit;

namespace Dapr.Testcontainers.Xunit.Attributes;

/// <summary>
/// Used to indicate that there's a minimum supported version of the Dapr runtime needed to run the indicated
/// integration test and that it should be skipped otherwise.
/// </summary>
/// <remarks>
/// This will include RCs of that indicated version in addition to stable versions.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class MinimumDaprRuntimeFactAttribute : FactAttribute
{
private const string RuntimeVersionEnvVarName = "DAPR_RUNTIME_VERSION";

/// <summary>
/// Initializes the <see cref="MinimumDaprRuntimeFactAttribute"/> instance.
/// </summary>
/// <param name="minimumVersion">The minimum supported version.</param>
public MinimumDaprRuntimeFactAttribute(string minimumVersion)
{
if (!DaprRuntimeVersionGate.IsMinimumSatisfied(minimumVersion, out var reason))
Skip = reason;
}

private static class DaprRuntimeVersionGate
{
public static bool IsMinimumSatisfied(string minimumVersion, out string? reason)
{
if (!TryParseVersion(minimumVersion, out var minVersion))
throw new ArgumentException(
$"Invalid minimum Dapr runtime version '{minimumVersion}'.",
nameof(minimumVersion));

var currentRaw = Environment.GetEnvironmentVariable(RuntimeVersionEnvVarName);
if (string.IsNullOrWhiteSpace(currentRaw) ||
string.Equals(currentRaw, "latest", StringComparison.OrdinalIgnoreCase))
{
reason = null;
return true;
}

if (!TryParseVersion(currentRaw, out var currentVersion))
{
reason = null;
return true;
}

if (currentVersion >= minVersion)
{
reason = null;
return true;
}

reason = $"Requires Dapr runtime >= {minimumVersion} (current: {currentRaw}).";
return false;
}

private static bool TryParseVersion(string value, out Version version)
{
version = default!;
if (string.IsNullOrWhiteSpace(value))
return false;

var trimmed = value.Trim();
if (trimmed.StartsWith("v", StringComparison.OrdinalIgnoreCase))
trimmed = trimmed[1..];

var withoutMetadata = trimmed.Split('+', 2)[0];
var withoutPrerelease = withoutMetadata.Split('-', 2)[0];
var parts = withoutPrerelease.Split('.', StringSplitOptions.RemoveEmptyEntries);

if (parts.Length < 2)
return false;

if (!int.TryParse(parts[0], out var major))
return false;
if (!int.TryParse(parts[1], out var minor))
return false;

var patch = 0;
if (parts.Length >= 3 && !int.TryParse(parts[2], out patch))
return false;

version = new Version(major, minor, patch);
return true;
}
}
}
14 changes: 14 additions & 0 deletions src/Dapr.Testcontainers.Xunit/Dapr.Testcontainers.Xunit.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Title>XUnit extensions for Dapr.Testcontainers</Title>
<Description>Provides XUnit-specific extension capabilities for testing with Dapr.Testcontainers</Description>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="xunit.extensibility.core" />
</ItemGroup>

</Project>
117 changes: 84 additions & 33 deletions src/Dapr.Testcontainers/Common/DaprHarnessBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,60 +22,111 @@ namespace Dapr.Testcontainers.Common;
/// <summary>
/// Builds the Dapr harnesses for different building blocks.
/// </summary>
/// <param name="options">The Dapr runtime options.</param>
/// <param name="environment">The isolated environment instance.</param>
/// <param name="startApp">The test app to run.</param>
public sealed class DaprHarnessBuilder(DaprRuntimeOptions options, DaprTestEnvironment? environment = null, Func<int, Task>? startApp = null)
public sealed class DaprHarnessBuilder
{
/// <summary>
/// Builds a workflow harness.
/// The Dapr container runtime options.
/// </summary>
/// <param name="componentsDir">The path to the Dapr resources.</param>
public WorkflowHarness BuildWorkflow(string componentsDir) => new(componentsDir, startApp, options, environment);

private DaprRuntimeOptions _options { get; set; } = new("1.17.0-rc.3");
/// <summary>
/// Builds a distributed lock harness.
/// The isolated test environment to use with the harness, if any.
/// </summary>
/// <param name="componentsDir">The path to the Dapr resources.</param>
public DistributedLockHarness BuildDistributedLock(string componentsDir) => new(componentsDir, startApp, options, environment);

private DaprTestEnvironment? _environment { get; set; }
/// <summary>
/// Builds a conversation harness.
/// The directory containing the Dapr component resources.
/// </summary>
/// <param name="componentsDir">The path to the Dapr resources.</param>
public ConversationHarness BuildConversation(string componentsDir) => new(componentsDir, startApp, options, environment);

private string _componentsDirectory { get; set; }
/// <summary>
/// Builds a cryptography harness.
/// An application to run at startup.
/// </summary>
/// <param name="componentsDir">The path to the Dapr resources.</param>
/// <param name="keysDir">The path to the cryptography keys.</param>
public CryptographyHarness BuildCryptography(string componentsDir, string keysDir) =>
new(componentsDir, startApp, keysDir, options, environment);
private Func<int, Task>? _startApp { get; set; }

/// <summary>
/// Builds a jobs harness.
/// Builds the Dapr harnesses for different building blocks.
/// </summary>
/// <param name="componentsDirectory">The path to the Dapr component resources.</param>
public DaprHarnessBuilder(string componentsDirectory)
{
_componentsDirectory = componentsDirectory;
}

/// <summary>
/// Sets the <see cref="DaprRuntimeOptions"/> on the builder.
/// </summary>
/// <param name="componentsDir">The path to the Dapr resources.</param>
public JobsHarness BuildJobs(string componentsDir) => new(componentsDir, startApp, options, environment);
/// <param name="options">Options for configuring the Dapr container runtime.</param>
/// <returns>This instance of the <see cref="DaprHarnessBuilder"/>.</returns>
public DaprHarnessBuilder WithOptions(DaprRuntimeOptions options)
{
this._options = options;
return this;
}

// /// <summary>
// /// Sets an application to run as part of startup.
// /// </summary>
// /// <param name="startApp">The starting application.</param>
// /// <returns></returns>
// public DaprHarnessBuilder WithStartUp(Func<int, Task> startApp)
// {
// this._startApp = startApp;
// return this;
// }

/// <summary>
/// Sets the shared test environment.
/// </summary>
/// <param name="environment">The test environment to be used by the harness.</param>
/// <returns>This instance of the <see cref="DaprHarnessBuilder"/>.</returns>
public DaprHarnessBuilder WithEnvironment(DaprTestEnvironment environment)
{
this._environment = environment;
return this;
}

/// <summary>
/// Builds a PubSub harness.
/// Builds a workflow harness.
/// </summary>
/// <param name="componentsDir">The path to the Dapr resources.</param>
public PubSubHarness BuildPubSub(string componentsDir) => new(componentsDir, startApp, options, environment);
public WorkflowHarness BuildWorkflow() => new(_componentsDirectory, _startApp, _options, _environment);

/// <summary>
/// Builds a state management harness.
/// Builds a distributed lock harness.
/// </summary>
/// <param name="componentsDir">The path to the Dapr resources.</param>
public StateManagementHarness BuildStateManagement(string componentsDir) => new(componentsDir, startApp, options, environment);
public DistributedLockHarness BuildDistributedLock() => new(_componentsDirectory, _startApp, _options, _environment);
//
// /// <summary>
// /// Builds a conversation harness.
// /// </summary>
// public ConversationHarness BuildConversation() => new(_componentsDirectory, _startApp, _options, _environment);
//
// /// <summary>
// /// Builds a cryptography harness.
// /// </summary>
// /// <param name="keysDir">The path to the cryptography keys.</param>
// public CryptographyHarness BuildCryptography(string keysDir) =>
// new(_componentsDirectory, _startApp, keysDir, _options, _environment);

/// <summary>
/// Builds an actor harness.
/// Builds a jobs harness.
/// </summary>
/// <param name="componentsDir">The path to the Dapr resources.</param>
public ActorHarness BuildActors(string componentsDir) => new(componentsDir, startApp, options, environment);
public JobsHarness BuildJobs() => new(_componentsDirectory, _startApp, _options, _environment);

// /// <summary>
// /// Builds a PubSub harness.
// /// </summary>
// /// <param name="componentsDir">The path to the Dapr resources.</param>
// public PubSubHarness BuildPubSub(string componentsDir) => new(_componentsDirectory, _startApp, _options, _environment);
//
// /// <summary>
// /// Builds a state management harness.
// /// </summary>
// /// <param name="componentsDir">The path to the Dapr resources.</param>
// public StateManagementHarness BuildStateManagement(string componentsDir) => new(_componentsDirectory, _startApp, _options, _environment);
//
// /// <summary>
// /// Builds an actor harness.
// /// </summary>
// /// <param name="componentsDir">The path to the Dapr resources.</param>
// public ActorHarness BuildActors(string componentsDir) => new(_componentsDirectory, _startApp, _options, _environment);

/// <summary>
/// Creates a test application builder for the specified harness.
Expand Down
6 changes: 6 additions & 0 deletions src/Dapr.Workflow.Abstractions/IWorkflowContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,10 @@ public interface IWorkflowContext
/// <c>true</c> if the orchestration or operation is currently being replayed; otherwise <c>false</c>.
/// </value>
bool IsReplaying { get; }

/// <summary>
/// Returns true/false according to patch-based versioning semantics.
/// </summary>
/// <param name="patchName">Case-sensitive patch name.</param>
bool IsPatched(string patchName);
}
3 changes: 3 additions & 0 deletions src/Dapr.Workflow.Abstractions/WorkflowContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public abstract class WorkflowContext : IWorkflowContext
/// </value>
public abstract bool IsReplaying { get; }

/// <inheritdoc />
public abstract bool IsPatched(string patchName);

/// <summary>
/// Asynchronously invokes an activity by name and with the specified input value.
/// </summary>
Expand Down
7 changes: 4 additions & 3 deletions src/Dapr.Workflow/Client/WorkflowGrpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// ------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Dapr.Workflow.Serialization;
Expand Down Expand Up @@ -106,8 +107,7 @@ public override async Task<WorkflowMetadata> WaitForWorkflowStartAsync(string in
}

/// <inheritdoc />
public override async Task<WorkflowMetadata> WaitForWorkflowCompletionAsync(string instanceId, bool getInputsAndOutputs = true,
CancellationToken cancellationToken = default)
public override async Task<WorkflowMetadata> WaitForWorkflowCompletionAsync(string instanceId, bool getInputsAndOutputs = true, CancellationToken cancellationToken = default)
{
while (true)
{
Expand Down Expand Up @@ -223,6 +223,7 @@ public override ValueTask DisposeAsync()
private string SerializeToJson(object? obj) => obj == null ? string.Empty : serializer.Serialize(obj);

private static bool IsTerminalStatus(WorkflowRuntimeStatus status) =>
status is WorkflowRuntimeStatus.Completed or WorkflowRuntimeStatus.Failed
status is WorkflowRuntimeStatus.Completed
or WorkflowRuntimeStatus.Failed
or WorkflowRuntimeStatus.Terminated;
}
Loading
Loading