diff --git a/all.sln b/all.sln
index c5ca7a6ea..aada63d9f 100644
--- a/all.sln
+++ b/all.sln
@@ -245,6 +245,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Versioning.Ru
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowVersioning", "examples\Workflow\WorkflowVersioning\WorkflowVersioning.csproj", "{837E02A5-D1C0-4F60-AF93-71117BF3B6DC}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows", "test\Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows\Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows.csproj", "{97CAEE0B-4020-4A86-97DA-9900FDF4DFC6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -651,6 +653,10 @@ Global
{837E02A5-D1C0-4F60-AF93-71117BF3B6DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{837E02A5-D1C0-4F60-AF93-71117BF3B6DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{837E02A5-D1C0-4F60-AF93-71117BF3B6DC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {97CAEE0B-4020-4A86-97DA-9900FDF4DFC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {97CAEE0B-4020-4A86-97DA-9900FDF4DFC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {97CAEE0B-4020-4A86-97DA-9900FDF4DFC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {97CAEE0B-4020-4A86-97DA-9900FDF4DFC6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -770,6 +776,7 @@ Global
{1AD32297-630E-4DFB-B3E4-CAFCE993F27F} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
{4FF7F075-2818-41E4-A88F-743417EA0A99} = {0AF0FE8D-C234-4F04-8514-32206ACE01BD}
{837E02A5-D1C0-4F60-AF93-71117BF3B6DC} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
+ {97CAEE0B-4020-4A86-97DA-9900FDF4DFC6} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
diff --git a/examples/Workflow/CrossAppVersioning/CrossAppVersioning.csproj b/examples/Workflow/CrossAppVersioning/CrossAppVersioning.csproj
new file mode 100644
index 000000000..cc095b683
--- /dev/null
+++ b/examples/Workflow/CrossAppVersioning/CrossAppVersioning.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net10.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/Workflow/CrossAppVersioning/Program.cs b/examples/Workflow/CrossAppVersioning/Program.cs
new file mode 100644
index 000000000..42eae3c67
--- /dev/null
+++ b/examples/Workflow/CrossAppVersioning/Program.cs
@@ -0,0 +1,16 @@
+using Dapr.Workflow.Versioning;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Enable workflow versioning and allow the generator to scan referenced assemblies.
+builder.Services.AddDaprWorkflowVersioning();
+
+var app = builder.Build();
+
+app.MapGet("/registry", (IServiceProvider services) =>
+{
+ var registry = GeneratedWorkflowVersionRegistry.GetWorkflowVersionRegistry(services);
+ return Results.Ok(registry);
+});
+
+await app.RunAsync();
diff --git a/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs b/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs
index d0e40d559..b4219f033 100644
--- a/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs
+++ b/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs
@@ -32,10 +32,15 @@ public sealed class WorkflowSourceGenerator : IIncrementalGenerator
{
private const string WorkflowBaseMetadataName = "Dapr.Workflow.Workflow`2";
private const string WorkflowVersionAttributeFullName = "Dapr.Workflow.Versioning.WorkflowVersionAttribute";
+ private const string ScanReferencesPropertyName = "build_property.DaprWorkflowVersioningScanReferences";
///
public void Initialize(IncrementalGeneratorInitializationContext context)
{
+ var scanReferences = context.AnalyzerConfigOptionsProvider.Select((options, _) =>
+ options.GlobalOptions.TryGetValue(ScanReferencesPropertyName, out var value) &&
+ string.Equals(value, "true", StringComparison.OrdinalIgnoreCase));
+
// Cache the attribute symbol
var known = context.CompilationProvider.Select((c, _) =>
new KnownSymbols(
@@ -157,8 +162,43 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}
});
+ var referenced = context.CompilationProvider
+ .Combine(known)
+ .Combine(scanReferences)
+ .Select((input, _) =>
+ {
+ var ((compilation, ks), scan) = input;
+ if (!scan)
+ return ImmutableArray.Empty;
+
+ if (ks.WorkflowBase is null)
+ return ImmutableArray.Empty;
+
+ var list = new List();
+ foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols)
+ {
+ list.AddRange(DiscoverReferencedWorkflows(assembly, ks, compilation.Assembly));
+ }
+
+ return list.ToImmutableArray();
+ });
+
// Collect and emit
- context.RegisterSourceOutput(discovered.Collect(), (spc, items) =>
+ var discoveredAll = discovered.Collect()
+ .Combine(referenced)
+ .Select((input, _) =>
+ {
+ var (current, extra) = input;
+ if (extra.IsDefaultOrEmpty)
+ return current;
+
+ var list = new List(current.Length + extra.Length);
+ list.AddRange(current);
+ list.AddRange(extra);
+ return list.ToImmutableArray();
+ });
+
+ context.RegisterSourceOutput(discoveredAll, (spc, items) =>
{
var workflows = items.Where(x => x is not null).ToList();
@@ -282,6 +322,87 @@ private static DiscoveredWorkflow BuildDiscoveredWorkflow(
);
}
+ private static IEnumerable DiscoverReferencedWorkflows(
+ IAssemblySymbol assemblySymbol,
+ KnownSymbols knownSymbols,
+ IAssemblySymbol currentAssembly)
+ {
+ foreach (var type in EnumerateTypes(assemblySymbol.GlobalNamespace))
+ {
+ if (!IsAccessibleFromAssembly(type, currentAssembly))
+ continue;
+
+ if (!InheritsFromWorkflow(type, knownSymbols.WorkflowBase))
+ continue;
+
+ AttributeData? attrData = null;
+ if (knownSymbols.WorkflowVersionAttribute is not null)
+ {
+ attrData = type.GetAttributes()
+ .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownSymbols.WorkflowVersionAttribute));
+ }
+
+ attrData ??= type.GetAttributes().FirstOrDefault(a =>
+ string.Equals(a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ $"global::{WorkflowVersionAttributeFullName}", StringComparison.Ordinal));
+
+ yield return BuildDiscoveredWorkflow(type, attrData);
+ }
+ }
+
+ private static IEnumerable EnumerateTypes(INamespaceSymbol root)
+ {
+ foreach (var member in root.GetMembers())
+ {
+ switch (member)
+ {
+ case INamespaceSymbol ns:
+ foreach (var type in EnumerateTypes(ns))
+ yield return type;
+ break;
+ case INamedTypeSymbol type:
+ foreach (var nested in EnumerateNestedTypes(type))
+ yield return nested;
+ break;
+ }
+ }
+ }
+
+ private static IEnumerable EnumerateNestedTypes(INamedTypeSymbol type)
+ {
+ yield return type;
+ foreach (var nested in type.GetTypeMembers())
+ {
+ foreach (var child in EnumerateNestedTypes(nested))
+ yield return child;
+ }
+ }
+
+ private static bool IsAccessibleFromAssembly(INamedTypeSymbol type, IAssemblySymbol currentAssembly)
+ {
+ for (var containing = type; containing is not null; containing = containing.ContainingType)
+ {
+ if (!IsAccessibleCore(containing, currentAssembly))
+ return false;
+ }
+
+ return true;
+ }
+
+ private static bool IsAccessibleCore(INamedTypeSymbol type, IAssemblySymbol currentAssembly)
+ {
+ switch (type.DeclaredAccessibility)
+ {
+ case Accessibility.Public:
+ return true;
+ case Accessibility.Internal:
+ case Accessibility.ProtectedOrInternal:
+ return type.ContainingAssembly.GivesAccessTo(currentAssembly);
+ default:
+ return false;
+ }
+ }
+
///
/// Final source-emission step for the generator. Receives the collected set of discovered
/// workflow descriptors and adds the generated registry/registration source to the compilation.
diff --git a/src/Dapr.Workflow.Versioning.Runtime/Dapr.Workflow.Versioning.Runtime.csproj b/src/Dapr.Workflow.Versioning.Runtime/Dapr.Workflow.Versioning.Runtime.csproj
index 4807ee2d0..97f6bda9c 100644
--- a/src/Dapr.Workflow.Versioning.Runtime/Dapr.Workflow.Versioning.Runtime.csproj
+++ b/src/Dapr.Workflow.Versioning.Runtime/Dapr.Workflow.Versioning.Runtime.csproj
@@ -14,6 +14,7 @@
+
diff --git a/test/Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows/CrossAssemblyWorkflows.cs b/test/Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows/CrossAssemblyWorkflows.cs
new file mode 100644
index 000000000..8fab2989c
--- /dev/null
+++ b/test/Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows/CrossAssemblyWorkflows.cs
@@ -0,0 +1,40 @@
+// ------------------------------------------------------------------------
+// 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 Dapr.Workflow;
+using Dapr.Workflow.Versioning;
+
+namespace Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows;
+
+public static class CrossAssemblyWorkflowConstants
+{
+ public const string CanonicalName = "CrossAppWorkflow";
+}
+
+[WorkflowVersion(CanonicalName = CrossAssemblyWorkflowConstants.CanonicalName, Version = "1")]
+public sealed class CrossAppWorkflowV1 : Workflow
+{
+ public override Task RunAsync(WorkflowContext context, string input)
+ {
+ return Task.FromResult($"v1:{input}");
+ }
+}
+
+[WorkflowVersion(CanonicalName = CrossAssemblyWorkflowConstants.CanonicalName, Version = "2")]
+public sealed class CrossAppWorkflowV2 : Workflow
+{
+ public override Task RunAsync(WorkflowContext context, string input)
+ {
+ return Task.FromResult($"v2:{input}");
+ }
+}
diff --git a/test/Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows/Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows.csproj b/test/Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows/Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows.csproj
new file mode 100644
index 000000000..be5c325db
--- /dev/null
+++ b/test/Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows/Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows.csproj
@@ -0,0 +1,14 @@
+
+
+
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/test/Dapr.IntegrationTest.Workflow.Versioning/CrossAssemblyScanIntegrationTests.cs b/test/Dapr.IntegrationTest.Workflow.Versioning/CrossAssemblyScanIntegrationTests.cs
new file mode 100644
index 000000000..0e731ad98
--- /dev/null
+++ b/test/Dapr.IntegrationTest.Workflow.Versioning/CrossAssemblyScanIntegrationTests.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 Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows;
+using Dapr.Workflow.Versioning;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dapr.IntegrationTest.Workflow.Versioning;
+
+public sealed class CrossAssemblyScanIntegrationTests
+{
+ [Fact]
+ public void ShouldDiscoverReferencedWorkflowsWhenEnabled()
+ {
+ var services = new ServiceCollection();
+ services.AddDaprWorkflowVersioning();
+
+ using var provider = services.BuildServiceProvider();
+ var registry = GeneratedWorkflowVersionRegistry.GetWorkflowVersionRegistry(provider);
+
+ Assert.True(registry.TryGetValue(CrossAssemblyWorkflowConstants.CanonicalName, out var versions));
+ Assert.NotNull(versions);
+ Assert.Contains(versions, v => v.EndsWith("CrossAppWorkflowV1", StringComparison.Ordinal));
+ Assert.Contains(versions, v => v.EndsWith("CrossAppWorkflowV2", StringComparison.Ordinal));
+
+ var latest = NormalizeWorkflowTypeName(versions![0]);
+ Assert.Equal("CrossAppWorkflowV2", latest);
+ }
+
+ private static string NormalizeWorkflowTypeName(string typeName)
+ {
+ var trimmed = typeName;
+ if (trimmed.StartsWith("global::", StringComparison.Ordinal))
+ {
+ trimmed = trimmed["global::".Length..];
+ }
+
+ var lastDot = trimmed.LastIndexOf('.');
+ return lastDot >= 0 ? trimmed[(lastDot + 1)..] : trimmed;
+ }
+}
diff --git a/test/Dapr.IntegrationTest.Workflow.Versioning/Dapr.IntegrationTest.Workflow.Versioning.csproj b/test/Dapr.IntegrationTest.Workflow.Versioning/Dapr.IntegrationTest.Workflow.Versioning.csproj
index 8abb476c4..9d0a2c997 100644
--- a/test/Dapr.IntegrationTest.Workflow.Versioning/Dapr.IntegrationTest.Workflow.Versioning.csproj
+++ b/test/Dapr.IntegrationTest.Workflow.Versioning/Dapr.IntegrationTest.Workflow.Versioning.csproj
@@ -4,8 +4,13 @@
enable
enable
false
+ true
+
+
+
+
@@ -25,6 +30,7 @@
+
-
\ No newline at end of file
+