Support splitting validation metadata from validatable type
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
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:
```csharp
[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:
```csharp
// 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:
```csharp
public interface IValidatorFor
{
abstract static IEnumerable Validate(TTarget object, ValidationContext validationContext);
}
```
These would be discovered like any other validation metadata, so for example combining with the previous example for `MetadataType`:
```csharp
[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, IValidatorFor
{
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 Validate(CreatePersonDTO object, ValidationContext validationContext)
{
// Custom validation logic here
return [];
}
public static IEnumerable Validate(UpdatePersonDTO object, ValidationContext validationContext)
{
// Custom validation logic here
return [];
}
}
```
Contributor guide
Assessment
This issue has not been assessed yet.