Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public FromKeyedServicesAttribute(object? key) { }
public partial interface IServiceCollection : System.Collections.Generic.ICollection<Microsoft.Extensions.DependencyInjection.ServiceDescriptor>, System.Collections.Generic.IEnumerable<Microsoft.Extensions.DependencyInjection.ServiceDescriptor>, System.Collections.Generic.IList<Microsoft.Extensions.DependencyInjection.ServiceDescriptor>, System.Collections.IEnumerable
{
}
public partial interface IServiceCollectionValidator
{
Microsoft.Extensions.DependencyInjection.ValidationResult Validate(System.Collections.Generic.IReadOnlyList<Microsoft.Extensions.DependencyInjection.ServiceDescriptor> services);
}
Comment on lines +43 to +46
public partial interface IServiceProviderFactory<TContainerBuilder> where TContainerBuilder : notnull
{
TContainerBuilder CreateBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection services);
Expand Down Expand Up @@ -143,6 +147,12 @@ public static partial class ServiceCollectionServiceExtensions
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddTransient<TService, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) where TService : class where TImplementation : class, TService { throw null; }
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddTransient<TService, TImplementation>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Func<System.IServiceProvider, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService { throw null; }
}
public static partial class ServiceCollectionValidationExtensions
{
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidator<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TValidator>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) where TValidator : class, Microsoft.Extensions.DependencyInjection.IServiceCollectionValidator { throw null; }
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidator(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.DependencyInjection.IServiceCollectionValidator validator) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidator(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Func<System.IServiceProvider, System.Collections.Generic.IReadOnlyList<Microsoft.Extensions.DependencyInjection.ServiceDescriptor>, Microsoft.Extensions.DependencyInjection.ValidationResult> validator) { throw null; }
}
public partial class ServiceDescriptor
{
public ServiceDescriptor(System.Type serviceType, System.Func<System.IServiceProvider, object> factory, Microsoft.Extensions.DependencyInjection.ServiceLifetime lifetime) { }
Expand Down Expand Up @@ -243,6 +253,18 @@ public static partial class ServiceProviderServiceExtensions
public static System.Collections.Generic.IEnumerable<T> GetServices<T>(this System.IServiceProvider provider) { throw null; }
public static T? GetService<T>(this System.IServiceProvider provider) { throw null; }
}
public readonly partial struct ValidationResult
{
private readonly object _dummy;
private readonly int _dummyPrimitive;
public ValidationResult(System.Collections.Generic.IReadOnlyList<string> errors) { }
public static Microsoft.Extensions.DependencyInjection.ValidationResult Success { get { throw null; } }
public System.Collections.Generic.IReadOnlyList<string> Errors { get { throw null; } }
public bool IsSuccess { get { throw null; } }
public static Microsoft.Extensions.DependencyInjection.ValidationResult Fail(string error) { throw null; }
public static Microsoft.Extensions.DependencyInjection.ValidationResult Fail(System.Collections.Generic.IReadOnlyList<string> errors) { throw null; }
public static Microsoft.Extensions.DependencyInjection.ValidationResult operator +(Microsoft.Extensions.DependencyInjection.ValidationResult left, Microsoft.Extensions.DependencyInjection.ValidationResult right) { throw null; }
}
}
namespace Microsoft.Extensions.DependencyInjection.Extensions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extension methods for registering <see cref="IServiceCollectionValidator"/> instances with an <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionValidationExtensions
{
/// <summary>
/// Registers <typeparamref name="TValidator"/> as an <see cref="IServiceCollectionValidator"/> singleton.
/// </summary>
/// <typeparam name="TValidator">The validator type. It will be instantiated by the DI container, allowing constructor injection.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the validator to.</param>
/// <returns>The <paramref name="services"/> to allow chaining.</returns>
public static IServiceCollection AddValidator<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TValidator>(this IServiceCollection services)
where TValidator : class, IServiceCollectionValidator
{
ArgumentNullException.ThrowIfNull(services);

services.AddSingleton<IServiceCollectionValidator, TValidator>();
return services;
}

/// <summary>
/// Registers the given <paramref name="validator"/> instance as an <see cref="IServiceCollectionValidator"/> singleton.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the validator to.</param>
/// <param name="validator">The validator instance to register.</param>
/// <returns>The <paramref name="services"/> to allow chaining.</returns>
public static IServiceCollection AddValidator(this IServiceCollection services, IServiceCollectionValidator validator)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(validator);

services.AddSingleton<IServiceCollectionValidator>(validator);
return services;
}

/// <summary>
/// Registers a delegate as an <see cref="IServiceCollectionValidator"/> singleton.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the validator to.</param>
/// <param name="validator">
/// A delegate that performs the validation. The first parameter is the built <see cref="IServiceProvider"/>,
/// which can be used to resolve services; the second parameter is a read-only view of the registered
/// <see cref="ServiceDescriptor"/> instances.
/// </param>
/// <returns>The <paramref name="services"/> to allow chaining.</returns>
public static IServiceCollection AddValidator(
this IServiceCollection services,
Func<IServiceProvider, IReadOnlyList<ServiceDescriptor>, ValidationResult> validator)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(validator);

services.AddSingleton<IServiceCollectionValidator>(
sp => new DelegateValidator(sp, validator));
return services;
}

private sealed class DelegateValidator : IServiceCollectionValidator
{
private readonly IServiceProvider _serviceProvider;
private readonly Func<IServiceProvider, IReadOnlyList<ServiceDescriptor>, ValidationResult> _validator;

public DelegateValidator(
IServiceProvider serviceProvider,
Func<IServiceProvider, IReadOnlyList<ServiceDescriptor>, ValidationResult> validator)
{
_serviceProvider = serviceProvider;
_validator = validator;
}

public ValidationResult Validate(IReadOnlyList<ServiceDescriptor> services)
=> _validator(_serviceProvider, services);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;

namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Defines a mechanism to validate service registrations when building a service provider.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public interface IServiceCollectionValidator
{
/// <summary>
/// Validates the service registrations described by <paramref name="services"/>.
/// </summary>
/// <param name="services">A read-only view of the service descriptors registered in the container.</param>
/// <returns>
/// A <see cref="ValidationResult"/> that is <see cref="ValidationResult.Success"/> when validation passed,
/// or contains one or more error messages when validation failed.
/// </returns>
ValidationResult Validate(IReadOnlyList<ServiceDescriptor> services);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;

namespace Microsoft.Extensions.DependencyInjection
{
/// <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

{
private static readonly string[] s_empty = Array.Empty<string>();

private readonly string[]? _errors;

/// <summary>
/// Initializes a new instance of the <see cref="ValidationResult"/> struct with the specified errors.
/// </summary>
/// <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 +21 to +36
}
}

private ValidationResult(string[] errors)
{
_errors = errors.Length == 0 ? null : errors;
}

/// <summary>
/// Gets a <see cref="ValidationResult"/> that represents a successful validation.
/// </summary>
public static ValidationResult Success { get; } = new ValidationResult(s_empty);

/// <summary>
/// Gets the list of validation errors.
/// </summary>
/// <value>
/// A read-only list of error messages, or an empty list when validation succeeded.
/// </value>
public IReadOnlyList<string> Errors => _errors ?? s_empty;

/// <summary>
/// Gets a value that indicates whether the validation succeeded.
/// </summary>
/// <value>
/// <see langword="true" /> if validation succeeded and there are no errors; otherwise, <see langword="false" />.
/// </value>
public bool IsSuccess => _errors is null;

/// <summary>
/// Creates a <see cref="ValidationResult"/> that represents a failure with the specified error message.
/// </summary>
/// <param name="error">The validation error message.</param>
/// <returns>A <see cref="ValidationResult"/> with the specified error.</returns>
public static ValidationResult Fail(string error)
{
ArgumentNullException.ThrowIfNull(error);
return new ValidationResult(new[] { error });
}

/// <summary>
/// Creates a <see cref="ValidationResult"/> that represents a failure with the specified error messages.
/// </summary>
/// <param name="errors">The list of validation error messages.</param>
/// <returns>A <see cref="ValidationResult"/> with the specified errors.</returns>
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 +82 to +97

/// <summary>
/// Combines two <see cref="ValidationResult"/> instances by aggregating their errors.
/// </summary>
/// <param name="left">The first <see cref="ValidationResult"/>.</param>
/// <param name="right">The second <see cref="ValidationResult"/>.</param>
/// <returns>
/// A new <see cref="ValidationResult"/> whose <see cref="Errors"/> contains the errors from both operands.
/// </returns>
/// <remarks>
/// Each application of this operator allocates a new array to hold the combined errors.
/// When aggregating many <see cref="ValidationResult"/> values, prefer collecting all errors
/// manually and constructing a single <see cref="ValidationResult"/> via
/// <see cref="Fail(IReadOnlyList{string})"/> to avoid repeated allocations.
/// </remarks>
public static ValidationResult operator +(ValidationResult left, ValidationResult right)
{
if (left.IsSuccess)
{
return right;
}

if (right.IsSuccess)
{
return left;
}

var errors = new string[left._errors!.Length + right._errors!.Length];
Array.Copy(left._errors, errors, left._errors.Length);
Array.Copy(right._errors, 0, errors, left._errors.Length, right._errors.Length);
return new ValidationResult(errors);
}
Comment on lines +99 to +129
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,8 @@
<data name="NoKeyedServiceRegistered" xml:space="preserve">
<value>No keyed service for type '{0}' using key type '{1}' has been registered.</value>
</data>
<data name="ValidatorsFailedWithErrors" xml:space="preserve">
<value>Some service registrations failed validation:
{0}</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace Microsoft.Extensions.DependencyInjection
{
Expand All @@ -26,7 +28,7 @@ public static ServiceProvider BuildServiceProvider(this IServiceCollection servi
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> containing service descriptors.</param>
/// <param name="validateScopes">
/// <c>true</c> to perform check verifying that scoped services never gets resolved from root provider; otherwise <c>false</c>.
/// <see langword="true" /> to perform check verifying that scoped services never gets resolved from root provider; otherwise <see langword="false" />.
/// </param>
/// <returns>The <see cref="ServiceProvider"/>.</returns>
public static ServiceProvider BuildServiceProvider(this IServiceCollection services, bool validateScopes)
Expand All @@ -43,12 +45,80 @@ public static ServiceProvider BuildServiceProvider(this IServiceCollection servi
/// Configures various service provider behaviors.
/// </param>
/// <returns>The <see cref="ServiceProvider"/>.</returns>
/// <exception cref="InvalidOperationException">
/// One or more registered <see cref="IServiceCollectionValidator"/> instances reported validation errors.
/// </exception>
public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(options);

return new ServiceProvider(services, options);
var provider = new ServiceProvider(services, options);

RunValidators(provider, services);

return provider;
}
Comment on lines +56 to +61

private static void RunValidators(ServiceProvider provider, IServiceCollection 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;
}
Comment on lines +65 to +91

IReadOnlyList<ServiceDescriptor> descriptors = services is IReadOnlyList<ServiceDescriptor> readOnly
? readOnly
: new ReadOnlyCollection<ServiceDescriptor>((IList<ServiceDescriptor>)services);
Comment on lines +93 to +95

List<string>? errors = null;
try
{
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 +100 to +121
}
}
}
Loading
Loading