Skip to content

Commit a0991ea

Browse files
authored
[Validation] Support generated ValidatableTypeAttribute for Blazor (#63115)
The ValidationsGenerator now supports discovering types marked with ValidatableTypeAttribute from two sources: * Framework attribute: Microsoft.Extensions.Validation.ValidatableTypeAttribute (from Microsoft.Extensions.Validation package) * SDK-generated attribute: Microsoft.Extensions.Validation.Embedded.ValidatableTypeAttribute (embedded by Razor SDK in projects with .razor files) This enables developers to use [ValidatableType] in Blazor projects without explicitly referencing the experimental validation framework attribute, as the Razor SDK automatically embeds the attribute as needed on the assembly.
1 parent 4f805f0 commit a0991ea

6 files changed

+541
-7
lines changed

src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,32 @@ public static IncrementalValuesProvider<T> Concat<T>(
6363
});
6464
}
6565

66+
public static IncrementalValuesProvider<T> Concat<T>(
67+
this IncrementalValuesProvider<ImmutableArray<T>> first,
68+
IncrementalValuesProvider<T> second)
69+
{
70+
return first.Collect()
71+
.Combine(second.Collect())
72+
.SelectMany((tuple, _) =>
73+
{
74+
if (tuple.Left.IsEmpty)
75+
{
76+
return tuple.Right;
77+
}
78+
79+
var results = ImmutableArray.CreateBuilder<T>(tuple.Left.Length + tuple.Right.Length);
80+
for (var i = 0; i < tuple.Left.Length; i++)
81+
{
82+
results.AddRange(tuple.Left[i]);
83+
}
84+
for (var i = 0; i < tuple.Right.Length; i++)
85+
{
86+
results.AddRange(tuple.Right[i]);
87+
}
88+
return results.DrainToImmutable();
89+
});
90+
}
91+
6692
private sealed class ImmutableArrayEqualityComparer<T> : IEqualityComparer<ImmutableArray<T>>
6793
{
6894
public static readonly ImmutableArrayEqualityComparer<T> Instance = new();

src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
3-
43
using System.Collections.Generic;
54
using System.Collections.Immutable;
65
using System.Threading;
@@ -24,7 +23,7 @@ internal ImmutableArray<ValidatableType> TransformValidatableTypeWithAttribute(G
2423
var wellKnownTypes = WellKnownTypes.GetOrCreate(context.SemanticModel.Compilation);
2524
if (TryExtractValidatableType((ITypeSymbol)context.TargetSymbol, wellKnownTypes, ref validatableTypes, ref visitedTypes))
2625
{
27-
return [..validatableTypes];
26+
return [.. validatableTypes];
2827
}
2928
return [];
3029
}

src/Validation/gen/ValidationsGenerator.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
5+
using System.Collections.Immutable;
46
using System.Linq;
7+
using System.Runtime.InteropServices;
8+
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
59
using Microsoft.CodeAnalysis;
610

711
namespace Microsoft.Extensions.Validation;
@@ -16,24 +20,40 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
1620
predicate: FindAddValidation,
1721
transform: TransformAddValidation
1822
);
19-
// Extract types that have been marked with [ValidatableType].
20-
var validatableTypesWithAttribute = context.SyntaxProvider.ForAttributeWithMetadataName(
23+
24+
// Extract types that have been marked with framework [ValidatableType].
25+
var frameworkValidatableTypes = context.SyntaxProvider.ForAttributeWithMetadataName(
2126
"Microsoft.Extensions.Validation.ValidatableTypeAttribute",
2227
predicate: ShouldTransformSymbolWithAttribute,
2328
transform: TransformValidatableTypeWithAttribute
2429
);
30+
31+
// Extract types that have been marked with generated [ValidatableType].
32+
var generatedValidatableTypes = context.SyntaxProvider.ForAttributeWithMetadataName(
33+
"Microsoft.Extensions.Validation.Embedded.ValidatableTypeAttribute",
34+
predicate: ShouldTransformSymbolWithAttribute,
35+
transform: TransformValidatableTypeWithAttribute
36+
);
37+
38+
// Combine both sources of validatable types
39+
var validatableTypesWithAttribute = frameworkValidatableTypes.Concat(generatedValidatableTypes);
40+
2541
// Extract all minimal API endpoints in the application.
2642
var endpoints = context.SyntaxProvider
2743
.CreateSyntaxProvider(
2844
predicate: FindEndpoints,
2945
transform: TransformEndpoints)
3046
.Where(endpoint => endpoint is not null);
47+
3148
// Extract validatable types from all endpoints.
3249
var validatableTypesFromEndpoints = endpoints
3350
.Select(ExtractValidatableEndpoint);
51+
3452
// Join all validatable types encountered in the type graph.
35-
var validatableTypes = validatableTypesWithAttribute
36-
.Concat(validatableTypesFromEndpoints)
53+
var allValidatableTypesProviders = validatableTypesFromEndpoints
54+
.Concat(validatableTypesWithAttribute);
55+
56+
var validatableTypes = allValidatableTypesProviders
3757
.Distinct(ValidatableTypeComparer.Instance)
3858
.Collect();
3959

@@ -42,6 +62,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
4262

4363
// Emit the IValidatableInfo resolver injection and
4464
// ValidatableTypeInfo for all validatable types.
45-
context.RegisterSourceOutput(emitInputs, Emit);
65+
context.RegisterSourceOutput(emitInputs, (context, emitInputs) =>
66+
Emit(context, (emitInputs.Left, emitInputs.Right)));
4667
}
4768
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.Validation.GeneratorTests;
5+
using VerifyXunit;
6+
using Xunit;
7+
8+
namespace Microsoft.Extensions.Validation.GeneratorTests;
9+
10+
[UsesVerify]
11+
public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
12+
{
13+
private const string GeneratedAttributeSource = """
14+
// <auto-generated/>
15+
namespace Microsoft.CodeAnalysis
16+
{
17+
[global::System.AttributeUsage(global::System.AttributeTargets.All, AllowMultiple = true, Inherited = false)]
18+
internal sealed class EmbeddedAttribute : global::System.Attribute
19+
{
20+
}
21+
}
22+
23+
namespace Microsoft.Extensions.Validation.Embedded
24+
{
25+
[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
26+
[global::System.AttributeUsage(global::System.AttributeTargets.Class)]
27+
internal sealed class ValidatableTypeAttribute : global::System.Attribute
28+
{
29+
}
30+
}
31+
""";
32+
33+
[Fact]
34+
public async Task CanDiscoverGeneratedValidatableTypeAttribute()
35+
{
36+
var source = """
37+
38+
namespace MyApp
39+
{
40+
using Microsoft.AspNetCore.Builder;
41+
using Microsoft.Extensions.DependencyInjection;
42+
using System.ComponentModel.DataAnnotations;
43+
using Microsoft.Extensions.Validation.Embedded;
44+
45+
public class Program
46+
{
47+
public static void Main(string[] args)
48+
{
49+
var builder = WebApplication.CreateBuilder(args);
50+
builder.Services.AddValidation();
51+
var app = builder.Build();
52+
app.MapPost("/customers", (Customer customer) => "OK");
53+
app.Run();
54+
}
55+
}
56+
57+
[ValidatableType]
58+
public class Customer
59+
{
60+
[Required]
61+
public string Name { get; set; } = "";
62+
63+
[EmailAddress]
64+
public string Email { get; set; } = "";
65+
}
66+
}
67+
""";
68+
69+
// Combine the generated attribute with the user's source
70+
var combinedSource = GeneratedAttributeSource + "\n" + source;
71+
72+
await Verify(combinedSource, out var compilation);
73+
}
74+
75+
[Fact]
76+
public async Task CanUseBothFrameworkAndGeneratedValidatableTypeAttributes()
77+
{
78+
var source = """
79+
namespace MyApp
80+
{
81+
using Microsoft.AspNetCore.Builder;
82+
using Microsoft.Extensions.DependencyInjection;
83+
using System.ComponentModel.DataAnnotations;
84+
using Microsoft.Extensions.Validation.Embedded;
85+
86+
public class Program
87+
{
88+
public static void Main(string[] args)
89+
{
90+
var builder = WebApplication.CreateBuilder(args);
91+
builder.Services.AddValidation();
92+
var app = builder.Build();
93+
app.MapPost("/customers", (Customer customer) => "OK");
94+
app.Run();
95+
}
96+
}
97+
98+
// Using framework attribute
99+
[Microsoft.Extensions.Validation.ValidatableType]
100+
public class Customer
101+
{
102+
[Required]
103+
public string Name { get; set; } = "";
104+
105+
[EmailAddress]
106+
public string Email { get; set; } = "";
107+
}
108+
109+
// Using generated attribute
110+
[ValidatableType]
111+
public class Product
112+
{
113+
[Required]
114+
public string ProductName { get; set; } = "";
115+
116+
[Range(0, double.MaxValue)]
117+
public decimal Price { get; set; }
118+
}
119+
}
120+
""";
121+
122+
// Combine the generated attribute with the user's source
123+
var combinedSource = GeneratedAttributeSource + "\n" + source;
124+
125+
await Verify(combinedSource, out var compilation);
126+
}
127+
}

0 commit comments

Comments
 (0)