Validation source generator emits ValidatableTypeInfo for records with no validation attributes
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
Description
The validation source generator treats record primary constructor parameters differently from every other member: they are added unconditionally, without checking whether anything about them is actually validatable.
In `ValidationsGenerator.TypesParser.ExtractValidatableMembers`, the property path gates each member:
```csharp
var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, validatableTypes, visitedTypes);
// If the member has no validation attributes or validatable types, skip it.
if (!HasValidationAttributes(member, wellKnownTypes) && !hasValidatableType)
{
continue;
}
```
The record primary constructor path above it has no equivalent check. After the service/`SkipValidation`/accessibility/`JsonIgnore` filters, it calls `members.Add(...)` unconditionally.
Because a type is emitted whenever `members` is non-empty, any record with a primary constructor becomes a validatable type, which then makes every property of that record type validatable in its containing types.
### Repro
With `AddValidation()` and an endpoint taking `Holder`:
```csharp
public class Holder
{
[Required] public string Name { get; set; } = "";
public PlainRecord? Rec { get; set; }
public PlainClass? Cls { get; set; }
}
public record PlainRecord(int Number, string Text);
public class PlainClass
{
public int Number { get; set; }
public string Text { get; set; } = "";
}
```
`PlainRecord` is emitted with members `Number` and `Text`, and `Holder.Rec` becomes a validatable member. `PlainClass` is correctly not emitted and `Holder.Cls` is not a member, despite being the same shape.
The asymmetry is visible within a single record too. For
```csharp
public record MixedRecord(int CtorParam, string CtorText)
{
public int BodyProperty { get; set; }
public string BodyText { get; set; } = "";
}
```
only `CtorParam` and `CtorText` are emitted. The body properties are correctly skipped.
### Impact
- Generated code size grows with every record in the app, including DTOs and strongly typed IDs that carry no validation.
- Each request pays reflection and traversal for members that can never produce an error.
- Records and classes of identical shape behave differently, which is surprising.
- Useless recursion consumes the `MaxDepth` budget (default 32), so deeply nested record graphs can hit the depth limit where equivalent classes would not.
### Suggested fix
Apply the same gate to the record path, checking the parameter as well as the property. Attributes on record primary constructor parameters bind to the parameter rather than the property by default, and the runtime already accounts for this in `ValidationAttributeCache.GetPropertyValidationAttributes`, which inspects constructor parameters alongside the property. The generator side needs the same treatment:
```csharp
var hasValidatableType = TryExtractValidatableType(correspondingProperty.Type, wellKnownTypes, validatableTypes, visitedTypes);
if (!HasValidationAttributes(parameter, wellKnownTypes)
&& !HasValidationAttributes(correspondingProperty, wellKnownTypes)
&& !hasValidatableType)
{
continue;
}
```
`HasValidationAttributes` already accepts an `ISymbol`, so it works for both. Note the existing call site discards the `TryExtractValidatableType` result with `_ =`, so that would need to be captured.
Contributor guide
Research direction
Start in ValidationsGenerator.TypesParser.ExtractValidatableMembers and compare the record primary-constructor path with the existing property gate; inspect ValidationAttributeCache.GetPropertyValidationAttributes for how parameter and property attributes are handled. Add regression coverage using the PlainRecord, PlainClass, and MixedRecord repros, and verify records without validation attributes or validatable members are no longer emitted.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend-api-design, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100