Skip to content

Support splitting validation metadata from validatable type #61781

Open
@DamianEdwards

Description

@DamianEdwards

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 [];
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcfeature-validationIssues related to model validation in minimal and controller-based APIs

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions