Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Implement async validation support for Microsoft.Extensions.Validation - #66487

#66487
Merged
Youssef1313 merged 92 commits into
maindotnet/aspnetcore:mainfrom
dev/ygerges/async-valdotnet/aspnetcore:dev/ygerges/async-valCopy head branch name to clipboard
Jun 23, 2026
Merged

Implement async validation support for Microsoft.Extensions.Validation#66487
Youssef1313 merged 92 commits into
maindotnet/aspnetcore:mainfrom
dev/ygerges/async-valdotnet/aspnetcore:dev/ygerges/async-valCopy head branch name to clipboard

Conversation

@Youssef1313

@Youssef1313 Youssef1313 commented Apr 27, 2026

Copy link
Copy Markdown
Member

Fixes #64609

In MEV, we have multiple "levels" of validations:

  • Validating ValidationAttributes on the parameter
  • Validating ValidationAttributes on the property.
  • Validating ValidationAttributes on the type
  • Validating IValidatableObject

Each level can be a mix of sync and async (except IValidatableObject - either sync or async).

The current implementation aims to:

  • run async validation attributes on the same element concurrently.
  • run validation on collections concurrently.
  • run property validations on a specific type concurrently.
  • keeps the same sync ordering consistent, so type-level attributes need to wait for property validations and only run in case of errors, and IValidatableObject also waits on type-level attributes validation and only runs in case of error.

On every element, we:

  • Validates the non-AsyncValidationAttributes first.
  • Starts all tasks for all available AsyncValidationAttributes.
  • As soon as any AsyncValidationAttribute finishes, its result will be reported, if error, via the OnValidationError event.

In addition, the following changes are made:

  • ValidationErrors no longer has a public setter. It will be problematic to do so if we go with "full parallelism" across all the levels. The knowledge of how to synchronize creating the dictionary will now be handled solely by ValidateContext.
  • ValidationErrors changed to be IReadOnlyDictionary. The concrete Dictionary isn't thread-safe. This could have been IDictionary instead of IReadOnlyDictionary. This needs discussion. Using IReadOnlyDictionary limits the external ability to add errors to the context. However, this was done to keep the dictionary lazy and keep the property null when no validation errors happen.
  • Exceptions during validations are no longer transformed to validation errors. This matches BCL's implementation of Validator, and makes more sense.
  • The public API of MEV needs to be carefully reviewed to decide what should be allowed and what's not etc.

@github-actions github-actions Bot added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label Apr 27, 2026
@Youssef1313
Youssef1313 force-pushed the dev/ygerges/async-val branch 2 times, most recently from 9f2b358 to 3507c74 Compare May 4, 2026 08:03
@Youssef1313
Youssef1313 force-pushed the dev/ygerges/async-val branch 2 times, most recently from 751b5ef to 2decfdb Compare May 27, 2026 08:20
@github-actions github-actions Bot added area-infrastructure Includes: MSBuild projects/targets, build scripts, CI, Installers and shared framework and removed needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically labels May 29, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hey @dotnet/aspnet-build, looks like this PR is something you want to take a look at.

Comment thread src/Validation/src/ValidatableParameterInfo.cs Outdated
Comment thread src/Validation/src/ValidatableTypeInfo.cs Outdated
@Youssef1313 Youssef1313 changed the title Incomplete prototype for async val Implement async validation support for Microsoft.Extensions.Validation Jun 1, 2026
@Youssef1313 Youssef1313 added area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-validation Issues related to model validation in minimal and controller-based APIs and removed area-infrastructure Includes: MSBuild projects/targets, build scripts, CI, Installers and shared framework labels Jun 4, 2026
@Youssef1313
Youssef1313 force-pushed the dev/ygerges/async-val branch from 2309efa to d0d3a8e Compare June 4, 2026 20:10
@Youssef1313
Youssef1313 marked this pull request as ready for review June 19, 2026 20:39

@javiercn javiercn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't looked at the tests in detail, but there are a few more things to look into

Comment thread src/Validation/src/ValidatableParameterInfo.cs Outdated
Comment on lines +194 to +219
if (nextUseNeedsClone)
{
currentContext = context.CopyWithState(originalState);
(clonedContexts ??= new()).Add(currentContext);
nextUseNeedsClone = false;
}

currentContext.CurrentValidationPath = $"{currentPrefix}[{index}]";
try
{
var task = validatableType.ValidateAsync(item, currentContext, cancellationToken);

if (task.IsCompletedSuccessfully)
{
await task;
}
else
{
nextUseNeedsClone = true;
(tasks ??= new()).Add(task);
}
}
catch (Exception ex)
{
(tasks ??= new()).Add(Task.FromException(ex));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a RunValidation helper or something like that, isn't it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javiercn I assume you want to share a helper between ValidatableTypeInfo, ValidatablePropertyInfo, and ValidatableParameterInfo for the similar logic there.

But what parts do you exactly want to share.

The ValidateAsync calls are on different interfaces and probably can't be shared, the setting of validation path is also different. The only thing appears to the small IsCompletelySuccessfully check.

Also if we want to share the catch with Task.FromException logic, we will need that helper method to call ValidateAsync, so we will be passing around a Func<Task>. That lambda is going to capture some state and allocates, unless we have RunValidation<TState> and pass that state as a parameter, where the code shared seems to be small anyways.

try
// First validate direct members
var originalState = context.CaptureMutableState();
(List<Task>? localValidationTasks, List<ValidateContext>? clonedContexts) = await ValidateMembers(value, context, originalState, localValidationTasks: null, clonedContexts: null, cancellationToken);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(List? localValidationTasks, List? clonedContexts) = await ValidateMembers(value, context, originalState, localValidationTasks: null, clonedContexts: null, cancellationToken)

Consider passing a readonly struct here instead of the 6 parameters or at least grouping (List<Task>? localValidationTasks, List<ValidateContext>? clonedContexts) in a struct and passing and returning that

Comment thread src/Validation/src/ValidateContext.cs Outdated
Comment on lines 129 to 143
internal void AddValidationErrorSuppressEvent(string path, IEnumerable<string> errors)
{
ValidationErrors ??= [];

if (ValidationErrors.TryGetValue(key, out var existingErrors) && !existingErrors.Contains(error))
var validationErrors = _validationErrors;
if (validationErrors is null)
{
ValidationErrors[key] = [.. existingErrors, error];
}
else
{
ValidationErrors[key] = [error];
var newDictionary = new ConcurrentDictionary<string, IEnumerable<string>>();
validationErrors = Interlocked.CompareExchange(ref _validationErrors, newDictionary, null) ?? newDictionary;
}

OnValidationError?.Invoke(new ValidationErrorContext
var existingErrors = (ConcurrentQueue<string>)validationErrors.GetOrAdd(path, static _ => new ConcurrentQueue<string>());
foreach (var error in errors)
{
Name = name,
Path = key,
Errors = [error],
Container = container
});
existingErrors.Enqueue(error);
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can probably significantly simplify this significantly. I believe we can always merge the errors without locking because we will only merge the errors after all the tasks have completed (we have awaited them) so there won't be any callback running concurrently at that point that is tied to the original context. So, we don't need the concurrent dictionary, and we don't need the concurrent queue. This is because the moment we detect that a task didn't finish synchronously that context is given exclusively to that task, and that propagates across levels, so even if we are validating Customer -> Order -> Product and product has a property that goes async, the task for validating product will be asyn (so another context would be created for any additional property in Order) and similar to other properties in Customer beyond Order. We can just use a dictionary and merge the errors into a list.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think currently we add the validation errors as soon as they come.

For example:

public class Order
{
    [MyAsyncVal1]
    [MyAsyncVal2]
    public string S { get; }
}

Both attributes will be validating concurrently, and they use the same context, no cloning needed.

}
};

await ValidationHelpers.ValidateAttributesAsync(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation with the callback and the generics is unnecessarily complex. Just define a private readonly context parameter and have one method to do the validation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent is to reduce the code duplication, and the use of the TState is only to allow the lambdas to be static to reduce allocations, it's a common pattern IMO.

I'm happy to refactor though if you see a better way. But I'm not following on the suggestion:

This implementation with the callback and the generics is unnecessarily complex. Just define a private readonly context parameter and have one method to do the validation.

Do you mean a readonly struct that we pass around? What would this struct contain and how does it simplify?

@javiercn

Copy link
Copy Markdown
Member

Proposal: factor the concurrent-validation bookkeeping into one place

I'd like to recommend adjusting this code to make it simpler and easier to read — fewer moving parts for a reader to hold in their head, with the same behavior. I'd apply it as four self-contained steps on top of this branch.

Step 1 — Extract the clone-on-async loop into one type

Right now the "snapshot state → loop → IsCompletedSuccessfully → clone-on-async → collect → Task.WhenAll → merge" sequence is hand-written three times: in ValidatableTypeInfo.ValidateMembers, the ValidatablePropertyInfo collection branch, and the ValidatableParameterInfo collection branch. I'd pull it into a single small value type, nested in ValidateContext:

internal struct AsyncValidationTracker
{
    // ... fields: _originalContext, _originalState, _currentContext, _pendingTasks, _clonedContexts, _nextNeedsClone

    // Reuses the context while validations complete synchronously; clones only after one goes async,
    // so two concurrently-running validations never share a context.
    public ValidateContext NextContext()
    {
        if (_nextNeedsClone)
        {
            _currentContext = _originalContext.CopyWithState(_originalState);
            (_clonedContexts ??= []).Add(_currentContext);
            _nextNeedsClone = false;
        }

        return _currentContext;
    }

    public void Track(Task validationTask)
    {
        if (validationTask.IsCompletedSuccessfully)
        {
            return; // synchronous: keep using the same context
        }

        _nextNeedsClone = true; // the next item must get its own clone
        (_pendingTasks ??= []).Add(validationTask);
    }

    // Stays fully synchronous when nothing was tracked; otherwise awaits all and merges clone errors back.
    public Task<bool> CompleteAsync()
        => _pendingTasks is null ? s_noClonedErrors : AwaitAndMergeAsync(_pendingTasks, _clonedContexts, _originalContext);
}

Each of the three call sites then reads as a flat loop:

var tracker = context.TrackAsyncValidations();
foreach (var item in enumerable)
{
    if (item is not null && options.TryGetValidatableTypeInfo(item.GetType(), out var t))
    {
        var itemContext = tracker.NextContext();           // clones only after an async item
        itemContext.CurrentValidationPath = $"{prefix}[{index}]";
        try { tracker.Track(t.ValidateAsync(item, itemContext, ct)); }
        catch (Exception ex) { tracker.Track(Task.FromException(ex)); }
    }
    index++;
}
await tracker.CompleteAsync();

The clone-on-demand semantics are unchanged: the original context is mutated in place across synchronous items, and a CopyWithState clone is created only for the item that follows one that didn't complete synchronously. Because the three copies become one, two existing inconsistencies disappear on their own: the parameter collection branch currently doesn't restore CurrentValidationPath after it runs (the property branch does), and it carries a redundant nextUseNeedsClone = false; the property branch doesn't. Unifying the loop resolves both.

Step 2 — Keep the synchronous attribute path off the async machinery

ValidationHelpers.ValidateAttributesAsync is async and always awaits, so even an element with only synchronous attributes (the common case) goes through the async path. I'd make it non-async and only build the task list / linked CTS when async work actually exists:

internal Task ValidateAttributesAsync(
    object? value,
    object? container,
    IValidationErrorReporter reporter,
    CancellationToken cancellationToken)
{
    var validationAttributes = reporter.GetValidationAttributes();
    return ValidateSynchronousAttributes(validationAttributes, value, container, reporter)
        ? ValidateAsynchronousAttributes(validationAttributes, value, container, reporter, cancellationToken)
        : Task.CompletedTask; // no async attributes: no state machine, no allocation
}

Step 3 — Replace the callback+state with a reporter the infos implement

The runner currently takes an Action<ValidateContext, ValidationResult, ValidationAttribute, TState> plus a TState tuple threaded through a 7-parameter core. I'd introduce one internal interface:

internal interface IValidationErrorReporter
{
    ValidationAttribute[] GetValidationAttributes();
    void ReportError(ValidateContext context, object? container, ValidationAttribute attribute, ValidationResult result);
}

and have the three infos implement it directly (explicitly, so nothing leaks onto the public surface). The error-reporting logic moves next to the data it needs, e.g. on ValidatableParameterInfo:

void IValidationErrorReporter.ReportError(ValidateContext context, object? container, ValidationAttribute attribute, ValidationResult result)
{
    var errorMessage = context.ResolveAttributeErrorMessage(Name, context.ValidationContext.DisplayName, declaringType: null, attribute, result);
    if (errorMessage is null)
    {
        return;
    }

    var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}";
    context.AddValidationError(new ValidationErrorContext
    {
        Name = Name,
        Path = key,
        Errors = [errorMessage],
        Container = container,
    });
}

so the call site becomes a single line, passing the info itself as the reporter (no per-call object, no generic):

await context.ValidateAttributesAsync(value, container: null, this, cancellationToken);

Since the reporter supplies its own attributes via GetValidationAttributes(), the runner loses its attribute-array parameter. With the callback gone I'd move the runner onto ValidateContext next to ResolveAttributeErrorMessage/AddValidationError and delete ValidationHelpers.cs.

Step 4 — Tighten visibility

With the tracker nested in ValidateContext, the three helpers it alone uses — CaptureMutableState, CopyWithState, MergeErrorsFromClonedContexts — move from internal to private.

What this buys us, concretely

Reduced coupling

  • The three Validatable*Info files stop reaching into ValidateContext's cloning internals: their dependency on CaptureMutableState / CopyWithState / MergeErrorsFromClonedContexts drops from 3 call sites across 3 files to 0 (those methods become private, with a single in-type caller).
  • The dependency on a separate ValidationHelpers static class is removed (1 file and type deleted).
  • Attribute reporting no longer flows through a 4-type-argument delegate and a positional tuple; it's a direct interface call.

Improved cohesion

  • The clone/await/merge algorithm exists in one type instead of three, so it can't drift.
  • Attribute resolution, recording, and execution now live together on ValidateContext instead of being split across ValidateContext + ValidationHelpers + three call-site lambdas.
  • Each info reports its own errors from the class that owns Name/DeclaringType, rather than from a captured tuple.

Easier to run (fewer paths / allocations)

  • The all-synchronous attribute path returns Task.CompletedTask with no async state machine; a fully synchronous validation makes 0 context clones.
  • The reporter is the existing info instance (this), so attribute validation allocates no per-call reporter object, and the <TReporter>/<TState> generic is gone.

Easier to follow

  • The runner entry point goes from 6 parameters + a delegate + a generic to 4 parameters, no delegate, no generic.
  • The collection loops drop from ~6 levels of nesting to ~3.
  • Net size is about flat: the touched files shrink by 123 lines (380 removed / 257 added), with the shared logic relocated into two small single-purpose files (123 lines total).

Behavior is unchanged — this is a structure-only change.

@javiercn

Copy link
Copy Markdown
Member

Proposal: drop the synchronization on ValidationErrors — the tasks already are the synchronization

I'd like to recommend simplifying the error store. Today it is a ConcurrentDictionary<string, IEnumerable<string>> whose values are ConcurrentQueue<string>, created via Interlocked.CompareExchange. That machinery exists to tolerate concurrent writers — but with one small change to how async attributes record their results, there are no concurrent writers at all, and the whole store can be a plain Dictionary<string, List<string>>.

The shape of the validation is fan-out / fan-in: every concurrent region starts a set of tasks and then awaits them with Task.WhenAll before anything reads or aggregates. If nothing writes a given context's store during the fan-out, then the await is the synchronization (it establishes the happens-before edge), and the store never needs to be thread-safe.

Two facts make that true:

  1. Across members/items, copy-on-suspend already gives every concurrently-running unit its own context, so they write to different stores.
  2. Within one element, the only shared writer is the async-attribute fan-out — and we can make those tasks return their result instead of writing, then apply them on one thread after the fan-in.

Fan-out / fan-in, within one element

flowchart TD
    A["ValidateAttributesAsync (one element)"] --> B["fan-out: one Task per AsyncValidationAttribute"]
    B --> C1["attr 1: GetValidationResultAsync"]
    B --> C2["attr 2: GetValidationResultAsync"]
    B --> C3["attr N: GetValidationResultAsync"]
    C1 --> D["fan-in: await Task.WhenAll (results in attribute order)"]
    C2 --> D
    C3 --> D
    D --> E["apply failures on ONE thread: reporter.ReportError(...)"]
    E --> F["plain Dictionary write, no synchronization"]
Loading

Fan-out / fan-in, across members (copy-on-suspend)

flowchart TD
    A["ValidateAsync (type)"] --> B["fan-out members through the tracker"]
    B --> M0["member 0 -> context C0 (the parent)"]
    B --> M1["member 1 -> clone C1"]
    B --> M2["member 2 -> clone C2"]
    M0 --> W["fan-in: await Task.WhenAll"]
    M1 --> W
    M2 --> W
    W --> G["merge: copy C1, C2 into C0"]
    G --> H["one thread reads/aggregates; the await was the synchronization"]
Loading

The crucial property both diagrams share: nothing reads or aggregates a store until after the fan-in, and each store has a single writer. So the concurrency is entirely contained by the tasks.

Steps

Step 1 — async attributes return their result instead of writing

Each attribute task closes over the same context today and calls reporter.ReportError(this, ...) concurrently. I'd have it return the failure and let the caller apply the failures on one thread after Task.WhenAll:

// Before: every attribute task writes to the shared context concurrently
private async Task RunAsyncValidationAttribute(AsyncValidationAttribute attribute, object? value, object? container, IValidationErrorReporter reporter, ...)
{
    var result = await attribute.GetValidationResultAsync(value, ValidationContext, linkedCts.Token);
    if (result is not null && result != ValidationResult.Success)
    {
        reporter.ReportError(this, container, attribute, result);   // concurrent write
        linkedCts.Cancel();
    }
}
// After: the task returns its failure; nothing shared is written during the fan-out
private async Task<AttributeFailure?> RunAsyncValidationAttribute(AsyncValidationAttribute attribute, object? value, ...)
{
    var result = await attribute.GetValidationResultAsync(value, ValidationContext, linkedCts.Token);
    if (result is not null && result != ValidationResult.Success)
    {
        linkedCts.Cancel();                          // first-error short-circuit preserved
        return new AttributeFailure(attribute, result);
    }

    return null;
}

// The caller applies the failures on one thread, after the fan-in:
var failures = await Task.WhenAll(tasks);            // results in attribute order
foreach (var failure in failures)
{
    if (failure is { } f)
    {
        reporter.ReportError(this, container, f.Attribute, f.Result);
    }
}

Step 2 — make the store a plain Dictionary

With no concurrent writer, the concurrent collections and the Interlocked lazy-init are unnecessary:

// Before
private ConcurrentDictionary<string, IEnumerable<string>>? _validationErrors;
// ...
var validationErrors = _validationErrors;
if (validationErrors is null)
{
    var newDictionary = new ConcurrentDictionary<string, IEnumerable<string>>();
    validationErrors = Interlocked.CompareExchange(ref _validationErrors, newDictionary, null) ?? newDictionary;
}
var existingErrors = (ConcurrentQueue<string>)validationErrors.GetOrAdd(path, static _ => new ConcurrentQueue<string>());
foreach (var error in errors) { existingErrors.Enqueue(error); }
// After
private Dictionary<string, IEnumerable<string>>? _validationErrors;
// ...
_validationErrors ??= new();
if (_validationErrors.TryGetValue(path, out var existing))
{
    ((List<string>)existing).AddRange(errors);       // same context, same key -> more messages
}
else
{
    _validationErrors[path] = new List<string>(errors);
}

Step 3 — make the merge a plain copy and delete AddValidationErrorSuppressEvent

Each cloned context owns a disjoint slice of the key space (one member or collection item, so a distinct path prefix), so merging clones into the parent has nothing to reconcile — it is a straight dictionary copy. That removes the only other caller of the suppress helper, so it folds away:

// After: the merge is a disjoint dictionary copy; the event already fired on the clone, so it stays silent
private bool MergeErrorsFromClonedContexts(List<ValidateContext>? clonedContexts)
{
    if (clonedContexts is null) { return false; }

    var hasErrors = false;
    foreach (var clonedContext in clonedContexts)
    {
        if (clonedContext._validationErrors is null) { continue; }

        hasErrors = true;
        _validationErrors ??= new();
        foreach (var entry in clonedContext._validationErrors)
        {
            _validationErrors[entry.Key] = entry.Value;   // disjoint keys -> plain copy, reuse the list
        }
    }

    return hasErrors;
}

AddValidationErrorSuppressEvent is deleted; AddValidationError keeps the only per-key append (for the within-context case of several attributes on one member) and raises the event itself.

Why this is safe

  • Single writer per context. Copy-on-suspend guarantees at most one suspended unit per context (the next one clones), and Step 1 means an element's attributes no longer write at all during the fan-out — they return. So every store is written by exactly one thread.
  • The await is the barrier. Reads and the merge happen strictly after await Task.WhenAll, which establishes happens-before from every task's writes to the aggregating thread. No lock or concurrent collection is needed for visibility.
  • Disjoint keys on merge. Distinct members and collection items produce distinct path prefixes, so copying clone stores into the parent never collides. The per-key list (several messages under one key) is only needed within a context and stays in AddValidationError.
  • Behavior preserved. The first-error short-circuit (linkedCts.Cancel()) is unchanged, exception propagation is unchanged, and the same errors are produced.
  • Deterministic ordering, as a bonus. Because Task.WhenAll yields results in input order and the merge copies clones in member/item order, errors now land in validation order instead of the ConcurrentDictionary's hash/completion order.

What it removes

  • ConcurrentDictionary<string, IEnumerable<string>> -> Dictionary<string, List<string>>
  • ConcurrentQueue<string> values -> List<string>
  • Interlocked.CompareExchange lazy-init -> ??= new()
  • AddValidationErrorSuppressEvent -> deleted (merge is a direct copy)
  • The using System.Collections.Concurrent; import

…with no public API change and the same 115 tests passing.

Comment thread src/Validation/src/ValidationHelpers.cs Outdated

@javiercn javiercn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!!

Thanks for getting this done @Youssef1313!!

@Youssef1313

Copy link
Copy Markdown
Member Author

@copilot Revert the changes under src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests, and run all generator tests and ensure they all pass. Make any necessary changes to the tests to make sure they pass. Do NOT modify any product code or any file outside src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests.

Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com>

Copilot AI commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@copilot Revert the changes under src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests, and run all generator tests and ensure they all pass. Make any necessary changes to the tests to...

Done. Reverted all PR changes under src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests to their pre-PR state. All generator tests pass.

@Youssef1313
Youssef1313 merged commit b546278 into main Jun 23, 2026
25 checks passed
@Youssef1313
Youssef1313 deleted the dev/ygerges/async-val branch June 23, 2026 13:50
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview6 milestone Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-validation Issues related to model validation in minimal and controller-based APIs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support async validation in minimal APIs

6 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.