Skip to content
1 change: 1 addition & 0 deletions samples/ConsoleApp/ConsoleApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<!-- Using p2p references so we can show latest changes in samples. -->
<ProjectReference Include="$(SrcRoot)Client/Grpc/Client.Grpc.csproj" />
<ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" />
<ProjectReference Include="$(SrcRoot)Analyzers/Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions samples/ConsoleAppMinimal/ConsoleAppMinimal.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<!-- Using p2p references so we can show latest changes in samples. -->
<ProjectReference Include="$(SrcRoot)Client/Grpc/Client.Grpc.csproj" />
<ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" />
<ProjectReference Include="$(SrcRoot)Analyzers/Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<ProjectReference Include="$(SrcRoot)Client/AzureManaged/Client.AzureManaged.csproj" />
<ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" />
<ProjectReference Include="$(SrcRoot)Worker/AzureManaged/Worker.AzureManaged.csproj" />
<ProjectReference Include="$(SrcRoot)Analyzers/Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>

3 changes: 1 addition & 2 deletions samples/LargePayloadConsoleApp/LargePayloadConsoleApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
<ProjectReference Include="$(SrcRoot)Client/AzureManaged/Client.AzureManaged.csproj" />
<ProjectReference Include="$(SrcRoot)Worker/AzureManaged/Worker.AzureManaged.csproj" />
<ProjectReference Include="$(SrcRoot)Extensions/AzureBlobPayloads/AzureBlobPayloads.csproj" />
<ProjectReference Include="$(SrcRoot)Analyzers/Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>


Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public override async Task<string> RunAsync(TaskOrchestrationContext context, st

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

return $"Stock {symbol} price: ${currentPrice:F2} at {DateTime.UtcNow}";
return $"Stock {symbol} price: ${currentPrice:F2} at {context.CurrentUtcDateTime}";
}
catch (Exception ex)
{
Expand Down
1 change: 1 addition & 0 deletions samples/ScheduleConsoleApp/ScheduleConsoleApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
<ProjectReference Include="..\..\src\Client\AzureManaged\Client.AzureManaged.csproj" />
<ProjectReference Include="..\..\src\Worker\AzureManaged\Worker.AzureManaged.csproj" />
<ProjectReference Include="..\..\src\ScheduledTasks\ScheduledTasks.csproj" />
<ProjectReference Include="..\..\src\Analyzers\Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions samples/ScheduleWebApp/ScheduleWebApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
<ProjectReference Include="..\..\src\Client\AzureManaged\Client.AzureManaged.csproj" />
<ProjectReference Include="..\..\src\Worker\AzureManaged\Worker.AzureManaged.csproj" />
<ProjectReference Include="..\..\src\ScheduledTasks\ScheduledTasks.csproj" />
<ProjectReference Include="..\..\src\Analyzers\Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
204 changes: 108 additions & 96 deletions src/Analyzers/Orchestration/OrchestrationAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ public override void Initialize(AnalysisContext context)
{
KnownTypeSymbols knownSymbols = new(context.Compilation);

if (knownSymbols.FunctionOrchestrationAttribute == null || knownSymbols.FunctionNameAttribute == null ||
knownSymbols.TaskOrchestratorInterface == null ||
knownSymbols.DurableTaskRegistry == null)
// Check if at least one orchestration type can be detected
bool canAnalyzeDurableFunctions = knownSymbols.FunctionOrchestrationAttribute != null && knownSymbols.FunctionNameAttribute != null;
bool canAnalyzeTaskOrchestrator = knownSymbols.TaskOrchestratorInterface != null && knownSymbols.TaskOrchestrationContext != null;
bool canAnalyzeFuncOrchestrator = knownSymbols.DurableTaskRegistry != null;

if (!canAnalyzeDurableFunctions && !canAnalyzeTaskOrchestrator && !canAnalyzeFuncOrchestrator)
{
// symbols not available in this compilation, skip analysis
// no symbols available in this compilation, skip analysis
return;
}

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

// look for Durable Functions Orchestrations
context.RegisterSyntaxNodeAction(
ctx =>
if (canAnalyzeDurableFunctions)
{
ctx.CancellationToken.ThrowIfCancellationRequested();

if (ctx.ContainingSymbol is not IMethodSymbol methodSymbol)
context.RegisterSyntaxNodeAction(
ctx =>
{
return;
}
ctx.CancellationToken.ThrowIfCancellationRequested();

if (!methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.FunctionOrchestrationAttribute))
{
return;
}
if (ctx.ContainingSymbol is not IMethodSymbol methodSymbol)
{
return;
}

if (!methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute, out string functionName))
{
return;
}
if (!methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.FunctionOrchestrationAttribute!))
{
return;
}

if (!methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute!, out string functionName))
{
return;
}

var rootMethodSyntax = (MethodDeclarationSyntax)ctx.Node;
var rootMethodSyntax = (MethodDeclarationSyntax)ctx.Node;

visitor.VisitDurableFunction(ctx.SemanticModel, rootMethodSyntax, methodSymbol, functionName, ctx.ReportDiagnostic);
},
SyntaxKind.MethodDeclaration);
visitor.VisitDurableFunction(ctx.SemanticModel, rootMethodSyntax, methodSymbol, functionName, ctx.ReportDiagnostic);
},
SyntaxKind.MethodDeclaration);
}

// look for ITaskOrchestrator/TaskOrchestrator`2 Orchestrations
context.RegisterSyntaxNodeAction(
ctx =>
if (canAnalyzeTaskOrchestrator)
{
ctx.CancellationToken.ThrowIfCancellationRequested();

if (ctx.ContainingSymbol is not INamedTypeSymbol classSymbol)
context.RegisterSyntaxNodeAction(
ctx =>
{
return;
}
ctx.CancellationToken.ThrowIfCancellationRequested();

bool implementsITaskOrchestrator = classSymbol.AllInterfaces.Any(i => i.Equals(knownSymbols.TaskOrchestratorInterface, SymbolEqualityComparer.Default));
if (!implementsITaskOrchestrator)
{
return;
}
if (ctx.ContainingSymbol is not INamedTypeSymbol classSymbol)
{
return;
}

IEnumerable<IMethodSymbol> orchestrationMethods = classSymbol.GetMembers().OfType<IMethodSymbol>()
.Where(m => m.Parameters.Any(p => p.Type.Equals(knownSymbols.TaskOrchestrationContext, SymbolEqualityComparer.Default)));
bool implementsITaskOrchestrator = classSymbol.AllInterfaces.Any(i => i.Equals(knownSymbols.TaskOrchestratorInterface, SymbolEqualityComparer.Default));
if (!implementsITaskOrchestrator)
{
return;
}

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

foreach (IMethodSymbol? methodSymbol in orchestrationMethods)
{
IEnumerable<MethodDeclarationSyntax> methodSyntaxes = methodSymbol.GetSyntaxNodes();
foreach (MethodDeclarationSyntax rootMethodSyntax in methodSyntaxes)
string functionName = classSymbol.Name;

foreach (IMethodSymbol? methodSymbol in orchestrationMethods)
{
visitor.VisitTaskOrchestrator(ctx.SemanticModel, rootMethodSyntax, methodSymbol, functionName, ctx.ReportDiagnostic);
IEnumerable<MethodDeclarationSyntax> methodSyntaxes = methodSymbol.GetSyntaxNodes();
foreach (MethodDeclarationSyntax rootMethodSyntax in methodSyntaxes)
{
visitor.VisitTaskOrchestrator(ctx.SemanticModel, rootMethodSyntax, methodSymbol, functionName, ctx.ReportDiagnostic);
}
}
}
},
SyntaxKind.ClassDeclaration);
},
SyntaxKind.ClassDeclaration);
}

// look for OrchestratorFunc Orchestrations
context.RegisterOperationAction(
ctx =>
if (canAnalyzeFuncOrchestrator)
{
if (ctx.Operation is not IInvocationOperation invocation)
context.RegisterOperationAction(
ctx =>
{
return;
}
if (ctx.Operation is not IInvocationOperation invocation)
{
return;
}

if (!SymbolEqualityComparer.Default.Equals(invocation.Type, knownSymbols.DurableTaskRegistry))
{
return;
}
if (!SymbolEqualityComparer.Default.Equals(invocation.Type, knownSymbols.DurableTaskRegistry))
{
return;
}

// there are 8 AddOrchestratorFunc overloads
if (invocation.TargetMethod.Name != "AddOrchestratorFunc")
{
return;
}
// there are 8 AddOrchestratorFunc overloads
if (invocation.TargetMethod.Name != "AddOrchestratorFunc")
{
return;
}

// all overloads have the parameter 'orchestrator', either as an Action or a Func
IArgumentOperation orchestratorArgument = invocation.Arguments.First(a => a.Parameter!.Name == "orchestrator");
if (orchestratorArgument.Value is not IDelegateCreationOperation delegateCreationOperation)
{
return;
}
// all overloads have the parameter 'orchestrator', either as an Action or a Func
IArgumentOperation orchestratorArgument = invocation.Arguments.First(a => a.Parameter!.Name == "orchestrator");
if (orchestratorArgument.Value is not IDelegateCreationOperation delegateCreationOperation)
{
return;
}

// obtains the method symbol from the delegate creation operation
IMethodSymbol? methodSymbol = null;
SyntaxNode? methodSyntax = null;
switch (delegateCreationOperation.Target)
{
case IAnonymousFunctionOperation lambdaOperation:
// use the containing symbol of the lambda (e.g. the class declaring it) as the method symbol
methodSymbol = ctx.ContainingSymbol as IMethodSymbol;
methodSyntax = delegateCreationOperation.Syntax;
break;
case IMethodReferenceOperation methodReferenceOperation:
// use the method reference as the method symbol
methodSymbol = methodReferenceOperation.Method;
methodSyntax = methodReferenceOperation.Method.DeclaringSyntaxReferences.First().GetSyntax();
break;
default:
break;
}

if (methodSymbol == null || methodSyntax == null)
{
return;
}
// obtains the method symbol from the delegate creation operation
IMethodSymbol? methodSymbol = null;
SyntaxNode? methodSyntax = null;
switch (delegateCreationOperation.Target)
{
case IAnonymousFunctionOperation _:
// use the containing symbol of the lambda (e.g. the class declaring it) as the method symbol
methodSymbol = ctx.ContainingSymbol as IMethodSymbol;
methodSyntax = delegateCreationOperation.Syntax;
break;
case IMethodReferenceOperation methodReferenceOperation:
// use the method reference as the method symbol
methodSymbol = methodReferenceOperation.Method;
methodSyntax = methodReferenceOperation.Method.DeclaringSyntaxReferences.First().GetSyntax();
break;
default:
break;
}

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

visitor.VisitFuncOrchestrator(ctx.Operation.SemanticModel!, methodSyntax, methodSymbol, orchestrationName, ctx.ReportDiagnostic);
},
OperationKind.Invocation);
// try to get the name of the orchestration from the method call, otherwise use the containing type name
IArgumentOperation nameArgument = invocation.Arguments.First(a => a.Parameter!.Name == "name");
Optional<object?> name = nameArgument.GetConstantValueFromAttribute(ctx.Operation.SemanticModel!, ctx.CancellationToken);
string orchestrationName = name.Value?.ToString() ?? methodSymbol.Name;

visitor.VisitFuncOrchestrator(ctx.Operation.SemanticModel!, methodSyntax, methodSymbol, orchestrationName, ctx.ReportDiagnostic);
},
OperationKind.Invocation);
}
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,58 @@ public async Task FuncOrchestratorWithDateTimeOffsetHasDiag()
await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix);
}

[Fact]
public async Task TaskOrchestratorSdkOnlyHasDiag()
{
// Tests that the analyzer works with SDK-only references (without Azure Functions assemblies)
string code = Wrapper.WrapTaskOrchestratorSdkOnly(@"
public class MyOrchestrator : TaskOrchestrator<string, DateTime>
{
public override Task<DateTime> RunAsync(TaskOrchestrationContext context, string input)
{
return Task.FromResult({|#0:DateTime.Now|});
}
}
");

string fix = Wrapper.WrapTaskOrchestratorSdkOnly(@"
public class MyOrchestrator : TaskOrchestrator<string, DateTime>
{
public override Task<DateTime> RunAsync(TaskOrchestrationContext context, string input)
{
return Task.FromResult(context.CurrentUtcDateTime);
}
}
");

DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("RunAsync", "System.DateTime.Now", "MyOrchestrator");

await VerifyCS.VerifySdkOnlyCodeFixAsync(code, expected, fix);
}

[Fact]
public async Task FuncOrchestratorSdkOnlyWithLambdaHasDiag()
{
// Tests that the analyzer works with SDK-only references (without Azure Functions assemblies)
string code = Wrapper.WrapFuncOrchestratorSdkOnly(@"
tasks.AddOrchestratorFunc(""HelloSequence"", context =>
{
return {|#0:DateTime.Now|};
});
");

string fix = Wrapper.WrapFuncOrchestratorSdkOnly(@"
tasks.AddOrchestratorFunc(""HelloSequence"", context =>
{
return context.CurrentUtcDateTime;
});
");

DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Main", "System.DateTime.Now", "HelloSequence");

await VerifyCS.VerifySdkOnlyCodeFixAsync(code, expected, fix);
}

static DiagnosticResult BuildDiagnostic()
{
return VerifyCS.Diagnostic(DateTimeOrchestrationAnalyzer.DiagnosticId);
Expand Down
21 changes: 20 additions & 1 deletion test/Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier.Durable.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.CodeAnalysis.Diagnostics;
Expand Down Expand Up @@ -30,4 +30,23 @@ public static async Task VerifyDurableTaskAnalyzerAsync(string source, Action<Te

await test.RunAsync(CancellationToken.None);
}

/// <summary>
/// Runs analyzer test with SDK-only references (without Azure Functions assemblies).
/// Used to test orchestration detection in non-function scenarios.
/// </summary>
public static async Task VerifySdkOnlyAnalyzerAsync(string source, Action<Test>? configureTest = null, params DiagnosticResult[] expected)
{
Test test = new()
{
TestCode = source,
ReferenceAssemblies = References.SdkOnlyAssemblies,
};

test.ExpectedDiagnostics.AddRange(expected);

configureTest?.Invoke(test);

await test.RunAsync(CancellationToken.None);
}
}
Loading
Loading