Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public sealed partial class ValidationsGenerator : IIncrementalGenerator
{
internal static bool ShouldTransformSymbolWithAttribute(SyntaxNode syntaxNode, CancellationToken cancellationToken)
{
return syntaxNode is ClassDeclarationSyntax;
return syntaxNode is ClassDeclarationSyntax or RecordDeclarationSyntax;
}

internal ImmutableArray<ValidatableType> TransformValidatableTypeWithAttribute(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Validation.GeneratorTests;
public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
{
[Fact]
public async Task CanValidateTypesWithAttribute()
public async Task CanValidateClassTypesWithAttribute()
{
var source = """
#pragma warning disable ASP0029
Expand Down Expand Up @@ -378,4 +378,373 @@ async Task ValidInputProducesNoWarnings(IValidatableInfo validatableInfo)
}
});
}

[Fact]
public async Task CanValidateRecordTypesWithAttribute()
{
var source = """
#pragma warning disable ASP0029

using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder();

builder.Services.AddValidation();

var app = builder.Build();

app.Run();

[ValidatableType]
public record ComplexType
{
[Range(10, 100)]
public int IntegerWithRange { get; set; } = 10;

[Range(10, 100), Display(Name = "Valid identifier")]
public int IntegerWithRangeAndDisplayName { get; set; } = 50;

[Required]
public SubType PropertyWithMemberAttributes { get; set; } = new SubType();

public SubType PropertyWithoutMemberAttributes { get; set; } = new SubType();

public SubTypeWithInheritance PropertyWithInheritance { get; set; } = new SubTypeWithInheritance();

public List<SubType> ListOfSubTypes { get; set; } = [];

[CustomValidation(ErrorMessage = "Value must be an even number")]
public int IntegerWithCustomValidationAttribute { get; set; }

[CustomValidation, Range(10, 100)]
public int PropertyWithMultipleAttributes { get; set; } = 10;
}

public class CustomValidationAttribute : ValidationAttribute
{
public override bool IsValid(object? value) => value is int number && number % 2 == 0;
}

public record SubType
{
[Required]
public string RequiredProperty { get; set; } = "some-value";

[StringLength(10)]
public string? StringWithLength { get; set; }
}

public record SubTypeWithInheritance : SubType
{
[EmailAddress]
public string? EmailString { get; set; }
}
""";
await Verify(source, out var compilation);
VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) =>
{
Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo));

await InvalidIntegerWithRangeProducesError(validatableTypeInfo);
await InvalidIntegerWithRangeAndDisplayNameProducesError(validatableTypeInfo);
await MissingRequiredSubtypePropertyProducesError(validatableTypeInfo);
await InvalidRequiredSubtypePropertyProducesError(validatableTypeInfo);
await InvalidSubTypeWithInheritancePropertyProducesError(validatableTypeInfo);
await InvalidListOfSubTypesProducesError(validatableTypeInfo);
await InvalidPropertyWithDerivedValidationAttributeProducesError(validatableTypeInfo);
await InvalidPropertyWithMultipleAttributesProducesError(validatableTypeInfo);
await InvalidPropertyWithCustomValidationProducesError(validatableTypeInfo);
await ValidInputProducesNoWarnings(validatableTypeInfo);

async Task InvalidIntegerWithRangeProducesError(IValidatableInfo validatableInfo)
{
var instance = Activator.CreateInstance(type);
type.GetProperty("IntegerWithRange")?.SetValue(instance, 5);
var context = new ValidateContext
{
ValidationOptions = validationOptions,
ValidationContext = new ValidationContext(instance)
};

await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);

Assert.Collection(context.ValidationErrors, kvp =>
{
Assert.Equal("IntegerWithRange", kvp.Key);
Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single());
});
}

async Task InvalidIntegerWithRangeAndDisplayNameProducesError(IValidatableInfo validatableInfo)
{
var instance = Activator.CreateInstance(type);
type.GetProperty("IntegerWithRangeAndDisplayName")?.SetValue(instance, 5);
var context = new ValidateContext
{
ValidationOptions = validationOptions,
ValidationContext = new ValidationContext(instance)
};

await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);

Assert.Collection(context.ValidationErrors, kvp =>
{
Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key);
Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single());
});
}

async Task MissingRequiredSubtypePropertyProducesError(IValidatableInfo validatableInfo)
{
var instance = Activator.CreateInstance(type);
type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, null);
var context = new ValidateContext
{
ValidationOptions = validationOptions,
ValidationContext = new ValidationContext(instance)
};

await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);

Assert.Collection(context.ValidationErrors, kvp =>
{
Assert.Equal("PropertyWithMemberAttributes", kvp.Key);
Assert.Equal("The PropertyWithMemberAttributes field is required.", kvp.Value.Single());
});
}

async Task InvalidRequiredSubtypePropertyProducesError(IValidatableInfo validatableInfo)
{
var instance = Activator.CreateInstance(type);
var subType = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
subType.GetType().GetProperty("RequiredProperty")?.SetValue(subType, "");
subType.GetType().GetProperty("StringWithLength")?.SetValue(subType, "way-too-long");
type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, subType);
var context = new ValidateContext
{
ValidationOptions = validationOptions,
ValidationContext = new ValidationContext(instance)
};

await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);

Assert.Collection(context.ValidationErrors,
kvp =>
{
Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key);
Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
},
kvp =>
{
Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key);
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
});
}

async Task InvalidSubTypeWithInheritancePropertyProducesError(IValidatableInfo validatableInfo)
{
var instance = Activator.CreateInstance(type);
var inheritanceType = Activator.CreateInstance(type.Assembly.GetType("SubTypeWithInheritance")!);
inheritanceType.GetType().GetProperty("RequiredProperty")?.SetValue(inheritanceType, "");
inheritanceType.GetType().GetProperty("StringWithLength")?.SetValue(inheritanceType, "way-too-long");
inheritanceType.GetType().GetProperty("EmailString")?.SetValue(inheritanceType, "not-an-email");
type.GetProperty("PropertyWithInheritance")?.SetValue(instance, inheritanceType);
var context = new ValidateContext
{
ValidationOptions = validationOptions,
ValidationContext = new ValidationContext(instance)
};

await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);

Assert.Collection(context.ValidationErrors,
kvp =>
{
Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key);
Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single());
},
kvp =>
{
Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key);
Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
},
kvp =>
{
Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key);
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
});
}

async Task InvalidListOfSubTypesProducesError(IValidatableInfo validatableInfo)
{
var instance = Activator.CreateInstance(type);
var subTypeList = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("SubType")!));

// Create first invalid item
var subType1 = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
subType1.GetType().GetProperty("RequiredProperty")?.SetValue(subType1, "");
subType1.GetType().GetProperty("StringWithLength")?.SetValue(subType1, "way-too-long");

// Create second invalid item
var subType2 = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
subType2.GetType().GetProperty("RequiredProperty")?.SetValue(subType2, "valid");
subType2.GetType().GetProperty("StringWithLength")?.SetValue(subType2, "way-too-long");

// Create valid item
var subType3 = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
subType3.GetType().GetProperty("RequiredProperty")?.SetValue(subType3, "valid");
subType3.GetType().GetProperty("StringWithLength")?.SetValue(subType3, "valid");

// Add to list
subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType1]);
subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType2]);
subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType3]);

type.GetProperty("ListOfSubTypes")?.SetValue(instance, subTypeList);
var context = new ValidateContext
{
ValidationOptions = validationOptions,
ValidationContext = new ValidationContext(instance)
};

await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);

Assert.Collection(context.ValidationErrors,
kvp =>
{
Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key);
Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
},
kvp =>
{
Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key);
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
},
kvp =>
{
Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key);
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
});
}

async Task InvalidPropertyWithDerivedValidationAttributeProducesError(IValidatableInfo validatableInfo)
{
var instance = Activator.CreateInstance(type);
type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 5); // Odd number, should fail
var context = new ValidateContext
{
ValidationOptions = validationOptions,
ValidationContext = new ValidationContext(instance)
};

await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);

Assert.Collection(context.ValidationErrors, kvp =>
{
Assert.Equal("IntegerWithCustomValidationAttribute", kvp.Key);
Assert.Equal("Value must be an even number", kvp.Value.Single());
});
}

async Task InvalidPropertyWithMultipleAttributesProducesError(IValidatableInfo validatableInfo)
{
var instance = Activator.CreateInstance(type);
type.GetProperty("PropertyWithMultipleAttributes")?.SetValue(instance, 5);
var context = new ValidateContext
{
ValidationOptions = validationOptions,
ValidationContext = new ValidationContext(instance)
};

await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);

Assert.Collection(context.ValidationErrors, kvp =>
{
Assert.Equal("PropertyWithMultipleAttributes", kvp.Key);
Assert.Collection(kvp.Value,
error =>
{
Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error);
},
error =>
{
Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error);
});
});
}

async Task InvalidPropertyWithCustomValidationProducesError(IValidatableInfo validatableInfo)
{
var instance = Activator.CreateInstance(type);
type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 3); // Odd number should fail
var context = new ValidateContext
{
ValidationOptions = validationOptions,
ValidationContext = new ValidationContext(instance)
};

await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);

Assert.Collection(context.ValidationErrors, kvp =>
{
Assert.Equal("IntegerWithCustomValidationAttribute", kvp.Key);
Assert.Equal("Value must be an even number", kvp.Value.Single());
});
}

async Task ValidInputProducesNoWarnings(IValidatableInfo validatableInfo)
{
var instance = Activator.CreateInstance(type);

// Set all properties with valid values
type.GetProperty("IntegerWithRange")?.SetValue(instance, 50);
type.GetProperty("IntegerWithRangeAndDisplayName")?.SetValue(instance, 50);

// Create and set PropertyWithMemberAttributes
var subType1 = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
subType1.GetType().GetProperty("RequiredProperty")?.SetValue(subType1, "valid");
subType1.GetType().GetProperty("StringWithLength")?.SetValue(subType1, "valid");
type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, subType1);

// Create and set PropertyWithoutMemberAttributes
var subType2 = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
subType2.GetType().GetProperty("RequiredProperty")?.SetValue(subType2, "valid");
subType2.GetType().GetProperty("StringWithLength")?.SetValue(subType2, "valid");
type.GetProperty("PropertyWithoutMemberAttributes")?.SetValue(instance, subType2);

// Create and set PropertyWithInheritance
var inheritanceType = Activator.CreateInstance(type.Assembly.GetType("SubTypeWithInheritance")!);
inheritanceType.GetType().GetProperty("RequiredProperty")?.SetValue(inheritanceType, "valid");
inheritanceType.GetType().GetProperty("StringWithLength")?.SetValue(inheritanceType, "valid");
inheritanceType.GetType().GetProperty("EmailString")?.SetValue(inheritanceType, "[email protected]");
type.GetProperty("PropertyWithInheritance")?.SetValue(instance, inheritanceType);

// Create empty list for ListOfSubTypes
var emptyList = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("SubType")!));
type.GetProperty("ListOfSubTypes")?.SetValue(instance, emptyList);

// Set custom validation attributes
type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 2); // Even number should pass
type.GetProperty("PropertyWithMultipleAttributes")?.SetValue(instance, 12);

var context = new ValidateContext
{
ValidationOptions = validationOptions,
ValidationContext = new ValidationContext(instance)
};

await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);

Assert.Null(context.ValidationErrors);
}
});
}
}
Loading
Loading