Skip to content

Commit 9af09bb

Browse files
CopilotYoussef1313
andauthored
Fix ValidationsGenerator CS8785 when AddValidation is called from multiple sites (#66675)
* Initial plan * Fix ValidationsGenerator CS8785 when AddValidation is called multiple times Agent-Logs-Url: https://github.com/dotnet/aspnetcore/sessions/3fae42b3-2f95-4903-ac81-68a84017e338 Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> * Small adjustments * Update ValidationsGenerator.Emitter.cs * Address review comment * Address review comment * Fix CanGenerateWhenAddValidationCalledMultipleTimes test: add System.ComponentModel.Primitives reference and update snapshot Agent-Logs-Url: https://github.com/dotnet/aspnetcore/sessions/20227e2b-a4e2-47de-b4c5-b3fdd1c71a61 Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> Co-authored-by: Youssef1313 <youssefvictor00@gmail.com>
1 parent 6309245 commit 9af09bb

5 files changed

Lines changed: 399 additions & 6 deletions

File tree

src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,19 @@ public sealed partial class ValidationsGenerator : IIncrementalGenerator
1515
public static string GeneratedCodeConstructor => $@"global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(ValidationsGenerator).Assembly.FullName}"", ""{typeof(ValidationsGenerator).Assembly.GetName().Version}"")";
1616
public static string GeneratedCodeAttribute => $"[{GeneratedCodeConstructor}]";
1717

18-
internal static void Emit(SourceProductionContext context, (InterceptableLocation? AddValidation, ImmutableArray<ValidatableType> ValidatableTypes) emitInputs)
18+
internal static void Emit(SourceProductionContext context, (ImmutableArray<InterceptableLocation?> AddValidationLocations, ImmutableArray<ValidatableType> ValidatableTypes) emitInputs)
1919
{
20-
if (emitInputs.AddValidation is null)
20+
var locations = emitInputs.AddValidationLocations.OfType<InterceptableLocation>().ToImmutableArray();
21+
if (locations.IsEmpty)
2122
{
2223
// Avoid generating code if no AddValidation call was found.
2324
return;
2425
}
25-
var source = Emit(emitInputs.AddValidation, emitInputs.ValidatableTypes);
26+
var source = Emit(locations, emitInputs.ValidatableTypes);
2627
context.AddSource("ValidatableInfoResolver.g.cs", SourceText.From(source, Encoding.UTF8));
2728
}
2829

29-
private static string Emit(InterceptableLocation addValidation, ImmutableArray<ValidatableType> validatableTypes) => $$"""
30+
private static string Emit(ImmutableArray<InterceptableLocation> addValidationLocations, ImmutableArray<ValidatableType> validatableTypes) => $$"""
3031
#nullable enable annotations
3132
//------------------------------------------------------------------------------
3233
// <auto-generated>
@@ -115,7 +116,7 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn
115116
{{GeneratedCodeAttribute}}
116117
file static class GeneratedServiceCollectionExtensions
117118
{
118-
{{addValidation.GetInterceptsLocationAttributeSyntax()}}
119+
{{EmitAddValidationInterceptorAttributes(addValidationLocations)}}
119120
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action<global::Microsoft.Extensions.Validation.ValidationOptions>? configureOptions = null)
120121
{
121122
// Use non-extension method to avoid infinite recursion.
@@ -325,6 +326,26 @@ private sealed record CacheKey(
325326
}
326327
""";
327328

329+
private static string EmitAddValidationInterceptorAttributes(ImmutableArray<InterceptableLocation> addValidations)
330+
{
331+
// We filtered early for empty locations, so we are sure we have at least one.
332+
var firstAttribute = addValidations[0].GetInterceptsLocationAttributeSyntax();
333+
if (addValidations.Length == 1)
334+
{
335+
// Common case
336+
return firstAttribute;
337+
}
338+
339+
// This is the less common case.
340+
var sb = new StringBuilder(firstAttribute);
341+
for (var i = 1; i < addValidations.Length; i++)
342+
{
343+
sb.AppendLine().Append(" ").Append(addValidations[i].GetInterceptsLocationAttributeSyntax());
344+
}
345+
346+
return sb.ToString();
347+
}
348+
328349
private static string EmitTypeChecks(ImmutableArray<ValidatableType> validatableTypes)
329350
{
330351
var sw = new StringWriter();

src/Validation/gen/ValidationsGenerator.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5050
.Distinct(ValidatableTypeComparer.Instance)
5151
.Collect();
5252

53-
var emitInputs = addValidation
53+
// Collect all AddValidation call sites to avoid emitting duplicate hint names
54+
// when AddValidation() is called multiple times in the same project.
55+
var addValidationLocations = addValidation.Collect();
56+
57+
var emitInputs = addValidationLocations
5458
.Combine(validatableTypes);
5559

5660
// Emit the IValidatableInfo resolver injection and

src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.NoOp.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,57 @@ await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvi
178178
});
179179
});
180180
}
181+
182+
[Fact]
183+
public async Task CanGenerateWhenAddValidationCalledMultipleTimes()
184+
{
185+
var source = """
186+
using System;
187+
using System.ComponentModel.DataAnnotations;
188+
using System.Collections.Generic;
189+
using System.Threading.Tasks;
190+
using Microsoft.AspNetCore.Builder;
191+
using Microsoft.AspNetCore.Http;
192+
using Microsoft.Extensions.Validation;
193+
using Microsoft.AspNetCore.Routing;
194+
using Microsoft.Extensions.DependencyInjection;
195+
196+
var builder = WebApplication.CreateBuilder();
197+
198+
builder.Services.AddValidation();
199+
builder.Services.AddValidation(); // second call should not cause CS8785
200+
201+
var app = builder.Build();
202+
203+
app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed"));
204+
205+
app.Run();
206+
207+
public class ComplexType
208+
{
209+
[Range(10, 100)]
210+
public int IntegerWithRange { get; set; } = 10;
211+
}
212+
""";
213+
await Verify(source, out var compilation);
214+
// Verify that types are still validated even with multiple AddValidation calls
215+
await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) =>
216+
{
217+
var payload = """
218+
{
219+
"IntegerWithRange": 5
220+
}
221+
""";
222+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
223+
224+
await endpoint.RequestDelegate(context);
225+
226+
var problemDetails = await AssertBadRequest(context);
227+
Assert.Collection(problemDetails.Errors, kvp =>
228+
{
229+
Assert.Equal("IntegerWithRange", kvp.Key);
230+
Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single());
231+
});
232+
});
233+
}
181234
}

src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ internal static Task Verify(string source, out Compilation compilation)
5757
MetadataReference.CreateFromFile(typeof(IFormFileCollection).Assembly.Location),
5858
MetadataReference.CreateFromFile(typeof(PipeReader).Assembly.Location),
5959
MetadataReference.CreateFromFile(typeof(System.ComponentModel.DataAnnotations.ValidationAttribute).Assembly.Location),
60+
MetadataReference.CreateFromFile(typeof(System.ComponentModel.DisplayNameAttribute).Assembly.Location),
6061
MetadataReference.CreateFromFile(typeof(RouteData).Assembly.Location),
6162
MetadataReference.CreateFromFile(typeof(IFeatureCollection).Assembly.Location),
6263
MetadataReference.CreateFromFile(typeof(ValidateOptionsResult).Assembly.Location),

0 commit comments

Comments
 (0)