Skip to content

[WIP] Add IServiceCollectionValidator for DI validation#128246

Draft
Copilot wants to merge 4 commits into
mainfrom
copilot/add-isc-collection-validator
Draft

[WIP] Add IServiceCollectionValidator for DI validation#128246
Copilot wants to merge 4 commits into
mainfrom
copilot/add-isc-collection-validator

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 15, 2026

  • Create ValidationResult readonly struct in Abstractions package
  • Create IServiceCollectionValidator interface in Abstractions package
  • Create ServiceCollectionValidationExtensions in Abstractions package (with generic, instance, and delegate overloads)
  • Integrate validator execution into BuildServiceProvider in main DI package
  • Add error message string resource to main DI package
  • Update Abstractions ref file with new public API surface
  • Add tests to ServiceProviderValidationTests.cs
  • Build verified - both libraries build cleanly
  • All 1380 tests pass (including 11 new validator tests)

…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>
Copilot AI requested review from Copilot and removed request for Copilot May 15, 2026 08:56
Copilot AI requested review from Copilot and removed request for Copilot May 15, 2026 09:10
Copilot stopped work on behalf of rosebyte due to an error May 15, 2026 09:19
Copilot AI requested a review from rosebyte May 15, 2026 09:19
/// <summary>
/// Represents the result of an <see cref="IServiceCollectionValidator"/> validation.
/// </summary>
public readonly struct ValidationResult
Copy link
Copy Markdown
Member

@tarekgh tarekgh May 15, 2026

Choose a reason for hiding this comment

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

Is it ok to use the same type name ValidationResult which already used in the data annotations?

https://source.dot.net/#System.ComponentModel.Annotations/System/ComponentModel/DataAnnotations/ValidationResult.cs,15

@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-extensions-dependencyinjection
See info in area-owners.md if you want to be subscribed.

Copilot AI review requested due to automatic review settings May 18, 2026 05:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 with Success/Fail/operator +), and ServiceCollectionValidationExtensions.AddValidator (generic, instance, and delegate overloads), reflected in the ref assembly.
  • BuildServiceProvider(IServiceCollection, ServiceProviderOptions) now resolves all registered IServiceCollectionValidator singletons, invokes them, and throws InvalidOperationException (after disposing the provider) when any return failures.
  • New ValidatorsFailedWithErrors string resource and 11 new xUnit tests in ServiceProviderValidationTests.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.

Comment on lines +43 to +46
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.
Comment on lines +82 to +97
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);
}
Comment on lines +21 to +36
/// <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];
}
Comment on lines +56 to +61
var provider = new ServiceProvider(services, options);

RunValidators(provider, services);

return provider;
}
Comment on lines +93 to +95
IReadOnlyList<ServiceDescriptor> descriptors = services is IReadOnlyList<ServiceDescriptor> readOnly
? readOnly
: new ReadOnlyCollection<ServiceDescriptor>((IList<ServiceDescriptor>)services);
Comment on lines +65 to +91
// 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;
}
Comment on lines +100 to +121
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)));
}
Comment on lines +670 to +692
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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add IServiceCollectionValidator for optional DI validation at BuildServiceProvider

4 participants