diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/HtmlEntitiesInAttributes/TestComponent.cshtml b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/HtmlEntitiesInAttributes/TestComponent.cshtml
new file mode 100644
index 00000000000..465673e10a7
--- /dev/null
+++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/HtmlEntitiesInAttributes/TestComponent.cshtml
@@ -0,0 +1,3 @@
+
A
+B @DateTime.Now
+Test
diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentMarkupEncodingPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentMarkupEncodingPass.cs
index f338c6b1e3f..590baa56471 100644
--- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentMarkupEncodingPass.cs
+++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentMarkupEncodingPass.cs
@@ -156,6 +156,47 @@ public override void VisitHtml(HtmlContentIntermediateNode node)
}
}
+ public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node)
+ {
+ // Decode HTML entities in attribute values to ensure consistency between
+ // static attributes (e.g., ) and
+ // mixed attributes (e.g., ).
+ //
+ // For static string literals, we decode entities at compile time.
+ // This matches the behavior when the HTML parser processes pure static markup.
+ //
+ // Note: We don't skip encoding based on _avoidEncodingContent like VisitHtml does,
+ // because attribute values don't use the HasEncodedContent flag and are always
+ // written as string literals in the generated code. The entities should always be
+ // decoded at compile time for consistency.
+
+ foreach (var child in node.Children)
+ {
+ if (child is not HtmlIntermediateToken token || token.Content.IsNullOrEmpty())
+ {
+ // We only care about Html tokens.
+ continue;
+ }
+
+ // Check if there are any ampersands (potential entities)
+ if (token.Content.IndexOf('&') >= 0)
+ {
+ // Try to decode HTML entities
+ if (TryDecodeHtmlEntities(token.Content.AsMemory(), out var decoded))
+ {
+ token.UpdateContent(decoded);
+ }
+ // If decoding fails (invalid/unknown entity), keep the original content.
+ // This differs from VisitHtml which sets node.HasEncodedContent=true on failure.
+ // HtmlAttributeValueIntermediateNode doesn't have a HasEncodedContent property
+ // because attributes are always output as string literals in the generated code.
+ }
+ }
+
+ // Continue walking the tree
+ base.VisitHtmlAttributeValue(node);
+ }
+
private static bool TryDecodeHtmlEntities(ReadOnlyMemory content, [NotNullWhen(true)] out string? decoded)
{
decoded = null;