Skip to content

Implementing async validation logic should not require implementing sync IsValid #131498

Description

@danroth27

To write an async validation rule — say a uniqueness check against a database — an author derives from AsyncValidationAttribute and implements IsValidAsync. But IsValid(object?, ValidationContext) is also abstract, so they must implement a synchronous method too:

error CS0534: 'UniqueUsernameAttribute' does not implement inherited abstract member
'AsyncValidationAttribute.IsValid(object?, ValidationContext)'

That method is usually irrelevant to what the author is trying to express. Their rule needs I/O; there's nothing meaningful for the sync overload to do. So it becomes a required stub whose correct contents aren't obvious.

The choice is confusing and error-prone

How should a user implement IsValid, and why does AsyncValidationAttribute require it at all? AsyncValidationAttribute is for defining async validation logic; for sync logic there is already ValidationAttribute. It derives from ValidationAttribute so that it can influence what happens when only the synchronous validation path is called. The framework's own validators could detect that case and offer configurable behavior, but these attributes may also be consumed by validation systems the framework doesn't control — attribute.IsValid(...) and attribute.GetValidationResult(...) are public entry points that bypass Validator entirely, so the behavior has to live on the attribute itself.

This is not a rare path. Existing DataAnnotations-based validation systems are synchronous only; asynchronous validation is new in .NET 11 and opt-in. So whatever an author writes in IsValid is what essentially every existing consumer of that attribute will execute today. The synchronous overload is the common case at the moment, not the fallback.

In most cases the right behavior is to throw. Running sync-only validation when async validation is required is a programming error, and the alternatives are both worse:

  • Silently passing means the rule doesn't run at all, which is a security risk.
  • Failing is also wrong, because there is no reasonable message to show the user — they have no control over whether validation was invoked synchronously or asynchronously.

This does mean a system running synchronous validation will hit an exception when handed a newer type that requires async validation. That is the correct outcome: running sync validation on a type that requires async validation is invalid, and silently ignoring the validation logic would be a security risk. It is also worth noting that leaving IsValid to the author does not avoid this — it only makes the result unpredictable. Since existing systems are sync-only, they are the ones that will execute whatever each author wrote, so the behavior they see is currently outside the framework's control.

Requiring users to implement IsValid themselves pushes them toward two bad shapes:

  • Sync-over-async. Users will try to implement IsValid in terms of the async logic. This may work in limited cases, but in environments like ASP.NET Core it can cause thread pool starvation.
  • Returning success. Users may assume both sync and async validation always run, and implement IsValid to always return valid. I've seen AI coding assistants make exactly this assumption.

The same applies to IAsyncValidatableObject

IAsyncValidatableObject derives from IValidatableObject and declares only ValidateAsync, so implementing async object-level validation likewise requires supplying a synchronous Validate with the same unhelpful choice of contents.

Proposal

Make AsyncValidationAttribute.IsValid virtual, with a default implementation that throws to indicate synchronous validation isn't supported for the attribute. Give IAsyncValidatableObject.Validate an equivalent default interface implementation.

  • Authors writing async validation implement only the async member.
  • Authors who have a correct synchronous equivalent can still override it.
  • Existing sync-only validation systems get one predictable behavior instead of whatever each author happened to write.

Related

This issue is about the authoring requirement only, not whether a synchronous contract should exist.

Observed on 11.0.0-preview.6.26359.118.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions