diff --git a/src/Components/Analyzers/src/DiagnosticDescriptors.cs b/src/Components/Analyzers/src/DiagnosticDescriptors.cs
index 5f67edaf8447..5259cbd0ba6c 100644
--- a/src/Components/Analyzers/src/DiagnosticDescriptors.cs
+++ b/src/Components/Analyzers/src/DiagnosticDescriptors.cs
@@ -92,4 +92,13 @@ internal static class DiagnosticDescriptors
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: CreateLocalizableResourceString(nameof(Resources.PersistentStateShouldNotHavePropertyInitializer_Description)));
+
+ public static readonly DiagnosticDescriptor UseInvokeVoidAsyncForObjectReturn = new(
+ "BL0010",
+ CreateLocalizableResourceString(nameof(Resources.UseInvokeVoidAsyncForObjectReturn_Title)),
+ CreateLocalizableResourceString(nameof(Resources.UseInvokeVoidAsyncForObjectReturn_Format)),
+ Usage,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: CreateLocalizableResourceString(nameof(Resources.UseInvokeVoidAsyncForObjectReturn_Description)));
}
diff --git a/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs b/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs
new file mode 100644
index 000000000000..fa668db8357a
--- /dev/null
+++ b/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs
@@ -0,0 +1,166 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+#nullable enable
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+///
+/// Analyzer that detects usage of InvokeAsync<object> and recommends using InvokeVoidAsync instead.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class InvokeAsyncOfObjectAnalyzer : DiagnosticAnalyzer
+{
+ private const string JSRuntimeExtensionsTypeName = "Microsoft.JSInterop.JSRuntimeExtensions";
+ private const string JSObjectReferenceExtensionsTypeName = "Microsoft.JSInterop.JSObjectReferenceExtensions";
+ private const string JSInProcessRuntimeExtensionsTypeName = "Microsoft.JSInterop.JSInProcessRuntimeExtensions";
+ private const string JSInProcessObjectReferenceExtensionsTypeName = "Microsoft.JSInterop.JSInProcessObjectReferenceExtensions";
+
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+
+ context.RegisterCompilationStartAction(compilationContext =>
+ {
+ // Cache type lookups once per compilation
+ var ijsRuntimeType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSRuntime");
+ var ijsObjectReferenceType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSObjectReference");
+ var ijsInProcessRuntimeType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSInProcessRuntime");
+ var ijsInProcessObjectReferenceType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSInProcessObjectReference");
+ var jsRuntimeExtensionsType = compilationContext.Compilation.GetTypeByMetadataName(JSRuntimeExtensionsTypeName);
+ var jsObjectReferenceExtensionsType = compilationContext.Compilation.GetTypeByMetadataName(JSObjectReferenceExtensionsTypeName);
+ var jsInProcessRuntimeExtensionsType = compilationContext.Compilation.GetTypeByMetadataName(JSInProcessRuntimeExtensionsTypeName);
+ var jsInProcessObjectReferenceExtensionsType = compilationContext.Compilation.GetTypeByMetadataName(JSInProcessObjectReferenceExtensionsTypeName);
+ var objectType = compilationContext.Compilation.GetSpecialType(SpecialType.System_Object);
+
+ if (ijsRuntimeType is null && ijsObjectReferenceType is null)
+ {
+ // JSInterop types are not available
+ return;
+ }
+
+ compilationContext.RegisterOperationAction(operationContext =>
+ {
+ var invocation = (IInvocationOperation)operationContext.Operation;
+ var targetMethod = invocation.TargetMethod;
+
+ // Check if the method is named InvokeAsync and is generic
+ if (targetMethod.Name != "InvokeAsync" || !targetMethod.IsGenericMethod)
+ {
+ return;
+ }
+
+ // Check if the type argument is object
+ if (targetMethod.TypeArguments.Length != 1 ||
+ !SymbolEqualityComparer.Default.Equals(targetMethod.TypeArguments[0], objectType))
+ {
+ return;
+ }
+
+ // Check if the method is on IJSRuntime, IJSObjectReference, or their in-process variants
+ // This includes extension methods on these types
+ var containingType = targetMethod.ContainingType;
+ var receiverType = GetReceiverType(invocation);
+
+ if (!IsJSInteropType(receiverType, ijsRuntimeType, ijsObjectReferenceType, ijsInProcessRuntimeType, ijsInProcessObjectReferenceType) &&
+ !IsJSInteropExtensionClass(containingType, jsRuntimeExtensionsType, jsObjectReferenceExtensionsType, jsInProcessRuntimeExtensionsType, jsInProcessObjectReferenceExtensionsType))
+ {
+ return;
+ }
+
+ operationContext.ReportDiagnostic(Diagnostic.Create(
+ DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn,
+ invocation.Syntax.GetLocation()));
+ }, OperationKind.Invocation);
+ });
+ }
+
+ private static ITypeSymbol? GetReceiverType(IInvocationOperation invocation)
+ {
+ // For extension methods, the first argument is the receiver
+ if (invocation.TargetMethod.IsExtensionMethod && invocation.Arguments.Length > 0)
+ {
+ return invocation.Arguments[0].Value.Type;
+ }
+
+ // For instance methods
+ return invocation.Instance?.Type;
+ }
+
+ private static bool IsJSInteropType(
+ ITypeSymbol? type,
+ INamedTypeSymbol? ijsRuntimeType,
+ INamedTypeSymbol? ijsObjectReferenceType,
+ INamedTypeSymbol? ijsInProcessRuntimeType,
+ INamedTypeSymbol? ijsInProcessObjectReferenceType)
+ {
+ if (type is null)
+ {
+ return false;
+ }
+
+ // Check if the type implements any of the JSInterop interfaces
+ if (ijsRuntimeType is not null && ImplementsInterface(type, ijsRuntimeType))
+ {
+ return true;
+ }
+
+ if (ijsObjectReferenceType is not null && ImplementsInterface(type, ijsObjectReferenceType))
+ {
+ return true;
+ }
+
+ if (ijsInProcessRuntimeType is not null && ImplementsInterface(type, ijsInProcessRuntimeType))
+ {
+ return true;
+ }
+
+ if (ijsInProcessObjectReferenceType is not null && ImplementsInterface(type, ijsInProcessObjectReferenceType))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool ImplementsInterface(ITypeSymbol type, INamedTypeSymbol interfaceType)
+ {
+ if (SymbolEqualityComparer.Default.Equals(type, interfaceType))
+ {
+ return true;
+ }
+
+ foreach (var iface in type.AllInterfaces)
+ {
+ if (SymbolEqualityComparer.Default.Equals(iface, interfaceType))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool IsJSInteropExtensionClass(
+ INamedTypeSymbol containingType,
+ INamedTypeSymbol? jsRuntimeExtensionsType,
+ INamedTypeSymbol? jsObjectReferenceExtensionsType,
+ INamedTypeSymbol? jsInProcessRuntimeExtensionsType,
+ INamedTypeSymbol? jsInProcessObjectReferenceExtensionsType)
+ {
+ // Use symbol equality comparison instead of string comparison
+ return SymbolEqualityComparer.Default.Equals(containingType, jsRuntimeExtensionsType) ||
+ SymbolEqualityComparer.Default.Equals(containingType, jsObjectReferenceExtensionsType) ||
+ SymbolEqualityComparer.Default.Equals(containingType, jsInProcessRuntimeExtensionsType) ||
+ SymbolEqualityComparer.Default.Equals(containingType, jsInProcessObjectReferenceExtensionsType);
+ }
+}
diff --git a/src/Components/Analyzers/src/Resources.resx b/src/Components/Analyzers/src/Resources.resx
index 6a23211094aa..5c193fd5c376 100644
--- a/src/Components/Analyzers/src/Resources.resx
+++ b/src/Components/Analyzers/src/Resources.resx
@@ -198,4 +198,13 @@
Property with [PersistentState] should not have initializer
+
+ JavaScript interop calls that do not expect a return value should use InvokeVoidAsync instead of InvokeAsync<object>. Using InvokeAsync<object> may cause serialization issues with non-serializable JavaScript values.
+
+
+ Use 'InvokeVoidAsync' instead of 'InvokeAsync<object>'. Return values of type 'object' cannot be deserialized and may cause serialization errors if the JavaScript function returns a non-serializable value.
+
+
+ Use InvokeVoidAsync instead of InvokeAsync<object>
+
\ No newline at end of file
diff --git a/src/Components/Analyzers/test/InvokeAsyncOfObjectAnalyzerTest.cs b/src/Components/Analyzers/test/InvokeAsyncOfObjectAnalyzerTest.cs
new file mode 100644
index 000000000000..a8fa25afa05b
--- /dev/null
+++ b/src/Components/Analyzers/test/InvokeAsyncOfObjectAnalyzerTest.cs
@@ -0,0 +1,507 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TestHelper;
+
+namespace Microsoft.AspNetCore.Components.Analyzers.Test;
+
+public class InvokeAsyncOfObjectAnalyzerTest : DiagnosticVerifier
+{
+ protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() => new InvokeAsyncOfObjectAnalyzer();
+
+ private static readonly string JSInteropDeclarations = @"
+ namespace Microsoft.JSInterop
+ {
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ public interface IJSRuntime
+ {
+ ValueTask InvokeAsync(string identifier, object[] args);
+ ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args);
+ }
+
+ public interface IJSObjectReference : System.IAsyncDisposable
+ {
+ ValueTask InvokeAsync(string identifier, object[] args);
+ ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args);
+ }
+
+ public interface IJSInProcessRuntime : IJSRuntime
+ {
+ TValue Invoke(string identifier, params object[] args);
+ }
+
+ public interface IJSInProcessObjectReference : IJSObjectReference
+ {
+ TValue Invoke(string identifier, params object[] args);
+ }
+
+ public static class JSRuntimeExtensions
+ {
+ public static ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, params object[] args)
+ => default;
+ public static ValueTask InvokeAsync(this IJSRuntime jsRuntime, string identifier, params object[] args)
+ => default;
+ public static ValueTask InvokeAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args)
+ => default;
+ public static ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args)
+ => default;
+ }
+
+ public static class JSObjectReferenceExtensions
+ {
+ public static ValueTask InvokeVoidAsync(this IJSObjectReference jsObjectReference, string identifier, params object[] args)
+ => default;
+ public static ValueTask InvokeAsync(this IJSObjectReference jsObjectReference, string identifier, params object[] args)
+ => default;
+ public static ValueTask InvokeAsync(this IJSObjectReference jsObjectReference, string identifier, CancellationToken cancellationToken, params object[] args)
+ => default;
+ public static ValueTask InvokeVoidAsync(this IJSObjectReference jsObjectReference, string identifier, CancellationToken cancellationToken, params object[] args)
+ => default;
+ }
+ }
+";
+
+ [Fact]
+ public void NoDiagnosticForInvokeVoidAsync()
+ {
+ var test = @"
+ namespace ConsoleApplication1
+ {
+ using Microsoft.JSInterop;
+ using System.Threading.Tasks;
+
+ class TestClass
+ {
+ private IJSRuntime _jsRuntime;
+
+ public async Task TestMethod()
+ {
+ await _jsRuntime.InvokeVoidAsync(""myFunction"");
+ }
+ }
+ }" + JSInteropDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void NoDiagnosticForInvokeAsyncWithTypedReturn()
+ {
+ var test = @"
+ namespace ConsoleApplication1
+ {
+ using Microsoft.JSInterop;
+ using System.Threading.Tasks;
+
+ class TestClass
+ {
+ private IJSRuntime _jsRuntime;
+
+ public async Task TestMethod()
+ {
+ return await _jsRuntime.InvokeAsync(""myFunction"");
+ }
+ }
+ }" + JSInteropDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void DiagnosticForInvokeAsyncWithObjectReturn()
+ {
+ var test = @"
+ namespace ConsoleApplication1
+ {
+ using Microsoft.JSInterop;
+ using System.Threading.Tasks;
+
+ class TestClass
+ {
+ private IJSRuntime _jsRuntime;
+
+ public async Task TestMethod()
+ {
+ await _jsRuntime.InvokeAsync