Skip to content

Add IServiceCollectionValidator for optional DI validation at BuildServiceProvider #119646

@iatsuta

Description

@iatsuta

Background and motivation

The default .NET DI container (Microsoft.Extensions.DependencyInjection) provides minimal validation during BuildServiceProvider, such as ValidateScopes and ValidateOnBuild. However, it lacks a standardized, extensible mechanism for developers to validate service registrations at build time.

Currently, developers must implement custom validation and invoke it manually after configuring services. This approach is error-prone and easy to forget, leading to runtime errors from misconfigured dependencies, such as:

Duplicate singleton services used in constructors expecting a single instance.

Missing required services for dependent constructors.

Other potential DI misconfigurations.

By introducing a standard IServiceCollectionValidator interface, the container could optionally:

Aggregate all validation errors during BuildServiceProvider.

Support multiple custom validators.

Maintain full backward compatibility: projects that do not use validators remain unaffected.

This would provide early feedback on misconfigurations, reduce runtime errors, and align with the existing philosophy of optional validation in the DI container.

API Proposal

namespace Microsoft.Extensions.DependencyInjection;

public interface IServiceCollectionValidator
{
    ValidationResult Validate(IServiceCollection services, object? options);
}

public static class ServiceCollectionValidationExtensions
{
    public static IServiceCollection AddValidator<TValidator>(this IServiceCollection services)
        where TValidator : IServiceCollectionValidator, new();

    public static IServiceCollection AddValidator(this IServiceCollection services, IServiceCollectionValidator validator);
}

public record ValidationResult(IReadOnlyList<string> Errors)
{
    bool IsSuccess { get; }
    static ValidationResult Success { get; }
    static ValidationResult operator +(ValidationResult left, ValidationResult right);
}

API Usage

var services = new ServiceCollection();

// Register services
services.AddSingleton<IService, ServiceImpl>();
services.AddSingleton<Consumer>();

// Register validators
services.AddValidator<DuplicateServiceUsageValidator>();

// Build the provider — validators run automatically
var provider = services.BuildServiceProvider();

// If validation fails, BuildServiceProvider throws with aggregated errors

Alternative Designs

using Xunit;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using System;

namespace CommonFramework.DependencyInjection.Tests
{
    public interface IService { }
    public class ServiceImpl : IService { }

    public class Consumer
    {
        public Consumer(IService service) { }
    }

    public class DuplicateServiceUsageValidatorTests
    {
        [Fact]
        public void BuildServiceProvider_ShouldThrow_WhenDuplicateSingletonUsedInConstructor()
        {
            // Arrange
            var services = new ServiceCollection();

            // Duplicate singleton
            services.AddSingleton<IService, ServiceImpl>();
            services.AddSingleton<IService, ServiceImpl>();

            // Consumer depends on single IService
            services.AddSingleton<Consumer>();

            // Register validator
            services.AddValidator<DuplicateServiceUsageValidator>();

            // Act
            Action act = () => 
            {
                // Instead of manually calling Validate, we simulate 
                // a "BuildServiceProvider" step that internally calls Validate
                services.Validate();
                var provider = services.BuildServiceProvider();
            };

            // Assert
            act.Should().Throw<InvalidOperationException>()
            .Where(ex => ex.Message.Contains("has been registered many times"));
        }
    }
}

Currently, developers must manually implement and invoke custom validators after configuring services, which is error-prone and scattered. A reference implementation with unit tests is available here: DuplicateServiceUsageValidatorTests

Risks

Backward compatibility: Fully preserved. Projects that do not register any IServiceCollectionValidator continue to work exactly as before.

Breaking changes: None. No existing APIs are modified.

Performance: Minimal impact. Validators are only invoked if explicitly registered, before BuildServiceProvider.

Error reporting: Any exceptions are limited to scenarios where custom validators detect misconfigurations. They do not affect projects that do not use validators.

This ensures that the proposal introduces optional, safe, and isolated functionality without breaking existing applications.

Metadata

Metadata

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions