diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorSerializationAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorSerializationAnalyzer.cs
new file mode 100644
index 000000000..935602d93
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorSerializationAnalyzer.cs
@@ -0,0 +1,549 @@
+// ------------------------------------------------------------------------
+// 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.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Dapr.Actors.Analyzers;
+
+///
+/// Analyzes Dapr Actor classes and their interfaces for correct serialization attribute usage.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class ActorSerializationAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly LocalizableString ActorInterfaceMissingIActorTitle = new LocalizableResourceString(nameof(Resources.ActorInterfaceMissingIActorTitle), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString ActorInterfaceMissingIActorMessageFormat = new LocalizableResourceString(nameof(Resources.ActorInterfaceMissingIActorMessageFormat), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString ActorInterfaceMissingIActorDescription = new LocalizableResourceString(nameof(Resources.ActorInterfaceMissingIActorDescription), Resources.ResourceManager, typeof(Resources));
+
+ /// Actor interface should inherit from IActor.
+ public static readonly DiagnosticDescriptor ActorInterfaceMissingIActor = new(
+ "DAPR1405",
+ ActorInterfaceMissingIActorTitle,
+ ActorInterfaceMissingIActorMessageFormat,
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: ActorInterfaceMissingIActorDescription);
+
+ private static readonly LocalizableString EnumMissingEnumMemberAttributeTitle = new LocalizableResourceString(nameof(Resources.EnumMissingEnumMemberAttributeTitle), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString EnumMissingEnumMemberAttributeMessageFormat = new LocalizableResourceString(nameof(Resources.EnumMissingEnumMemberAttributeMessageFormat), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString EnumMissingEnumMemberAttributeDescription = new LocalizableResourceString(nameof(Resources.EnumMissingEnumMemberAttributeDescription), Resources.ResourceManager, typeof(Resources));
+
+ /// Enum members in Actor types should use EnumMember attribute.
+ public static readonly DiagnosticDescriptor EnumMissingEnumMemberAttribute = new(
+ "DAPR1406",
+ EnumMissingEnumMemberAttributeTitle,
+ EnumMissingEnumMemberAttributeMessageFormat,
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: EnumMissingEnumMemberAttributeDescription);
+
+ private static readonly LocalizableString WeaklyTypedActorJsonPropertyRecommendationTitle = new LocalizableResourceString(nameof(Resources.WeaklyTypedActorJsonPropertyRecommendationTitle), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString WeaklyTypedActorJsonPropertyRecommendationMessageFormat = new LocalizableResourceString(nameof(Resources.WeaklyTypedActorJsonPropertyRecommendationMessageFormat), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString WeaklyTypedActorJsonPropertyRecommendationDescription = new LocalizableResourceString(nameof(Resources.WeaklyTypedActorJsonPropertyRecommendationDescription), Resources.ResourceManager, typeof(Resources));
+
+ /// Consider using JsonPropertyName for property name consistency.
+ public static readonly DiagnosticDescriptor WeaklyTypedActorJsonPropertyRecommendation = new(
+ "DAPR1407",
+ WeaklyTypedActorJsonPropertyRecommendationTitle,
+ WeaklyTypedActorJsonPropertyRecommendationMessageFormat,
+ "Usage",
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: true,
+ description: WeaklyTypedActorJsonPropertyRecommendationDescription);
+
+ private static readonly LocalizableString ComplexTypeInActorNeedsAttributesTitle = new LocalizableResourceString(nameof(Resources.ComplexTypeInActorNeedsAttributesTitle), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString ComplexTypeInActorNeedsAttributesMessageFormat = new LocalizableResourceString(nameof(Resources.ComplexTypeInActorNeedsAttributesMessageFormat), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString ComplexTypeInActorNeedsAttributesDescription = new LocalizableResourceString(nameof(Resources.ComplexTypeInActorNeedsAttributesDescription), Resources.ResourceManager, typeof(Resources));
+
+ /// Complex types used in Actor methods need serialization attributes.
+ public static readonly DiagnosticDescriptor ComplexTypeInActorNeedsAttributes = new(
+ "DAPR1408",
+ ComplexTypeInActorNeedsAttributesTitle,
+ ComplexTypeInActorNeedsAttributesMessageFormat,
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: ComplexTypeInActorNeedsAttributesDescription);
+
+ private static readonly LocalizableString ActorMethodParameterNeedsValidationTitle = new LocalizableResourceString(nameof(Resources.ActorMethodParameterNeedsValidationTitle), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString ActorMethodParameterNeedsValidationMessageFormat = new LocalizableResourceString(nameof(Resources.ActorMethodParameterNeedsValidationMessageFormat), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString ActorMethodParameterNeedsValidationDescription = new LocalizableResourceString(nameof(Resources.ActorMethodParameterNeedsValidationDescription), Resources.ResourceManager, typeof(Resources));
+
+ /// Actor method parameter needs proper serialization attributes.
+ public static readonly DiagnosticDescriptor ActorMethodParameterNeedsValidation = new(
+ "DAPR1409",
+ ActorMethodParameterNeedsValidationTitle,
+ ActorMethodParameterNeedsValidationMessageFormat,
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: ActorMethodParameterNeedsValidationDescription);
+
+ private static readonly LocalizableString ActorMethodReturnTypeNeedsValidationTitle = new LocalizableResourceString(nameof(Resources.ActorMethodReturnTypeNeedsValidationTitle), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString ActorMethodReturnTypeNeedsValidationMessageFormat = new LocalizableResourceString(nameof(Resources.ActorMethodReturnTypeNeedsValidationMessageFormat), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString ActorMethodReturnTypeNeedsValidationDescription = new LocalizableResourceString(nameof(Resources.ActorMethodReturnTypeNeedsValidationDescription), Resources.ResourceManager, typeof(Resources));
+
+ /// Actor method return type needs proper serialization attributes.
+ public static readonly DiagnosticDescriptor ActorMethodReturnTypeNeedsValidation = new(
+ "DAPR1410",
+ ActorMethodReturnTypeNeedsValidationTitle,
+ ActorMethodReturnTypeNeedsValidationMessageFormat,
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: ActorMethodReturnTypeNeedsValidationDescription);
+
+ /// Collection types in Actor methods need element type validation.
+ internal static readonly DiagnosticDescriptor CollectionTypeInActorNeedsElementValidation = new(
+ "DAPR1411",
+ "Collection types in Actor methods need element type validation",
+ "Collection type '{0}' in Actor method contains elements of type '{1}' which needs proper serialization attributes",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "Collection types used in Actor methods should contain elements with proper serialization attributes.");
+
+ /// Record types should use DataContract and DataMember attributes for Actor serialization.
+ internal static readonly DiagnosticDescriptor RecordTypeNeedsDataContractAttributes = new(
+ "DAPR1412",
+ "Record types should use DataContract and DataMember attributes for Actor serialization",
+ "Record '{0}' should be decorated with [DataContract] and have [DataMember] attributes on properties for proper Actor serialization",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "Record types used in Actor methods should have [DataContract] attribute and [DataMember] attributes on all properties for reliable serialization.");
+
+ /// Actor class implementation should implement an interface that inherits from IActor.
+ internal static readonly DiagnosticDescriptor ActorClassMissingInterface = new(
+ "DAPR1413",
+ "Actor class implementation should implement an interface that inherits from IActor",
+ "Actor class '{0}' should implement an interface that inherits from IActor",
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "Actor class implementations should implement an interface that inherits from IActor for proper Actor pattern implementation.");
+
+ /// All types must either expose a public parameterless constructor or be decorated with the DataContractAttribute attribute.
+ internal static readonly DiagnosticDescriptor TypeMissingParameterlessConstructorOrDataContract = new(
+ "DAPR1414",
+ "All types must either expose a public parameterless constructor or be decorated with the DataContractAttribute attribute",
+ "Type '{0}' must either have a public parameterless constructor or be decorated with [DataContract] attribute for proper serialization",
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "All types used in Actor methods must either expose a public parameterless constructor or be decorated with the DataContractAttribute attribute for reliable serialization.");
+
+ ///
+ public override ImmutableArray SupportedDiagnostics =>
+ [
+ ActorInterfaceMissingIActor,
+ EnumMissingEnumMemberAttribute,
+ WeaklyTypedActorJsonPropertyRecommendation,
+ ComplexTypeInActorNeedsAttributes,
+ ActorMethodParameterNeedsValidation,
+ ActorMethodReturnTypeNeedsValidation,
+ CollectionTypeInActorNeedsElementValidation,
+ RecordTypeNeedsDataContractAttributes,
+ ActorClassMissingInterface,
+ TypeMissingParameterlessConstructorOrDataContract
+,
+ ];
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration);
+ context.RegisterSyntaxNodeAction(AnalyzeInterfaceDeclaration, SyntaxKind.InterfaceDeclaration);
+ context.RegisterSyntaxNodeAction(AnalyzeEnumDeclaration, SyntaxKind.EnumDeclaration);
+ }
+
+ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
+ {
+ var classDeclaration = (ClassDeclarationSyntax)context.Node;
+ var semanticModel = context.SemanticModel;
+ var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
+
+ if (classSymbol == null)
+ {
+ return;
+ }
+
+ if (!InheritsFromActor(classSymbol))
+ {
+ return;
+ }
+
+ CheckActorInterfaces(context, classDeclaration, classSymbol);
+ CheckActorClassImplementsIActorInterface(context, classDeclaration, classSymbol);
+ CheckActorMethodTypes(context, classSymbol);
+ }
+
+ private static void AnalyzeInterfaceDeclaration(SyntaxNodeAnalysisContext context)
+ {
+ var interfaceDeclaration = (InterfaceDeclarationSyntax)context.Node;
+ var semanticModel = context.SemanticModel;
+ var interfaceSymbol = semanticModel.GetDeclaredSymbol(interfaceDeclaration);
+
+ if (interfaceSymbol == null)
+ {
+ return;
+ }
+
+ if (interfaceDeclaration.Identifier.ValueText.StartsWith("I") && interfaceDeclaration.Identifier.ValueText.EndsWith("Actor") && !InheritsFromIActor(interfaceSymbol))
+ {
+ var diagnostic = Diagnostic.Create(
+ ActorInterfaceMissingIActor,
+ interfaceDeclaration.Identifier.GetLocation(),
+ interfaceSymbol.Name);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static void AnalyzeEnumDeclaration(SyntaxNodeAnalysisContext context)
+ {
+ var enumDeclaration = (EnumDeclarationSyntax)context.Node;
+ var semanticModel = context.SemanticModel;
+ var enumSymbol = semanticModel.GetDeclaredSymbol(enumDeclaration);
+
+ if (enumSymbol == null)
+ {
+ return;
+ }
+
+ foreach (var member in enumSymbol.GetMembers().OfType())
+ {
+ if (HasAttribute(member, "EnumMemberAttribute", "EnumMember"))
+ {
+ continue;
+ }
+
+ var memberDeclaration = enumDeclaration.Members
+ .FirstOrDefault(m => m.Identifier.ValueText == member.Name);
+
+ if (memberDeclaration != null)
+ {
+ var diagnostic = Diagnostic.Create(
+ EnumMissingEnumMemberAttribute,
+ memberDeclaration.Identifier.GetLocation(),
+ member.Name,
+ enumSymbol.Name);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+
+ private static bool InheritsFromActor(INamedTypeSymbol classSymbol)
+ {
+ var baseType = classSymbol.BaseType;
+ while (baseType != null)
+ {
+ if (baseType.Name == "Actor" && baseType.ContainingNamespace?.ToDisplayString() == "Dapr.Actors.Runtime")
+ {
+ return true;
+ }
+
+ baseType = baseType.BaseType;
+ }
+
+ return false;
+ }
+
+ private static bool InheritsFromIActor(INamedTypeSymbol interfaceSymbol)
+ {
+ return interfaceSymbol.AllInterfaces.Any(i =>
+ i.Name == "IActor" && i.ContainingNamespace?.ToDisplayString() == "Dapr.Actors");
+ }
+
+ private static bool HasAttribute(ISymbol symbol, params string[] attributeNames)
+ {
+ return symbol.GetAttributes().Any(attr =>
+ attributeNames.Contains(attr.AttributeClass?.Name) ||
+ attributeNames.Contains(attr.AttributeClass?.MetadataName));
+ }
+
+ private static void CheckActorInterfaces(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classSymbol)
+ {
+ foreach (var interfaceType in classSymbol.Interfaces)
+ {
+ if (interfaceType.Name.StartsWith("I") && interfaceType.Name.EndsWith("Actor") && !InheritsFromIActor(interfaceType))
+ {
+ var diagnostic = Diagnostic.Create(
+ ActorInterfaceMissingIActor,
+ classDeclaration.Identifier.GetLocation(),
+ interfaceType.Name);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+
+ private static void CheckActorClassImplementsIActorInterface(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classSymbol)
+ {
+ var implementsIActorInterface = classSymbol.Interfaces.Any(interfaceType => InheritsFromIActor(interfaceType));
+
+ if (!implementsIActorInterface)
+ {
+ var diagnostic = Diagnostic.Create(
+ ActorClassMissingInterface,
+ classDeclaration.Identifier.GetLocation(),
+ classSymbol.Name);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static void CheckActorMethodTypes(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol)
+ {
+ var iActorInterfaces = classSymbol.AllInterfaces.Where(InheritsFromIActor).ToList();
+
+ foreach (var interfaceMethod in iActorInterfaces.SelectMany(i => i.GetMembers().OfType()))
+ {
+ var implementation = classSymbol.FindImplementationForInterfaceMember(interfaceMethod) as IMethodSymbol;
+ if (implementation == null)
+ {
+ continue;
+ }
+
+ if (!implementation.ReturnsVoid)
+ {
+ CheckMethodReturnType(context, implementation);
+ }
+
+ foreach (var parameter in implementation.Parameters)
+ {
+ CheckMethodParameter(context, implementation, parameter);
+ }
+ }
+ }
+
+ private static void CheckMethodReturnType(SyntaxNodeAnalysisContext context, IMethodSymbol method)
+ {
+ var returnType = method.ReturnType;
+ var location = method.Locations.FirstOrDefault();
+
+ if (location == null)
+ {
+ return;
+ }
+
+ if (returnType is INamedTypeSymbol namedReturnType &&
+ namedReturnType.IsGenericType &&
+ namedReturnType.Name == "Task" &&
+ namedReturnType.TypeArguments.Length == 1)
+ {
+ returnType = namedReturnType.TypeArguments[0];
+ }
+
+ ValidateTypeForSerialization(context, returnType, location, method.Name, isParameter: false);
+ }
+
+ private static void CheckMethodParameter(SyntaxNodeAnalysisContext context, IMethodSymbol method, IParameterSymbol parameter)
+ {
+ var location = parameter.Locations.FirstOrDefault();
+ if (location == null)
+ {
+ return;
+ }
+
+ ValidateTypeForSerialization(context, parameter.Type, location, method.Name, isParameter: true, parameter.Name);
+ }
+
+ private static void ValidateTypeForSerialization(SyntaxNodeAnalysisContext context, ITypeSymbol type, Location location, string methodName, bool isParameter, string? parameterName = null)
+ {
+ if (IsPrimitiveOrKnownType(type))
+ {
+ return;
+ }
+
+ if (IsCollectionType(type))
+ {
+ CheckCollectionElementType(context, type, location, methodName, isParameter, parameterName);
+ return;
+ }
+
+ if (type is not INamedTypeSymbol namedType ||
+ (namedType.TypeKind != TypeKind.Class && namedType.TypeKind != TypeKind.Struct))
+ {
+ return;
+ }
+
+ if (namedType.IsRecord)
+ {
+ CheckRecordSymbolForDataContractAttributes(context, namedType, location);
+ return;
+ }
+
+ if (!HasParameterlessConstructorOrDataContract(namedType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ TypeMissingParameterlessConstructorOrDataContract,
+ location,
+ namedType.Name));
+ }
+
+ if (!HasProperSerializationAttributes(namedType))
+ {
+ if (isParameter)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ ActorMethodParameterNeedsValidation,
+ location,
+ parameterName,
+ type.Name,
+ methodName));
+ }
+ else
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ ActorMethodReturnTypeNeedsValidation,
+ location,
+ type.Name,
+ methodName));
+ }
+ }
+ }
+
+ private static bool IsCollectionType(ITypeSymbol type)
+ {
+ if (type is not INamedTypeSymbol namedType)
+ {
+ return false;
+ }
+
+ var collectionTypeNames = new[]
+ {
+ "IEnumerable", "ICollection", "IList", "IDictionary",
+ "List", "Array", "Dictionary", "HashSet", "Queue", "Stack"
+ };
+
+ return collectionTypeNames.Any(name =>
+ namedType.Name == name ||
+ namedType.AllInterfaces.Any(i => i.Name == name)) ||
+ type.TypeKind == TypeKind.Array;
+ }
+
+ private static void CheckCollectionElementType(SyntaxNodeAnalysisContext context, ITypeSymbol collectionType, Location location, string methodName, bool isParameter, string? parameterName)
+ {
+ IEnumerable elementTypes = Enumerable.Empty();
+
+ if (collectionType.TypeKind == TypeKind.Array && collectionType is IArrayTypeSymbol arrayType)
+ {
+ elementTypes = new[] { arrayType.ElementType };
+ }
+ else if (collectionType is INamedTypeSymbol namedType && namedType.IsGenericType)
+ {
+ // For generic collections (including Dictionary and IDictionary),
+ // validate all type arguments, not just the first one.
+ elementTypes = namedType.TypeArguments;
+ }
+
+ foreach (var elementType in elementTypes)
+ {
+ if (elementType == null || IsPrimitiveOrKnownType(elementType))
+ {
+ continue;
+ }
+
+ if (elementType is INamedTypeSymbol namedElementType &&
+ (namedElementType.TypeKind == TypeKind.Class || namedElementType.TypeKind == TypeKind.Struct) &&
+ !HasProperSerializationAttributes(namedElementType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ CollectionTypeInActorNeedsElementValidation,
+ location,
+ collectionType.Name,
+ elementType.Name));
+ }
+ }
+ }
+
+ private static bool HasProperSerializationAttributes(INamedTypeSymbol type)
+ {
+ return HasAttribute(type, "DataContractAttribute", "DataContract") ||
+ HasAttribute(type, "SerializableAttribute", "Serializable") ||
+ HasAttribute(type, "JsonObjectAttribute", "JsonObject") ||
+ IsPrimitiveOrKnownType(type);
+ }
+
+ private static bool IsPrimitiveOrKnownType(ITypeSymbol type)
+ {
+ if (type.TypeKind == TypeKind.Enum)
+ {
+ return true;
+ }
+
+ var typeName = type.ToDisplayString();
+ var knownTypes = new[]
+ {
+ "byte", "sbyte", "short", "int", "long", "ushort", "uint", "ulong",
+ "float", "double", "bool", "char", "decimal", "object", "string",
+ "System.DateTime", "System.TimeSpan", "System.Guid", "System.Uri",
+ "System.Xml.XmlQualifiedName", "System.Threading.Tasks.Task", "void"
+ };
+
+ return knownTypes.Contains(typeName) ||
+ typeName.StartsWith("System.Threading.Tasks.Task<") ||
+ typeName == "System.Void";
+ }
+
+ private static void CheckRecordSymbolForDataContractAttributes(SyntaxNodeAnalysisContext context, INamedTypeSymbol recordType, Location usageLocation)
+ {
+ if (!HasAttribute(recordType, "DataContractAttribute", "DataContract"))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ RecordTypeNeedsDataContractAttributes,
+ usageLocation,
+ recordType.Name));
+ return;
+ }
+
+ foreach (var property in recordType.GetMembers().OfType()
+ .Where(p => p.DeclaredAccessibility == Accessibility.Public && !HasDataMemberAttribute(p)))
+ {
+ var propLocation = property.Locations.FirstOrDefault() ?? usageLocation;
+ context.ReportDiagnostic(Diagnostic.Create(
+ RecordTypeNeedsDataContractAttributes,
+ propLocation,
+ recordType.Name));
+ }
+ }
+
+ private static bool HasDataMemberAttribute(IPropertySymbol property) =>
+ HasAttribute(property, "DataMemberAttribute", "DataMember");
+
+ private static bool HasParameterlessConstructorOrDataContract(INamedTypeSymbol type)
+ {
+ if (HasAttribute(type, "DataContractAttribute", "DataContract"))
+ {
+ return true;
+ }
+
+ var constructors = type.Constructors;
+
+ if (!constructors.Any() && type.TypeKind == TypeKind.Class)
+ {
+ return true;
+ }
+
+ return constructors.Any(c =>
+ c.DeclaredAccessibility == Accessibility.Public &&
+ c.Parameters.Length == 0);
+ }
+}
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorSerializationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorSerializationCodeFixProvider.cs
new file mode 100644
index 000000000..622d3f8c7
--- /dev/null
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorSerializationCodeFixProvider.cs
@@ -0,0 +1,338 @@
+// ------------------------------------------------------------------------
+// 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 code fixes for Actor serialization diagnostics.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorSerializationCodeFixProvider)), Shared]
+public sealed class ActorSerializationCodeFixProvider : CodeFixProvider
+{
+ ///
+ public override ImmutableArray FixableDiagnosticIds =>
+ ImmutableArray.Create(
+ ActorSerializationAnalyzer.ActorInterfaceMissingIActor.Id,
+ ActorSerializationAnalyzer.EnumMissingEnumMemberAttribute.Id,
+ ActorSerializationAnalyzer.WeaklyTypedActorJsonPropertyRecommendation.Id,
+ ActorSerializationAnalyzer.ComplexTypeInActorNeedsAttributes.Id,
+ "DAPR1409",
+ "DAPR1410",
+ ActorSerializationAnalyzer.RecordTypeNeedsDataContractAttributes.Id
+ );
+
+ ///
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ ///
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root == null)
+ {
+ return;
+ }
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+ var node = root.FindNode(diagnosticSpan);
+
+ switch (diagnostic.Id)
+ {
+ case "DAPR1405":
+ RegisterAddIActorInterfaceFix(context, root, node, diagnostic);
+ break;
+
+ case "DAPR1406":
+ RegisterAddEnumMemberFix(context, root, node, diagnostic);
+ break;
+
+ case "DAPR1407":
+ RegisterAddJsonPropertyNameFix(context, root, node, diagnostic);
+ break;
+
+ case "DAPR1408":
+ case "DAPR1409":
+ case "DAPR1410":
+ RegisterAddDataContractFix(context, root, node, diagnostic);
+ break;
+
+ case "DAPR1412":
+ RegisterAddRecordDataContractFix(context, root, node, diagnostic);
+ break;
+ }
+ }
+ }
+
+ private static void RegisterAddIActorInterfaceFix(CodeFixContext context, SyntaxNode root, SyntaxNode node, Diagnostic diagnostic)
+ {
+ if (node is not InterfaceDeclarationSyntax interfaceDeclaration)
+ {
+ return;
+ }
+
+ var action = CodeAction.Create(
+ title: "Add IActor inheritance",
+ createChangedDocument: c => AddIActorInheritance(context.Document, root, interfaceDeclaration, c),
+ equivalenceKey: "AddIActor");
+
+ context.RegisterCodeFix(action, diagnostic);
+ }
+
+ private static void RegisterAddEnumMemberFix(CodeFixContext context, SyntaxNode root, SyntaxNode node, Diagnostic diagnostic)
+ {
+ if (node is not EnumMemberDeclarationSyntax enumMemberDeclaration)
+ {
+ return;
+ }
+
+ var action = CodeAction.Create(
+ title: "Add [EnumMember] attribute",
+ createChangedDocument: c => AddEnumMemberAttribute(context.Document, root, enumMemberDeclaration, c),
+ equivalenceKey: "AddEnumMember");
+
+ context.RegisterCodeFix(action, diagnostic);
+ }
+
+ private static void RegisterAddJsonPropertyNameFix(CodeFixContext context, SyntaxNode root, SyntaxNode node, Diagnostic diagnostic)
+ {
+ if (node is not PropertyDeclarationSyntax propertyDeclaration)
+ {
+ return;
+ }
+
+ var action = CodeAction.Create(
+ title: "Add [JsonPropertyName] attribute",
+ createChangedDocument: c => AddJsonPropertyNameAttribute(context.Document, root, propertyDeclaration, c),
+ equivalenceKey: "AddJsonPropertyName");
+
+ context.RegisterCodeFix(action, diagnostic);
+ }
+
+ private static void RegisterAddDataContractFix(CodeFixContext context, SyntaxNode root, SyntaxNode node, Diagnostic diagnostic)
+ {
+ if (node is ClassDeclarationSyntax classDeclaration)
+ {
+ var action = CodeAction.Create(
+ title: "Add [DataContract] attribute",
+ createChangedDocument: c => AddDataContractAttribute(context.Document, root, classDeclaration, c),
+ equivalenceKey: "AddDataContract");
+
+ context.RegisterCodeFix(action, diagnostic);
+ }
+ else if (node is StructDeclarationSyntax structDeclaration)
+ {
+ var action = CodeAction.Create(
+ title: "Add [DataContract] attribute",
+ createChangedDocument: c => AddDataContractAttributeToStruct(context.Document, root, structDeclaration, c),
+ equivalenceKey: "AddDataContractStruct");
+
+ context.RegisterCodeFix(action, diagnostic);
+ }
+ }
+
+ private static void RegisterAddRecordDataContractFix(CodeFixContext context, SyntaxNode root, SyntaxNode node, Diagnostic diagnostic)
+ {
+ if (node is RecordDeclarationSyntax recordDeclaration)
+ {
+ var action = CodeAction.Create(
+ title: "Add [DataContract] and [DataMember] attributes",
+ createChangedDocument: c => AddDataContractToRecord(context.Document, root, recordDeclaration, c),
+ equivalenceKey: "AddDataContractRecord");
+
+ context.RegisterCodeFix(action, diagnostic);
+ }
+ else if (node is ParameterSyntax parameter)
+ {
+ var parentRecord = parameter.Ancestors().OfType().FirstOrDefault();
+ if (parentRecord != null)
+ {
+ var action = CodeAction.Create(
+ title: "Add [DataMember] attribute to parameter",
+ createChangedDocument: c => AddDataMemberToRecordParameter(context.Document, root, parameter, c),
+ equivalenceKey: "AddDataMemberParameter");
+
+ context.RegisterCodeFix(action, diagnostic);
+ }
+ }
+ }
+
+ private static Task AddIActorInheritance(Document document, SyntaxNode root, InterfaceDeclarationSyntax interfaceDeclaration, CancellationToken cancellationToken)
+ {
+ var iactorType = SyntaxFactory.SimpleBaseType(SyntaxFactory.IdentifierName("IActor"));
+
+ BaseListSyntax baseList;
+ if (interfaceDeclaration.BaseList == null)
+ {
+ baseList = SyntaxFactory.BaseList(SyntaxFactory.SingletonSeparatedList(iactorType));
+ }
+ else
+ {
+ baseList = interfaceDeclaration.BaseList.AddTypes(iactorType);
+ }
+
+ var newInterfaceDeclaration = interfaceDeclaration.WithBaseList(baseList);
+ var newRoot = root.ReplaceNode(interfaceDeclaration, newInterfaceDeclaration);
+
+ newRoot = AddUsingIfMissing(newRoot, "Dapr.Actors");
+
+ return Task.FromResult(document.WithSyntaxRoot(newRoot));
+ }
+
+ private static Task AddEnumMemberAttribute(Document document, SyntaxNode root, EnumMemberDeclarationSyntax enumMemberDeclaration, CancellationToken cancellationToken)
+ {
+ var enumMemberAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("EnumMember"));
+ var attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(enumMemberAttribute));
+
+ var newEnumMemberDeclaration = enumMemberDeclaration.AddAttributeLists(attributeList);
+ var newRoot = root.ReplaceNode(enumMemberDeclaration, newEnumMemberDeclaration);
+
+ newRoot = AddUsingIfMissing(newRoot, "System.Runtime.Serialization");
+
+ return Task.FromResult(document.WithSyntaxRoot(newRoot));
+ }
+
+ private static Task AddJsonPropertyNameAttribute(Document document, SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration, CancellationToken cancellationToken)
+ {
+ var propertyName = propertyDeclaration.Identifier.ValueText;
+ if (string.IsNullOrEmpty(propertyName))
+ {
+ return Task.FromResult(document);
+ }
+
+ var camelCaseName = char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1);
+
+ var jsonPropertyNameAttribute = SyntaxFactory.Attribute(
+ SyntaxFactory.IdentifierName("JsonPropertyName"),
+ SyntaxFactory.AttributeArgumentList(
+ SyntaxFactory.SingletonSeparatedList(
+ SyntaxFactory.AttributeArgument(
+ SyntaxFactory.LiteralExpression(
+ SyntaxKind.StringLiteralExpression,
+ SyntaxFactory.Literal(camelCaseName))))));
+
+ var attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(jsonPropertyNameAttribute));
+
+ var newPropertyDeclaration = propertyDeclaration.AddAttributeLists(attributeList);
+ var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration);
+
+ newRoot = AddUsingIfMissing(newRoot, "System.Text.Json.Serialization");
+
+ return Task.FromResult(document.WithSyntaxRoot(newRoot));
+ }
+
+ private static Task AddDataContractAttribute(Document document, SyntaxNode root, ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
+ {
+ var dataContractAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DataContract"));
+ var attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(dataContractAttribute));
+
+ var newClassDeclaration = classDeclaration.AddAttributeLists(attributeList);
+ var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration);
+
+ newRoot = AddUsingIfMissing(newRoot, "System.Runtime.Serialization");
+
+ return Task.FromResult(document.WithSyntaxRoot(newRoot));
+ }
+
+ private static Task AddDataContractAttributeToStruct(Document document, SyntaxNode root, StructDeclarationSyntax structDeclaration, CancellationToken cancellationToken)
+ {
+ var dataContractAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DataContract"));
+ var attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(dataContractAttribute));
+
+ var newStructDeclaration = structDeclaration.AddAttributeLists(attributeList);
+ var newRoot = root.ReplaceNode(structDeclaration, newStructDeclaration);
+
+ newRoot = AddUsingIfMissing(newRoot, "System.Runtime.Serialization");
+
+ return Task.FromResult(document.WithSyntaxRoot(newRoot));
+ }
+
+ private static Task AddDataContractToRecord(Document document, SyntaxNode root, RecordDeclarationSyntax recordDeclaration, CancellationToken cancellationToken)
+ {
+ var newRoot = root;
+
+ var dataContractAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DataContract"));
+ var dataContractAttributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(dataContractAttribute));
+
+ var newRecordDeclaration = recordDeclaration.AddAttributeLists(dataContractAttributeList);
+
+ if (recordDeclaration.ParameterList != null)
+ {
+ var newParameters = new List();
+
+ foreach (var parameter in recordDeclaration.ParameterList.Parameters)
+ {
+ if (!parameter.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString().Contains("DataMember"))))
+ {
+ var dataMemberAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DataMember"));
+ var dataMemberAttributeList = SyntaxFactory.AttributeList(
+ SyntaxFactory.SingletonSeparatedList(dataMemberAttribute))
+ .WithTarget(SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.PropertyKeyword)));
+
+ newParameters.Add(parameter.AddAttributeLists(dataMemberAttributeList));
+ }
+ else
+ {
+ newParameters.Add(parameter);
+ }
+ }
+
+ var newParameterList = SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(newParameters));
+ newRecordDeclaration = newRecordDeclaration.WithParameterList(newParameterList);
+ }
+
+ newRoot = newRoot.ReplaceNode(recordDeclaration, newRecordDeclaration);
+
+ newRoot = AddUsingIfMissing(newRoot, "System.Runtime.Serialization");
+
+ return Task.FromResult(document.WithSyntaxRoot(newRoot));
+ }
+
+ private static Task AddDataMemberToRecordParameter(Document document, SyntaxNode root, ParameterSyntax parameter, CancellationToken cancellationToken)
+ {
+ var dataMemberAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DataMember"));
+ var attributeList = SyntaxFactory.AttributeList(
+ SyntaxFactory.SingletonSeparatedList(dataMemberAttribute))
+ .WithTarget(SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.PropertyKeyword)));
+
+ var newParameter = parameter.AddAttributeLists(attributeList);
+ var newRoot = root.ReplaceNode(parameter, newParameter);
+
+ newRoot = AddUsingIfMissing(newRoot, "System.Runtime.Serialization");
+
+ return Task.FromResult(document.WithSyntaxRoot(newRoot));
+ }
+
+ private static SyntaxNode AddUsingIfMissing(SyntaxNode root, string namespaceName)
+ {
+ if (root is CompilationUnitSyntax compilationUnit && !HasUsingDirective(compilationUnit, namespaceName))
+ {
+ var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(namespaceName));
+ return compilationUnit.AddUsings(usingDirective);
+ }
+
+ return root;
+ }
+
+ private static bool HasUsingDirective(CompilationUnitSyntax compilationUnit, string namespaceName) =>
+ compilationUnit.Usings.Any(u => u.Name?.ToString() == namespaceName);
+}
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md
index b96f00659..44f7cd748 100644
--- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md
@@ -1 +1,16 @@
-; Unshipped analyzer release
\ No newline at end of file
+; Unshipped analyzer release
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+DAPR1405 | Usage | Error | Actor interface should inherit from IActor.
+DAPR1406 | Usage | Warning | Enum members in Actor types should use EnumMember attribute.
+DAPR1407 | Usage | Info | Consider using JsonPropertyName for property name consistency.
+DAPR1408 | Usage | Warning | Complex types used in Actor methods need serialization attributes.
+DAPR1409 | Usage | Warning | Actor method parameter needs proper serialization attributes.
+DAPR1410 | Usage | Warning | Actor method return type needs proper serialization attributes.
+DAPR1411 | Usage | Warning | Collection types in Actor methods need element type validation.
+DAPR1412 | Usage | Warning | Record types should use DataContract and DataMember attributes for Actor serialization.
+DAPR1413 | Usage | Error | Actor class implementation should implement an interface that inherits from IActor.
+DAPR1414 | Usage | Error | All types must either expose a public parameterless constructor or be decorated with the DataContractAttribute attribute.
\ No newline at end of file
diff --git a/test/Dapr.Actors.Analyzers.Test/ActorSerializationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorSerializationAnalyzerTests.cs
new file mode 100644
index 000000000..17c1b6a87
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Test/ActorSerializationAnalyzerTests.cs
@@ -0,0 +1,337 @@
+// ------------------------------------------------------------------------
+// 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.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 ActorSerializationAnalyzerTests
+{
+#if NET8_0
+ private static readonly ReferenceAssemblies assemblies = ReferenceAssemblies.Net.Net80;
+#elif NET9_0
+ private static readonly ReferenceAssemblies assemblies = ReferenceAssemblies.Net.Net90;
+#elif NET10_0
+ private static readonly ReferenceAssemblies assemblies = ReferenceAssemblies.Net.Net100;
+#endif
+
+ private static CSharpAnalyzerTest CreateTest() =>
+ new()
+ {
+ ReferenceAssemblies = assemblies.AddPackages([new("Dapr.Actors", "1.16.1")])
+ };
+
+ [Fact]
+ public async Task ActorInterface_WithoutIActor_ShouldReportDAPR1405()
+ {
+ var context = CreateTest();
+ context.TestCode = """
+ using System.Threading.Tasks;
+ using Dapr.Actors;
+ using Dapr.Actors.Runtime;
+
+ public interface ITestActor
+ {
+ Task GetDataAsync();
+ }
+
+ public class TestActor : Actor, ITestActor
+ {
+ public TestActor(ActorHost host) : base(host) { }
+ public Task GetDataAsync() => Task.FromResult("data");
+ }
+ """;
+
+ // From AnalyzeInterfaceDeclaration: interface identifier location
+ context.ExpectedDiagnostics.Add(
+ new DiagnosticResult(ActorSerializationAnalyzer.ActorInterfaceMissingIActor)
+ .WithSpan(5, 18, 5, 28)
+ .WithArguments("ITestActor"));
+
+ // From CheckActorInterfaces: class identifier location
+ context.ExpectedDiagnostics.Add(
+ new DiagnosticResult(ActorSerializationAnalyzer.ActorInterfaceMissingIActor)
+ .WithSpan(10, 14, 10, 23)
+ .WithArguments("ITestActor"));
+
+ // From CheckActorClassImplementsIActorInterface: class identifier location
+ context.ExpectedDiagnostics.Add(
+ new DiagnosticResult(ActorSerializationAnalyzer.ActorClassMissingInterface)
+ .WithSpan(10, 14, 10, 23)
+ .WithArguments("TestActor"));
+
+ await context.RunAsync();
+ }
+
+ [Fact]
+ public async Task ActorInterface_WithIActor_ShouldNotReportDAPR1405()
+ {
+ var context = CreateTest();
+ context.TestCode = """
+ using System.Threading.Tasks;
+ using Dapr.Actors;
+ using Dapr.Actors.Runtime;
+
+ public interface ITestActor : IActor
+ {
+ Task GetDataAsync();
+ }
+
+ public class TestActor : Actor, ITestActor
+ {
+ public TestActor(ActorHost host) : base(host) { }
+ public Task GetDataAsync() => Task.FromResult("data");
+ }
+ """;
+
+ context.ExpectedDiagnostics.Clear();
+ await context.RunAsync();
+ }
+
+ [Fact]
+ public async Task EnumWithoutEnumMember_ShouldReportDAPR1406()
+ {
+ var context = CreateTest();
+ context.TestCode = """
+ using System.Threading.Tasks;
+ using Dapr.Actors.Runtime;
+
+ namespace Test
+ {
+ public enum TestEnum
+ {
+ Value1,
+ Value2
+ }
+ }
+ """;
+
+ context.ExpectedDiagnostics.Add(
+ new DiagnosticResult(ActorSerializationAnalyzer.EnumMissingEnumMemberAttribute)
+ .WithSpan(8, 9, 8, 15)
+ .WithArguments("Value1", "TestEnum"));
+
+ context.ExpectedDiagnostics.Add(
+ new DiagnosticResult(ActorSerializationAnalyzer.EnumMissingEnumMemberAttribute)
+ .WithSpan(9, 9, 9, 15)
+ .WithArguments("Value2", "TestEnum"));
+
+ await context.RunAsync();
+ }
+
+ [Fact]
+ public async Task EnumWithEnumMember_ShouldNotReportDAPR1406()
+ {
+ var context = CreateTest();
+ context.TestCode = """
+ using System.Runtime.Serialization;
+
+ namespace Test
+ {
+ public enum Season
+ {
+ [EnumMember]
+ Spring,
+ [EnumMember]
+ Summer
+ }
+ }
+ """;
+
+ context.ExpectedDiagnostics.Clear();
+ await context.RunAsync();
+ }
+
+ [Fact]
+ public async Task ActorMethodWithComplexParameter_ShouldReportDAPR1409()
+ {
+ var context = CreateTest();
+ context.TestCode = """
+ using System.Threading.Tasks;
+ using Dapr.Actors;
+ using Dapr.Actors.Runtime;
+
+ public class ComplexType
+ {
+ public string Name { get; set; } = string.Empty;
+ }
+
+ public interface ITestActor : IActor
+ {
+ Task ProcessDataAsync(ComplexType data);
+ }
+
+ public class TestActor : Actor, ITestActor
+ {
+ public TestActor(ActorHost host) : base(host) { }
+ public Task ProcessDataAsync(ComplexType data) => Task.CompletedTask;
+ }
+ """;
+
+ context.ExpectedDiagnostics.Add(
+ new DiagnosticResult(ActorSerializationAnalyzer.ActorMethodParameterNeedsValidation)
+ .WithSpan(18, 46, 18, 50)
+ .WithArguments("data", "ComplexType", "ProcessDataAsync"));
+
+ await context.RunAsync();
+ }
+
+ [Fact]
+ public async Task ActorMethodWithComplexReturnType_ShouldReportDAPR1410()
+ {
+ var context = CreateTest();
+ context.TestCode = """
+ using System.Threading.Tasks;
+ using Dapr.Actors;
+ using Dapr.Actors.Runtime;
+
+ public class ComplexResult
+ {
+ public string Value { get; set; } = string.Empty;
+ }
+
+ public interface ITestActor : IActor
+ {
+ Task GetResultAsync();
+ }
+
+ public class TestActor : Actor, ITestActor
+ {
+ public TestActor(ActorHost host) : base(host) { }
+ public Task GetResultAsync() => Task.FromResult(new ComplexResult());
+ }
+ """;
+
+ context.ExpectedDiagnostics.Add(
+ new DiagnosticResult(ActorSerializationAnalyzer.ActorMethodReturnTypeNeedsValidation)
+ .WithSpan(18, 32, 18, 46)
+ .WithArguments("ComplexResult", "GetResultAsync"));
+
+ await context.RunAsync();
+ }
+
+ [Fact]
+ public async Task ActorMethodWithDataContractType_ShouldNotReportDAPR1409()
+ {
+ var context = CreateTest();
+ context.TestCode = """
+ using System.Runtime.Serialization;
+ using System.Threading.Tasks;
+ using Dapr.Actors;
+ using Dapr.Actors.Runtime;
+
+ [DataContract]
+ public class ComplexType
+ {
+ [DataMember]
+ public string Name { get; set; } = string.Empty;
+ }
+
+ public interface ITestActor : IActor
+ {
+ Task ProcessDataAsync(ComplexType data);
+ }
+
+ public class TestActor : Actor, ITestActor
+ {
+ public TestActor(ActorHost host) : base(host) { }
+ public Task ProcessDataAsync(ComplexType data) => Task.CompletedTask;
+ }
+ """;
+
+ context.ExpectedDiagnostics.Clear();
+ await context.RunAsync();
+ }
+
+ [Fact]
+ public async Task ActorMethodWithPrimitiveTypes_ShouldNotReportDiagnostics()
+ {
+ var context = CreateTest();
+ context.TestCode = """
+ using System.Threading.Tasks;
+ using Dapr.Actors;
+ using Dapr.Actors.Runtime;
+
+ public interface ITestActor : IActor
+ {
+ Task ProcessAsync(string input, int count);
+ }
+
+ public class TestActor : Actor, ITestActor
+ {
+ public TestActor(ActorHost host) : base(host) { }
+ public Task ProcessAsync(string input, int count) => Task.FromResult(input);
+ }
+ """;
+
+ context.ExpectedDiagnostics.Clear();
+ await context.RunAsync();
+ }
+
+ [Fact]
+ public async Task ActorClassWithoutIActorInterface_ShouldReportDAPR1413()
+ {
+ var context = CreateTest();
+ context.TestCode = """
+ using System.Threading.Tasks;
+ using Dapr.Actors.Runtime;
+
+ public class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host) { }
+ public Task GetDataAsync() => Task.FromResult("data");
+ }
+ """;
+
+ context.ExpectedDiagnostics.Add(
+ new DiagnosticResult(ActorSerializationAnalyzer.ActorClassMissingInterface)
+ .WithSpan(4, 14, 4, 23)
+ .WithArguments("TestActor"));
+
+ await context.RunAsync();
+ }
+
+ [Fact]
+ public async Task RecordWithoutDataContract_ShouldReportDAPR1412()
+ {
+ var context = CreateTest();
+ context.TestCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Dapr.Actors;
+ using Dapr.Actors.Runtime;
+
+ public record Doodad(Guid Id, string Name);
+
+ public interface IDoodadActor : IActor
+ {
+ Task GetDoodadAsync();
+ }
+
+ public class DoodadActor : Actor, IDoodadActor
+ {
+ public DoodadActor(ActorHost host) : base(host) { }
+ public Task GetDoodadAsync() => Task.FromResult(new Doodad(Guid.NewGuid(), "test"));
+ }
+ """;
+
+ context.ExpectedDiagnostics.Add(
+ new DiagnosticResult(ActorSerializationAnalyzer.RecordTypeNeedsDataContractAttributes)
+ .WithSpan(16, 25, 16, 39)
+ .WithArguments("Doodad"));
+
+ await context.RunAsync();
+ }
+}