diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5ea87d5a5..21fccd0a7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -25,6 +25,7 @@
+
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/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs
new file mode 100644
index 000000000..164da75bd
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs
@@ -0,0 +1,122 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Dapr.Actors.Analyzers;
+
+///
+/// An analyzer for Dapr actors that validates that each discovered Actor implementation is properly registered with
+/// dependency injection during startup.
+///
+[DiagnosticAnalyzer((LanguageNames.CSharp))]
+public sealed class ActorRegistrationAnalyzer : DiagnosticAnalyzer
+{
+ internal static readonly DiagnosticDescriptor DiagnosticDescriptorActorRegistration = new(
+ id: "DAPR4002",
+ title: new LocalizableResourceString(nameof(Resources.DAPR4002Title), Resources.ResourceManager,
+ typeof(Resources)),
+ messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4002MessageFormat), Resources.ResourceManager,
+ typeof(Resources)),
+ category: "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ ///
+ /// Returns a set of descriptors for the diagnostics that this analyzer is capable of producing.
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(
+ DiagnosticDescriptorActorRegistration);
+
+ ///
+ /// Called once at session start to register actions in the analysis context.
+ ///
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeActorRegistration, SyntaxKind.ClassDeclaration);
+ }
+
+ private static void AnalyzeActorRegistration(SyntaxNodeAnalysisContext context)
+ {
+ var classDeclaration = (ClassDeclarationSyntax)context.Node;
+
+ if (classDeclaration.BaseList == null)
+ {
+ return;
+ }
+
+ var baseTypeSyntax = classDeclaration.BaseList.Types[0].Type;
+
+ if (context.SemanticModel.GetSymbolInfo(baseTypeSyntax).Symbol is not INamedTypeSymbol baseTypeSymbol)
+ {
+ return;
+ }
+
+ var actorTypeName = classDeclaration.Identifier.Text;
+ var isRegistered = CheckIfActorIsRegistered(actorTypeName, context.SemanticModel);
+ if (isRegistered)
+ {
+ return;
+ }
+
+ 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")
+ {
+ continue;
+ }
+
+ if (memberAccess.Name is not GenericNameSyntax typeArgumentList ||
+ typeArgumentList.TypeArgumentList.Arguments.Count <= 0)
+ {
+ continue;
+ }
+
+ switch (typeArgumentList.TypeArgumentList.Arguments[0])
+ {
+ case IdentifierNameSyntax typeArgument when typeArgument.Identifier.Text == actorTypeName:
+ case QualifiedNameSyntax qualifiedName when qualifiedName.Right.Identifier.Text == actorTypeName:
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs
new file mode 100644
index 000000000..828f99175
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs
@@ -0,0 +1,230 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Formatter = Microsoft.CodeAnalysis.Formatting.Formatter;
+
+namespace Dapr.Actors.Analyzers;
+
+///
+/// Provides code fixes for missing actor registrations during dependency injection configuration.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))]
+public sealed class ActorRegistrationCodeFixProvider : CodeFixProvider
+{
+ ///
+ /// Gets the diagnostic IDs that this provider can fix.
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR4002");
+
+ ///
+ /// 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)
+ {
+ const string title = "Register Dapr 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 (ModelExtensions.GetDeclaredSymbol(semanticModel, 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, 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/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md
index bc73dcb0b..f82775b19 100644
--- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md
@@ -6,4 +6,7 @@
Rule ID | Category | Severity | Notes
--------|----------|----------|------------------------------------------------------------------------------------
-DAPR4001 | Usage | Warning | Actor timer method invocations require the named callback method to exist on type.
\ No newline at end of file
+DAPR4001 | Usage | Warning | Actor timer method invocations require the named callback method to exist on type.
+DAPR4002 | Usage | Warning | The actor type is not registered with dependency injection
+DAPR4003 | Usage | Info | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors
+DAPR4004 | Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors.
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs
index dcc08b3c1..e07f509ed 100644
--- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs
@@ -13,4 +13,4 @@
using System.Runtime.CompilerServices;
-[assembly: InternalsVisibleTo("Dapr.Actors.Analyzers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )]
+[assembly: InternalsVisibleTo("Dapr.Actors.Analyzers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )]
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj
index a3c751c38..a75e37a3e 100644
--- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj
@@ -3,15 +3,16 @@
netstandard2.0
- false
enable
enable
This package contains Roslyn analyzers for Dapr.Actors.
true
+ $(NoWarn);RS1038
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersAnalyzer.cs
new file mode 100644
index 000000000..c85a80664
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersAnalyzer.cs
@@ -0,0 +1,90 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Dapr.Actors.Analyzers;
+
+///
+/// An analyzer for Dapr actors that validates that the handler is set up during initial setup to map
+/// the actor endpoints.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class MappedActorHandlersAnalyzer : DiagnosticAnalyzer
+{
+ internal static readonly DiagnosticDescriptor DiagnosticDescriptorMapActorsHandlers = new(
+ id: "DAPR4004",
+ title: new LocalizableResourceString(nameof(Resources.DAPR4004Title), Resources.ResourceManager,
+ typeof(Resources)),
+ messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4004MessageFormat), Resources.ResourceManager,
+ typeof(Resources)),
+ category: "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ ///
+ /// Returns a set of descriptors for the diagnostics that this analyzer is capable of producing.
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(
+ DiagnosticDescriptorMapActorsHandlers);
+
+ ///
+ /// Called once at session start to register actions in the analysis context.
+ ///
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeMapActorsHandlers, SyntaxKind.CompilationUnit);
+ }
+
+ private static void AnalyzeMapActorsHandlers(SyntaxNodeAnalysisContext context)
+ {
+ var addActorsInvocation = SharedUtilities.FindInvocation(context, "AddActors");
+
+ if (addActorsInvocation == null)
+ {
+ return;
+ }
+
+ var invokedByWebApplication = false;
+ var mapActorsHandlersInvocation = SharedUtilities.FindInvocation(context, "MapActorsHandlers");
+
+ if (mapActorsHandlersInvocation?.Expression is MemberAccessExpressionSyntax memberAccess)
+ {
+ var symbolInfo = ModelExtensions.GetSymbolInfo(context.SemanticModel, 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)
+ {
+ return;
+ }
+
+ var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapActorsHandlers, addActorsInvocation.GetLocation());
+ context.ReportDiagnostic(diagnostic);
+ }
+}
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersCodeFixProvider.cs
new file mode 100644
index 000000000..f5982ad8a
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersCodeFixProvider.cs
@@ -0,0 +1,134 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.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;
+
+///
+///
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MappedActorHandlersCodeFixProvider))]
+public sealed class MappedActorHandlersCodeFixProvider : CodeFixProvider
+{
+ ///
+ /// A list of diagnostic IDs that this provider can provide fixes for.
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR4004");
+
+ ///
+ /// 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)
+ {
+ const string title = "Register Dapr actor mappings";
+ 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 static 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 => invocation.Expression is MemberAccessExpressionSyntax { Name.Identifier.Text: "CreateBuilder", Expression: IdentifierNameSyntax
+ {
+ Identifier.Text: "WebApplication"
+ }
+ });
+
+ var variableDeclarator = createBuilderInvocation
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ var variableName = variableDeclarator.Identifier.Text;
+
+ var buildInvocation = invocationExpressions
+ .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax
+ {
+ Name.Identifier.Text: "Build",
+ Expression: 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, [mapActorsHandlersInvocation]);
+ root = root.ReplaceNode(parentBlock, newParentBlock);
+ }
+ else
+ {
+ var buildInvocationGlobalStatement = buildInvocation
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault();
+ var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(buildInvocationGlobalStatement,
+ [SyntaxFactory.GlobalStatement(mapActorsHandlersInvocation)]);
+ root = root.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax);
+ }
+
+ return document.WithSyntaxRoot(root);
+ }
+}
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationAnalyzer.cs
new file mode 100644
index 000000000..9d36454dd
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationAnalyzer.cs
@@ -0,0 +1,93 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Dapr.Actors.Analyzers;
+
+///
+/// An analyzer for Dapr Actors that suggests configuring JSON serialization during Actor DI registration for
+/// better interoperability with non-.NET actors throughout a Dapr project.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class PreferActorJsonSerializationAnalyzer : DiagnosticAnalyzer
+{
+ internal static readonly DiagnosticDescriptor DiagnosticDescriptorJsonSerialization = new(
+ id: "DAPR4003",
+ title: new LocalizableResourceString(nameof(Resources.DAPR4003Title), Resources.ResourceManager,
+ typeof(Resources)),
+ messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4003MessageFormat), Resources.ResourceManager,
+ typeof(Resources)),
+ category: "Usage",
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: true
+ );
+
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(
+ DiagnosticDescriptorJsonSerialization
+ );
+
+ ///
+ /// Called once at session start to register actions in the analysis context.
+ ///
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeSerialization, SyntaxKind.CompilationUnit);
+ }
+
+ private static void AnalyzeSerialization(SyntaxNodeAnalysisContext context)
+ {
+ var addActorsInvocation = SharedUtilities.FindInvocation(context, "AddActors");
+
+ if (addActorsInvocation is null)
+ {
+ return;
+ }
+
+ var optionsLambda = addActorsInvocation.ArgumentList.Arguments
+ .Select(arg => arg.Expression)
+ .OfType()
+ .FirstOrDefault();
+
+ if (optionsLambda == null)
+ {
+ return;
+ }
+
+ 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)
+ {
+ return;
+ }
+
+ var diagnostic = Diagnostic.Create(DiagnosticDescriptorJsonSerialization, addActorsInvocation.GetLocation());
+ context.ReportDiagnostic(diagnostic);
+ }
+}
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationCodeFixProvider.cs
new file mode 100644
index 000000000..fc3da9632
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationCodeFixProvider.cs
@@ -0,0 +1,133 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.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 the code fix to enable JSON serialization for Dapr actors.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PreferActorJsonSerializationCodeFixProvider))]
+public sealed class PreferActorJsonSerializationCodeFixProvider : CodeFixProvider
+{
+ ///
+ /// A list of diagnostic IDs that this provider can provide fixes for.
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR4003");
+
+ ///
+ /// 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)
+ {
+ const string 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)
+ {
+ return document;
+ }
+
+ 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!);
+
+ }
+
+ 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/Dapr.Actors.Analyzers/Resources.Designer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs
index af3dd797d..ac9c06d81 100644
--- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs
@@ -76,5 +76,59 @@ internal static string DAPR4001Title {
return ResourceManager.GetString("DAPR4001Title", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to The actor type '{0}' is not registered with dependency injection.
+ ///
+ internal static string DAPR4002MessageFormat {
+ get {
+ return ResourceManager.GetString("DAPR4002MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The actor type is not registered with dependency injection.
+ ///
+ internal static string DAPR4002Title {
+ get {
+ return ResourceManager.GetString("DAPR4002Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Set options.UseJsonSerialization to true to support interoperability with non-.NET actors.
+ ///
+ internal static string DAPR4003MessageFormat {
+ get {
+ return ResourceManager.GetString("DAPR4003MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Set options.UseJsonSerialization to true to support interoperability with non-.NET actors.
+ ///
+ internal static string DAPR4003Title {
+ get {
+ return ResourceManager.GetString("DAPR4003Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Call app.MapActorsHandlers to map endpoints for Dapr actors.
+ ///
+ internal static string DAPR4004MessageFormat {
+ get {
+ return ResourceManager.GetString("DAPR4004MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Call app.MapActorsHandlers to map endpoints for Dapr actors.
+ ///
+ internal static string DAPR4004Title {
+ get {
+ return ResourceManager.GetString("DAPR4004Title", resourceCulture);
+ }
+ }
}
}
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx
index 47f6c667e..93c313620 100644
--- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx
@@ -24,4 +24,22 @@
Actor timer method invocations require the named callback '{0}' method to exist on type '{1}'
+
+ The actor type is not registered with dependency injection
+
+
+ The actor type '{0}' is not registered with dependency injection
+
+
+ Set options.UseJsonSerialization to true to support interoperability with non-.NET actors
+
+
+ Set options.UseJsonSerialization to true to support interoperability with non-.NET actors
+
+
+ Call app.MapActorsHandlers to map endpoints for Dapr actors
+
+
+ Call app.MapActorsHandlers to map endpoints for Dapr actors
+
\ No newline at end of file
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/SharedUtilities.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/SharedUtilities.cs
new file mode 100644
index 000000000..d411ec16c
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/SharedUtilities.cs
@@ -0,0 +1,38 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Dapr.Actors.Analyzers;
+
+internal class SharedUtilities
+{
+ internal static 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;
+ }
+}
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerMethodPresentAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerCallbackMethodPresentAnalyzer.cs
similarity index 94%
rename from src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerMethodPresentAnalyzer.cs
rename to src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerCallbackMethodPresentAnalyzer.cs
index 59640fd58..49f03ece7 100644
--- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerMethodPresentAnalyzer.cs
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerCallbackMethodPresentAnalyzer.cs
@@ -12,7 +12,6 @@
// ------------------------------------------------------------------------
using System.Collections.Immutable;
-using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -25,12 +24,9 @@ namespace Dapr.Actors.Analyzers;
/// as the callback should actually exist on the type.
///
[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed class TimerMethodPresentAnalyzer : DiagnosticAnalyzer
+public sealed class TimerCallbackMethodPresentAnalyzer : DiagnosticAnalyzer
{
- ///
- /// The rule validated by the analyzer.
- ///
- public static readonly DiagnosticDescriptor DaprTimerCallbackMethodRule = new(
+ internal static readonly DiagnosticDescriptor DaprTimerCallbackMethodRule = new(
id: "DAPR4001",
title: new LocalizableResourceString(nameof(Resources.DAPR4001Title), Resources.ResourceManager, typeof(Resources)),
messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4001MessageFormat), Resources.ResourceManager, typeof(Resources)),
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/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);
+ }
+}
diff --git a/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationAnalyzerTests.cs
new file mode 100644
index 000000000..63a775b15
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationAnalyzerTests.cs
@@ -0,0 +1,180 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Actors.Analyzers.Tests;
+
+public class ActorRegistrationAnalyzerTests
+{
+ [Fact]
+ public async Task NotRegistered_ReportDiagnostic_DAPR4002()
+ {
+ const string testCode = """
+ using Dapr.Actors.Runtime;
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ """;
+
+ var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration)
+ .WithSpan(2, 7, 2, 16).WithMessage("The actor type 'TestActor' is not registered with dependency injection");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+
+ [Fact]
+ public async Task NotRegistered_ReportDiagnostic_DAPR4002_FullyQualified()
+ {
+ const string testCode = """
+ class TestActor : Dapr.Actors.Runtime.Actor
+ {
+ public TestActor(Dapr.Actors.Runtime.ActorHost host) : base(host)
+ {
+ }
+ }
+ """;
+
+ var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration)
+ .WithSpan(1, 7, 1, 16).WithMessage("The actor type 'TestActor' is not registered with dependency injection");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+
+
+ [Fact]
+ public async Task NotRegistered_ReportDiagnostic_DAPR4002_NamespaceAlias()
+ {
+ const string testCode = """
+ using alias = Dapr.Actors.Runtime;
+ class TestActor : alias.Actor
+ {
+ public TestActor(alias.ActorHost host) : base(host)
+ {
+ }
+ }
+ """;
+
+ var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration)
+ .WithSpan(2, 15, 2, 24).WithMessage("The actor type 'TestActor' is not registered with dependency injection");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+
+ [Fact]
+ public async Task Registered_ReportNoDiagnostic()
+ {
+ const string 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 Registered_ReportNoDiagnostic_WithNamespace()
+ {
+ const string 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 Registered_ReportNoDiagnostic_WithNamespaceAlias()
+ {
+ const string 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);
+ }
+}
diff --git a/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationCodeFixProviderTests.cs
new file mode 100644
index 000000000..9ae4afc68
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationCodeFixProviderTests.cs
@@ -0,0 +1,186 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Actors.Analyzers.Tests;
+
+public class ActorRegistrationCodeFixProviderTests
+{
+ [Fact]
+ public async Task RegisterActor()
+ {
+ const string 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)
+ {
+ }
+ }
+
+ """;
+
+ const string 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()
+ {
+ const string 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)
+ {
+ }
+ }
+
+ """;
+
+ const string 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()
+ {
+ const string 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)
+ {
+ }
+ }
+ }
+
+ """;
+
+ const string 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.Tests/Dapr.Actors.Analyzers.Tests.csproj b/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj
index d801c2dfd..482379a80 100644
--- a/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj
+++ b/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj
@@ -9,8 +9,11 @@
+
+
+
@@ -29,7 +32,7 @@
-
+
diff --git a/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersAnalyzerTests.cs
new file mode 100644
index 000000000..ed649f986
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersAnalyzerTests.cs
@@ -0,0 +1,71 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Actors.Analyzers.Tests;
+
+public class MappedActorHandlersAnalyzerTests
+{
+ [Fact]
+ public async Task ReportDiagnostic()
+ {
+ const string 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(MappedActorHandlersAnalyzer.DiagnosticDescriptorMapActorsHandlers)
+ .WithSpan(10, 25, 13, 27).WithMessage("Call app.MapActorsHandlers to map endpoints for Dapr actors");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+
+ [Fact]
+ public async Task ReportNoDiagnostic()
+ {
+ const string 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.Tests/MappedActorHandlersCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersCodeFixProviderTests.cs
new file mode 100644
index 000000000..7c66a2656
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersCodeFixProviderTests.cs
@@ -0,0 +1,131 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Actors.Analyzers.Tests;
+
+public class MappedActorHandlersCodeFixProviderTests
+{
+ [Fact]
+ public async Task RegisterActor()
+ {
+ const string 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)
+ {
+ }
+ }
+ """;
+
+ const string 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()
+ {
+ const string 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)
+ {
+ }
+ }
+
+ """;
+
+ const string 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.Tests/PreferActorJsonSerializationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationAnalyzerTests.cs
new file mode 100644
index 000000000..2fd8f5e77
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationAnalyzerTests.cs
@@ -0,0 +1,73 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Actors.Analyzers.Tests;
+
+public class PreferActorJsonSerializationAnalyzerTests
+{
+ [Fact]
+ public async Task ReportDiagnostic()
+ {
+ const string 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(PreferActorJsonSerializationAnalyzer.DiagnosticDescriptorJsonSerialization)
+ .WithSpan(10, 25, 12, 27)
+ .WithMessage("Set options.UseJsonSerialization to true to support interoperability with non-.NET actors");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+
+ [Fact]
+ public async Task ReportNoDiagnostic()
+ {
+ const string 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.Tests/PreferActorJsonSerializationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationCodeFixProviderTests.cs
new file mode 100644
index 000000000..0a7288c24
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationCodeFixProviderTests.cs
@@ -0,0 +1,62 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Actors.Analyzers.Tests;
+
+public class PreferActorJsonSerializationCodeFixProviderTests
+{
+ [Fact]
+ public async Task UseJsonSerialization()
+ {
+ const string 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();
+ }
+ }
+ """;
+
+ const string 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.Tests/TestUtilities.cs b/test/Dapr.Actors.Analyzers.Tests/TestUtilities.cs
new file mode 100644
index 000000000..acfec6baa
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Tests/TestUtilities.cs
@@ -0,0 +1,88 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Collections.Immutable;
+using System.Reflection;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Dapr.Actors.Analyzers.Tests;
+
+internal static class TestUtilities
+{
+ public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code)
+ {
+ var workspace = new AdhocWorkspace();
+
+#if 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(
+ [
+ new ActorRegistrationAnalyzer(),
+ new MappedActorHandlersAnalyzer(),
+ new PreferActorJsonSerializationAnalyzer(),
+ new TimerCallbackMethodPresentAnalyzer()
+ ]);
+
+ // 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) => type.Assembly
+ .GetReferencedAssemblies()
+ .Select(x => Assembly.Load(x.FullName))
+ .Append(type.Assembly)
+ .Select(x => x.Location)
+ .ToImmutableArray();
+}
diff --git a/test/Dapr.Actors.Analyzers.Tests/TimerMethodPresentAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Tests/TimerCallbackMethodPresentAnalyzerTests.cs
similarity index 91%
rename from test/Dapr.Actors.Analyzers.Tests/TimerMethodPresentAnalyzerTests.cs
rename to test/Dapr.Actors.Analyzers.Tests/TimerCallbackMethodPresentAnalyzerTests.cs
index bdcf0e469..e3fd47f31 100644
--- a/test/Dapr.Actors.Analyzers.Tests/TimerMethodPresentAnalyzerTests.cs
+++ b/test/Dapr.Actors.Analyzers.Tests/TimerCallbackMethodPresentAnalyzerTests.cs
@@ -11,14 +11,13 @@
// limitations under the License.
// ------------------------------------------------------------------------
-using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
namespace Dapr.Actors.Analyzers.Tests;
-public class TimerMethodPresentAnalyzerTests
+public class TimerCallbackMethodPresentAnalyzerTests
{
#if NET8_0
private static readonly ReferenceAssemblies assemblies = ReferenceAssemblies.Net.Net80;
@@ -29,7 +28,7 @@ public class TimerMethodPresentAnalyzerTests
[Fact]
public async Task TestActor_TimerRegistration_NotPresent()
{
- var context = new CSharpAnalyzerTest();
+ var context = new CSharpAnalyzerTest();
context.ReferenceAssemblies = assemblies.AddPackages([
new ("Dapr.Actors", "1.15.3")
]);
@@ -54,7 +53,7 @@ public async Task DoSomethingAsync()
[Fact]
public async Task TestActor_TimerRegistration_NameOfCallbackPresent()
{
- var context = new CSharpAnalyzerTest();
+ var context = new CSharpAnalyzerTest();
context.ReferenceAssemblies = assemblies.AddPackages([
new ("Dapr.Actors", "1.15.3")
]);
@@ -85,7 +84,7 @@ private static async Task TimerCallback(byte[] data)
[Fact]
public async Task TestActor_TimerRegistration_LiteralCallbackPresent()
{
- var context = new CSharpAnalyzerTest();
+ var context = new CSharpAnalyzerTest();
context.ReferenceAssemblies = assemblies.AddPackages([
new ("Dapr.Actors", "1.15.3")
]);
@@ -116,7 +115,7 @@ private static async Task TimerCallback(byte[] data)
[Fact]
public async Task TestActor_TimerRegistration_CallbackNotPresent()
{
- var context = new CSharpAnalyzerTest();
+ var context = new CSharpAnalyzerTest();
context.ReferenceAssemblies = assemblies.AddPackages([
new ("Dapr.Actors", "1.15.3")
]);
@@ -134,7 +133,7 @@ public async Task DoSomethingAsync()
}
""";
- context.ExpectedDiagnostics.Add(new DiagnosticResult(TimerMethodPresentAnalyzer.DaprTimerCallbackMethodRule)
+ context.ExpectedDiagnostics.Add(new DiagnosticResult(TimerCallbackMethodPresentAnalyzer.DaprTimerCallbackMethodRule)
.WithSpan(8, 45, 8, 60)
.WithArguments("TimerCallback", "TestActorTimerRegistrationTimerCallbackNotPresent"));
await context.RunAsync();
diff --git a/test/Dapr.Actors.Analyzers.Tests/VerifyAnalyzer.cs b/test/Dapr.Actors.Analyzers.Tests/VerifyAnalyzer.cs
new file mode 100644
index 000000000..87136e411
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Tests/VerifyAnalyzer.cs
@@ -0,0 +1,83 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Dapr.Actors.Analyzers.Tests;
+
+internal static class VerifyAnalyzer
+{
+ public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) => new(descriptor);
+
+ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
+ where TAnalyzer : DiagnosticAnalyzer, new()
+ {
+ await VerifyAnalyzerAsync(source, null, expected);
+ }
+
+ public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected)
+ where TAnalyzer : DiagnosticAnalyzer, new()
+ {
+ var test = new Test { TestCode = source };
+
+#if 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 = TestUtilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList();
+ metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(MappedActorHandlersAnalyzer)));
+ metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(PreferActorJsonSerializationAnalyzer)));
+ metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimerCallbackMethodPresentAnalyzer)));
+ metadataReferences.AddRange(TestUtilities.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 sealed class Test : CSharpAnalyzerTest
+ where TAnalyzer : DiagnosticAnalyzer, new()
+ {
+ 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.Tests/VerifyCodeFix.cs b/test/Dapr.Actors.Analyzers.Tests/VerifyCodeFix.cs
new file mode 100644
index 000000000..d09e427af
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Tests/VerifyCodeFix.cs
@@ -0,0 +1,70 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+
+namespace Dapr.Actors.Analyzers.Tests;
+
+internal static class VerifyCodeFix
+{
+ public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new()
+ {
+ var (diagnostics, document, workspace) = await TestUtilities.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();
+
+ var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode);
+ var normalizedNewCode = NormalizeWhitespace(newCode);
+
+ Assert.Equal(normalizedExpectedCode, normalizedNewCode);
+ return;
+
+ // Normalize whitespace
+ string NormalizeWhitespace(string input)
+ {
+ var separator = new[] { ' ', '\r', '\n' };
+ return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries));
+ }
+ }
+}