[WIP] Add IServiceCollectionValidator for DI validation#128246
Conversation
…rviceProvider Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/7f9fbc0f-fc3d-49a0-827c-b36f71b15b48 Co-authored-by: rosebyte <14963300+rosebyte@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/7f9fbc0f-fc3d-49a0-827c-b36f71b15b48 Co-authored-by: rosebyte <14963300+rosebyte@users.noreply.github.com>
| /// <summary> | ||
| /// Represents the result of an <see cref="IServiceCollectionValidator"/> validation. | ||
| /// </summary> | ||
| public readonly struct ValidationResult |
There was a problem hiding this comment.
Is it ok to use the same type name ValidationResult which already used in the data annotations?
|
Tagging subscribers to this area: @dotnet/area-extensions-dependencyinjection |
There was a problem hiding this comment.
Pull request overview
Adds a pluggable validation extensibility point to Microsoft.Extensions.DependencyInjection. Consumers can register one or more IServiceCollectionValidator implementations (or a delegate) that are invoked during BuildServiceProvider and can fail the build with aggregated error messages. The PR is marked WIP and the new public surface has not yet been through dotnet/runtime's API review process.
Changes:
- New abstractions:
IServiceCollectionValidator,ValidationResult(readonly struct withSuccess/Fail/operator +), andServiceCollectionValidationExtensions.AddValidator(generic, instance, and delegate overloads), reflected in the ref assembly. BuildServiceProvider(IServiceCollection, ServiceProviderOptions)now resolves all registeredIServiceCollectionValidatorsingletons, invokes them, and throwsInvalidOperationException(after disposing the provider) when any return failures.- New
ValidatorsFailedWithErrorsstring resource and 11 new xUnit tests inServiceProviderValidationTests.cs.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/IServiceCollectionValidator.cs |
New interface defining the validation contract. |
src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ValidationResult.cs |
New readonly struct representing success/failure with error messages and a + operator. |
src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/Extensions/ServiceCollectionValidationExtensions.cs |
AddValidator overloads (generic, instance, delegate) plus internal DelegateValidator. |
src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs |
Ref-assembly additions for the new public surface. |
src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceCollectionContainerBuilderExtensions.cs |
Runs registered validators inside BuildServiceProvider, disposes the provider and throws on failure. |
src/libraries/Microsoft.Extensions.DependencyInjection/src/Resources/Strings.resx |
Adds the ValidatorsFailedWithErrors aggregated error message resource. |
src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderValidationTests.cs |
Adds 11 tests covering success, failure aggregation, delegate overload, and constructor injection. |
| public partial interface IServiceCollectionValidator | ||
| { | ||
| Microsoft.Extensions.DependencyInjection.ValidationResult Validate(System.Collections.Generic.IReadOnlyList<Microsoft.Extensions.DependencyInjection.ServiceDescriptor> services); | ||
| } |
| /// <summary> | ||
| /// Represents the result of an <see cref="IServiceCollectionValidator"/> validation. | ||
| /// </summary> | ||
| public readonly struct ValidationResult |
| /// Implementations are registered in the <see cref="IServiceCollection"/> via | ||
| /// <see cref="ServiceCollectionValidationExtensions.AddValidator{TValidator}(IServiceCollection)"/> or one of its overloads, | ||
| /// and are resolved and invoked automatically just before the built <see cref="System.IServiceProvider"/> is returned to the caller. | ||
| /// Because validators are resolved from the container, constructor injection of any registered service is fully supported. |
| public static ValidationResult Fail(IReadOnlyList<string> errors) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(errors); | ||
| if (errors.Count == 0) | ||
| { | ||
| return Success; | ||
| } | ||
|
|
||
| var copy = new string[errors.Count]; | ||
| for (int i = 0; i < errors.Count; i++) | ||
| { | ||
| copy[i] = errors[i]; | ||
| } | ||
|
|
||
| return new ValidationResult(copy); | ||
| } |
| /// <param name="errors">The list of validation errors.</param> | ||
| public ValidationResult(IReadOnlyList<string> errors) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(errors); | ||
|
|
||
| if (errors.Count == 0) | ||
| { | ||
| _errors = null; | ||
| } | ||
| else | ||
| { | ||
| _errors = new string[errors.Count]; | ||
| for (int i = 0; i < errors.Count; i++) | ||
| { | ||
| _errors[i] = errors[i]; | ||
| } |
| var provider = new ServiceProvider(services, options); | ||
|
|
||
| RunValidators(provider, services); | ||
|
|
||
| return provider; | ||
| } |
| IReadOnlyList<ServiceDescriptor> descriptors = services is IReadOnlyList<ServiceDescriptor> readOnly | ||
| ? readOnly | ||
| : new ReadOnlyCollection<ServiceDescriptor>((IList<ServiceDescriptor>)services); |
| // Fast path: avoid resolution overhead and EventSource noise when no validators are registered. | ||
| bool hasValidators = false; | ||
| foreach (ServiceDescriptor descriptor in services) | ||
| { | ||
| if (descriptor.ServiceType == typeof(IServiceCollectionValidator)) | ||
| { | ||
| hasValidators = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (!hasValidators) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| List<IServiceCollectionValidator>? validators = null; | ||
| foreach (IServiceCollectionValidator validator in provider.GetServices<IServiceCollectionValidator>()) | ||
| { | ||
| validators ??= new List<IServiceCollectionValidator>(); | ||
| validators.Add(validator); | ||
| } | ||
|
|
||
| if (validators is null) | ||
| { | ||
| return; | ||
| } |
| foreach (IServiceCollectionValidator validator in validators) | ||
| { | ||
| ValidationResult result = validator.Validate(descriptors); | ||
| if (!result.IsSuccess) | ||
| { | ||
| errors ??= new List<string>(); | ||
| errors.AddRange(result.Errors); | ||
| } | ||
| } | ||
| } | ||
| catch | ||
| { | ||
| provider.Dispose(); | ||
| throw; | ||
| } | ||
|
|
||
| if (errors is not null) | ||
| { | ||
| provider.Dispose(); | ||
| throw new InvalidOperationException( | ||
| SR.Format(SR.ValidatorsFailedWithErrors, string.Join(Environment.NewLine, errors))); | ||
| } |
| public void BuildServiceProvider_WithValidatorReturningError_Throws() | ||
| { | ||
| var services = new ServiceCollection(); | ||
| services.AddSingleton<IFoo, FooImpl>(); | ||
| services.AddValidator(new AlwaysFailValidator("test error")); | ||
|
|
||
| var ex = Assert.Throws<InvalidOperationException>(() => services.BuildServiceProvider()); | ||
|
|
||
| Assert.Contains("test error", ex.Message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void BuildServiceProvider_AggregatesErrorsFromMultipleValidators() | ||
| { | ||
| var services = new ServiceCollection(); | ||
| services.AddValidator(new AlwaysFailValidator("error1")); | ||
| services.AddValidator(new AlwaysFailValidator("error2")); | ||
|
|
||
| var ex = Assert.Throws<InvalidOperationException>(() => services.BuildServiceProvider()); | ||
|
|
||
| Assert.Contains("error1", ex.Message); | ||
| Assert.Contains("error2", ex.Message); | ||
| } |
ValidationResultreadonly struct in Abstractions packageIServiceCollectionValidatorinterface in Abstractions packageServiceCollectionValidationExtensionsin Abstractions package (with generic, instance, and delegate overloads)BuildServiceProviderin main DI packageServiceProviderValidationTests.cs