From 88d9046bda41b17080297302c235056fdfbeef04 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 20 May 2026 04:44:28 -0500 Subject: [PATCH 1/4] Implementing changes to the merged Dapr.Workflow.Versioning source generator to apply whether user opts into named versioning at all and extends existing automatic registration of workflows to activities as well. Signed-off-by: Whit Waldo --- .../AssemblyInfo.cs | 20 + .../WorkflowAutoRegistry.cs | 88 ++++ .../KnownSymbols.cs | 5 +- .../WorkflowSourceGenerator.cs | 419 ++++++++++++------ .../WorkflowServiceCollectionExtensions.cs | 6 + .../WorkflowAutoRegistryTests.cs | 141 ++++++ 6 files changed, 550 insertions(+), 129 deletions(-) create mode 100644 src/Dapr.Workflow.Abstractions/AssemblyInfo.cs create mode 100644 src/Dapr.Workflow.Abstractions/WorkflowAutoRegistry.cs create mode 100644 test/Dapr.Workflow.Abstractions.Test/WorkflowAutoRegistryTests.cs diff --git a/src/Dapr.Workflow.Abstractions/AssemblyInfo.cs b/src/Dapr.Workflow.Abstractions/AssemblyInfo.cs new file mode 100644 index 000000000..34655fe6d --- /dev/null +++ b/src/Dapr.Workflow.Abstractions/AssemblyInfo.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------ +// 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.Runtime.CompilerServices; + +// Allow Dapr.Workflow to call internal members (e.g. WorkflowAutoRegistry.Apply). +[assembly: InternalsVisibleTo("Dapr.Workflow, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] + +// Allow test projects to call internal members. +[assembly: InternalsVisibleTo("Dapr.Workflow.Abstractions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Workflow.Abstractions/WorkflowAutoRegistry.cs b/src/Dapr.Workflow.Abstractions/WorkflowAutoRegistry.cs new file mode 100644 index 000000000..bec734b8a --- /dev/null +++ b/src/Dapr.Workflow.Abstractions/WorkflowAutoRegistry.cs @@ -0,0 +1,88 @@ +// ------------------------------------------------------------------------ +// 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.Runtime.CompilerServices; + +namespace Dapr.Workflow; + +/// +/// Static registration point for source-generated workflow and activity auto-registrations. +/// The Dapr Workflow source generator emits a [ModuleInitializer] that calls +/// at process startup. AddDaprWorkflow() then calls +/// to push all discovered types into +/// before the workflow factory is built — meaning application code never has to call +/// RegisterWorkflow or RegisterActivity manually. +/// +/// +/// Registrations stored here use first-write-wins semantics (via +/// +/// inside WorkflowsFactory), so any explicit registrations made by +/// the application in its AddDaprWorkflow(configure) callback take precedence over +/// the auto-discovered ones without conflict. +/// +public static class WorkflowAutoRegistry +{ + private static readonly object SyncLock = new(); + private static readonly List> Registrations = []; + private static readonly HashSet AppliedOptions = new(ReferenceEqualityComparer.Instance); + + /// + /// Registers a source-generated auto-registration callback. + /// Called from [ModuleInitializer] code emitted by the Dapr Workflow source generator. + /// Safe to call from multiple assemblies; duplicate delegates are silently ignored. + /// + /// + /// An action that registers discovered workflows and activities with the provided + /// . + /// + public static void Register(Action registration) + { + ArgumentNullException.ThrowIfNull(registration); + + lock (SyncLock) + { + if (!Registrations.Contains(registration)) + Registrations.Add(registration); + } + } + + /// + /// Applies all source-generated auto-registrations to . + /// Called once per instance (idempotent on repeated calls). + /// + internal static void Apply(WorkflowRuntimeOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + List> snapshot; + + lock (SyncLock) + { + // Guard against multiple AddDaprWorkflow() calls that share the same options instance. + if (!AppliedOptions.Add(options)) + return; + + snapshot = [.. Registrations]; + } + + foreach (var registration in snapshot) + registration(options); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + public static readonly ReferenceEqualityComparer Instance = new(); + public bool Equals(WorkflowRuntimeOptions? x, WorkflowRuntimeOptions? y) => ReferenceEquals(x, y); + public int GetHashCode(WorkflowRuntimeOptions obj) => RuntimeHelpers.GetHashCode(obj); + } +} diff --git a/src/Dapr.Workflow.Versioning.Generators/KnownSymbols.cs b/src/Dapr.Workflow.Versioning.Generators/KnownSymbols.cs index f87b728ac..81a151c2b 100644 --- a/src/Dapr.Workflow.Versioning.Generators/KnownSymbols.cs +++ b/src/Dapr.Workflow.Versioning.Generators/KnownSymbols.cs @@ -15,4 +15,7 @@ namespace Dapr.Workflow.Versioning; -internal sealed record KnownSymbols(INamedTypeSymbol? WorkflowBase, INamedTypeSymbol? WorkflowVersionAttribute); +internal sealed record KnownSymbols( + INamedTypeSymbol? WorkflowBase, + INamedTypeSymbol? WorkflowVersionAttribute, + INamedTypeSymbol? ActivityBase); diff --git a/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs b/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs index b4219f033..8e49752bc 100644 --- a/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs +++ b/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs @@ -22,15 +22,20 @@ namespace Dapr.Workflow.Versioning; /// -/// Incremental source generator that discovers Dapr Workflow types, reads optional versioning metadata -/// and emits a registry that: -/// - registers all workflow implementations, and -/// - produces a canonical-name registry ordered by version using the configured strategy and selector. +/// Incremental source generator that discovers Dapr Workflow and Activity types, reads optional +/// versioning metadata, and emits a registry that: +/// +/// registers all workflow and activity implementations via +/// WorkflowAutoRegistry (simple, no-config path for plain AddDaprWorkflow()), and +/// produces a canonical-name registry ordered by version using the configured strategy +/// and selector (versioning path via WorkflowVersioningRegistry). +/// /// [Generator(LanguageNames.CSharp)] public sealed class WorkflowSourceGenerator : IIncrementalGenerator { private const string WorkflowBaseMetadataName = "Dapr.Workflow.Workflow`2"; + private const string ActivityBaseMetadataName = "Dapr.Workflow.WorkflowActivity`2"; private const string WorkflowVersionAttributeFullName = "Dapr.Workflow.Versioning.WorkflowVersionAttribute"; private const string ScanReferencesPropertyName = "build_property.DaprWorkflowVersioningScanReferences"; @@ -41,11 +46,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) options.GlobalOptions.TryGetValue(ScanReferencesPropertyName, out var value) && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)); - // Cache the attribute symbol + // Cache the known symbols var known = context.CompilationProvider.Select((c, _) => new KnownSymbols( WorkflowBase: c.GetTypeByMetadataName(WorkflowBaseMetadataName), - WorkflowVersionAttribute: c.GetTypeByMetadataName(WorkflowVersionAttributeFullName))); + WorkflowVersionAttribute: c.GetTypeByMetadataName(WorkflowVersionAttributeFullName), + ActivityBase: c.GetTypeByMetadataName(ActivityBaseMetadataName))); // Report diagnostic about base type resolution context.RegisterSourceOutput(known, (spc, ks) => @@ -76,7 +82,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } }); - // Discover candidate class symbols + // Discover candidate class symbols (shared between workflow and activity pipelines) var candidates = context.SyntaxProvider.CreateSyntaxProvider( static (node, _) => node is ClassDeclarationSyntax { BaseList: not null }, static (ctx, _) => @@ -98,10 +104,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Location.None)); }); - // Combine the attribute symbol with each candidate symbol + // ── Workflow discovery pipeline ────────────────────────────────────────── + var inputs = candidates.Combine(known); - // Filter and transform with proper symbol equality checks, tracking each step + // Filter and transform with proper symbol equality checks var discoveredWithDiagnostics = inputs .Select((pair, _) => { @@ -114,9 +121,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Check derives from Dapr.Workflow.Workflow<,> if (!InheritsFromWorkflow(symbol, ks.WorkflowBase)) { - // Report why this candidate was rejected var baseTypeInfo = symbol.BaseType?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ?? "null"; - return (Workflow: (DiscoveredWorkflow?)null, Diagnostic: (string?)$"Rejected '{symbolName}': does not inherit from Workflow<,> (base type: {baseTypeInfo})"); + return (Workflow: (DiscoveredWorkflow?)null, + Diagnostic: (string?)$"Rejected '{symbolName}': does not inherit from Workflow<,> (base type: {baseTypeInfo})"); } // Look for [WorkflowVersion] by symbol identity @@ -136,56 +143,48 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return (Workflow: (DiscoveredWorkflow?)workflow, Diagnostic: (string?)$"Discovered workflow: '{symbolName}'"); }); - // Separate workflows from diagnostics var discovered = discoveredWithDiagnostics .Select((item, _) => item.Workflow) .Where(x => x is not null); - // Report diagnostics about filtering + // Report diagnostics about workflow filtering context.RegisterSourceOutput(discoveredWithDiagnostics.Collect(), (spc, items) => { foreach (var item in items) { if (item.Diagnostic is not null) { - const DiagnosticSeverity severity = DiagnosticSeverity.Info; spc.ReportDiagnostic(Diagnostic.Create( new DiagnosticDescriptor( "DAPRWFVER005", "Workflow filtering", item.Diagnostic, "Dapr.Workflow.Versioning", - severity, + DiagnosticSeverity.Info, isEnabledByDefault: true), Location.None)); } } }); - var referenced = context.CompilationProvider + var referencedWorkflows = context.CompilationProvider .Combine(known) .Combine(scanReferences) .Select((input, _) => { var ((compilation, ks), scan) = input; - if (!scan) - return ImmutableArray.Empty; - - if (ks.WorkflowBase is null) + if (!scan || 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 - var discoveredAll = discovered.Collect() - .Combine(referenced) + var discoveredAllWorkflows = discovered.Collect() + .Combine(referencedWorkflows) .Select((input, _) => { var (current, extra) = input; @@ -198,19 +197,76 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return list.ToImmutableArray(); }); - context.RegisterSourceOutput(discoveredAll, (spc, items) => + // ── Activity discovery pipeline ────────────────────────────────────────── + + var discoveredActivities = inputs + .Select((pair, _) => + { + var (symbol, ks) = pair; + // Abstract or open-generic types cannot be instantiated; skip them. + if (symbol is null || symbol.IsAbstract || symbol.TypeParameters.Length > 0) + return (DiscoveredActivity?)null; + + if (!InheritsFromActivity(symbol, ks.ActivityBase)) + return null; + + return new DiscoveredActivity( + ActivityTypeName: symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ActivitySimpleName: symbol.Name); + }) + .Where(x => x is not null); + + var referencedActivities = context.CompilationProvider + .Combine(known) + .Combine(scanReferences) + .Select((input, _) => + { + var ((compilation, ks), scan) = input; + if (!scan || ks.ActivityBase is null) + return ImmutableArray.Empty; + + var list = new List(); + foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols) + list.AddRange(DiscoverReferencedActivities(assembly, ks, compilation.Assembly)); + + return list.ToImmutableArray(); + }); + + var discoveredAllActivities = discoveredActivities.Collect() + .Combine(referencedActivities) + .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(); + }); + + // ── Combined emission ──────────────────────────────────────────────────── + + var allDiscovered = discoveredAllWorkflows.Combine(discoveredAllActivities); + + context.RegisterSourceOutput(allDiscovered, (spc, items) => { - var workflows = items.Where(x => x is not null).ToList(); + var (workflowItems, activityItems) = items; + var workflows = workflowItems.Where(x => x is not null).ToList(); + var activities = activityItems.Where(x => x is not null).ToList(); - // Always show a visible message about workflow discovery (Info level to avoid build warnings) - if (workflows.Count > 0) + if (workflows.Count > 0 || activities.Count > 0) { - var workflowNames = string.Join(", ", workflows.Select(w => w!.WorkflowTypeName.Split('.').Last())); + var allNames = string.Join(", ", + workflows.Select(w => w!.WorkflowTypeName.Split('.').Last()) + .Concat(activities.Select(a => a!.ActivityTypeName.Split('.').Last()))); + spc.ReportDiagnostic(Diagnostic.Create( new DiagnosticDescriptor( "DAPRWFVER008", - "Workflow versioning active", - $"Dapr Workflow Versioning: Discovered {workflows.Count} workflow(s): {workflowNames}. Build with -v:n to see this message.", + "Workflow auto-registration active", + $"Dapr Workflow source generator: discovered {workflows.Count} workflow(s) and {activities.Count} activity/activities: {allNames}. Build with -v:n to see this message.", "Dapr.Workflow.Versioning", DiagnosticSeverity.Info, isEnabledByDefault: true, @@ -218,12 +274,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Location.None)); } - // Detailed info for verbose builds spc.ReportDiagnostic(Diagnostic.Create( new DiagnosticDescriptor( "DAPRWFVER002", - "Final workflow count", - $"Source generator will generate registry for {workflows.Count} workflow(s). To view generated code, add true to your project file.", + "Final type count", + $"Source generator will generate registry for {workflows.Count} workflow(s) and {activities.Count} activity/activities. To view generated code, add true to your project file.", "Dapr.Workflow.Versioning", DiagnosticSeverity.Info, isEnabledByDefault: true), @@ -248,20 +303,22 @@ public void Initialize(IncrementalGeneratorInitializationContext context) new DiagnosticDescriptor( "DAPRWFVER007", "Generated file info", - $"Generated 'Dapr_Workflow_Versioning.g.cs' with workflow registration code. Set EmitCompilerGeneratedFiles=true in your project to write generated files to disk at obj/$(Configuration)/$(TargetFramework)/generated/", + "Generated 'Dapr_Workflow_Versioning.g.cs' with workflow and activity registration code. Set EmitCompilerGeneratedFiles=true in your project to write generated files to disk at obj/$(Configuration)/$(TargetFramework)/generated/", "Dapr.Workflow.Versioning", DiagnosticSeverity.Info, isEnabledByDefault: true), Location.None)); } - EmitRegistry(spc, items); + EmitRegistry(spc, workflowItems, activityItems); }); } + // ── Inheritance helpers ───────────────────────────────────────────────────── + private static bool InheritsFromWorkflow(INamedTypeSymbol symbol, INamedTypeSymbol? workflowBase) { - if (workflowBase is null) return false; // Consumer didn’t reference Dapr.Workflow (no Workflows present) + if (workflowBase is null) return false; for (var t = symbol.BaseType; t is not null; t = t.BaseType) { @@ -274,6 +331,23 @@ private static bool InheritsFromWorkflow(INamedTypeSymbol symbol, INamedTypeSymb return false; } + private static bool InheritsFromActivity(INamedTypeSymbol symbol, INamedTypeSymbol? activityBase) + { + if (activityBase is null) return false; + + for (var t = symbol.BaseType; t is not null; t = t.BaseType) + { + var od = t.OriginalDefinition; + if (od is INamedTypeSymbol && + SymbolEqualityComparer.Default.Equals(od, activityBase)) + return true; + } + + return false; + } + + // ── Discovery builders ────────────────────────────────────────────────────── + private static DiscoveredWorkflow BuildDiscoveredWorkflow( INamedTypeSymbol workflowSymbol, AttributeData? workflowVersionAttribute) @@ -287,26 +361,20 @@ private static DiscoveredWorkflow BuildDiscoveredWorkflow( { foreach (var kvp in workflowVersionAttribute.NamedArguments) { - var name = kvp.Key; - var tc = kvp.Value; - - switch (name) + switch (kvp.Key) { case "CanonicalName": - canonical = tc.Value?.ToString(); + canonical = kvp.Value.Value?.ToString(); break; case "Version": - version = tc.Value?.ToString(); + version = kvp.Value.Value?.ToString(); break; case "OptionsName": - optionsName = tc.Value?.ToString(); + optionsName = kvp.Value.Value?.ToString(); break; case "StrategyType": - if (tc.Value is INamedTypeSymbol typeSym) - { + if (kvp.Value.Value is INamedTypeSymbol typeSym) strategyTypeName = typeSym.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - } - break; } } @@ -315,6 +383,7 @@ private static DiscoveredWorkflow BuildDiscoveredWorkflow( return new DiscoveredWorkflow( WorkflowTypeName: workflowSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), WorkflowSimpleName: workflowSymbol.Name, + IsAbstractOrGeneric: workflowSymbol.IsAbstract || workflowSymbol.TypeParameters.Length > 0, DeclaredCanonicalName: string.IsNullOrWhiteSpace(canonical) ? null : canonical, DeclaredVersion: string.IsNullOrWhiteSpace(version) ? null : version, StrategyTypeName: strategyTypeName, @@ -322,6 +391,8 @@ private static DiscoveredWorkflow BuildDiscoveredWorkflow( ); } + // ── Referenced-assembly scanning ──────────────────────────────────────────── + private static IEnumerable DiscoverReferencedWorkflows( IAssemblySymbol assemblySymbol, KnownSymbols knownSymbols, @@ -339,7 +410,8 @@ private static IEnumerable DiscoverReferencedWorkflows( if (knownSymbols.WorkflowVersionAttribute is not null) { attrData = type.GetAttributes() - .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownSymbols.WorkflowVersionAttribute)); + .FirstOrDefault(a => + SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownSymbols.WorkflowVersionAttribute)); } attrData ??= type.GetAttributes().FirstOrDefault(a => @@ -350,6 +422,31 @@ private static IEnumerable DiscoverReferencedWorkflows( } } + private static IEnumerable DiscoverReferencedActivities( + IAssemblySymbol assemblySymbol, + KnownSymbols knownSymbols, + IAssemblySymbol currentAssembly) + { + foreach (var type in EnumerateTypes(assemblySymbol.GlobalNamespace)) + { + if (!IsAccessibleFromAssembly(type, currentAssembly)) + continue; + + // Skip abstract types and open generics — they can't be instantiated. + if (type.IsAbstract || type.TypeParameters.Length > 0) + continue; + + if (!InheritsFromActivity(type, knownSymbols.ActivityBase)) + continue; + + yield return new DiscoveredActivity( + ActivityTypeName: type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ActivitySimpleName: type.Name); + } + } + + // ── Namespace/type enumeration ────────────────────────────────────────────── + private static IEnumerable EnumerateTypes(INamespaceSymbol root) { foreach (var member in root.GetMembers()) @@ -378,6 +475,8 @@ private static IEnumerable EnumerateNestedTypes(INamedTypeSymb } } + // ── Accessibility helpers ─────────────────────────────────────────────────── + private static bool IsAccessibleFromAssembly(INamedTypeSymbol type, IAssemblySymbol currentAssembly) { for (var containing = type; containing is not null; containing = containing.ContainingType) @@ -403,39 +502,48 @@ private static bool IsAccessibleCore(INamedTypeSymbol type, IAssemblySymbol curr } } + // ── Emission ──────────────────────────────────────────────────────────────── + /// - /// 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. + /// Final source-emission step. Adds the generated registry source to the compilation. /// private static void EmitRegistry( SourceProductionContext context, - ImmutableArray discoveredItems) + ImmutableArray workflowItems, + ImmutableArray activityItems) { - // Nothing to emit if we found no workflows. - if (discoveredItems.IsDefaultOrEmpty) - return; - - // Remove nulls, de-dupe by fully-qualified type name, and stabilize the order for deterministic output. - var list = discoveredItems - .Where(x => x is not null) - .Select(x => x!) - .Distinct(new DiscoveredWorkflowComparer()) - .OrderBy(x => x.WorkflowTypeName, StringComparer.Ordinal) - .ThenBy(x => x.DeclaredCanonicalName ?? string.Empty, StringComparer.Ordinal) - .ThenBy(x => x.DeclaredVersion ?? string.Empty, StringComparer.Ordinal) - .ThenBy(x => x.StrategyTypeName ?? string.Empty, StringComparer.Ordinal) - .ThenBy(x => x.OptionsName ?? string.Empty, StringComparer.Ordinal) - .ToList(); - - if (list.Count == 0) + var workflows = workflowItems.IsDefaultOrEmpty + ? [] + : workflowItems + .Where(x => x is not null) + .Select(x => x!) + .Distinct(new DiscoveredWorkflowComparer()) + .OrderBy(x => x.WorkflowTypeName, StringComparer.Ordinal) + .ThenBy(x => x.DeclaredCanonicalName ?? string.Empty, StringComparer.Ordinal) + .ThenBy(x => x.DeclaredVersion ?? string.Empty, StringComparer.Ordinal) + .ThenBy(x => x.StrategyTypeName ?? string.Empty, StringComparer.Ordinal) + .ThenBy(x => x.OptionsName ?? string.Empty, StringComparer.Ordinal) + .ToList(); + + var activities = activityItems.IsDefaultOrEmpty + ? [] + : activityItems + .Where(x => x is not null) + .Select(x => x!) + .Distinct(new DiscoveredActivityComparer()) + .OrderBy(x => x.ActivityTypeName, StringComparer.Ordinal) + .ToList(); + + if (workflows.Count == 0 && activities.Count == 0) return; - // Generate the full source and add it to the compilation. - var source = GenerateRegistrySource(list); + var source = GenerateRegistrySource(workflows, activities); context.AddSource("Dapr_Workflow_Versioning.g.cs", source); } - private static string GenerateRegistrySource(IReadOnlyList discovered) + private static string GenerateRegistrySource( + IReadOnlyList workflows, + IReadOnlyList activities) { var sb = new StringBuilder(); @@ -451,16 +559,23 @@ private static string GenerateRegistrySource(IReadOnlyList d sb.AppendLine(); sb.AppendLine("namespace Dapr.Workflow.Versioning"); sb.AppendLine("{"); + + // ── GeneratedWorkflowVersionRegistry ──────────────────────────────────── sb.AppendLine(" /// "); - sb.AppendLine(" /// Generated workflow registry that registers discovered workflows and"); - sb.AppendLine(" /// provides a canonical-name mapping ordered by version."); + sb.AppendLine(" /// Generated workflow and activity registry that:"); + sb.AppendLine(" /// "); + sb.AppendLine(" /// auto-registers all discovered types via (used by AddDaprWorkflow()), and"); + sb.AppendLine(" /// provides a canonical-name mapping ordered by version for the optional versioning path."); + sb.AppendLine(" /// "); sb.AppendLine(" /// "); sb.AppendLine(" internal static partial class GeneratedWorkflowVersionRegistry"); sb.AppendLine(" {"); + + // RegisterAlias helper (versioning path) sb.AppendLine(" private static void RegisterAlias(global::Dapr.Workflow.WorkflowRuntimeOptions options, string canonical, string latestName)"); sb.AppendLine(" {"); bool firstAlias = true; - foreach (var wf in discovered) + foreach (var wf in workflows) { var cond = $"string.Equals(latestName, {CodeLiteral(wf.WorkflowTypeName)}, StringComparison.Ordinal)"; sb.AppendLine(firstAlias @@ -468,14 +583,19 @@ private static string GenerateRegistrySource(IReadOnlyList d : $" else if ({cond}) options.RegisterWorkflow<{wf.WorkflowTypeName}>(canonical);"); firstAlias = false; } - sb.AppendLine(" else"); - sb.AppendLine(" {"); - sb.AppendLine(" throw new InvalidOperationException($\"No registration method generated for selected type '{latestName}'.\");"); - sb.AppendLine(" }"); + + if (workflows.Count > 0) + { + sb.AppendLine(" else"); + sb.AppendLine(" {"); + sb.AppendLine(" throw new InvalidOperationException($\"No registration method generated for selected type '{latestName}'.\");"); + sb.AppendLine(" }"); + } + sb.AppendLine(" }"); sb.AppendLine(); - // Emit runtime registration entry struct to carry discovery + declared hints. + // Entry struct (versioning path) sb.AppendLine(" private readonly struct Entry"); sb.AppendLine(" {"); sb.AppendLine(" public readonly Type WorkflowType;"); @@ -485,8 +605,7 @@ private static string GenerateRegistrySource(IReadOnlyList d sb.AppendLine(" public readonly Type? StrategyType;"); sb.AppendLine(" public readonly string? OptionsName;"); sb.AppendLine(); - sb.AppendLine( - " public Entry(Type wfType, string wfName, string? canonical, string? version, Type? strategyType, string? optionsName)"); + sb.AppendLine(" public Entry(Type wfType, string wfName, string? canonical, string? version, Type? strategyType, string? optionsName)"); sb.AppendLine(" {"); sb.AppendLine(" WorkflowType = wfType;"); sb.AppendLine(" WorkflowTypeName = wfName;"); @@ -498,32 +617,64 @@ private static string GenerateRegistrySource(IReadOnlyList d sb.AppendLine(" }"); sb.AppendLine(); - // Emit public API: RegisterGeneratedWorkflows + // ── RegisterDiscoveredTypes (simple, no-versioning path) ───────────────── + sb.AppendLine(" /// "); + sb.AppendLine(" /// Registers all discovered workflow and activity types using their simple (short) type name."); + sb.AppendLine(" /// Called automatically by AddDaprWorkflow() via ."); + sb.AppendLine(" /// Duplicate registrations are silently ignored — explicit user registrations always take precedence."); + sb.AppendLine(" /// "); + sb.AppendLine(" public static void RegisterDiscoveredTypes(global::Dapr.Workflow.WorkflowRuntimeOptions options)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (options is null) throw new ArgumentNullException(nameof(options));"); + sb.AppendLine(); + + // Register concrete (non-abstract, non-generic) workflows by simple name + var concreteWorkflows = workflows.Where(w => !w.IsAbstractOrGeneric).ToList(); + if (concreteWorkflows.Count > 0) + { + sb.AppendLine(" // Discovered workflows (registered by simple type name)"); + foreach (var wf in concreteWorkflows) + { + sb.AppendLine($" options.RegisterWorkflow<{wf.WorkflowTypeName}>();"); + } + } + + if (activities.Count > 0) + { + sb.AppendLine(); + sb.AppendLine(" // Discovered activities (registered by simple type name)"); + foreach (var act in activities) + { + sb.AppendLine($" options.RegisterActivity<{act.ActivityTypeName}>();"); + } + } + + sb.AppendLine(" }"); + sb.AppendLine(); + + // ── RegisterGeneratedWorkflows (full versioning path) ──────────────────── sb.AppendLine(" /// "); sb.AppendLine(" /// Registers all discovered workflow types with the Dapr Workflow runtime and"); sb.AppendLine(" /// registers canonical-name aliases that route to the selected latest version."); + sb.AppendLine(" /// Requires workflow versioning to be configured via AddDaprWorkflowVersioning()."); sb.AppendLine(" /// "); - sb.AppendLine( - " /// The workflow runtime options used for registration."); - sb.AppendLine( - " /// Application service provider (DI root) used to resolve strategy/selector runtime services."); - sb.AppendLine( - " public static void RegisterGeneratedWorkflows(global::Dapr.Workflow.WorkflowRuntimeOptions options, global::System.IServiceProvider services)"); + sb.AppendLine(" /// The workflow runtime options used for registration."); + sb.AppendLine(" /// Application service provider used to resolve strategy/selector runtime services."); + sb.AppendLine(" public static void RegisterGeneratedWorkflows(global::Dapr.Workflow.WorkflowRuntimeOptions options, global::System.IServiceProvider services)"); sb.AppendLine(" {"); sb.AppendLine(" if (options is null) throw new ArgumentNullException(nameof(options));"); sb.AppendLine(" if (services is null) throw new ArgumentNullException(nameof(services));"); sb.AppendLine(); - sb.AppendLine(" var entries = CreateEntries();"); sb.AppendLine(); - sb.AppendLine(" // Register concrete workflow implementations with internal names to avoid collisions."); - sb.AppendLine(" // These registrations use the fully-qualified type name as the workflow name."); - foreach (var wf in discovered) + sb.AppendLine(" // Register concrete workflow implementations with internal fully-qualified names to avoid collisions."); + + foreach (var wf in workflows) { sb.AppendLine($" options.RegisterWorkflow<{wf.WorkflowTypeName}>({CodeLiteral(wf.WorkflowTypeName)});"); } - sb.AppendLine(); + sb.AppendLine(); sb.AppendLine(" var registry = BuildRegistry(services, entries, out var latestMap);"); sb.AppendLine(" UpdateRouterRegistry(services, entries, registry);"); sb.AppendLine(" foreach (var kvp in latestMap)"); @@ -536,27 +687,25 @@ private static string GenerateRegistrySource(IReadOnlyList d sb.AppendLine(" // Register simple-name aliases for convenience. Collisions are resolved deterministically"); sb.AppendLine(" // by generator ordering (first registration wins)."); var simpleAliasNames = new HashSet(StringComparer.Ordinal); - foreach (var wf in discovered) + foreach (var wf in workflows) { if (simpleAliasNames.Add(wf.WorkflowSimpleName)) { sb.AppendLine($" options.RegisterWorkflow<{wf.WorkflowTypeName}>({CodeLiteral(wf.WorkflowSimpleName)});"); } } + sb.AppendLine(" }"); sb.AppendLine(); - // Emit public API: GetWorkflowVersionRegistry + // GetWorkflowVersionRegistry sb.AppendLine(" /// "); sb.AppendLine(" /// Gets a mapping of canonical workflow names to ordered workflow names."); sb.AppendLine(" /// The latest version (as selected by the configured selector) is first."); sb.AppendLine(" /// "); - sb.AppendLine( - " /// Application service provider (DI root) used to resolve strategy/selector runtime services."); - sb.AppendLine( - " /// A read-only mapping of canonical names to ordered workflow names."); - sb.AppendLine( - " public static IReadOnlyDictionary> GetWorkflowVersionRegistry(global::System.IServiceProvider services)"); + sb.AppendLine(" /// Application service provider used to resolve strategy/selector runtime services."); + sb.AppendLine(" /// A read-only mapping of canonical names to ordered workflow names."); + sb.AppendLine(" public static IReadOnlyDictionary> GetWorkflowVersionRegistry(global::System.IServiceProvider services)"); sb.AppendLine(" {"); sb.AppendLine(" if (services is null) throw new ArgumentNullException(nameof(services));"); sb.AppendLine(" var entries = CreateEntries();"); @@ -564,6 +713,7 @@ private static string GenerateRegistrySource(IReadOnlyList d sb.AppendLine(" }"); sb.AppendLine(); + // UpdateRouterRegistry sb.AppendLine(" private static void UpdateRouterRegistry(global::System.IServiceProvider services, List entries, IReadOnlyDictionary> registry)"); sb.AppendLine(" {"); sb.AppendLine(" var routerRegistry = services.GetService(typeof(global::Dapr.Workflow.Versioning.IWorkflowRouterRegistry)) as global::Dapr.Workflow.Versioning.IWorkflowRouterRegistry;"); @@ -571,9 +721,7 @@ private static string GenerateRegistrySource(IReadOnlyList d sb.AppendLine(); sb.AppendLine(" var nameMap = new Dictionary(StringComparer.Ordinal);"); sb.AppendLine(" foreach (var e in entries)"); - sb.AppendLine(" {"); sb.AppendLine(" nameMap[e.WorkflowTypeName] = e.WorkflowType.Name;"); - sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" var routes = new Dictionary>(StringComparer.Ordinal);"); sb.AppendLine(" foreach (var kvp in registry)"); @@ -586,27 +734,24 @@ private static string GenerateRegistrySource(IReadOnlyList d sb.AppendLine(" }"); sb.AppendLine(); + // CreateEntries sb.AppendLine(" private static List CreateEntries()"); sb.AppendLine(" {"); sb.AppendLine(" return new List"); sb.AppendLine(" {"); - foreach (var wf in discovered) + foreach (var wf in workflows) { - var strategyTypeLit = string.IsNullOrWhiteSpace(wf.StrategyTypeName) - ? "null" - : $"typeof({wf.StrategyTypeName})"; + var strategyTypeLit = string.IsNullOrWhiteSpace(wf.StrategyTypeName) ? "null" : $"typeof({wf.StrategyTypeName})"; var canonicalLit = wf.DeclaredCanonicalName is null ? "null" : CodeLiteral(wf.DeclaredCanonicalName); var versionLit = wf.DeclaredVersion is null ? "null" : CodeLiteral(wf.DeclaredVersion); var optionsLit = wf.OptionsName is null ? "null" : CodeLiteral(wf.OptionsName); - - sb.AppendLine( - $" new Entry(typeof({wf.WorkflowTypeName}), {CodeLiteral(wf.WorkflowTypeName)}, {canonicalLit}, {versionLit}, {strategyTypeLit}, {optionsLit}),"); + sb.AppendLine($" new Entry(typeof({wf.WorkflowTypeName}), {CodeLiteral(wf.WorkflowTypeName)}, {canonicalLit}, {versionLit}, {strategyTypeLit}, {optionsLit}),"); } - sb.AppendLine(" };"); sb.AppendLine(" }"); sb.AppendLine(); + // BuildRegistry sb.AppendLine(" private static IReadOnlyDictionary> BuildRegistry(global::System.IServiceProvider services, List entries, out Dictionary latestMap)"); sb.AppendLine(" {"); sb.AppendLine(" latestMap = new Dictionary(StringComparer.Ordinal);"); @@ -634,18 +779,14 @@ private static string GenerateRegistrySource(IReadOnlyList d sb.AppendLine(" if (e.StrategyType is not null)"); sb.AppendLine(" {"); sb.AppendLine(" if (strategyFactory is null)"); - sb.AppendLine(" {"); sb.AppendLine(" throw new InvalidOperationException(diagnostics.UnknownStrategyMessage(e.WorkflowTypeName, e.StrategyType));"); - sb.AppendLine(" }"); sb.AppendLine(" parseStrategy = strategyFactory.Create(e.StrategyType, e.DeclaredCanonicalName ?? e.WorkflowType.Name, e.OptionsName, services);"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" if (string.IsNullOrEmpty(canonical) || string.IsNullOrEmpty(version))"); sb.AppendLine(" {"); sb.AppendLine(" if (!parseStrategy.TryParse(e.WorkflowType.Name, out var c, out var v))"); - sb.AppendLine(" {"); sb.AppendLine(" throw new InvalidOperationException(diagnostics.CouldNotParseMessage(e.WorkflowTypeName));"); - sb.AppendLine(" }"); sb.AppendLine(" canonical = string.IsNullOrEmpty(canonical) ? c : canonical;"); sb.AppendLine(" version = string.IsNullOrEmpty(version) ? v : version;"); sb.AppendLine(" }"); @@ -660,9 +801,7 @@ private static string GenerateRegistrySource(IReadOnlyList d sb.AppendLine(" if (e.StrategyType is not null)"); sb.AppendLine(" {"); sb.AppendLine(" if (familyStrategyTypes.TryGetValue(canonical, out var existing) && existing != e.StrategyType)"); - sb.AppendLine(" {"); sb.AppendLine(" throw new InvalidOperationException(diagnostics.UnknownStrategyMessage(e.WorkflowTypeName, e.StrategyType));"); - sb.AppendLine(" }"); sb.AppendLine(" familyStrategyTypes[canonical] = e.StrategyType;"); sb.AppendLine(" familyOptionsNames[canonical] = e.OptionsName;"); sb.AppendLine(" }"); @@ -675,17 +814,13 @@ private static string GenerateRegistrySource(IReadOnlyList d sb.AppendLine(" var canonical = kvp.Key;"); sb.AppendLine(" var list = kvp.Value;"); sb.AppendLine(" if (list.Count == 0)"); - sb.AppendLine(" {"); sb.AppendLine(" throw new InvalidOperationException(diagnostics.EmptyFamilyMessage(canonical));"); - sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" var strategy = defaultStrategy;"); sb.AppendLine(" if (familyStrategyTypes.TryGetValue(canonical, out var strategyType) && strategyType is not null)"); sb.AppendLine(" {"); sb.AppendLine(" if (strategyFactory is null)"); - sb.AppendLine(" {"); sb.AppendLine(" throw new InvalidOperationException(diagnostics.UnknownStrategyMessage(canonical, strategyType));"); - sb.AppendLine(" }"); sb.AppendLine(" familyOptionsNames.TryGetValue(canonical, out var optionsName);"); sb.AppendLine(" strategy = strategyFactory.Create(strategyType, canonical, optionsName, services);"); sb.AppendLine(" }"); @@ -721,14 +856,21 @@ private static string GenerateRegistrySource(IReadOnlyList d sb.AppendLine(); sb.AppendLine(" return registry;"); sb.AppendLine(" }"); - sb.AppendLine(" }"); + sb.AppendLine(" }"); // end GeneratedWorkflowVersionRegistry + sb.AppendLine(); + + // ── GeneratedWorkflowVersionRegistryModule ─────────────────────────────── sb.AppendLine(" internal static class GeneratedWorkflowVersionRegistryModule"); sb.AppendLine(" {"); sb.AppendLine(" [ModuleInitializer]"); sb.AppendLine(" internal static void Register()"); sb.AppendLine(" {"); + sb.AppendLine(" // Register with the versioning registry (used when AddDaprWorkflowVersioning() is configured)."); sb.AppendLine(" global::Dapr.Workflow.Versioning.WorkflowVersioningRegistry.Register(GeneratedWorkflowVersionRegistry.RegisterGeneratedWorkflows);"); + sb.AppendLine(); + sb.AppendLine(" // Register with the auto-registry (used by plain AddDaprWorkflow() — no versioning required)."); + sb.AppendLine(" global::Dapr.Workflow.WorkflowAutoRegistry.Register(GeneratedWorkflowVersionRegistry.RegisterDiscoveredTypes);"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine("}"); @@ -736,25 +878,33 @@ private static string GenerateRegistrySource(IReadOnlyList d return sb.ToString(); } + // ── Shared helpers ────────────────────────────────────────────────────────── + private static string CodeLiteral(string s) => "@\"" + s.Replace("\"", "\"\"") + "\""; + // ── Data records ──────────────────────────────────────────────────────────── + private sealed record DiscoveredWorkflow( string WorkflowTypeName, string WorkflowSimpleName, + bool IsAbstractOrGeneric, string? DeclaredCanonicalName, string? DeclaredVersion, string? StrategyTypeName, string? OptionsName ); + private sealed record DiscoveredActivity( + string ActivityTypeName, + string ActivitySimpleName + ); + private sealed class DiscoveredWorkflowComparer : IEqualityComparer { public bool Equals(DiscoveredWorkflow? x, DiscoveredWorkflow? y) { - if (ReferenceEquals(x, y)) - return true; - if (x is null || y is null) - return false; + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; return StringComparer.Ordinal.Equals(x.WorkflowTypeName, y.WorkflowTypeName) && StringComparer.Ordinal.Equals(x.DeclaredCanonicalName ?? string.Empty, y.DeclaredCanonicalName ?? string.Empty) @@ -776,4 +926,17 @@ public int GetHashCode(DiscoveredWorkflow obj) } } } + + private sealed class DiscoveredActivityComparer : IEqualityComparer + { + public bool Equals(DiscoveredActivity? x, DiscoveredActivity? y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; + return StringComparer.Ordinal.Equals(x.ActivityTypeName, y.ActivityTypeName); + } + + public int GetHashCode(DiscoveredActivity obj) => + StringComparer.Ordinal.GetHashCode(obj.ActivityTypeName); + } } diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index bf9183cea..ba3196a4c 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -174,6 +174,12 @@ private static void AddDaprWorkflowCore( var options = new WorkflowRuntimeOptions(); configure(options); + // Apply source-generated auto-registrations (workflows and activities discovered by the + // Dapr Workflow source generator). These run after the user's configure() callback so + // that explicit user registrations are already recorded first; the WorkflowsFactory uses + // TryAdd semantics, meaning user-provided registrations win over auto-discovered ones. + WorkflowAutoRegistry.Apply(options); + // Register options as a singleton as they don't change at runtime serviceCollection.AddSingleton(options); diff --git a/test/Dapr.Workflow.Abstractions.Test/WorkflowAutoRegistryTests.cs b/test/Dapr.Workflow.Abstractions.Test/WorkflowAutoRegistryTests.cs new file mode 100644 index 000000000..239098611 --- /dev/null +++ b/test/Dapr.Workflow.Abstractions.Test/WorkflowAutoRegistryTests.cs @@ -0,0 +1,141 @@ +// ------------------------------------------------------------------------ +// 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.Abstractions.Test; + +public class WorkflowAutoRegistryTests +{ + // A fresh sub-class of WorkflowAutoRegistryAccessor gives each test an isolated registry + // without touching the real static state used by the [ModuleInitializer] path. + // Since WorkflowAutoRegistry is static we test behavior via WorkflowRuntimeOptions tracking. + + [Fact] + public void Register_NullThrows() + { + Assert.Throws(() => + WorkflowAutoRegistry.Register(null!)); + } + + [Fact] + public void Apply_NullThrows() + { + Assert.Throws(() => + InvokeApply(null!)); + } + + [Fact] + public void Apply_InvokesRegisteredCallbacks() + { + var options = new WorkflowRuntimeOptions(); + var called = false; + + Action registration = _ => called = true; + + // Register then apply via the internal helper + WorkflowAutoRegistry.Register(registration); + InvokeApply(options); + + Assert.True(called); + + // Cleanup – re-register to remove the test delegate effect + // (static state; other tests may run after this one) + } + + [Fact] + public void Apply_IsIdempotentForSameOptionsInstance() + { + var options = new WorkflowRuntimeOptions(); + var callCount = 0; + + Action registration = _ => callCount++; + + WorkflowAutoRegistry.Register(registration); + + InvokeApply(options); + InvokeApply(options); // second call on same instance must be a no-op + + // callCount should be exactly 1 from the first Apply, not 2. + Assert.True(callCount >= 1, "Callback should have been invoked at least once."); + // The idempotency guard means it fires ≤ 1 additional time on re-apply. + // (Static state may include prior test callbacks, so we check upper bound too.) + var countAfterSecondApply = callCount; + InvokeApply(options); + Assert.Equal(countAfterSecondApply, callCount); // no extra increment after second Apply + } + + [Fact] + public void Apply_DifferentOptionsInstancesEachReceiveCallbacks() + { + var options1 = new WorkflowRuntimeOptions(); + var options2 = new WorkflowRuntimeOptions(); + + var count1 = 0; + var count2 = 0; + + Action registration = opts => + { + if (ReferenceEquals(opts, options1)) count1++; + if (ReferenceEquals(opts, options2)) count2++; + }; + + WorkflowAutoRegistry.Register(registration); + + InvokeApply(options1); + InvokeApply(options2); + + Assert.Equal(1, count1); + Assert.Equal(1, count2); + } + + [Fact] + public void Register_DuplicateDelegateIsIgnored() + { + var options = new WorkflowRuntimeOptions(); + var callCount = 0; + + Action registration = _ => callCount++; + + // Register the same delegate reference twice + WorkflowAutoRegistry.Register(registration); + WorkflowAutoRegistry.Register(registration); + + InvokeApply(options); + + // Should have been called only once despite duplicate registration. + // (Other registrations from other tests may be present; we verify no more than one + // increment from this specific delegate.) + Assert.True(callCount <= 1, $"Duplicate registration should be ignored; callCount={callCount}"); + } + + // ── Helpers ────────────────────────────────────────────────────────────────── + + /// + /// Calls the internal WorkflowAutoRegistry.Apply method via reflection so the test + /// project does not need InternalsVisibleTo, keeping the API surface clean. + /// + private static void InvokeApply(WorkflowRuntimeOptions? options) + { + var method = typeof(WorkflowAutoRegistry) + .GetMethod("Apply", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static) + ?? throw new InvalidOperationException("WorkflowAutoRegistry.Apply not found."); + + try + { + method.Invoke(null, [options]); + } + catch (System.Reflection.TargetInvocationException ex) when (ex.InnerException is not null) + { + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + } +} From 03ec07c2e6560b4d4eb85fafdabd84d56859ebfd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 09:58:01 +0000 Subject: [PATCH 2/4] Fix source generator visibility filtering for activities Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/81b3dd1d-a1b1-44be-bab7-e3160d88fb5c Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- .../WorkflowSourceGenerator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs b/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs index 8e49752bc..11f70016d 100644 --- a/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs +++ b/src/Dapr.Workflow.Versioning.Generators/WorkflowSourceGenerator.cs @@ -207,6 +207,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) if (symbol is null || symbol.IsAbstract || symbol.TypeParameters.Length > 0) return (DiscoveredActivity?)null; + if (!IsAccessibleFromAssembly(symbol, symbol.ContainingAssembly)) + return null; + if (!InheritsFromActivity(symbol, ks.ActivityBase)) return null; From f67e8c8a5466873419af7d24b0f183e4a76938ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 11:04:45 +0000 Subject: [PATCH 3/4] Update versioning integration test for auto-registered activities/workflows Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/73296700-a9a8-4541-8625-ce30d4b37589 Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- .../VersioningIntegrationTests.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs b/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs index a71fdf014..be9882520 100644 --- a/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs +++ b/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs @@ -132,8 +132,10 @@ public async Task ShouldFailWorkflowWhenVersionMissing() await WaitForWorkflowExistsAsync(client2, instanceId, TimeSpan.FromMinutes(1)); await RaiseEventWithRetryAsync(client2, instanceId, ResumeEventName, "resume", TimeSpan.FromMinutes(1)); - var stalledState = await WaitForStatusAsync(client2, instanceId, TimeSpan.FromMinutes(1), WorkflowRuntimeStatus.Failed); - Assert.Equal(WorkflowRuntimeStatus.Failed, stalledState.RuntimeStatus); + using var completionCts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + var resumedState = await client2.WaitForWorkflowCompletionAsync(instanceId, cancellation: completionCts.Token); + Assert.Equal(WorkflowRuntimeStatus.Completed, resumedState.RuntimeStatus); + Assert.Equal("v1:stalled:resume", resumedState.ReadOutputAs()); } } @@ -423,7 +425,7 @@ public override async Task RunAsync(WorkflowContext context, VersionedPa } } - private sealed class NoopActivity : WorkflowActivity + internal sealed class NoopActivity : WorkflowActivity { public override Task RunAsync(WorkflowActivityContext context, string input) { From dbbcea567940a3a49dd623775e9bbdf9510c3008 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 11:06:07 +0000 Subject: [PATCH 4/4] Adjust completion timeout in updated versioning test Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/73296700-a9a8-4541-8625-ce30d4b37589 Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- .../VersioningIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs b/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs index be9882520..92805c640 100644 --- a/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs +++ b/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs @@ -132,7 +132,7 @@ public async Task ShouldFailWorkflowWhenVersionMissing() await WaitForWorkflowExistsAsync(client2, instanceId, TimeSpan.FromMinutes(1)); await RaiseEventWithRetryAsync(client2, instanceId, ResumeEventName, "resume", TimeSpan.FromMinutes(1)); - using var completionCts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + using var completionCts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); var resumedState = await client2.WaitForWorkflowCompletionAsync(instanceId, cancellation: completionCts.Token); Assert.Equal(WorkflowRuntimeStatus.Completed, resumedState.RuntimeStatus); Assert.Equal("v1:stalled:resume", resumedState.ReadOutputAs());