dotnet / dotnet/aspnetcore

Support splitting validation metadata from validatable type

Open
#61,781 0 comments 4 reactions 0 assignees View on GitHub
area-minimal feature-validation
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.