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
Copy Markdown

@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
Copy Markdown
Contributor

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

Copy link
Copy Markdown
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.

Comment thread src/Mvc/Mvc.TagHelpers/src/PartialTagHelper.cs
Comment thread src/Mvc/Mvc.TagHelpers/test/PartialTagHelperTest.cs
Comment thread .github/skills/fix-issue/tests/test-ai-summary-comment.sh Outdated
Comment thread .github/skills/fix-issue/tests/test-ai-summary-comment.sh Outdated
Comment thread .github/skills/write-tests/SKILL.md Outdated
…-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
Copy Markdown

@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.

@wtgodbe wtgodbe added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates and removed area-infrastructure Includes: MSBuild projects/targets, build scripts, CI, Installers and shared framework labels Mar 5, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime.
To make sure no conflicting changes have occurred, please rerun validation before merging. You can do this by leaving an /azp run comment here (requires commit rights), or by simply closing and reopening.

@dotnet-policy-service dotnet-policy-service Bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates community-contribution Indicates that the PR has been added by a community member pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants