File: DependencyInjectionValidation.cs
Web Access
Project: Microsoft.DotNet.Internal.DependencyInjection.Testing.csproj (Microsoft.DotNet.Internal.DependencyInjection.Testing)
// 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.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.DotNet.Internal.DependencyInjection.Testing;

public static class DependencyInjectionValidation
{
    private static readonly ImmutableList<string> s_exemptTypes = ImmutableList.Create(
        "System.Fabric.ServiceContext",
        "System.Fabric.StatelessServiceContext",
        "System.Fabric.StatefulServiceContext",
        "Microsoft.DotNet.ServiceFabric.ServiceHost.IServiceLoadReporter",
        "Microsoft.Extensions.Options.IConfigureOptions`1",
        "Microsoft.Extensions.Caching.Memory.MemoryCacheOptions",
        "Microsoft.Extensions.Caching.Memory.MemoryDistributedCacheOptions",
        "Microsoft.Extensions.DependencyInjection.IServiceScopeFactory",
        "System.IServiceProvider"
    );

    private static readonly ImmutableList<string> s_exemptNamespaces = ImmutableList.Create(
        "Microsoft.ApplicationInsights.AspNetCore",
        "Microsoft.AspNetCore"
    );

    public static bool IsDependencyResolutionCoherent(
        Action<ServiceCollection> register,
        out string errorMessage,
        IEnumerable<Type> additionalScopedTypes = null,
        IEnumerable<Type> additionalSingletonTypes = null)
    {
        errorMessage = null;

        StringBuilder allErrors = new StringBuilder();
        allErrors.Append("The following types are not resolvable:");

        var services = new ServiceCollection();

        register(services);

        bool allResolved = true;

        foreach (ServiceDescriptor service in services)
        {
            if (service.ImplementationType == null)
            {
                continue;
            }

            if (IsExemptType(service.ImplementationType) || IsExemptType(service.ServiceType))
            {
                continue;
            }

            if (!IsTypeResolvable(service.ImplementationType, services, allErrors, service.Lifetime))
            {
                allResolved = false;
            }
        }

        foreach (Type scopedType in additionalScopedTypes ?? Enumerable.Empty<Type>())
        {
            if (!IsTypeResolvable(scopedType, services, allErrors, ServiceLifetime.Scoped))
            {
                allResolved = false;
            }
        }

        foreach (Type scopedType in additionalSingletonTypes ?? Enumerable.Empty<Type>())
        {
            if (!IsTypeResolvable(scopedType, services, allErrors, ServiceLifetime.Singleton))
            {
                allResolved = false;
            }
        }

        if (!allResolved)
            errorMessage = allErrors.ToString();

        return allResolved;
    }

    private static bool IsTypeResolvable(
        Type type,
        ServiceCollection services,
        StringBuilder msgBuilder,
        ServiceLifetime serviceLifetime)
    {
        ConstructorInfo[] constructors = type
            .GetConstructors(BindingFlags.Public | BindingFlags.Instance)
            .OrderBy(c => c.GetParameters().Length)
            .ToArray();

        if (constructors.Length == 0)
        {
            // zero constructor things are implicitly constructable
            return true;
        }

        string errorMessage = null;
        foreach (ConstructorInfo ctor in constructors)
        {
            if (IsConstructorResolvable(ctor, services, errorMessage == null, serviceLifetime, out string ctorMsg))
            {
                return true;
            }

            errorMessage ??= ctorMsg;
        }

        msgBuilder.AppendLine();
        msgBuilder.AppendLine();
        msgBuilder.AppendLine(errorMessage);

        return false;
    }

    private static bool IsConstructorResolvable(
        ConstructorInfo ctor,
        ServiceCollection services,
        bool recordErrors,
        ServiceLifetime serviceLifetime,
        out string errorMessage)
    {
        errorMessage = null;
        bool resolvedAllParameters = true;
        StringBuilder msgBuilder = null;
        if (recordErrors)
        {
            msgBuilder = new StringBuilder();
            msgBuilder.Append("Type ");
            msgBuilder.Append(ctor.DeclaringType.FullName);
            msgBuilder.Append(" could not find registered definition for parameter(s): ");
        }

        foreach (ParameterInfo p in ctor.GetParameters())
        {
            ServiceDescriptor parameterService = services.FirstOrDefault(s => IsMatchingServiceRegistration(s.ServiceType, p.ParameterType));
            if (parameterService != null)
            {
                if (serviceLifetime == ServiceLifetime.Singleton &&
                    parameterService.Lifetime == ServiceLifetime.Scoped)
                {
                    if (!resolvedAllParameters)
                    {
                        msgBuilder.Append(", ");
                    }

                    msgBuilder.Append("<SCOPED> ");
                    msgBuilder.Append(p.Name);
                    msgBuilder.Append(" of type ");
                    msgBuilder.Append(GetDisplayName(p.ParameterType));

                    resolvedAllParameters = false;
                }

                continue;
            }

            // Save the first error message, since it's likely to be the most useful
            if (recordErrors)
            {
                if (!resolvedAllParameters)
                {
                    msgBuilder.Append(", ");
                }

                msgBuilder.Append(p.Name);
                msgBuilder.Append(" of type ");
                msgBuilder.Append(GetDisplayName(p.ParameterType));
            }

            resolvedAllParameters = false;
        }

        if (recordErrors && !resolvedAllParameters)
        {
            errorMessage = msgBuilder.ToString();
        }

        return resolvedAllParameters;
    }

    private static string GetDisplayName(Type type)
    {
        if (type.IsConstructedGenericType)
        {
            // The name of IOptions<Pizza> is "IOptions`1"
            // The full name has the other types, but they are all fully qualified (and also still have the `1 on them)
            string baseName = type.Name.Split('`')[0];
            return $"{baseName}<{string.Join(", ", type.GetGenericArguments().Select(GetDisplayName))}>";
        }

        return type.Name;
    }

    private static bool IsMatchingServiceRegistration(Type serviceType, Type parameterType)
    {
        // If it's options, lets make sure they are configured
        if (parameterType.IsConstructedGenericType)
        {
            Type parameterRoot = parameterType.GetGenericTypeDefinition();
            if (IsOptionsType(parameterRoot))
            {
                if (!serviceType.IsConstructedGenericType) return false;

                Type optionType = parameterType.GenericTypeArguments[0];

                if (IsExemptType(optionType))
                    return true;

                Type serviceRoot = serviceType.GetGenericTypeDefinition();
                return serviceRoot.FullName == "Microsoft.Extensions.Options.IConfigureOptions`1" &&
                    serviceType.GenericTypeArguments[0] == optionType;
            }
        }

        if (IsExemptType(parameterType))
        {
            return true;
        }

        if (serviceType == parameterType) return true;
        if (!parameterType.IsConstructedGenericType) return false;
        Type def = parameterType.GetGenericTypeDefinition();
        if (def == typeof(IEnumerable<>))
        {
            // IEnumerable can be zero, and that's fine
            return true;
        }

        return serviceType == def;
    }

    private static bool IsOptionsType(Type parameterRoot)
    {
        switch (parameterRoot.FullName)
        {
            case "Microsoft.Extensions.Options.IOptions`1":
            case "Microsoft.Extensions.Options.IOptionsMonitor`1":
            case "Microsoft.Extensions.Options.IOptionsSnapshot`1":
                return true;
            default:
                return false;
        }
    }

    private static bool IsExemptType(Type type)
    {
        if (type.IsConstructedGenericType)
            return IsExemptType(type.GetGenericTypeDefinition());

        return s_exemptTypes.Contains(type.FullName) || s_exemptNamespaces.Any(n => type.FullName.StartsWith(n));
    }
}