diff --git a/src/BlazorWebFormsComponents.Analyzers.Test/AllAnalyzersIntegrationTests.cs b/src/BlazorWebFormsComponents.Analyzers.Test/AllAnalyzersIntegrationTests.cs
index 6f999cc63..baf285672 100644
--- a/src/BlazorWebFormsComponents.Analyzers.Test/AllAnalyzersIntegrationTests.cs
+++ b/src/BlazorWebFormsComponents.Analyzers.Test/AllAnalyzersIntegrationTests.cs
@@ -41,6 +41,7 @@ public class AllAnalyzersIntegrationTests
"BWFC021", // FindControlUsageAnalyzer
"BWFC022", // PageClientScriptUsageAnalyzer
"BWFC023", // IPostBackEventHandlerUsageAnalyzer
+ "BWFC025", // NonSerializableViewStateAnalyzer
};
#region ID Registration and Uniqueness
diff --git a/src/BlazorWebFormsComponents.Analyzers.Test/IsPostBackUsageAnalyzerTests.cs b/src/BlazorWebFormsComponents.Analyzers.Test/IsPostBackUsageAnalyzerTests.cs
index b3598ee29..b48730bac 100644
--- a/src/BlazorWebFormsComponents.Analyzers.Test/IsPostBackUsageAnalyzerTests.cs
+++ b/src/BlazorWebFormsComponents.Analyzers.Test/IsPostBackUsageAnalyzerTests.cs
@@ -27,7 +27,7 @@ protected void DoInit() { }
";
private static DiagnosticResult ExpectBWFC003(string memberName) =>
- new DiagnosticResult(IsPostBackUsageAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
+ new DiagnosticResult(IsPostBackUsageAnalyzer.DiagnosticId, DiagnosticSeverity.Info)
.WithArguments(memberName);
#region Positive cases — BWFC003 SHOULD fire
diff --git a/src/BlazorWebFormsComponents.Analyzers.Test/NonSerializableViewStateAnalyzerTests.cs b/src/BlazorWebFormsComponents.Analyzers.Test/NonSerializableViewStateAnalyzerTests.cs
new file mode 100644
index 000000000..f7596798c
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Analyzers.Test/NonSerializableViewStateAnalyzerTests.cs
@@ -0,0 +1,217 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace BlazorWebFormsComponents.Analyzers.Test;
+
+using AnalyzerTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest<
+ NonSerializableViewStateAnalyzer,
+ DefaultVerifier>;
+
+///
+/// Tests for BWFC025: ViewState value may not be JSON-serializable.
+///
+public class NonSerializableViewStateAnalyzerTests
+{
+ private const string StubSource = @"
+using System;
+using System.Collections.Generic;
+
+public class ViewStateDictionary : Dictionary
+{
+ public void Set(string key, T value) { this[key] = value; }
+}
+
+public class PageBase
+{
+ public ViewStateDictionary ViewState { get; } = new ViewStateDictionary();
+}
+";
+
+ private static DiagnosticResult ExpectBWFC025(string typeName) =>
+ new DiagnosticResult(NonSerializableViewStateAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
+ .WithArguments(typeName);
+
+ #region Negative cases — BWFC025 should NOT fire
+
+ [Fact]
+ public async Task ViewState_AssignString_NoDiagnostic()
+ {
+ var source = @"
+public class MyPage : PageBase
+{
+ public void Page_Load()
+ {
+ ViewState[""key""] = ""hello"";
+ }
+}";
+
+ var test = new AnalyzerTest
+ {
+ TestState = { Sources = { source, StubSource } }
+ };
+ await test.RunAsync();
+ }
+
+ [Fact]
+ public async Task ViewState_AssignInt_NoDiagnostic()
+ {
+ var source = @"
+public class MyPage : PageBase
+{
+ public void Page_Load()
+ {
+ ViewState[""key""] = 42;
+ }
+}";
+
+ var test = new AnalyzerTest
+ {
+ TestState = { Sources = { source, StubSource } }
+ };
+ await test.RunAsync();
+ }
+
+ [Fact]
+ public async Task ViewState_AssignBool_NoDiagnostic()
+ {
+ var source = @"
+public class MyPage : PageBase
+{
+ public void Page_Load()
+ {
+ ViewState[""key""] = true;
+ }
+}";
+
+ var test = new AnalyzerTest
+ {
+ TestState = { Sources = { source, StubSource } }
+ };
+ await test.RunAsync();
+ }
+
+ [Fact]
+ public async Task ViewState_SetGeneric_String_NoDiagnostic()
+ {
+ var source = @"
+public class MyPage : PageBase
+{
+ public void Page_Load()
+ {
+ ViewState.Set(""key"", ""val"");
+ }
+}";
+
+ var test = new AnalyzerTest
+ {
+ TestState = { Sources = { source, StubSource } }
+ };
+ await test.RunAsync();
+ }
+
+ #endregion
+
+ #region Positive cases — BWFC025 SHOULD fire
+
+ [Fact]
+ public async Task ViewState_AssignDisposable_Diagnostic()
+ {
+ var source = @"
+using System.IO;
+
+public class MyPage : PageBase
+{
+ public void Page_Load()
+ {
+ {|#0:ViewState[""key""] = new MemoryStream()|};
+ }
+}";
+
+ var test = new AnalyzerTest
+ {
+ TestState = { Sources = { source, StubSource } },
+ ExpectedDiagnostics = { ExpectBWFC025("System.IO.MemoryStream").WithLocation(0) }
+ };
+ await test.RunAsync();
+ }
+
+ [Fact]
+ public async Task ViewState_AssignDataTable_Diagnostic()
+ {
+ // Use a minimal stub for DataTable since System.Data is not referenced
+ var dataStub = @"
+namespace System.Data
+{
+ public class DataTable
+ {
+ public string TableName { get; set; }
+ }
+}
+";
+
+ var source = @"
+using System.Data;
+
+public class MyPage : PageBase
+{
+ public void Page_Load()
+ {
+ {|#0:ViewState[""key""] = new DataTable()|};
+ }
+}";
+
+ var test = new AnalyzerTest
+ {
+ TestState = { Sources = { source, StubSource, dataStub } },
+ ExpectedDiagnostics = { ExpectBWFC025("System.Data.DataTable").WithLocation(0) }
+ };
+ await test.RunAsync();
+ }
+
+ [Fact]
+ public async Task ViewState_AssignDelegate_Diagnostic()
+ {
+ var source = @"
+using System;
+
+public class MyPage : PageBase
+{
+ public void Page_Load()
+ {
+ Action callback = () => { };
+ {|#0:ViewState[""key""] = callback|};
+ }
+}";
+
+ var test = new AnalyzerTest
+ {
+ TestState = { Sources = { source, StubSource } },
+ ExpectedDiagnostics = { ExpectBWFC025("System.Action").WithLocation(0) }
+ };
+ await test.RunAsync();
+ }
+
+ [Fact]
+ public async Task ThisViewState_AssignDisposable_Diagnostic()
+ {
+ var source = @"
+using System.IO;
+
+public class MyPage : PageBase
+{
+ public void Page_Load()
+ {
+ {|#0:this.ViewState[""key""] = new MemoryStream()|};
+ }
+}";
+
+ var test = new AnalyzerTest
+ {
+ TestState = { Sources = { source, StubSource } },
+ ExpectedDiagnostics = { ExpectBWFC025("System.IO.MemoryStream").WithLocation(0) }
+ };
+ await test.RunAsync();
+ }
+
+ #endregion
+}
diff --git a/src/BlazorWebFormsComponents.Analyzers.Test/ViewStateUsageAnalyzerTests.cs b/src/BlazorWebFormsComponents.Analyzers.Test/ViewStateUsageAnalyzerTests.cs
index 088499000..e6d5523f9 100644
--- a/src/BlazorWebFormsComponents.Analyzers.Test/ViewStateUsageAnalyzerTests.cs
+++ b/src/BlazorWebFormsComponents.Analyzers.Test/ViewStateUsageAnalyzerTests.cs
@@ -27,7 +27,7 @@ public class PageBase
";
private static DiagnosticResult ExpectBWFC002(string memberName) =>
- new DiagnosticResult(ViewStateUsageAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
+ new DiagnosticResult(ViewStateUsageAnalyzer.DiagnosticId, DiagnosticSeverity.Info)
.WithArguments(memberName);
#region Positive cases — BWFC002 SHOULD fire
diff --git a/src/BlazorWebFormsComponents.Analyzers/AnalyzerReleases.Unshipped.md b/src/BlazorWebFormsComponents.Analyzers/AnalyzerReleases.Unshipped.md
index f08e374e8..a3f26cae4 100644
--- a/src/BlazorWebFormsComponents.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/BlazorWebFormsComponents.Analyzers/AnalyzerReleases.Unshipped.md
@@ -3,8 +3,8 @@
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
BWFC001 | Usage | Warning | MissingParameterAttributeAnalyzer
-BWFC002 | Usage | Warning | ViewStateUsageAnalyzer
-BWFC003 | Usage | Warning | IsPostBackUsageAnalyzer
+BWFC002 | Usage | Info | ViewStateUsageAnalyzer (was Warning — ViewState now works as migration shim)
+BWFC003 | Usage | Info | IsPostBackUsageAnalyzer (was Warning — IsPostBack now works via BWFC shim)
BWFC004 | Usage | Warning | ResponseRedirectAnalyzer
BWFC005 | Usage | Warning | SessionUsageAnalyzer
BWFC010 | Usage | Info | RequiredAttributeAnalyzer
@@ -16,3 +16,4 @@ BWFC020 | Migration | Info | ViewStatePropertyPatternAnalyzer
BWFC021 | Migration | Warning | FindControlUsageAnalyzer
BWFC022 | Migration | Warning | PageClientScriptUsageAnalyzer
BWFC023 | Migration | Warning | IPostBackEventHandlerUsageAnalyzer
+BWFC025 | Usage | Warning | NonSerializableViewStateAnalyzer
diff --git a/src/BlazorWebFormsComponents.Analyzers/IsPostBackUsageAnalyzer.cs b/src/BlazorWebFormsComponents.Analyzers/IsPostBackUsageAnalyzer.cs
index f6a5a480a..33f10e168 100644
--- a/src/BlazorWebFormsComponents.Analyzers/IsPostBackUsageAnalyzer.cs
+++ b/src/BlazorWebFormsComponents.Analyzers/IsPostBackUsageAnalyzer.cs
@@ -8,7 +8,7 @@ namespace BlazorWebFormsComponents.Analyzers
{
///
/// Analyzer that detects IsPostBack and Page.IsPostBack usage patterns.
- /// Blazor uses component lifecycle instead of the postback model.
+ /// IsPostBack works via the BWFC shim; suggests Blazor lifecycle methods for new code.
///
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class IsPostBackUsageAnalyzer : DiagnosticAnalyzer
@@ -16,8 +16,8 @@ public class IsPostBackUsageAnalyzer : DiagnosticAnalyzer
public const string DiagnosticId = "BWFC003";
private static readonly LocalizableString Title = "IsPostBack usage detected";
- private static readonly LocalizableString MessageFormat = "'{0}' checks IsPostBack \u2014 Blazor doesn't have postbacks. Use lifecycle methods (OnInitialized, OnParametersSet) instead.";
- private static readonly LocalizableString Description = "Flags IsPostBack and Page.IsPostBack checks. Blazor uses component lifecycle instead of postback model.";
+ private static readonly LocalizableString MessageFormat = "'{0}' checks IsPostBack \u2014 this works via the BWFC shim (SSR: checks HTTP POST, Interactive: tracks initialization). For new code, prefer OnInitialized lifecycle.";
+ private static readonly LocalizableString Description = "Flags IsPostBack and Page.IsPostBack checks. IsPostBack works via the BWFC shim during migration; prefer Blazor lifecycle methods for new code.";
private const string Category = "Usage";
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
@@ -25,7 +25,7 @@ public class IsPostBackUsageAnalyzer : DiagnosticAnalyzer
Title,
MessageFormat,
Category,
- DiagnosticSeverity.Warning,
+ DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: Description);
diff --git a/src/BlazorWebFormsComponents.Analyzers/NonSerializableViewStateAnalyzer.cs b/src/BlazorWebFormsComponents.Analyzers/NonSerializableViewStateAnalyzer.cs
new file mode 100644
index 000000000..dcb58edf4
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Analyzers/NonSerializableViewStateAnalyzer.cs
@@ -0,0 +1,151 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace BlazorWebFormsComponents.Analyzers
+{
+ ///
+ /// Analyzer that detects ViewState assignments storing types that are
+ /// unlikely to be JSON-serializable, which will fail at runtime in SSR mode.
+ ///
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class NonSerializableViewStateAnalyzer : DiagnosticAnalyzer
+ {
+ public const string DiagnosticId = "BWFC025";
+
+ private static readonly LocalizableString Title = "ViewState value may not be JSON-serializable";
+ private static readonly LocalizableString MessageFormat = "ViewState assignment stores '{0}' which may not be JSON-serializable. SSR ViewState uses System.Text.Json serialization \u2014 ensure the type has a parameterless constructor and public properties.";
+ private static readonly LocalizableString Description = "Detects ViewState assignments storing types that are unlikely to be JSON-serializable, such as IDisposable implementations, delegates, DataSet/DataTable, and System.Web types.";
+ private const string Category = "Usage";
+
+ private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
+ DiagnosticId,
+ Title,
+ MessageFormat,
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: Description);
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeAssignment, SyntaxKind.SimpleAssignmentExpression);
+ context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
+ }
+
+ private static void AnalyzeAssignment(SyntaxNodeAnalysisContext context)
+ {
+ var assignment = (AssignmentExpressionSyntax)context.Node;
+
+ // Check if left side is ViewState["key"] or this.ViewState["key"]
+ if (!(assignment.Left is ElementAccessExpressionSyntax elementAccess))
+ return;
+
+ if (!IsViewStateAccess(elementAccess))
+ return;
+
+ var typeInfo = context.SemanticModel.GetTypeInfo(assignment.Right, context.CancellationToken);
+ if (typeInfo.Type != null && IsLikelyNonSerializable(typeInfo.Type))
+ {
+ var diagnostic = Diagnostic.Create(Rule, assignment.GetLocation(), typeInfo.Type.ToDisplayString());
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
+ {
+ var invocation = (InvocationExpressionSyntax)context.Node;
+
+ // Check for ViewState.Set("key", value) or this.ViewState.Set("key", value)
+ if (!(invocation.Expression is MemberAccessExpressionSyntax memberAccess))
+ return;
+
+ if (memberAccess.Name.Identifier.Text != "Set")
+ return;
+
+ if (!IsViewStateExpression(memberAccess.Expression))
+ return;
+
+ // Get the type argument from Set
+ if (memberAccess.Name is GenericNameSyntax genericName &&
+ genericName.TypeArgumentList.Arguments.Count == 1)
+ {
+ var typeArg = genericName.TypeArgumentList.Arguments[0];
+ var typeSymbol = context.SemanticModel.GetTypeInfo(typeArg, context.CancellationToken).Type;
+ if (typeSymbol != null && IsLikelyNonSerializable(typeSymbol))
+ {
+ var diagnostic = Diagnostic.Create(Rule, invocation.GetLocation(), typeSymbol.ToDisplayString());
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ else if (invocation.ArgumentList.Arguments.Count >= 2)
+ {
+ // Non-generic Set("key", value) — check the value argument type
+ var valueArg = invocation.ArgumentList.Arguments[1];
+ var typeInfo = context.SemanticModel.GetTypeInfo(valueArg.Expression, context.CancellationToken);
+ if (typeInfo.Type != null && IsLikelyNonSerializable(typeInfo.Type))
+ {
+ var diagnostic = Diagnostic.Create(Rule, invocation.GetLocation(), typeInfo.Type.ToDisplayString());
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+
+ private static bool IsViewStateAccess(ElementAccessExpressionSyntax elementAccess)
+ {
+ return IsViewStateExpression(elementAccess.Expression);
+ }
+
+ private static bool IsViewStateExpression(ExpressionSyntax expr)
+ {
+ // ViewState
+ if (expr is IdentifierNameSyntax identifier)
+ return identifier.Identifier.Text == "ViewState";
+
+ // this.ViewState
+ if (expr is MemberAccessExpressionSyntax memberAccess)
+ return memberAccess.Name.Identifier.Text == "ViewState" &&
+ memberAccess.Expression is ThisExpressionSyntax;
+
+ return false;
+ }
+
+ private static bool IsLikelyNonSerializable(ITypeSymbol type)
+ {
+ // Delegates and event handlers are never serializable
+ if (type.TypeKind == TypeKind.Delegate)
+ return true;
+
+ // Check for IDisposable (DB connections, streams, etc.)
+ if (type.AllInterfaces.Any(i => i.Name == "IDisposable"))
+ return true;
+
+ var fullName = type.ToDisplayString();
+
+ // System.Data types (DataSet, DataTable, etc.) — common Web Forms pattern, not JSON-friendly
+ if (fullName.StartsWith("System.Data."))
+ return true;
+
+ // System.Web types — not available in Blazor
+ if (fullName.StartsWith("System.Web."))
+ return true;
+
+ // Stream types
+ if (fullName.StartsWith("System.IO.Stream") || fullName == "System.IO.MemoryStream" || fullName == "System.IO.FileStream")
+ return true;
+
+ // Network types
+ if (fullName.StartsWith("System.Net."))
+ return true;
+
+ return false;
+ }
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Analyzers/ViewStatePropertyPatternAnalyzer.cs b/src/BlazorWebFormsComponents.Analyzers/ViewStatePropertyPatternAnalyzer.cs
index 877f3eb56..1e3959760 100644
--- a/src/BlazorWebFormsComponents.Analyzers/ViewStatePropertyPatternAnalyzer.cs
+++ b/src/BlazorWebFormsComponents.Analyzers/ViewStatePropertyPatternAnalyzer.cs
@@ -9,7 +9,7 @@ namespace BlazorWebFormsComponents.Analyzers
{
///
/// Analyzer that detects properties using ViewState for backing storage.
- /// These should be converted to [Parameter] properties for Blazor compatibility.
+ /// These work during migration; suggests converting to [Parameter] properties when ready.
///
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ViewStatePropertyPatternAnalyzer : DiagnosticAnalyzer
@@ -17,8 +17,8 @@ public class ViewStatePropertyPatternAnalyzer : DiagnosticAnalyzer
public const string DiagnosticId = "BWFC020";
private static readonly LocalizableString Title = "ViewState-backed property detected";
- private static readonly LocalizableString MessageFormat = "Property '{0}' uses ViewState for storage. Convert to a [Parameter] property for Blazor compatibility.";
- private static readonly LocalizableString Description = "Properties that use ViewState for backing storage should be converted to auto-properties with [Parameter] for Blazor.";
+ private static readonly LocalizableString MessageFormat = "Property '{0}' uses ViewState for storage — this works during migration. Consider converting to a [Parameter] property or component field when ready to adopt native Blazor patterns.";
+ private static readonly LocalizableString Description = "Properties that use ViewState for backing storage work during migration. Consider converting to auto-properties with [Parameter] when adopting native Blazor patterns.";
private const string Category = "Migration";
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
diff --git a/src/BlazorWebFormsComponents.Analyzers/ViewStateUsageAnalyzer.cs b/src/BlazorWebFormsComponents.Analyzers/ViewStateUsageAnalyzer.cs
index 4bbe038c0..c512c6290 100644
--- a/src/BlazorWebFormsComponents.Analyzers/ViewStateUsageAnalyzer.cs
+++ b/src/BlazorWebFormsComponents.Analyzers/ViewStateUsageAnalyzer.cs
@@ -8,7 +8,7 @@ namespace BlazorWebFormsComponents.Analyzers
{
///
/// Analyzer that detects ViewState["key"] usage patterns.
- /// Blazor has no ViewState; suggest using component state instead.
+ /// ViewState works as a migration shim; suggests native Blazor state for new code.
///
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ViewStateUsageAnalyzer : DiagnosticAnalyzer
@@ -16,8 +16,8 @@ public class ViewStateUsageAnalyzer : DiagnosticAnalyzer
public const string DiagnosticId = "BWFC002";
private static readonly LocalizableString Title = "ViewState usage detected";
- private static readonly LocalizableString MessageFormat = "'{0}' uses ViewState \u2014 Blazor has no ViewState. Use component state (fields/properties) instead.";
- private static readonly LocalizableString Description = "Flags ViewState[\"key\"] patterns in code. Blazor has no ViewState; use component state instead.";
+ private static readonly LocalizableString MessageFormat = "'{0}' uses ViewState \u2014 this works as a migration shim via ViewStateDictionary. For new code, prefer component fields or [Parameter] properties.";
+ private static readonly LocalizableString Description = "Flags ViewState[\"key\"] patterns in code. ViewState works during migration via ViewStateDictionary; prefer native Blazor state for new code.";
private const string Category = "Usage";
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
@@ -25,7 +25,7 @@ public class ViewStateUsageAnalyzer : DiagnosticAnalyzer
Title,
MessageFormat,
Category,
- DiagnosticSeverity.Warning,
+ DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: Description);