diff --git a/Directory.Packages.props b/Directory.Packages.props
index efb48fcc4..1a7179428 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,6 +7,7 @@
+
@@ -21,6 +22,7 @@
+
@@ -49,4 +51,4 @@
-
+
\ No newline at end of file
diff --git a/all.sln b/all.sln
index 9a163b1d9..ad2443dc0 100644
--- a/all.sln
+++ b/all.sln
@@ -155,6 +155,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers", "src\Dapr.Actors.Analyzers\Dapr.Actors.Analyzers.csproj", "{D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers.Test", "test\Dapr.Actors.Analyzers.Test\Dapr.Actors.Analyzers.Test.csproj", "{65FC7DEA-B6DF-4542-8459-90B9FDAEA541}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -403,6 +407,14 @@ Global
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43}.Release|Any CPU.Build.0 = Release|Any CPU
+ {65FC7DEA-B6DF-4542-8459-90B9FDAEA541}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {65FC7DEA-B6DF-4542-8459-90B9FDAEA541}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {65FC7DEA-B6DF-4542-8459-90B9FDAEA541}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {65FC7DEA-B6DF-4542-8459-90B9FDAEA541}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -477,6 +489,8 @@ Global
{D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
{9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488}
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
+ {65FC7DEA-B6DF-4542-8459-90B9FDAEA541} = {DD020B34-460F-455F-8D17-CF4A949F100B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
diff --git a/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs b/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs
new file mode 100644
index 000000000..02edc7b3a
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs
@@ -0,0 +1,206 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Dapr.Actors.Analyzers;
+
+///
+/// Analyzes actor registration in Dapr applications.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class ActorAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly DiagnosticDescriptor DiagnosticDescriptorActorRegistration = new(
+ "DAPR0001",
+ "Actor class not registered",
+ "The actor class '{0}' is not registered",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor DiagnosticDescriptorJsonSerialization = new(
+ "DAPR0002",
+ "Use JsonSerialization",
+ "Add options.UseJsonSerialization to support interoperability with non-.NET actors",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor DiagnosticDescriptorMapActorsHandlers = new(
+ "DAPR0003",
+ "Call MapActorsHandlers",
+ "Call app.MapActorsHandlers to map endpoints for Dapr actors",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ ///
+ /// Gets the supported diagnostics for this analyzer.
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(
+ DiagnosticDescriptorActorRegistration,
+ DiagnosticDescriptorJsonSerialization,
+ DiagnosticDescriptorMapActorsHandlers);
+
+ ///
+ /// Initializes the analyzer.
+ ///
+ /// The analysis context.
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeActorRegistration, SyntaxKind.ClassDeclaration);
+ context.RegisterSyntaxNodeAction(AnalyzeSerialization, SyntaxKind.CompilationUnit);
+ context.RegisterSyntaxNodeAction(AnalyzeMapActorsHandlers, SyntaxKind.CompilationUnit);
+ }
+
+ private void AnalyzeActorRegistration(SyntaxNodeAnalysisContext context)
+ {
+ var classDeclaration = (ClassDeclarationSyntax)context.Node;
+
+ if (classDeclaration.BaseList != null)
+ {
+ var baseTypeSyntax = classDeclaration.BaseList.Types[0].Type;
+
+ if (context.SemanticModel.GetSymbolInfo(baseTypeSyntax).Symbol is INamedTypeSymbol baseTypeSymbol)
+ {
+ var baseTypeName = baseTypeSymbol.ToDisplayString();
+
+ {
+ var actorTypeName = classDeclaration.Identifier.Text;
+ bool isRegistered = CheckIfActorIsRegistered(actorTypeName, context.SemanticModel);
+ if (!isRegistered)
+ {
+ var diagnostic = Diagnostic.Create(DiagnosticDescriptorActorRegistration, classDeclaration.Identifier.GetLocation(), actorTypeName);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+ }
+ }
+
+ private static bool CheckIfActorIsRegistered(string actorTypeName, SemanticModel semanticModel)
+ {
+ var methodInvocations = new List();
+ foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees)
+ {
+ var root = syntaxTree.GetRoot();
+ methodInvocations.AddRange(root.DescendantNodes().OfType());
+ }
+
+ foreach (var invocation in methodInvocations)
+ {
+ if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
+ {
+ continue;
+ }
+
+ var methodName = memberAccess.Name.Identifier.Text;
+ if (methodName == "RegisterActor")
+ {
+ if (memberAccess.Name is GenericNameSyntax typeArgumentList && typeArgumentList.TypeArgumentList.Arguments.Count > 0)
+ {
+ if (typeArgumentList.TypeArgumentList.Arguments[0] is IdentifierNameSyntax typeArgument)
+ {
+ if (typeArgument.Identifier.Text == actorTypeName)
+ {
+ return true;
+ }
+ }
+ else if (typeArgumentList.TypeArgumentList.Arguments[0] is QualifiedNameSyntax qualifiedName)
+ {
+ if (qualifiedName.Right.Identifier.Text == actorTypeName)
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private void AnalyzeSerialization(SyntaxNodeAnalysisContext context)
+ {
+ var addActorsInvocation = FindInvocation(context, "AddActors");
+
+ if (addActorsInvocation != null)
+ {
+ var optionsLambda = addActorsInvocation.ArgumentList.Arguments
+ .Select(arg => arg.Expression)
+ .OfType()
+ .FirstOrDefault();
+
+ if (optionsLambda != null)
+ {
+ var lambdaBody = optionsLambda.Body;
+ var assignments = lambdaBody.DescendantNodes().OfType();
+
+ var useJsonSerialization = assignments.Any(assignment =>
+ assignment.Left is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name is IdentifierNameSyntax identifier &&
+ identifier.Identifier.Text == "UseJsonSerialization" &&
+ assignment.Right is LiteralExpressionSyntax literal &&
+ literal.Token.ValueText == "true");
+
+ if (!useJsonSerialization)
+ {
+ var diagnostic = Diagnostic.Create(DiagnosticDescriptorJsonSerialization, addActorsInvocation.GetLocation());
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+ }
+
+ private InvocationExpressionSyntax? FindInvocation(SyntaxNodeAnalysisContext context, string methodName)
+ {
+ foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees)
+ {
+ var root = syntaxTree.GetRoot();
+ var invocation = root.DescendantNodes().OfType()
+ .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name.Identifier.Text == methodName);
+
+ if (invocation != null)
+ {
+ return invocation;
+ }
+ }
+
+ return null;
+ }
+
+ private void AnalyzeMapActorsHandlers(SyntaxNodeAnalysisContext context)
+ {
+ var addActorsInvocation = FindInvocation(context, "AddActors");
+
+ if (addActorsInvocation != null)
+ {
+ bool invokedByWebApplication = false;
+ var mapActorsHandlersInvocation = FindInvocation(context, "MapActorsHandlers");
+
+ if (mapActorsHandlersInvocation?.Expression is MemberAccessExpressionSyntax memberAccess)
+ {
+ var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess.Expression);
+ if (symbolInfo.Symbol is ILocalSymbol localSymbol)
+ {
+ var type = localSymbol.Type;
+ if (type.ToDisplayString() == "Microsoft.AspNetCore.Builder.WebApplication")
+ {
+ invokedByWebApplication = true;
+ }
+ }
+ }
+
+ if (mapActorsHandlersInvocation == null || !invokedByWebApplication)
+ {
+ var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapActorsHandlers, addActorsInvocation.GetLocation());
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+}
diff --git a/src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs
new file mode 100644
index 000000000..df3e832ce
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs
@@ -0,0 +1,120 @@
+using System.Composition;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Dapr.Actors.Analyzers;
+
+///
+/// Provides code fix to enable JSON serialization for actors.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))]
+[Shared]
+public class ActorJsonSerializationCodeFixProvider : CodeFixProvider
+{
+ ///
+ /// Gets the diagnostic IDs that this provider can fix.
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR0002");
+
+ ///
+ /// Gets the FixAllProvider for this code fix provider.
+ ///
+ /// The FixAllProvider.
+ public override FixAllProvider? GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ ///
+ /// Registers code fixes for the specified diagnostics.
+ ///
+ /// The context to register the code fixes.
+ /// A task representing the asynchronous operation.
+ public override Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var title = "Use JSON serialization";
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title,
+ createChangedDocument: c => UseJsonSerializationAsync(context.Document, context.Diagnostics.First(), c),
+ equivalenceKey: title),
+ context.Diagnostics);
+ return Task.CompletedTask;
+ }
+
+ private async Task UseJsonSerializationAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
+ {
+ (_, var addActorsInvocation) = await FindAddActorsInvocationAsync(document.Project, cancellationToken);
+
+ if (addActorsInvocation == null)
+ {
+ return document;
+ }
+
+ var optionsLambda = addActorsInvocation?.ArgumentList.Arguments
+ .Select(arg => arg.Expression)
+ .OfType()
+ .FirstOrDefault();
+
+ if (optionsLambda == null || optionsLambda.Body is not BlockSyntax optionsBlock)
+ return document;
+
+ // Extract the parameter name from the lambda expression
+ var parameterName = optionsLambda.Parameter.Identifier.Text;
+
+ // Check if the lambda body already contains the assignment
+ var assignmentExists = optionsBlock.Statements
+ .OfType()
+ .Any(statement => statement.Expression is AssignmentExpressionSyntax assignment &&
+ assignment.Left is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name is IdentifierNameSyntax identifier &&
+ identifier.Identifier.Text == parameterName &&
+ memberAccess.Name.Identifier.Text == "UseJsonSerialization");
+
+ if (!assignmentExists)
+ {
+ var assignmentStatement = SyntaxFactory.ExpressionStatement(
+ SyntaxFactory.AssignmentExpression(
+ SyntaxKind.SimpleAssignmentExpression,
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName(parameterName),
+ SyntaxFactory.IdentifierName("UseJsonSerialization")),
+ SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression)));
+
+ var newOptionsBlock = optionsBlock.AddStatements(assignmentStatement);
+ var root = await document.GetSyntaxRootAsync(cancellationToken);
+ var newRoot = root?.ReplaceNode(optionsBlock, newOptionsBlock);
+ return document.WithSyntaxRoot(newRoot!);
+ }
+
+ return document;
+ }
+
+ private async Task<(Document?, InvocationExpressionSyntax?)> FindAddActorsInvocationAsync(Project project, CancellationToken cancellationToken)
+ {
+ var compilation = await project.GetCompilationAsync(cancellationToken);
+
+ foreach (var syntaxTree in compilation!.SyntaxTrees)
+ {
+ var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken);
+
+ var addActorsInvocation = syntaxRoot.DescendantNodes()
+ .OfType()
+ .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name.Identifier.Text == "AddActors");
+
+ if (addActorsInvocation != null)
+ {
+ var document = project.GetDocument(addActorsInvocation.SyntaxTree);
+ return (document, addActorsInvocation);
+ }
+ }
+
+ return (null, null);
+ }
+}
diff --git a/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs
new file mode 100644
index 000000000..efb7d2994
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs
@@ -0,0 +1,220 @@
+using System.Collections.Immutable;
+using System.Composition;
+using System.Diagnostics;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Formatting;
+
+namespace Dapr.Actors.Analyzers;
+
+///
+/// Provides code fixes for actor registration issues.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))]
+[Shared]
+public class ActorRegistrationCodeFixProvider : CodeFixProvider
+{
+ ///
+ /// Gets the diagnostic IDs that this provider can fix.
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR0001");
+
+ ///
+ /// Registers code fixes for the specified diagnostics.
+ ///
+ /// The context to register the code fixes.
+ /// A task representing the asynchronous operation.
+ public override Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var title = "Register actor";
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title,
+ createChangedDocument: c => RegisterActorAsync(context.Document, context.Diagnostics.First(), c),
+ equivalenceKey: title),
+ context.Diagnostics);
+ return Task.CompletedTask;
+ }
+
+ private async Task RegisterActorAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken);
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+ var classDeclaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First();
+
+ if (root == null || classDeclaration == null)
+ return document;
+
+ // Get the semantic model
+ var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
+
+ if (semanticModel == null)
+ return document;
+
+ // Get the symbol for the class declaration
+
+ if (semanticModel.GetDeclaredSymbol(classDeclaration, cancellationToken) is not INamedTypeSymbol classSymbol)
+ return document;
+
+ // Get the fully qualified name
+ var actorType = classSymbol.ToDisplayString(new SymbolDisplayFormat(
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces));
+
+ if (string.IsNullOrEmpty(actorType))
+ return document;
+
+ // Get the compilation
+ var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
+
+ if (compilation == null)
+ return document;
+
+ (var targetDocument, var addActorsInvocation) = await FindAddActorsInvocationAsync(document.Project, cancellationToken);
+
+ if (addActorsInvocation == null)
+ {
+ (targetDocument, addActorsInvocation) = await CreateAddActorsInvocation(document.Project, cancellationToken);
+ }
+
+ if (addActorsInvocation == null)
+ return document;
+
+ var targetRoot = await addActorsInvocation.SyntaxTree.GetRootAsync(cancellationToken);
+
+ if (targetRoot == null || targetDocument == null)
+ return document;
+
+ // Find the options lambda block
+ var optionsLambda = addActorsInvocation?.ArgumentList.Arguments
+ .Select(arg => arg.Expression)
+ .OfType()
+ .FirstOrDefault();
+
+ if (optionsLambda == null || optionsLambda.Body is not BlockSyntax optionsBlock)
+ return document;
+
+ // Extract the parameter name from the lambda expression
+ var parameterName = optionsLambda.Parameter.Identifier.Text;
+
+ // Create the new workflow registration statement
+ var registerWorkflowStatement = SyntaxFactory.ParseStatement($"{parameterName}.Actors.RegisterActor<{actorType}>();");
+
+ // Add the new registration statement to the options block
+ var newOptionsBlock = optionsBlock.AddStatements(registerWorkflowStatement);
+
+ // Replace the old options block with the new one
+ var newRoot = targetRoot?.ReplaceNode(optionsBlock, newOptionsBlock);
+
+ // Format the new root.
+ newRoot = Formatter.Format(newRoot!, document.Project.Solution.Workspace);
+
+ return targetDocument.WithSyntaxRoot(newRoot);
+ }
+
+ ///
+ /// Gets the FixAllProvider for this code fix provider.
+ ///
+ /// The FixAllProvider.
+ public override FixAllProvider? GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ private async Task<(Document?, InvocationExpressionSyntax?)> FindAddActorsInvocationAsync(Project project, CancellationToken cancellationToken)
+ {
+ var compilation = await project.GetCompilationAsync(cancellationToken);
+
+ foreach (var syntaxTree in compilation!.SyntaxTrees)
+ {
+ var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken);
+
+ var addActorsInvocation = syntaxRoot.DescendantNodes()
+ .OfType()
+ .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name.Identifier.Text == "AddActors");
+
+ if (addActorsInvocation != null)
+ {
+ var document = project.GetDocument(addActorsInvocation.SyntaxTree);
+ return (document, addActorsInvocation);
+ }
+ }
+
+ return (null, null);
+ }
+
+ private async Task FindCreateBuilderInvocationAsync(Project project, CancellationToken cancellationToken)
+ {
+ var compilation = await project.GetCompilationAsync(cancellationToken);
+
+ foreach (var syntaxTree in compilation!.SyntaxTrees)
+ {
+ var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken);
+
+ // Find the invocation expression for WebApplication.CreateBuilder()
+ var createBuilderInvocation = syntaxRoot.DescendantNodes()
+ .OfType()
+ .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Expression is IdentifierNameSyntax identifier &&
+ identifier.Identifier.Text == "WebApplication" &&
+ memberAccess.Name.Identifier.Text == "CreateBuilder");
+
+ if (createBuilderInvocation != null)
+ {
+ return createBuilderInvocation;
+ }
+ }
+
+ return null;
+ }
+
+ private async Task<(Document?, InvocationExpressionSyntax?)> CreateAddActorsInvocation(Project project, CancellationToken cancellationToken)
+ {
+ var createBuilderInvocation = await FindCreateBuilderInvocationAsync(project, cancellationToken);
+
+ var variableDeclarator = createBuilderInvocation?.Ancestors()
+ .OfType()
+ .FirstOrDefault();
+
+ var builderVariable = variableDeclarator?.Identifier.Text;
+
+ if (createBuilderInvocation != null)
+ {
+ var targetRoot = await createBuilderInvocation.SyntaxTree.GetRootAsync(cancellationToken);
+ var document = project.GetDocument(createBuilderInvocation.SyntaxTree);
+
+ if (createBuilderInvocation.Expression is MemberAccessExpressionSyntax { Expression: IdentifierNameSyntax builderIdentifier })
+ {
+ var addActorsStatement = SyntaxFactory.ParseStatement($"{builderVariable}.Services.AddActors(options => {{ }});");
+
+ if (createBuilderInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock)
+ {
+ var firstChild = parentBlock.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax);
+ var newParentBlock = parentBlock.InsertNodesAfter(firstChild, new[] { addActorsStatement });
+ targetRoot = targetRoot.ReplaceNode(parentBlock, newParentBlock);
+ }
+ else
+ {
+ var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault();
+ var firstChild = compilationUnitSyntax.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax);
+ var globalStatement = SyntaxFactory.GlobalStatement(addActorsStatement);
+ var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(firstChild, new[] { globalStatement });
+ targetRoot = targetRoot.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax);
+ }
+
+ var addActorsInvocation = targetRoot?.DescendantNodes()
+ .OfType()
+ .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name.Identifier.Text == "AddActors");
+
+ return (document, addActorsInvocation);
+ }
+ }
+
+ return (null, null);
+ }
+}
diff --git a/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md
new file mode 100644
index 000000000..d8abdf2e2
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md
@@ -0,0 +1,9 @@
+## Release 1.16
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|--------------------
+DAPR0001| Usage | Warning | The actor class '{0}' is not registered
+DAPR0002| Usage | Warning | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors
+DAPR0003| Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors
\ No newline at end of file
diff --git a/src/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md
new file mode 100644
index 000000000..b1b99aaf2
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,3 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj
new file mode 100644
index 000000000..910ecb295
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj
@@ -0,0 +1,43 @@
+
+
+
+ netstandard2.0
+
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+ This package contains Roslyn analyzers for actors.
+ $(PackageTags)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs
new file mode 100644
index 000000000..60dc7a4e8
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs
@@ -0,0 +1,125 @@
+using System.Collections.Immutable;
+using System.Composition;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Dapr.Actors.Analyzers;
+
+///
+/// Provides a code fix for the diagnostic "DAPR0003" by adding a call to MapActorsHandlers.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))]
+[Shared]
+public class MapActorsHandlersCodeFixProvider : CodeFixProvider
+{
+ ///
+ /// Gets the diagnostic IDs that this code fix provider can fix.
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR0003");
+
+ ///
+ /// Gets the FixAllProvider for this code fix provider.
+ ///
+ /// The FixAllProvider.
+ public override FixAllProvider? GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ ///
+ /// Registers code fixes for the specified diagnostic.
+ ///
+ /// A context for code fix registration.
+ /// A task that represents the asynchronous operation.
+ public override Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var title = "Call MapActorsHandlers";
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title,
+ createChangedDocument: c => AddMapActorsHandlersAsync(context.Document, context.Diagnostics.First(), c),
+ equivalenceKey: title),
+ context.Diagnostics);
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Adds a call to MapActorsHandlers to the specified document.
+ ///
+ /// The document to modify.
+ /// The diagnostic to fix.
+ /// A cancellation token.
+ /// A task that represents the asynchronous operation. The task result contains the modified document.
+ private async Task AddMapActorsHandlersAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken);
+ var invocationExpressions = root!.DescendantNodes().OfType();
+
+ var createBuilderInvocation = invocationExpressions
+ .FirstOrDefault(invocation =>
+ {
+ return invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name.Identifier.Text == "CreateBuilder" &&
+ memberAccess.Expression is IdentifierNameSyntax identifier &&
+ identifier.Identifier.Text == "WebApplication";
+ });
+
+ var variableDeclarator = createBuilderInvocation
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ var variableName = variableDeclarator.Identifier.Text;
+
+ var buildInvocation = invocationExpressions
+ .FirstOrDefault(invocation =>
+ {
+ return invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name.Identifier.Text == "Build" &&
+ memberAccess.Expression is IdentifierNameSyntax identifier &&
+ identifier.Identifier.Text == variableName;
+ });
+
+ var buildVariableDeclarator = buildInvocation
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ var buildVariableName = buildVariableDeclarator.Identifier.Text;
+
+ var mapActorsHandlersInvocation = SyntaxFactory.ExpressionStatement(
+ SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName(buildVariableName),
+ SyntaxFactory.IdentifierName("MapActorsHandlers"))));
+
+ if (buildInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock)
+ {
+ var localDeclaration = buildInvocation
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ var newParentBlock = parentBlock.InsertNodesAfter(localDeclaration, new[] { mapActorsHandlersInvocation });
+ root = root.ReplaceNode(parentBlock, newParentBlock);
+ }
+ else
+ {
+ var buildInvocationGlobalStatement = buildInvocation
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault();
+ var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(buildInvocationGlobalStatement,
+ new[] { SyntaxFactory.GlobalStatement(mapActorsHandlersInvocation) });
+ root = root.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax);
+ }
+
+ return document.WithSyntaxRoot(root);
+ }
+}
diff --git a/src/Dapr.Actors/Dapr.Actors.csproj b/src/Dapr.Actors/Dapr.Actors.csproj
index bcb8d830f..1c12bcd8e 100644
--- a/src/Dapr.Actors/Dapr.Actors.csproj
+++ b/src/Dapr.Actors/Dapr.Actors.csproj
@@ -7,6 +7,7 @@
+
diff --git a/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs
new file mode 100644
index 000000000..8d6b9abed
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs
@@ -0,0 +1,313 @@
+using Microsoft.CodeAnalysis;
+
+namespace Dapr.Actors.Analyzers.Test;
+
+public class ActorAnalyzerTests
+{
+ public class ActorNotRegistered
+ {
+ [Fact]
+ public async Task ReportDiagnostic_DAPR0001()
+ {
+ var testCode = @"
+ using Dapr.Actors.Runtime;
+
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning)
+ .WithSpan(4, 23, 4, 32).WithMessage("The actor class 'TestActor' is not registered");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+
+ [Fact]
+ public async Task ReportDiagnostic_DAPR0001_FullyQualified()
+ {
+ var testCode = @"
+ class TestActor : Dapr.Actors.Runtime.Actor
+ {
+ public TestActor(Dapr.Actors.Runtime.ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning)
+ .WithSpan(2, 23, 2, 32).WithMessage("The actor class 'TestActor' is not registered");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+
+ [Fact]
+ public async Task ReportDiagnostic_DAPR0001_NamespaceAlias()
+ {
+ var testCode = @"
+ using alias = Dapr.Actors.Runtime;
+
+ class TestActor : alias.Actor
+ {
+ public TestActor(alias.ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning)
+ .WithSpan(4, 23, 4, 32).WithMessage("The actor class 'TestActor' is not registered");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+ }
+
+ public class ActorRegistered
+ {
+ [Fact]
+ public async Task ReportNoDiagnostic()
+ {
+ var testCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.Actors.RegisterActor();
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+ }
+ }
+
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode);
+ }
+
+ [Fact]
+ public async Task ReportNoDiagnostic_WithNamespace()
+ {
+ var testCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.Actors.RegisterActor();
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+ }
+ }
+
+ namespace TestNamespace
+ {
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ }
+ ";
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode);
+ }
+
+ [Fact]
+ public async Task ReportNoDiagnostic_WithNamespaceAlias ()
+ {
+ var testCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+ using alias = TestNamespace;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.Actors.RegisterActor();
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+ }
+ }
+
+ namespace TestNamespace
+ {
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ }
+ ";
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode);
+ }
+ }
+
+ public class JsonSerialization
+ {
+ [Fact]
+ public async Task ReportDiagnostic()
+ {
+ var testCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+ }
+ }
+ ";
+
+ var expected = VerifyAnalyzer.Diagnostic("DAPR0002", DiagnosticSeverity.Warning)
+ .WithSpan(12, 25, 14, 27).WithMessage("Add options.UseJsonSerialization to support interoperability with non-.NET actors");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+
+ [Fact]
+ public async Task ReportNoDiagnostic()
+ {
+ var testCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+ }
+ }
+ ";
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode);
+ }
+ }
+
+ public class MapActorsHandlers
+ {
+ [Fact]
+ public async Task ReportDiagnostic()
+ {
+ var testCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+ }
+ }
+ ";
+
+ var expected = VerifyAnalyzer.Diagnostic("DAPR0003", DiagnosticSeverity.Warning)
+ .WithSpan(12, 25, 15, 27).WithMessage("Call app.MapActorsHandlers to map endpoints for Dapr actors");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+
+ [Fact]
+ public async Task ReportNoDiagnostic()
+ {
+ var testCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+ }
+ }
+ ";
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode);
+ }
+ }
+}
diff --git a/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs
new file mode 100644
index 000000000..7d3fe6cad
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs
@@ -0,0 +1,55 @@
+namespace Dapr.Actors.Analyzers.Test;
+
+public class ActorJsonSerializationCodeFixProviderTests
+{
+ [Fact]
+ public async Task UseJsonSerialization()
+ {
+ var code = @"
+ //using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+ }
+ }
+ ";
+
+ var expectedChangedCode = @"
+ //using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+ }
+ }
+ ";
+
+ await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ }
+}
diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs
new file mode 100644
index 000000000..d148ed90b
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs
@@ -0,0 +1,175 @@
+namespace Dapr.Actors.Analyzers.Test;
+
+public class ActorRegistrationCodeFixProviderTests
+{
+ [Fact]
+ public async Task RegisterActor()
+ {
+ var code = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+ }
+ }
+
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ var expectedChangedCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.UseJsonSerialization = true;
+ options.Actors.RegisterActor();
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+ }
+ }
+
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ }
+
+ [Fact]
+ public async Task RegisterActor_WhenAddActorsIsNotFound()
+ {
+ var code = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ var app = builder.Build();
+ }
+ }
+
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ var expectedChangedCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.Actors.RegisterActor();
+ });
+
+ var app = builder.Build();
+ }
+ }
+
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ }
+
+ [Fact]
+ public async Task RegisterActor_WhenAddActorsIsNotFound_TopLevelStatements()
+ {
+ var code = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+
+ var builder = WebApplication.CreateBuilder();
+
+ var app = builder.Build();
+
+ namespace TestNamespace
+ {
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ }
+ ";
+
+ var expectedChangedCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.Actors.RegisterActor();
+ });
+
+ var app = builder.Build();
+
+ namespace TestNamespace
+ {
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ }
+ ";
+
+ await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ }
+}
diff --git a/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj
new file mode 100644
index 000000000..934fcbfdd
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj
@@ -0,0 +1,35 @@
+
+
+
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs
new file mode 100644
index 000000000..8d0f58350
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs
@@ -0,0 +1,124 @@
+namespace Dapr.Actors.Analyzers.Test;
+
+public class MapActorsHandlersCodeFixProviderTests
+{
+ [Fact]
+ public async Task RegisterActor()
+ {
+ var code = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.Actors.RegisterActor();
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+ }
+ }
+
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ var expectedChangedCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.Actors.RegisterActor();
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+ }
+ }
+
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ }
+
+ [Fact]
+ public async Task RegisterActor_TopLevelStatements()
+ {
+ var code = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.Actors.RegisterActor();
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ var expectedChangedCode = @"
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.Actors.RegisterActor();
+ options.UseJsonSerialization = true;
+ });
+
+ var app = builder.Build();
+
+ app.MapActorsHandlers();
+
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ ";
+
+ await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ }
+}
diff --git a/test/Dapr.Actors.Analyzers.Test/Utilities.cs b/test/Dapr.Actors.Analyzers.Test/Utilities.cs
new file mode 100644
index 000000000..4b6beed0a
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Test/Utilities.cs
@@ -0,0 +1,77 @@
+using System.Collections.Immutable;
+using System.Reflection;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace Dapr.Actors.Analyzers.Test;
+
+internal static class Utilities
+{
+ public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code)
+ {
+ var workspace = new AdhocWorkspace();
+
+#if NET6_0
+ var referenceAssemblies = ReferenceAssemblies.Net.Net60;
+#elif NET7_0
+ var referenceAssemblies = ReferenceAssemblies.Net.Net70;
+#elif NET8_0
+ var referenceAssemblies = ReferenceAssemblies.Net.Net80;
+#elif NET9_0
+ var referenceAssemblies = ReferenceAssemblies.Net.Net90;
+#endif
+
+ // Create a new project with necessary references
+ var project = workspace.AddProject("TestProject", LanguageNames.CSharp)
+ .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication)
+ .WithSpecificDiagnosticOptions(new Dictionary
+ {
+ { "CS1701", ReportDiagnostic.Suppress }
+ }))
+ .AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, default))
+ .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
+ .AddMetadataReferences(GetAllReferencesNeededForType(typeof(WebApplication)))
+ .AddMetadataReferences(GetAllReferencesNeededForType(typeof(IHost)))
+ .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsEndpointRouteBuilderExtensions)))
+ .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions)));
+
+ // Add the document to the project
+ var document = project.AddDocument("TestDocument.cs", code);
+
+ // Get the syntax tree and create a compilation
+ var syntaxTree = await document.GetSyntaxTreeAsync() ?? throw new InvalidOperationException("Syntax tree is null");
+ var compilation = CSharpCompilation.Create("TestCompilation")
+ .AddSyntaxTrees(syntaxTree)
+ .AddReferences(project.MetadataReferences)
+ .WithOptions(project.CompilationOptions!);
+
+ var compilationWithAnalyzer = compilation.WithAnalyzers(
+ ImmutableArray.Create(
+ new ActorAnalyzer()));
+
+ // Get diagnostics from the compilation
+ var diagnostics = await compilationWithAnalyzer.GetAllDiagnosticsAsync();
+ return (diagnostics, document, workspace);
+ }
+
+ public static MetadataReference[] GetAllReferencesNeededForType(Type type)
+ {
+ var files = GetAllAssemblyFilesNeededForType(type);
+
+ return files.Select(x => MetadataReference.CreateFromFile(x)).Cast().ToArray();
+ }
+
+ private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type)
+ {
+ return type.Assembly.GetReferencedAssemblies()
+ .Select(x => Assembly.Load(x.FullName))
+ .Append(type.Assembly)
+ .Select(x => x.Location)
+ .ToImmutableArray();
+ }
+}
diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs
new file mode 100644
index 000000000..d114caedf
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs
@@ -0,0 +1,68 @@
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.Hosting;
+
+namespace Dapr.Actors.Analyzers.Test;
+
+internal static class VerifyAnalyzer
+{
+ public static DiagnosticResult Diagnostic(string diagnosticId, DiagnosticSeverity diagnosticSeverity)
+ {
+ return new DiagnosticResult(diagnosticId, diagnosticSeverity);
+ }
+
+ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
+ {
+ await VerifyAnalyzerAsync(source, null, expected);
+ }
+
+ public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected)
+ {
+ var test = new Test { TestCode = source };
+
+#if NET6_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net60;
+#elif NET7_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net70;
+#elif NET8_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80;
+#elif NET9_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net90;
+#endif
+
+ if (program != null)
+ {
+ test.TestState.Sources.Add(("Program.cs", program));
+ }
+
+ var metadataReferences = Utilities.GetAllReferencesNeededForType(typeof(ActorAnalyzer)).ToList();
+ metadataReferences.AddRange(Utilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions)));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location));
+
+ foreach (var reference in metadataReferences)
+ {
+ test.TestState.AdditionalReferences.Add(reference);
+ }
+
+ test.ExpectedDiagnostics.AddRange(expected);
+ await test.RunAsync(CancellationToken.None);
+ }
+
+ private class Test : CSharpAnalyzerTest
+ {
+ public Test()
+ {
+ SolutionTransforms.Add((solution, projectId) =>
+ {
+ var compilationOptions = solution.GetProject(projectId)!.CompilationOptions!;
+ solution = solution.WithProjectCompilationOptions(projectId, compilationOptions);
+ return solution;
+ });
+ }
+ }
+}
diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs b/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs
new file mode 100644
index 000000000..f7d0d7eb4
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs
@@ -0,0 +1,56 @@
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+
+namespace Dapr.Actors.Analyzers.Test;
+
+internal class VerifyCodeFix
+{
+ public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new()
+ {
+ var (diagnostics, document, workspace) = await Utilities.GetDiagnosticsAdvanced(code);
+
+ Assert.Single(diagnostics);
+
+ var diagnostic = diagnostics[0];
+
+ var codeFixProvider = new T();
+
+ CodeAction? registeredCodeAction = null;
+
+ var context = new CodeFixContext(document, diagnostic, (codeAction, _) =>
+ {
+ if (registeredCodeAction != null)
+ throw new Exception("Code action was registered more than once");
+
+ registeredCodeAction = codeAction;
+
+ }, CancellationToken.None);
+
+ await codeFixProvider.RegisterCodeFixesAsync(context);
+
+ if (registeredCodeAction == null)
+ throw new Exception("Code action was not registered");
+
+ var operations = await registeredCodeAction.GetOperationsAsync(CancellationToken.None);
+
+ foreach (var operation in operations)
+ {
+ operation.Apply(workspace, CancellationToken.None);
+ }
+
+ var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? throw new Exception("Updated document is null");
+ var newCode = (await updatedDocument.GetTextAsync()).ToString();
+
+ // Normalize whitespace
+ string NormalizeWhitespace(string input)
+ {
+ var separator = new[] { ' ', '\r', '\n' };
+ return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries));
+ }
+
+ var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode);
+ var normalizedNewCode = NormalizeWhitespace(newCode);
+
+ Assert.Equal(normalizedExpectedCode, normalizedNewCode);
+ }
+}