Skip to content

Commit 3bea221

Browse files
authored
Merge branch 'main' into copilot/add-strongly-typed-events
2 parents 5f6823a + 61274c2 commit 3bea221

File tree

14 files changed

+327
-111
lines changed

14 files changed

+327
-111
lines changed

samples/ConsoleApp/ConsoleApp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<!-- Using p2p references so we can show latest changes in samples. -->
2121
<ProjectReference Include="$(SrcRoot)Client/Grpc/Client.Grpc.csproj" />
2222
<ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" />
23+
<ProjectReference Include="$(SrcRoot)Analyzers/Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
2324
</ItemGroup>
2425

2526
</Project>

samples/ConsoleAppMinimal/ConsoleAppMinimal.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<!-- Using p2p references so we can show latest changes in samples. -->
2121
<ProjectReference Include="$(SrcRoot)Client/Grpc/Client.Grpc.csproj" />
2222
<ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" />
23+
<ProjectReference Include="$(SrcRoot)Analyzers/Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
2324
</ItemGroup>
2425

2526
</Project>

samples/ExceptionPropertiesSample/ExceptionPropertiesSample.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<ProjectReference Include="$(SrcRoot)Client/AzureManaged/Client.AzureManaged.csproj" />
2323
<ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" />
2424
<ProjectReference Include="$(SrcRoot)Worker/AzureManaged/Worker.AzureManaged.csproj" />
25+
<ProjectReference Include="$(SrcRoot)Analyzers/Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
2526
</ItemGroup>
2627

2728
</Project>
28-

samples/LargePayloadConsoleApp/LargePayloadConsoleApp.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
<ProjectReference Include="$(SrcRoot)Client/AzureManaged/Client.AzureManaged.csproj" />
1818
<ProjectReference Include="$(SrcRoot)Worker/AzureManaged/Worker.AzureManaged.csproj" />
1919
<ProjectReference Include="$(SrcRoot)Extensions/AzureBlobPayloads/AzureBlobPayloads.csproj" />
20+
<ProjectReference Include="$(SrcRoot)Analyzers/Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
2021
</ItemGroup>
2122

2223
</Project>
23-
24-

samples/ScheduleConsoleApp/Orchestrators/StockPriceOrchestrator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public override async Task<string> RunAsync(TaskOrchestrationContext context, st
1818

1919
logger.LogInformation("Current price for {symbol} is ${price:F2}", symbol, currentPrice);
2020

21-
return $"Stock {symbol} price: ${currentPrice:F2} at {DateTime.UtcNow}";
21+
return $"Stock {symbol} price: ${currentPrice:F2} at {context.CurrentUtcDateTime}";
2222
}
2323
catch (Exception ex)
2424
{

samples/ScheduleConsoleApp/ScheduleConsoleApp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
<ProjectReference Include="..\..\src\Client\AzureManaged\Client.AzureManaged.csproj" />
1818
<ProjectReference Include="..\..\src\Worker\AzureManaged\Worker.AzureManaged.csproj" />
1919
<ProjectReference Include="..\..\src\ScheduledTasks\ScheduledTasks.csproj" />
20+
<ProjectReference Include="..\..\src\Analyzers\Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
2021
</ItemGroup>
2122
</Project>

samples/ScheduleWebApp/ScheduleWebApp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
<ProjectReference Include="..\..\src\Client\AzureManaged\Client.AzureManaged.csproj" />
1919
<ProjectReference Include="..\..\src\Worker\AzureManaged\Worker.AzureManaged.csproj" />
2020
<ProjectReference Include="..\..\src\ScheduledTasks\ScheduledTasks.csproj" />
21+
<ProjectReference Include="..\..\src\Analyzers\Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
2122
</ItemGroup>
2223
</Project>

src/Analyzers/Orchestration/OrchestrationAnalyzer.cs

Lines changed: 108 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@ public override void Initialize(AnalysisContext context)
2727
{
2828
KnownTypeSymbols knownSymbols = new(context.Compilation);
2929

30-
if (knownSymbols.FunctionOrchestrationAttribute == null || knownSymbols.FunctionNameAttribute == null ||
31-
knownSymbols.TaskOrchestratorInterface == null ||
32-
knownSymbols.DurableTaskRegistry == null)
30+
// Check if at least one orchestration type can be detected
31+
bool canAnalyzeDurableFunctions = knownSymbols.FunctionOrchestrationAttribute != null && knownSymbols.FunctionNameAttribute != null;
32+
bool canAnalyzeTaskOrchestrator = knownSymbols.TaskOrchestratorInterface != null && knownSymbols.TaskOrchestrationContext != null;
33+
bool canAnalyzeFuncOrchestrator = knownSymbols.DurableTaskRegistry != null;
34+
35+
if (!canAnalyzeDurableFunctions && !canAnalyzeTaskOrchestrator && !canAnalyzeFuncOrchestrator)
3336
{
34-
// symbols not available in this compilation, skip analysis
37+
// no symbols available in this compilation, skip analysis
3538
return;
3639
}
3740

@@ -42,124 +45,133 @@ public override void Initialize(AnalysisContext context)
4245
}
4346

4447
// look for Durable Functions Orchestrations
45-
context.RegisterSyntaxNodeAction(
46-
ctx =>
48+
if (canAnalyzeDurableFunctions)
4749
{
48-
ctx.CancellationToken.ThrowIfCancellationRequested();
49-
50-
if (ctx.ContainingSymbol is not IMethodSymbol methodSymbol)
50+
context.RegisterSyntaxNodeAction(
51+
ctx =>
5152
{
52-
return;
53-
}
53+
ctx.CancellationToken.ThrowIfCancellationRequested();
5454

55-
if (!methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.FunctionOrchestrationAttribute))
56-
{
57-
return;
58-
}
55+
if (ctx.ContainingSymbol is not IMethodSymbol methodSymbol)
56+
{
57+
return;
58+
}
5959

60-
if (!methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute, out string functionName))
61-
{
62-
return;
63-
}
60+
if (!methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.FunctionOrchestrationAttribute!))
61+
{
62+
return;
63+
}
64+
65+
if (!methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute!, out string functionName))
66+
{
67+
return;
68+
}
6469

65-
var rootMethodSyntax = (MethodDeclarationSyntax)ctx.Node;
70+
var rootMethodSyntax = (MethodDeclarationSyntax)ctx.Node;
6671

67-
visitor.VisitDurableFunction(ctx.SemanticModel, rootMethodSyntax, methodSymbol, functionName, ctx.ReportDiagnostic);
68-
},
69-
SyntaxKind.MethodDeclaration);
72+
visitor.VisitDurableFunction(ctx.SemanticModel, rootMethodSyntax, methodSymbol, functionName, ctx.ReportDiagnostic);
73+
},
74+
SyntaxKind.MethodDeclaration);
75+
}
7076

7177
// look for ITaskOrchestrator/TaskOrchestrator`2 Orchestrations
72-
context.RegisterSyntaxNodeAction(
73-
ctx =>
78+
if (canAnalyzeTaskOrchestrator)
7479
{
75-
ctx.CancellationToken.ThrowIfCancellationRequested();
76-
77-
if (ctx.ContainingSymbol is not INamedTypeSymbol classSymbol)
80+
context.RegisterSyntaxNodeAction(
81+
ctx =>
7882
{
79-
return;
80-
}
83+
ctx.CancellationToken.ThrowIfCancellationRequested();
8184

82-
bool implementsITaskOrchestrator = classSymbol.AllInterfaces.Any(i => i.Equals(knownSymbols.TaskOrchestratorInterface, SymbolEqualityComparer.Default));
83-
if (!implementsITaskOrchestrator)
84-
{
85-
return;
86-
}
85+
if (ctx.ContainingSymbol is not INamedTypeSymbol classSymbol)
86+
{
87+
return;
88+
}
8789

88-
IEnumerable<IMethodSymbol> orchestrationMethods = classSymbol.GetMembers().OfType<IMethodSymbol>()
89-
.Where(m => m.Parameters.Any(p => p.Type.Equals(knownSymbols.TaskOrchestrationContext, SymbolEqualityComparer.Default)));
90+
bool implementsITaskOrchestrator = classSymbol.AllInterfaces.Any(i => i.Equals(knownSymbols.TaskOrchestratorInterface, SymbolEqualityComparer.Default));
91+
if (!implementsITaskOrchestrator)
92+
{
93+
return;
94+
}
9095

91-
string functionName = classSymbol.Name;
96+
IEnumerable<IMethodSymbol> orchestrationMethods = classSymbol.GetMembers().OfType<IMethodSymbol>()
97+
.Where(m => m.Parameters.Any(p => p.Type.Equals(knownSymbols.TaskOrchestrationContext, SymbolEqualityComparer.Default)));
9298

93-
foreach (IMethodSymbol? methodSymbol in orchestrationMethods)
94-
{
95-
IEnumerable<MethodDeclarationSyntax> methodSyntaxes = methodSymbol.GetSyntaxNodes();
96-
foreach (MethodDeclarationSyntax rootMethodSyntax in methodSyntaxes)
99+
string functionName = classSymbol.Name;
100+
101+
foreach (IMethodSymbol? methodSymbol in orchestrationMethods)
97102
{
98-
visitor.VisitTaskOrchestrator(ctx.SemanticModel, rootMethodSyntax, methodSymbol, functionName, ctx.ReportDiagnostic);
103+
IEnumerable<MethodDeclarationSyntax> methodSyntaxes = methodSymbol.GetSyntaxNodes();
104+
foreach (MethodDeclarationSyntax rootMethodSyntax in methodSyntaxes)
105+
{
106+
visitor.VisitTaskOrchestrator(ctx.SemanticModel, rootMethodSyntax, methodSymbol, functionName, ctx.ReportDiagnostic);
107+
}
99108
}
100-
}
101-
},
102-
SyntaxKind.ClassDeclaration);
109+
},
110+
SyntaxKind.ClassDeclaration);
111+
}
103112

104113
// look for OrchestratorFunc Orchestrations
105-
context.RegisterOperationAction(
106-
ctx =>
114+
if (canAnalyzeFuncOrchestrator)
107115
{
108-
if (ctx.Operation is not IInvocationOperation invocation)
116+
context.RegisterOperationAction(
117+
ctx =>
109118
{
110-
return;
111-
}
119+
if (ctx.Operation is not IInvocationOperation invocation)
120+
{
121+
return;
122+
}
112123

113-
if (!SymbolEqualityComparer.Default.Equals(invocation.Type, knownSymbols.DurableTaskRegistry))
114-
{
115-
return;
116-
}
124+
if (!SymbolEqualityComparer.Default.Equals(invocation.Type, knownSymbols.DurableTaskRegistry))
125+
{
126+
return;
127+
}
117128

118-
// there are 8 AddOrchestratorFunc overloads
119-
if (invocation.TargetMethod.Name != "AddOrchestratorFunc")
120-
{
121-
return;
122-
}
129+
// there are 8 AddOrchestratorFunc overloads
130+
if (invocation.TargetMethod.Name != "AddOrchestratorFunc")
131+
{
132+
return;
133+
}
123134

124-
// all overloads have the parameter 'orchestrator', either as an Action or a Func
125-
IArgumentOperation orchestratorArgument = invocation.Arguments.First(a => a.Parameter!.Name == "orchestrator");
126-
if (orchestratorArgument.Value is not IDelegateCreationOperation delegateCreationOperation)
127-
{
128-
return;
129-
}
135+
// all overloads have the parameter 'orchestrator', either as an Action or a Func
136+
IArgumentOperation orchestratorArgument = invocation.Arguments.First(a => a.Parameter!.Name == "orchestrator");
137+
if (orchestratorArgument.Value is not IDelegateCreationOperation delegateCreationOperation)
138+
{
139+
return;
140+
}
130141

131-
// obtains the method symbol from the delegate creation operation
132-
IMethodSymbol? methodSymbol = null;
133-
SyntaxNode? methodSyntax = null;
134-
switch (delegateCreationOperation.Target)
135-
{
136-
case IAnonymousFunctionOperation lambdaOperation:
137-
// use the containing symbol of the lambda (e.g. the class declaring it) as the method symbol
138-
methodSymbol = ctx.ContainingSymbol as IMethodSymbol;
139-
methodSyntax = delegateCreationOperation.Syntax;
140-
break;
141-
case IMethodReferenceOperation methodReferenceOperation:
142-
// use the method reference as the method symbol
143-
methodSymbol = methodReferenceOperation.Method;
144-
methodSyntax = methodReferenceOperation.Method.DeclaringSyntaxReferences.First().GetSyntax();
145-
break;
146-
default:
147-
break;
148-
}
149-
150-
if (methodSymbol == null || methodSyntax == null)
151-
{
152-
return;
153-
}
142+
// obtains the method symbol from the delegate creation operation
143+
IMethodSymbol? methodSymbol = null;
144+
SyntaxNode? methodSyntax = null;
145+
switch (delegateCreationOperation.Target)
146+
{
147+
case IAnonymousFunctionOperation _:
148+
// use the containing symbol of the lambda (e.g. the class declaring it) as the method symbol
149+
methodSymbol = ctx.ContainingSymbol as IMethodSymbol;
150+
methodSyntax = delegateCreationOperation.Syntax;
151+
break;
152+
case IMethodReferenceOperation methodReferenceOperation:
153+
// use the method reference as the method symbol
154+
methodSymbol = methodReferenceOperation.Method;
155+
methodSyntax = methodReferenceOperation.Method.DeclaringSyntaxReferences.First().GetSyntax();
156+
break;
157+
default:
158+
break;
159+
}
154160

155-
// try to get the name of the orchestration from the method call, otherwise use the containing type name
156-
IArgumentOperation nameArgument = invocation.Arguments.First(a => a.Parameter!.Name == "name");
157-
Optional<object?> name = nameArgument.GetConstantValueFromAttribute(ctx.Operation.SemanticModel!, ctx.CancellationToken);
158-
string orchestrationName = name.Value?.ToString() ?? methodSymbol.Name;
161+
if (methodSymbol == null || methodSyntax == null)
162+
{
163+
return;
164+
}
159165

160-
visitor.VisitFuncOrchestrator(ctx.Operation.SemanticModel!, methodSyntax, methodSymbol, orchestrationName, ctx.ReportDiagnostic);
161-
},
162-
OperationKind.Invocation);
166+
// try to get the name of the orchestration from the method call, otherwise use the containing type name
167+
IArgumentOperation nameArgument = invocation.Arguments.First(a => a.Parameter!.Name == "name");
168+
Optional<object?> name = nameArgument.GetConstantValueFromAttribute(ctx.Operation.SemanticModel!, ctx.CancellationToken);
169+
string orchestrationName = name.Value?.ToString() ?? methodSymbol.Name;
170+
171+
visitor.VisitFuncOrchestrator(ctx.Operation.SemanticModel!, methodSyntax, methodSymbol, orchestrationName, ctx.ReportDiagnostic);
172+
},
173+
OperationKind.Invocation);
174+
}
163175
});
164176
}
165177
}

src/InProcessTestHost/Sidecar/InMemoryOrchestrationService.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,12 @@ public void ReleaseLock(string instanceId)
739739

740740
public Task<OrchestrationState> WaitForInstanceAsync(string instanceId, CancellationToken cancellationToken)
741741
{
742+
// First, add the waiter before checking completion to avoid a race condition.
743+
// This ensures we don't miss a completion notification that happens between
744+
// checking the status and adding the waiter.
745+
var tcs = this.waiters.GetOrAdd(instanceId, _ => new TaskCompletionSource<OrchestrationState>());
746+
747+
// Now check if already completed - if so, complete the waiter immediately
742748
if (this.store.TryGetValue(instanceId, out SerializedInstanceState? state))
743749
{
744750
lock (state)
@@ -750,16 +756,18 @@ public Task<OrchestrationState> WaitForInstanceAsync(string instanceId, Cancella
750756
statusRecord.OrchestrationStatus == OrchestrationStatus.Failed ||
751757
statusRecord.OrchestrationStatus == OrchestrationStatus.Terminated)
752758
{
753-
// orchestration has already completed
754-
return Task.FromResult(statusRecord);
759+
// Orchestration has already completed - complete the waiter and clean it up
760+
if (tcs.TrySetResult(statusRecord))
761+
{
762+
this.waiters.TryRemove(instanceId, out _);
763+
}
755764
}
756765
}
757766
}
758767
}
759768

760769
// Caller will be notified when the instance completes.
761770
// The ContinueWith is just to enable cancellation: https://stackoverflow.com/a/25652873/2069
762-
var tcs = this.waiters.GetOrAdd(instanceId, _ => new TaskCompletionSource<OrchestrationState>());
763771
return tcs.Task.ContinueWith(t => t.GetAwaiter().GetResult(), cancellationToken);
764772
}
765773

test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,58 @@ public async Task FuncOrchestratorWithDateTimeOffsetHasDiag()
453453
await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix);
454454
}
455455

456+
[Fact]
457+
public async Task TaskOrchestratorSdkOnlyHasDiag()
458+
{
459+
// Tests that the analyzer works with SDK-only references (without Azure Functions assemblies)
460+
string code = Wrapper.WrapTaskOrchestratorSdkOnly(@"
461+
public class MyOrchestrator : TaskOrchestrator<string, DateTime>
462+
{
463+
public override Task<DateTime> RunAsync(TaskOrchestrationContext context, string input)
464+
{
465+
return Task.FromResult({|#0:DateTime.Now|});
466+
}
467+
}
468+
");
469+
470+
string fix = Wrapper.WrapTaskOrchestratorSdkOnly(@"
471+
public class MyOrchestrator : TaskOrchestrator<string, DateTime>
472+
{
473+
public override Task<DateTime> RunAsync(TaskOrchestrationContext context, string input)
474+
{
475+
return Task.FromResult(context.CurrentUtcDateTime);
476+
}
477+
}
478+
");
479+
480+
DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("RunAsync", "System.DateTime.Now", "MyOrchestrator");
481+
482+
await VerifyCS.VerifySdkOnlyCodeFixAsync(code, expected, fix);
483+
}
484+
485+
[Fact]
486+
public async Task FuncOrchestratorSdkOnlyWithLambdaHasDiag()
487+
{
488+
// Tests that the analyzer works with SDK-only references (without Azure Functions assemblies)
489+
string code = Wrapper.WrapFuncOrchestratorSdkOnly(@"
490+
tasks.AddOrchestratorFunc(""HelloSequence"", context =>
491+
{
492+
return {|#0:DateTime.Now|};
493+
});
494+
");
495+
496+
string fix = Wrapper.WrapFuncOrchestratorSdkOnly(@"
497+
tasks.AddOrchestratorFunc(""HelloSequence"", context =>
498+
{
499+
return context.CurrentUtcDateTime;
500+
});
501+
");
502+
503+
DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Main", "System.DateTime.Now", "HelloSequence");
504+
505+
await VerifyCS.VerifySdkOnlyCodeFixAsync(code, expected, fix);
506+
}
507+
456508
static DiagnosticResult BuildDiagnostic()
457509
{
458510
return VerifyCS.Diagnostic(DateTimeOrchestrationAnalyzer.DiagnosticId);

0 commit comments

Comments
 (0)