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
15 changes: 15 additions & 0 deletions all.sln
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Store.Next.Example07.Tests"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Store.Next.Example07.AppHost", "examples\Actor.Next\07-PerTypeOptions\Store.Next.Example07.AppHost\Store.Next.Example07.AppHost.csproj", "{B620B816-53AB-4DB1-B038-CCA35234AB0C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Versioning.Generators.Test", "test\Dapr.Workflow.Versioning.Generators.Test\Dapr.Workflow.Versioning.Generators.Test.csproj", "{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -2408,6 +2410,18 @@ Global
{B620B816-53AB-4DB1-B038-CCA35234AB0C}.Release|x64.Build.0 = Release|Any CPU
{B620B816-53AB-4DB1-B038-CCA35234AB0C}.Release|x86.ActiveCfg = Release|Any CPU
{B620B816-53AB-4DB1-B038-CCA35234AB0C}.Release|x86.Build.0 = Release|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Debug|x64.ActiveCfg = Debug|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Debug|x64.Build.0 = Debug|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Debug|x86.ActiveCfg = Debug|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Debug|x86.Build.0 = Debug|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Release|Any CPU.Build.0 = Release|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Release|x64.ActiveCfg = Release|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Release|x64.Build.0 = Release|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Release|x86.ActiveCfg = Release|Any CPU
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -2606,6 +2620,7 @@ Global
{9D3FA169-444F-4B8A-921E-17962829E457} = {451E5A30-5AC1-E437-EC03-1F862D847C4D}
{9C989F53-0991-4E47-A4D3-6E1EF76537D6} = {451E5A30-5AC1-E437-EC03-1F862D847C4D}
{B620B816-53AB-4DB1-B038-CCA35234AB0C} = {451E5A30-5AC1-E437-EC03-1F862D847C4D}
{FCDA0406-5FD9-4ACB-A3B1-C2685B034809} = {DD020B34-460F-455F-8D17-CF4A949F100B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
Diagnostic: (string?)$"Rejected '{symbolName}': does not inherit from Workflow<,> (base type: {baseTypeInfo})");
}

// Abstract or open-generic types cannot be instantiated; skip them.
if (symbol.IsAbstract || symbol.TypeParameters.Length > 0)
{
return (Workflow: (DiscoveredWorkflow?)null,
Diagnostic: (string?)$"Rejected '{symbolName}': abstract or open-generic workflow types are not registered.");
}

// Look for [WorkflowVersion] by symbol identity
AttributeData? attrData = null;
if (ks.WorkflowVersionAttribute is not null)
Expand Down Expand Up @@ -410,6 +417,10 @@ private static IEnumerable<DiscoveredWorkflow> DiscoverReferencedWorkflows(
if (!InheritsFromWorkflow(type, knownSymbols.WorkflowBase))
continue;

// Skip abstract types and open generics — they can't be instantiated.
if (type.IsAbstract || type.TypeParameters.Length > 0)
continue;

AttributeData? attrData = null;
if (knownSymbols.WorkflowVersionAttribute is not null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// ------------------------------------------------------------------------
// 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.
// ------------------------------------------------------------------------

// Regression coverage for https://github.com/dapr/dotnet-sdk/issues/1898
//
// An abstract *closed* (non-generic) workflow isolates the IsAbstract filter independent of
// the open-generic filter. The source generator must skip it, so neither its type name nor
// its declared canonical name may appear in the generated versioning registry.
//
// Gated to C# 16 / .NET 14 (which ships with the Dapr SDK 1.19 release) — this scenario is
// not exercised on prior target frameworks. The Dapr SDK 1.19 release will add .NET 14
// support; until that merge, this test is dormant (the type and test are compiled out).

#if NET14_0_OR_GREATER
using Dapr.Testcontainers.Xunit.Attributes;
using Dapr.Workflow;
using Dapr.Workflow.Versioning;
using Microsoft.Extensions.DependencyInjection;

namespace Dapr.IntegrationTest.Workflow.Versioning;

internal static class AbstractClosedWorkflowConstants
{
public const string CanonicalName = "AbstractClosedWorkflow";
}

[WorkflowVersion(CanonicalName = AbstractClosedWorkflowConstants.CanonicalName, Version = "1")]
internal abstract class AbstractClosedWorkflow : Workflow<string, string>
{
}

public sealed class AbstractClosedWorkflowRegressionTests
{
/// <summary>
/// The abstract closed (non-generic) workflow must be excluded from the generated
/// versioning registry: its declared canonical name must not be a registry key, and its
/// type name must not appear in any registered workflow list.
/// </summary>
[MinimumDaprRuntimeFact("1.19")]
public void AbstractClosedWorkflow_IsExcludedFromVersioningRegistry()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddDaprWorkflowVersioning();
services.AddDaprWorkflowBuilder(configureRuntime: _ => { });

using var provider = services.BuildServiceProvider();
var registry = GeneratedWorkflowVersionRegistry.GetWorkflowVersionRegistry(provider);

Assert.False(registry.ContainsKey(AbstractClosedWorkflowConstants.CanonicalName),
"The abstract closed workflow's canonical name must not appear as a registry key.");
Assert.DoesNotContain(
registry.SelectMany(kv => kv.Value),
v => v.Contains(nameof(AbstractClosedWorkflow), StringComparison.Ordinal));
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<!-- The generator under test (normal reference so the test can instantiate it). -->
<ProjectReference Include="..\..\src\Dapr.Workflow.Versioning.Generators\Dapr.Workflow.Versioning.Generators.csproj" />
<!-- Abstractions (so the generator resolves Workflow`2/WorkflowActivity`2 and the test source compiles). -->
<ProjectReference Include="..\..\src\Dapr.Workflow.Abstractions\Dapr.Workflow.Abstractions.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,100 @@
// ------------------------------------------------------------------------
// 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.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace Dapr.Workflow.Versioning.Generators.Test;

/// <summary>
/// In-process harness that runs <see cref="WorkflowSourceGenerator"/> over a snippet of
/// user source and returns the generated registry source plus generator diagnostics.
/// </summary>
internal static class GeneratorTestHarness
{
public const string RegistryGeneratedFileName = "Dapr_Workflow_Versioning.g.cs";

private static MetadataReference AbstractionsReference()
{
var path = Path.Combine(AppContext.BaseDirectory, "Dapr.Workflow.Abstractions.dll");
return MetadataReference.CreateFromFile(path);
}

private static MetadataReference VersioningAbstractionsReference()
{
var path = Path.Combine(AppContext.BaseDirectory, "Dapr.Workflow.Versioning.Abstractions.dll");
return MetadataReference.CreateFromFile(path);
}

/// <summary>
/// Compiles <paramref name="userSource"/> and runs the source generator, returning the
/// generated registry source (or an empty string when the generator emits nothing) and
/// any diagnostics the generator reports.
/// </summary>
public static Task<(string GeneratedSource, Diagnostic[] Diagnostics)> RunAsync(string userSource)
{
// Build a complete reference set from the assemblies already loaded into the test host
// (System.Runtime, System.Text.Json, Microsoft.Extensions.*, etc.) plus the abstractions
// assemblies under test. This avoids the fragility of hand-picking individual framework refs.
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
.Select(MetadataReference? (a) => MetadataReference.CreateFromFile(a.Location))
.Where(r => r is not null)
.Cast<MetadataReference>()
.ToList();
references.Add(AbstractionsReference());
references.Add(VersioningAbstractionsReference());

var compilation = CSharpCompilation.Create(
assemblyName: "TestAssembly",
syntaxTrees: new[] { CSharpSyntaxTree.ParseText(userSource) },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable));

var generator = new WorkflowSourceGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGenerators(compilation);
var runResult = driver.GetRunResult();

var generated = string.Empty;
foreach (var tree in runResult.GeneratedTrees.Where(tree => tree.FilePath.EndsWith(RegistryGeneratedFileName, StringComparison.Ordinal)))
{
generated = tree.ToString();
}

return Task.FromResult((generated, runResult.Diagnostics.ToArray()));
}

/// <summary>Asserts that no error-severity diagnostics were reported by the generator.</summary>
public static void AssertNoErrorDiagnostics(Diagnostic[] diagnostics)
{
var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
Assert.True(errors.Count == 0,
$"Expected no error diagnostics, but got: {string.Join("; ", errors.Select(d => d.ToString()))}");
}

/// <summary>
/// Re-parses the generated source and asserts it is syntactically valid C#. This catches
/// generator defects such as an orphan <c>else</c> branch (the root cause of issue #1898).
/// </summary>
public static void AssertNoSyntaxErrors(string source)
{
if (string.IsNullOrEmpty(source))
return;

var tree = CSharpSyntaxTree.ParseText(source);
var errors = tree.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
Assert.True(errors.Count == 0,
$"Expected no syntax errors in generated source, but got: {string.Join("; ", errors.Select(d => d.ToString()))}");
}
}
Loading
Loading