Repository: dotnet/sdk (2025-08-06T10:40:31Z)

Issue:
[Razor] Emit embedded `ValidatableTypeAttribute` in the presence of .razor files
# SDK Changes for Embedded ValidatableTypeAttribute Support

## Overview
Implement MSBuild properties and targets to conditionally include embedded attribute files and global usings based on the presence of `.razor` files in the project.

## Required Changes

### 1. MSBuild Property Definitions

```xml
<PropertyGroup>
  <!-- Detect if project contains .razor files -->
  <_HasRazorFiles Condition="'$(_HasRazorFiles)' == ''">false</_HasRazorFiles>
  <_HasRazorFiles Condition="'@(RazorComponent)' != '' OR '@(RazorPage)' != ''">true</_HasRazorFiles>
  
  <!-- Control embedded attribute generation (defaults to true when .razor files present) -->
  <GenerateEmbeddedValidatableTypeAttribute Condition="'$(GenerateEmbeddedValidatableTypeAttribute)' == '' AND '$(_HasRazorFiles)' == 'true'">true</GenerateEmbeddedValidatableTypeAttribute>
  <GenerateEmbeddedValidatableTypeAttribute Condition="'$(GenerateEmbeddedValidatableTypeAttribute)' == ''">false</GenerateEmbeddedValidatableTypeAttribute>
  
  <!-- Control global using generation (separate property for flexibility) -->
  <IncludeEmbeddedValidationGlobalUsing Condition="'$(IncludeEmbeddedValidationGlobalUsing)' == '' AND '$(GenerateEmbeddedValidatableTypeAttribute)' == 'true'">true</IncludeEmbeddedValidationGlobalUsing>
  <IncludeEmbeddedValidationGlobalUsing Condition="'$(IncludeEmbeddedValidationGlobalUsing)' == ''">false</IncludeEmbeddedValidationGlobalUsing>
</PropertyGroup>
```

### 2. Conditional File Inclusion

```xml
<ItemGroup Condition="'$(GenerateEmbeddedValidatableTypeAttribute)' == 'true'">
  <Compile Include="$(MSBuildThisFileDirectory)EmbeddedAttribute.cs">
    <Link>EmbeddedAttribute.cs</Link>
  </Compile>
  <Compile Include="$(MSBuildThisFileDirectory)ValidatableTypeAttribute.cs">
    <Link>ValidatableTypeAttribute.cs</Link>
  </Compile>
</ItemGroup>
```

### 3. Conditional Global Using

```xml
<ItemGroup Condition="'$(IncludeEmbeddedValidationGlobalUsing)' == 'true'">
  <Using Include="Microsoft.Extensions.Validation.Embedded" />
</ItemGroup>
```

### 4. Source Files to Create

#### EmbeddedAttribute.cs
```csharp
// <auto-generated/>
namespace Microsoft.CodeAnalysis
{
    [global::System.AttributeUsage(global::System.AttributeTargets.All, AllowMultiple = true, Inherited = false)]
    internal sealed class EmbeddedAttribute : global::System.Attribute
    {
    }
}
```

#### ValidatableTypeAttribute.cs
```csharp
// <auto-generated/>
namespace Microsoft.Extensions.Validation.Embedded;

[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
[global::System.AttributeUsage(global::System.AttributeTargets.Class)]
internal sealed class ValidatableTypeAttribute : global::System.Attribute
{
}
```

## Implementation Steps

1. **Add property definitions** to appropriate SDK targets/props file (RazorSDK) 
  - Conditionally include these only in 10.0 and onwards    
3. **Create the two source files** (`EmbeddedAttribute.cs` and `ValidatableTypeAttribute.cs`)
4. **Add conditional compilation items** to include source files when property is enabled
5. **Add conditional global using** to automatically import the embedded namespace


Discussion:

Patch:
diff --git a/src/RazorSdk/Razor.slnf b/src/RazorSdk/Razor.slnf
index 21dd975b0fc0..54a7357ce0a8 100644
--- a/src/RazorSdk/Razor.slnf
+++ b/src/RazorSdk/Razor.slnf
@@ -16,6 +16,7 @@
       "test\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj",
       "test\\Microsoft.NET.Sdk.Razor.Tests\\Microsoft.NET.Sdk.Razor.Tests.csproj",
       "test\\Microsoft.NET.Sdk.Razor.Tool.Tests\\Microsoft.NET.Sdk.Razor.Tool.Tests.csproj",
+      "test\\Microsoft.NET.Sdk.StaticWebAssets.Tests\\Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj",
       "test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj"
     ]
   }
diff --git a/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets b/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets
index 919d24fa54a5..2c5b6daf2d49 100644
--- a/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets
+++ b/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets
@@ -587,6 +587,84 @@ Copyright (c) .NET Foundation. All rights reserved.
     </ItemGroup>
   </Target>
 
+  <!--
+    Target to detect presence of .razor files and set related properties for embedded ValidatableTypeAttribute support
+    This target runs after component items are resolved to ensure accurate detection
+  -->
+  <Target Name="_AddEmbeddedValidationAttribute" BeforeTargets="GenerateGlobalUsings" DependsOnTargets="ResolveRazorComponentInputs;ResolveRazorGenerateInputs">
+    <PropertyGroup Condition="'$(_TargetingNET100OrLater)' == 'true'">
+      <_HasRazorFiles Condition="'@(RazorComponent)' != '' OR '@(RazorGenerate->WithMetadataValue('Extension', '.razor'))' != '' OR '@(Content->WithMetadataValue('Extension', '.razor'))' != ''">true</_HasRazorFiles>
+      
+      <!-- Set embedded attribute generation property based on .razor file detection -->
+      <GenerateEmbeddedValidatableTypeAttribute Condition="'$(GenerateEmbeddedValidatableTypeAttribute)' == '' AND '$(_HasRazorFiles)' == 'true'">true</GenerateEmbeddedValidatableTypeAttribute>
+      <GenerateEmbeddedValidatableTypeAttribute Condition="'$(GenerateEmbeddedValidatableTypeAttribute)' == ''">false</GenerateEmbeddedValidatableTypeAttribute>
+      
+      <!-- Set global using property based on attribute generation -->
+      <IncludeEmbeddedValidationGlobalUsing Condition="'$(IncludeEmbeddedValidationGlobalUsing)' == '' AND '$(GenerateEmbeddedValidatableTypeAttribute)' == 'true'">true</IncludeEmbeddedValidationGlobalUsing>
+      <IncludeEmbeddedValidationGlobalUsing Condition="'$(IncludeEmbeddedValidationGlobalUsing)' == ''">false</IncludeEmbeddedValidationGlobalUsing>
+    </PropertyGroup>
+    
+    <PropertyGroup Condition="'$(GenerateEmbeddedValidatableTypeAttribute)' == 'true' AND '$(_TargetingNET100OrLater)' == 'true'">
+      <_EmbeddedAttributeFile>$(IntermediateOutputPath)EmbeddedAttribute.cs</_EmbeddedAttributeFile>
+      <_ValidatableTypeAttributeFile>$(IntermediateOutputPath)ValidatableTypeAttribute.cs</_ValidatableTypeAttributeFile>
+    </PropertyGroup>
+
+    <!-- Generate EmbeddedAttribute source file -->
+    <ItemGroup Condition="'$(GenerateEmbeddedValidatableTypeAttribute)' == 'true' AND '$(_TargetingNET100OrLater)' == 'true'">
+      <_EmbeddedAttributeLines Include="// &lt;auto-generated/&gt;" />
+      <_EmbeddedAttributeLines Include="namespace Microsoft.CodeAnalysis" />
+      <_EmbeddedAttributeLines Include="{" />
+      <_EmbeddedAttributeLines Include="    internal sealed partial class EmbeddedAttribute : global::System.Attribute" />
+      <_EmbeddedAttributeLines Include="    {" />
+      <_EmbeddedAttributeLines Include="    }" />
+      <_EmbeddedAttributeLines Include="}" />
+    </ItemGroup>
+    
+    <WriteLinesToFile 
+      Condition="'$(GenerateEmbeddedValidatableTypeAttribute)' == 'true' AND '$(_TargetingNET100OrLater)' == 'true'"
+      File="$(_EmbeddedAttributeFile)"
+      Lines="@(_EmbeddedAttributeLines)"
+      Overwrite="true"
+      WriteOnlyWhenDifferent="true" />
+
+    <!-- Generate ValidatableTypeAttribute source file -->
+    <ItemGroup Condition="'$(GenerateEmbeddedValidatableTypeAttribute)' == 'true' AND '$(_TargetingNET100OrLater)' == 'true'">
+      <_ValidatableTypeAttributeLines Include="// &lt;auto-generated/&gt;" />
+      <_ValidatableTypeAttributeLines Include="namespace Microsoft.Extensions.Validation.Embedded" />
+      <_ValidatableTypeAttributeLines Include="{" />
+      <_ValidatableTypeAttributeLines Include="    [global::Microsoft.CodeAnalysis.EmbeddedAttribute]" />
+      <_ValidatableTypeAttributeLines Include="    [global::System.AttributeUsage(global::System.AttributeTargets.Class)]" />
+      <_ValidatableTypeAttributeLines Include="    internal sealed class ValidatableTypeAttribute : global::System.Attribute" />
+      <_ValidatableTypeAttributeLines Include="    {" />
+      <_ValidatableTypeAttributeLines Include="    }" />
+      <_ValidatableTypeAttributeLines Include="}" />
+    </ItemGroup>
+    
+    <WriteLinesToFile 
+      Condition="'$(GenerateEmbeddedValidatableTypeAttribute)' == 'true' AND '$(_TargetingNET100OrLater)' == 'true'"
+      File="$(_ValidatableTypeAttributeFile)"
+      Lines="@(_ValidatableTypeAttributeLines)"
+      Overwrite="true"
+      WriteOnlyWhenDifferent="true" />
+    
+    <!--
+      Conditional inclusion of embedded ValidatableTypeAttribute source files for .NET 10.0+
+    -->
+    <ItemGroup Condition="'$(GenerateEmbeddedValidatableTypeAttribute)' == 'true' AND '$(_TargetingNET100OrLater)' == 'true'">
+      <Compile Include="$(_EmbeddedAttributeFile)" />
+      <Compile Include="$(_ValidatableTypeAttributeFile)" />
+      <FileWrites Include="$(_EmbeddedAttributeFile)" />
+      <FileWrites Include="$(_ValidatableTypeAttributeFile)" />
+    </ItemGroup>
+
+    <!--
+      Conditional global using for embedded validation namespace
+    -->
+    <ItemGroup Condition="'$(IncludeEmbeddedValidationGlobalUsing)' == 'true' AND '$(_TargetingNET100OrLater)' == 'true'">
+      <Using Include="Microsoft.Extensions.Validation.Embedded" />
+    </ItemGroup>
+  </Target>
+
   <!-- This target validates that there is at most one scoped css file per component, that there are no scoped css files without a
      matching component, and then adds the associated scope to the razor components that have a matching scoped css file.
   -->


Test patch:
diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/BuildWithComponentsIntegrationTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/BuildWithComponentsIntegrationTest.cs
index 1feb0fed8adf..a7c5eb5cee8e 100644
--- a/test/Microsoft.NET.Sdk.Razor.Tests/BuildWithComponentsIntegrationTest.cs
+++ b/test/Microsoft.NET.Sdk.Razor.Tests/BuildWithComponentsIntegrationTest.cs
@@ -57,5 +57,48 @@ private void Build_ComponentsWorks([CallerMemberName] string callerName = "")
             // Views should also appear in the app assembly.
             new FileInfo(Path.Combine(outputPath, "MvcWithComponents.dll")).AssemblyShould().ContainType("AspNetCoreGeneratedDocument.Views_Home_Index");
         }
+
+        [Fact]
+        public void Build_ComponentApp_IncludesEmbeddedValidatableTypeAttributeForNet100()
+        {
+            var testAsset = "RazorComponentApp";
+            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+            // Add a target to introspect the properties and compile items
+            var projectFile = Path.Combine(projectDirectory.Path, "ComponentApp.csproj");
+            var originalContent = File.ReadAllText(projectFile);
+            var modifiedContent = originalContent.Replace("</Project>", 
+                @"  <Target Name=""_IntrospectValidatableTypeAttribute"" BeforeTargets=""Build"">
+    <Message Text=""_HasRazorFiles: $(_HasRazorFiles)"" Importance=""High"" />
+    <Message Text=""GenerateEmbeddedValidatableTypeAttribute: $(GenerateEmbeddedValidatableTypeAttribute)"" Importance=""High"" />
+    <Message Text=""IncludeEmbeddedValidationGlobalUsing: $(IncludeEmbeddedValidationGlobalUsing)"" Importance=""High"" />
+    <Message Text=""_TargetingNET100OrLater: $(_TargetingNET100OrLater)"" Importance=""High"" />
+    <Message Text=""Compile items containing ValidatableTypeAttribute: @(Compile->WithMetadataValue('Filename', 'ValidatableTypeAttribute'))"" Importance=""High"" />
+    <Message Text=""Compile items containing EmbeddedAttribute: @(Compile->WithMetadataValue('Filename', 'EmbeddedAttribute'))"" Importance=""High"" />
+    <Message Text=""Using items: @(Using)"" Importance=""High"" />
+  </Target>
+</Project>");
+            
+            File.WriteAllText(projectFile, modifiedContent);
+
+            // Build with .NET 10.0 target framework
+            var build = new BuildCommand(projectDirectory);
+            var result = build.Execute($"/p:TargetFramework=net10.0");
+            
+            result.Should().Pass();
+
+            // Check that the properties were set correctly
+            result.Should().HaveStdOutContaining("_HasRazorFiles: true");
+            result.Should().HaveStdOutContaining("GenerateEmbeddedValidatableTypeAttribute: true");
+            result.Should().HaveStdOutContaining("IncludeEmbeddedValidationGlobalUsing: true");
+            result.Should().HaveStdOutContaining("_TargetingNET100OrLater: true");
+            
+            // Check that the source files were included
+            result.Should().HaveStdOutContaining("ValidatableTypeAttribute");
+            result.Should().HaveStdOutContaining("EmbeddedAttribute");
+            
+            // Check that the global using was included
+            result.Should().HaveStdOutContaining("Microsoft.Extensions.Validation.Embedded");
+        }
     }
 }
diff --git a/test/dotnet-watch.Tests/Build/EvaluationTests.cs b/test/dotnet-watch.Tests/Build/EvaluationTests.cs
index ba03080bd5c8..caef4de19907 100644
--- a/test/dotnet-watch.Tests/Build/EvaluationTests.cs
+++ b/test/dotnet-watch.Tests/Build/EvaluationTests.cs
@@ -212,7 +212,10 @@ await VerifyEvaluation(testAsset,
                 new("Project1/wwwroot/js/site.js", "wwwroot/js/site.js"),
                 new("RCL/Code.cs"),
                 new($"RCL/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+                new($"RCL/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/EmbeddedAttribute.cs", graphOnly: true),
                 new($"RCL/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/RCL.AssemblyInfo.cs", graphOnly: true),
+                new($"RCL/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/RCL.GlobalUsings.g.cs", graphOnly: true),
+                new($"RCL/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/ValidatableTypeAttribute.cs", graphOnly: true),
                 new("RCL/Page1.razor"),
                 new("RCL/Page1.razor.css"),
                 new("RCL/Page2.cshtml"),