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 @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
21 changes: 21 additions & 0 deletions examples/Workflow/CrossAppVersioning/CrossAppVersioning.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DaprWorkflowVersioningScanReferences>true</DaprWorkflowVersioningScanReferences>
</PropertyGroup>

<ItemGroup>
<CompilerVisibleProperty Include="DaprWorkflowVersioningScanReferences" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\WorkflowVersioning\WorkflowVersioning.csproj" />
<ProjectReference Include="..\..\..\src\Dapr.Workflow.Versioning.Generators\Dapr.Workflow.Versioning.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions examples/Workflow/CrossAppVersioning/Program.cs
Original file line number Diff line number Diff line change
@@ -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();
123 changes: 122 additions & 1 deletion src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/// <inheritdoc />
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(
Expand Down Expand Up @@ -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<DiscoveredWorkflow?>.Empty;

if (ks.WorkflowBase is null)
return ImmutableArray<DiscoveredWorkflow?>.Empty;

var list = new List<DiscoveredWorkflow?>();
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<DiscoveredWorkflow?>(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();

Expand Down Expand Up @@ -282,6 +322,87 @@ private static DiscoveredWorkflow BuildDiscoveredWorkflow(
);
}

private static IEnumerable<DiscoveredWorkflow> 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<INamedTypeSymbol> 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<INamedTypeSymbol> 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;
}
}

/// <summary>
/// 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<ItemGroup>
<ProjectReference Include="..\Dapr.Workflow.Versioning.Abstractions\Dapr.Workflow.Versioning.Abstractions.csproj" />
<ProjectReference Include="..\Dapr.Workflow\Dapr.Workflow.csproj" />
<ProjectReference Include="..\Dapr.Workflow\Dapr.Workflow.Abstractions.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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<string, string>
{
public override Task<string> RunAsync(WorkflowContext context, string input)
{
return Task.FromResult($"v1:{input}");
}
}

[WorkflowVersion(CanonicalName = CrossAssemblyWorkflowConstants.CanonicalName, Version = "2")]
public sealed class CrossAppWorkflowV2 : Workflow<string, string>
{
public override Task<string> RunAsync(WorkflowContext context, string input)
{
return Task.FromResult($"v2:{input}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Dapr.Workflow\Dapr.Workflow.csproj" />
<ProjectReference Include="..\..\src\Dapr.Workflow.Versioning.Abstractions\Dapr.Workflow.Versioning.Abstractions.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<DaprWorkflowVersioningScanReferences>true</DaprWorkflowVersioningScanReferences>
</PropertyGroup>

<ItemGroup>
<CompilerVisibleProperty Include="DaprWorkflowVersioningScanReferences" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
Expand All @@ -25,6 +30,7 @@
<ProjectReference Include="..\..\src\Dapr.Workflow.Versioning.Generators\Dapr.Workflow.Versioning.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\..\src\Dapr.Workflow.Versioning.Runtime\Dapr.Workflow.Versioning.Runtime.csproj" />
<ProjectReference Include="..\..\src\Dapr.Workflow\Dapr.Workflow.csproj" />
<ProjectReference Include="..\Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows\Dapr.IntegrationTest.Workflow.Versioning.ReferenceWorkflows.csproj" />
</ItemGroup>

</Project>
</Project>
Loading