Skip to content

Initialize PartialTagHelper.ViewData to support view-data-* prefix attributes#65577

Open
kubaflo wants to merge 1 commit intodotnet:mainfrom
kubaflo:fix/partial-taghelper-viewdata-null-9736
Open

Initialize PartialTagHelper.ViewData to support view-data-* prefix attributes#65577
kubaflo wants to merge 1 commit intodotnet:mainfrom
kubaflo:fix/partial-taghelper-viewdata-null-9736

Conversation

@kubaflo
Copy link

@kubaflo kubaflo commented Mar 1, 2026

🤖 AI Summary

🔍 Automated Fix Report
🔍 Pre-Flight — Context & Validation

Issue: #9736 — Partial Tag Helper view-data-* attributes have null ViewData

Area: src/Mvc/Mvc.TagHelpers/

Root Cause: PartialTagHelper.ViewData is initialized as a new ViewDataDictionary in the constructor, but it's not connected to the parent ViewContext.ViewData. When view-data-* attributes are set, they write into this disconnected dictionary, so the data never reaches the partial view.

Classification: Bug — initialization ordering issue


🧪 Test — Bug Reproduction

Test File: src/Mvc/Mvc.TagHelpers/test/PartialTagHelperTest.cs

Test Added: ProcessAsync_UsesViewDataFromViewContext_WhenViewDataPrefixAttributesSet — verifies that view-data-* prefix attributes are accessible to the partial view via ViewData.

Strategy: Set ViewContext on the tag helper, then add a view-data-message attribute, and verify the ViewData dictionary contains the expected key-value pair.


🚦 Gate — Test Verification & Regression

Gate Result: ✅ All 24 PartialTagHelper tests pass

Test Command:

dotnet test src/Mvc/Mvc.TagHelpers/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test.csproj --filter "FullyQualifiedName~PartialTagHelper" --no-restore -v q

Regression: No failures in existing test suite.


🔧 Fix — Analysis & Comparison (✅ 2 passed, ❌ 2 failed)

Fix: Ensure ViewData is initialized from ViewContext.ViewData before view-data-* prefix attributes are bound.

Attempt Approach Result
0 Initialize ViewData in ViewContext setter with null-guard ✅ Pass
1 Initialize ViewData at start of ProcessAsync ❌ Fail (too late, after attribute binding)
2 Override Init() to set ViewData ❌ Fail (public API surface issue)
3 Lazy-initialize ViewData in getter with ??= ✅ Pass
Attempt 0: PASS

Approach: Initialize ViewData in ViewContext setter with null-guard.

Added a _viewContext backing field. In the setter, when ViewData is null and ViewContext.ViewData is available, initialize ViewData = new ViewDataDictionary(value.ViewData). This ensures view-data-* attributes work since ViewContext is set during activation before markup attributes.

📄 Diff
diff --git a/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
index 890a92d6c2..de392d866b 100644
--- a/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
+++ b/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
@@ -25,6 +25,7 @@ public class PartialTagHelper : TagHelper
     private bool _hasModel;
     private bool _hasFor;
     private ModelExpression _for;
+    private ViewContext _viewContext;
 
     private readonly ICompositeViewEngine _viewEngine;
     private readonly IViewBufferScope _viewBufferScope;
@@ -98,7 +99,23 @@ public class PartialTagHelper : TagHelper
     /// </summary>
     [HtmlAttributeNotBound]
     [ViewContext]
-    public ViewContext ViewContext { get; set; }
+    public ViewContext ViewContext
+    {
+        get => _viewContext;
+        set
+        {
+            _viewContext = value;
+
+            // Initialize ViewData from the ViewContext so that view-data-* prefix
+            // attributes can be set without a NullReferenceException. When ViewData
+            // is explicitly set via the view-data attribute, the property initializer
+            // runs after ViewContext and overrides this value.
+            if (ViewData is null && value?.ViewData is not null)
+            {
+                ViewData = new ViewDataDictionary(value.ViewData);
+            }
+        }
+    }
 
     /// <inheritdoc />
     public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
Attempt 1: FAIL

Approach: Initialize ViewData at the start of ProcessAsync with ViewData ??= new ViewDataDictionary(ViewContext.ViewData).

This fails because view-data-* prefix attributes are bound to ViewData by the tag helper infrastructure before ProcessAsync is called. By the time we initialize it in ProcessAsync, the attribute values have already been lost.

📄 Diff
diff --git a/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
index 890a92d6c2..4021f73810 100644
--- a/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
+++ b/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
@@ -106,6 +106,10 @@ public class PartialTagHelper : TagHelper
         ArgumentNullException.ThrowIfNull(context);
         ArgumentNullException.ThrowIfNull(output);
 
+        // Ensure ViewData is initialized from ViewContext if not explicitly set via view-data attribute.
+        // This allows view-data-* prefix attributes to populate a connected dictionary.
+        ViewData ??= new ViewDataDictionary(ViewContext.ViewData);
+
         // Reset the TagName. We don't want `partial` to render.
         output.TagName = null;
 
Attempt 2: FAIL

Approach: Override Init(TagHelperContext) to initialize ViewData from ViewContext.

Init runs after activation (ViewContext set) but before attribute binding (view-data-* processed). However, this adds new public API surface requiring PublicAPI.Unshipped.txt updates, and the API analyzer signature mismatch was difficult to resolve.

📄 Diff
diff --git a/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
index 890a92d6c2..3c678d4652 100644
--- a/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
+++ b/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
@@ -100,6 +100,20 @@ public class PartialTagHelper : TagHelper
     [ViewContext]
     public ViewContext ViewContext { get; set; }
 
+    /// <inheritdoc />
+    public override void Init(TagHelperContext context)
+    {
+        base.Init(context);
+
+        // Initialize ViewData from ViewContext if not explicitly set.
+        // ViewContext is assigned during activation (before Init), so it's available here.
+        // view-data-* prefix attributes are bound after Init, writing into this dictionary.
+        if (ViewData is null && ViewContext?.ViewData is not null)
+        {
+            ViewData = new ViewDataDictionary(ViewContext.ViewData);
+        }
+    }
+
     /// <inheritdoc />
     public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
     {
Attempt 3: PASS

Approach: Lazy-initialize ViewData in its getter using ??= with ViewContext.ViewData.

Added _viewData backing field. ViewData getter uses ??= to lazily create a ViewDataDictionary from ViewContext.ViewData when first accessed. The setter allows explicit override. No changes to ViewContext property needed.

📄 Diff
diff --git a/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
index 890a92d6c2..e96d5686f1 100644
--- a/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
+++ b/src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
@@ -25,6 +25,7 @@ public class PartialTagHelper : TagHelper
     private bool _hasModel;
     private bool _hasFor;
     private ModelExpression _for;
+    private ViewDataDictionary _viewData;
 
     private readonly ICompositeViewEngine _viewEngine;
     private readonly IViewBufferScope _viewBufferScope;
@@ -91,7 +92,11 @@ public class PartialTagHelper : TagHelper
     /// <summary>
     /// A <see cref="ViewDataDictionary"/> to pass into the partial view.
     /// </summary>
-    public ViewDataDictionary ViewData { get; set; }
+    public ViewDataDictionary ViewData
+    {
+        get => _viewData ??= (ViewContext?.ViewData is not null ? new ViewDataDictionary(ViewContext.ViewData) : null);
+        set => _viewData = value;
+    }
 
     /// <summary>
     /// Gets the <see cref="Rendering.ViewContext"/> of the executing view.

Copilot AI review requested due to automatic review settings March 1, 2026 02:02
@kubaflo kubaflo requested review from a team and wtgodbe as code owners March 1, 2026 02:02
@github-actions github-actions bot added the area-infrastructure Includes: MSBuild projects/targets, build scripts, CI, Installers and shared framework label Mar 1, 2026
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Mar 1, 2026
@dotnet-policy-service
Copy link
Contributor

Hey @dotnet/aspnet-build, looks like this PR is something you want to take a look at.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a NullReferenceException in PartialTagHelper when using view-data-* prefixed attributes without explicitly providing view-data, by ensuring ViewData is initialized from the current ViewContext early enough for prefix-attribute binding.

Changes:

  • Initialize PartialTagHelper.ViewData from ViewContext.ViewData in the ViewContext setter when ViewData is still null.
  • Add MVC unit tests covering the regression and ensuring the auto-initialized ViewData doesn’t mutate the parent ViewContext.ViewData.
  • Add multiple new .github/skills/* skill docs/scripts and associated bash tests.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs Auto-initializes ViewData when ViewContext is set to support view-data-* prefix attributes.
src/Mvc/Mvc.TagHelpers/test/PartialTagHelperTest.cs Adds regression tests for view-data-* usage without explicit view-data.
.github/skills/write-tests/SKILL.md Adds documentation for a “write-tests” skill.
.github/skills/verify-tests/SKILL.md Adds documentation for verifying tests reproduce/fix a bug.
.github/skills/try-fix/SKILL.md Adds documentation for trying one alternative fix approach.
.github/skills/fix-issue/SKILL.md Adds a large “fix-issue” workflow skill definition.
.github/skills/fix-issue/tests/test-skill-definition.sh Adds bash tests validating the fix-issue skill definition content.
.github/skills/fix-issue/tests/test-ai-summary-comment.sh Adds bash tests for the AI summary comment scripts.
.github/skills/ai-summary-comment/SKILL.md Adds documentation for posting/updating a unified AI summary comment.
.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.sh Adds a script to post/update a PR comment by loading phase outputs and composing nested <details> sections.

…-data-* prefix attributes

When using <partial name="_Partial" view-data-key="value" /> without
explicitly setting the view-data attribute, the Razor runtime tried to
add items to a null ViewData dictionary, causing NullReferenceException.

Initialize ViewData from ViewContext.ViewData in the ViewContext property
setter so that view-data-* prefix attributes can be populated. When
ViewData is explicitly set via the view-data attribute, the property
initializer runs after ViewContext and overrides this value.

The copy ensures the parent ViewData is not contaminated.

Fixes dotnet#9736

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kubaflo kubaflo force-pushed the fix/partial-taghelper-viewdata-null-9736 branch from c656b40 to aa95d04 Compare March 1, 2026 11:34
Copy link

@iamteamstar iamteamstar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like your classes naming. very available. aprrove.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-infrastructure Includes: MSBuild projects/targets, build scripts, CI, Installers and shared framework community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants