Description
Scenario
It's sometimes advantageous to have the validation metadata for a given type come from a different type:
- My endpoint accepts a shared DTO type that can't be directly changed as the source isn't owned by the consuming project
- Layered types that are related and support metadata attributes for different scenarios and you want to avoid duplication, e.g. an EF entity type with metadata attributes that are honored when the database schema is generated, and a set of DTOs that represent the bindable surface-area of that entity type for the related API operations (CRUD)
Currently, there's no way to use the validation source generator to achieve this as the types it generates to implement IValidatableInfoResolver
, IValidatableInfo
, etc. are opaque and immutable (no setters, etc.). If you write code to manually try and re-use the generated types for TypeA
as the IValidatableInfoResolver
for TypeB
, the validation operation throws at runtime as calls to retrieve member values fail, and there's no way to mutate instances of the generated types to change the target Type
.
Proposal
To support this, we should consider supporting the existing System.ComponentModel.DataAnnotations.MetadataTypeAttribute
such that a type's validation metadata can be discovered from a different specified type:
[MetadataType(typeof(Person))]
public class CreatePersonDTO
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
public class Person
{
public long Id { get; set; }
[Required, StringLength(1, 100)]
public string? FirstName { get; set; }
[Required, StringLength(1, 100)]
public string? LastName { get; set; }
}
Additionally, we should introduce a new overload of the ValidatableTypeAttribute
that represents the same relationship in the opposite direction, for cases where the type being validated can't be modified by the consuming app:
// DTO types from a reference that can't be modified
public class CreatePersonDTO
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
public class UpdatePersonDTO
{
public int Id { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
// Type in the minimal API project that is used to define validation for the DTOs
[ValidatableType(typeof(CreatePersonDTO))]
[ValidatableType(typeof(UpdatePersonDTO))]
public class Person
{
public long Id { get; set; }
[Required, StringLength(1, 100)]
public string? FirstName { get; set; }
[Required, StringLength(1, 100)]
public string? LastName { get; set; }
}
When discovering types to generate the IValidatableInfoResolver
et al implementations for, these attributes would be honored. Members that match by name and type across the associated types would be considered as if the metadata had been applied on the target type member directly. The types can have unmatching members, they're simply ignored. Note that this isn't relying on the TypeDescriptor
sub-system at all, this logic would simply all be included in the validation source generator.
What about IValidatableObject
?
Today, IValidatableObject
is only supported directly on the object instance being validated by System.ComponentModel.DataAnnotations.Validator
and thus in places like MVC, etc. There is no mechanism to declare that a different type implements a custom validation method for the target type.
To support that scenario, we should consider introducing a new interface with a static abstract member such that a separate type can provide validation logic:
public interface IValidatorFor<TTarget>
{
abstract static IEnumerable<ValidationResult> Validate(TTarget object, ValidationContext validationContext);
}
These would be discovered like any other validation metadata, so for example combining with the previous example for MetadataType
:
[MetadataType(typeof(Person))]
public class CreatePersonDTO
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
[MetadataType(typeof(Person))]
public class UpdatePersonDTO
{
public int Id { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
public class Person : IValidatorFor<CreatePersonDTO>, IValidatorFor<UpdatePersonDTO>
{
public long Id { get; set; }
[Required, StringLength(1, 100)]
public string? FirstName { get; set; }
[Required, StringLength(1, 100)]
public string? LastName { get; set; }
public static IEnumerable<ValidationResult> Validate(CreatePersonDTO object, ValidationContext validationContext)
{
// Custom validation logic here
return [];
}
public static IEnumerable<ValidationResult> Validate(UpdatePersonDTO object, ValidationContext validationContext)
{
// Custom validation logic here
return [];
}
}