File: MEF\ExportProviderCache.cs
Web Access
Project: src\src\Workspaces\CoreTestUtilities\Microsoft.CodeAnalysis.Workspaces.Test.Utilities.csproj (Microsoft.CodeAnalysis.Workspaces.Test.Utilities)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.UnitTests.Remote;
using Microsoft.VisualStudio.Composition;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Test.Utilities;
 
public static class ExportProviderCache
{
    private static readonly PartDiscovery s_partDiscovery = CreatePartDiscovery(Resolver.DefaultInstance);
 
    private static readonly TestComposition s_defaultHostExportProviderComposition = TestComposition.Empty
        .AddAssemblies(MefHostServices.DefaultAssemblies)
        .AddParts(typeof(TestSerializerService.Factory));
    private static readonly Scope _localCompositionScope = new Scope("local");
    private static readonly Scope _remoteCompositionScope = new Scope("remote");
 
    internal static bool Enabled { get; private set; }
 
    internal static ExportProvider? LocalExportProviderForCleanup => _localCompositionScope.CurrentExportProvider;
    internal static ExportProvider? RemoteExportProviderForCleanup => _remoteCompositionScope.CurrentExportProvider;
 
    internal static void SetEnabled_OnlyUseExportProviderAttributeCanCall(bool value)
    {
        Enabled = value;
        if (!Enabled)
        {
            _localCompositionScope.Clear();
            _remoteCompositionScope.Clear();
        }
    }
 
    /// <summary>
    /// Use to create <see cref="IExportProviderFactory"/> for default instances of <see cref="MefHostServices"/>.
    /// </summary>
    public static IExportProviderFactory GetOrCreateExportProviderFactory(IEnumerable<Assembly> assemblies)
    {
        if (assemblies is ImmutableArray<Assembly> assembliesArray &&
            assembliesArray == MefHostServices.DefaultAssemblies)
        {
            return s_defaultHostExportProviderComposition.ExportProviderFactory;
        }
 
        return CreateExportProviderFactory(CreateAssemblyCatalog(assemblies), isRemoteHostComposition: false);
    }
 
    public static ComposableCatalog CreateAssemblyCatalog(IEnumerable<Assembly> assemblies, Resolver? resolver = null)
    {
        var discovery = resolver == null ? s_partDiscovery : CreatePartDiscovery(resolver);
 
        // If we run CreatePartsAsync on the test thread we may deadlock since it'll schedule stuff back
        // on the thread.
        var parts = Task.Run(async () => await discovery.CreatePartsAsync(assemblies).ConfigureAwait(false)).Result;
 
        return ComposableCatalog.Create(resolver ?? Resolver.DefaultInstance).AddParts(parts);
    }
 
    public static ComposableCatalog CreateTypeCatalog(IEnumerable<Type> types, Resolver? resolver = null)
    {
        var discovery = resolver == null ? s_partDiscovery : CreatePartDiscovery(resolver);
 
        // If we run CreatePartsAsync on the test thread we may deadlock since it'll schedule stuff back
        // on the thread.
        var parts = Task.Run(async () => await discovery.CreatePartsAsync(types).ConfigureAwait(false)).Result;
 
        return ComposableCatalog.Create(resolver ?? Resolver.DefaultInstance).AddParts(parts);
    }
 
    public static Resolver CreateResolver()
    {
        // simple assembly loader is stateless, so okay to share
        return new Resolver(SimpleAssemblyLoader.Instance);
    }
 
    public static PartDiscovery CreatePartDiscovery(Resolver resolver)
        => PartDiscovery.Combine(new AttributedPartDiscoveryV1(resolver), new AttributedPartDiscovery(resolver, isNonPublicSupported: true));
 
    public static ComposableCatalog WithParts(this ComposableCatalog catalog, IEnumerable<Type> types)
        => catalog.AddParts(CreateTypeCatalog(types).DiscoveredParts);
 
    /// <summary>
    /// Creates a <see cref="ComposableCatalog"/> derived from <paramref name="catalog"/>, but with all exported
    /// parts assignable to any type in <paramref name="types"/> removed from the catalog.
    /// </summary>
    public static ComposableCatalog WithoutPartsOfTypes(this ComposableCatalog catalog, IEnumerable<Type> types)
    {
        var parts = catalog.Parts.Where(composablePartDefinition => !IsExcludedPart(composablePartDefinition));
        return ComposableCatalog.Create(Resolver.DefaultInstance).AddParts(parts);
 
        bool IsExcludedPart(ComposablePartDefinition part)
        {
            return types.Any(excludedType => excludedType.IsAssignableFrom(part.Type));
        }
    }
 
    public static IExportProviderFactory CreateExportProviderFactory(ComposableCatalog catalog, bool isRemoteHostComposition)
    {
        var scope = isRemoteHostComposition ? _remoteCompositionScope : _localCompositionScope;
        var configuration = CompositionConfiguration.Create(catalog.WithCompositionService());
        var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration);
        var exportProviderFactory = runtimeComposition.CreateExportProviderFactory();
 
        return new SingleExportProviderFactory(scope, catalog, configuration, exportProviderFactory);
    }
 
    private sealed class SingleExportProviderFactory : IExportProviderFactory
    {
        private readonly Scope _scope;
        private readonly ComposableCatalog _catalog;
        private readonly CompositionConfiguration _configuration;
        private readonly IExportProviderFactory _exportProviderFactory;
 
        public SingleExportProviderFactory(Scope scope, ComposableCatalog catalog, CompositionConfiguration configuration, IExportProviderFactory exportProviderFactory)
        {
            _scope = scope;
            _catalog = catalog;
            _configuration = configuration;
            _exportProviderFactory = exportProviderFactory;
        }
 
        public ExportProvider GetOrCreateExportProvider()
        {
            if (!Enabled)
            {
                // The [UseExportProvider] attribute on tests ensures that the pre- and post-conditions of methods
                // in this type are met during test conditions.
                throw new InvalidOperationException($"{nameof(ExportProviderCache)} may only be used from tests marked with {nameof(UseExportProviderAttribute)}");
            }
 
            var expectedCatalog = Interlocked.CompareExchange(ref _scope.ExpectedCatalog, _catalog, null) ?? _catalog;
            RequireForSingleExportProvider(expectedCatalog == _catalog);
 
            var expected = _scope.ExpectedProviderForCatalog;
            if (expected == null)
            {
                foreach (var errorCollection in _configuration.CompositionErrors)
                {
                    foreach (var error in errorCollection)
                    {
                        foreach (var part in error.Parts)
                        {
                            foreach (var (importBinding, exportBindings) in part.SatisfyingExports)
                            {
                                if (exportBindings.Count <= 1)
                                {
                                    // Ignore composition errors for missing parts
                                    continue;
                                }
 
                                if (importBinding.ImportDefinition.Cardinality != ImportCardinality.ZeroOrMore)
                                {
                                    // This failure occurs when a binding fails because multiple exports were
                                    // provided but only a single one (at most) is expected. This typically occurs
                                    // when a test ExportProvider is created with a mock implementation without
                                    // first removing a value provided by default.
                                    throw new InvalidOperationException(
                                        "Failed to construct the MEF catalog for testing. Multiple exports were found for a part for which only one export is expected:" + Environment.NewLine
                                        + error.Message);
                                }
                            }
                        }
                    }
                }
 
                expected = _exportProviderFactory.CreateExportProvider();
                expected = Interlocked.CompareExchange(ref _scope.ExpectedProviderForCatalog, expected, null) ?? expected;
                Interlocked.CompareExchange(ref _scope.CurrentExportProvider, expected, null);
            }
 
            var exportProvider = _scope.CurrentExportProvider;
            RequireForSingleExportProvider(exportProvider == expected);
 
            return exportProvider;
        }
 
        ExportProvider IExportProviderFactory.CreateExportProvider()
        {
            // Currently this implementation deviates from the typical behavior of IExportProviderFactory. For the
            // duration of a single test, an instance of SingleExportProviderFactory will continue returning the
            // same ExportProvider instance each time this method is called.
            //
            // It may be clearer to refactor the implementation to only allow one call to CreateExportProvider in
            // the context of a single test. https://github.com/dotnet/roslyn/issues/25863
            return GetOrCreateExportProvider();
        }
 
        private void RequireForSingleExportProvider([DoesNotReturnIf(false)] bool condition)
        {
            if (!condition)
            {
                // The ExportProvider provides services that act as singleton instances in the context of an
                // application (this include cases of multiple exports, where the 'singleton' is the list of all
                // exports matching the contract). When reasoning about the behavior of test code, it is valuable to
                // know service instances will be used in a consistent manner throughout the execution of a test,
                // regardless of whether they are passed as arguments or obtained through requests to the
                // ExportProvider.
                //
                // Restricting a test to a single ExportProvider guarantees that objects that *look* like singletons
                // will *behave* like singletons for the duration of the test. Each test is expected to create and
                // use its ExportProvider in a consistent manner.
                //
                // A test that validates remote services is allowed to create a couple of ExportProviders:
                // one for local workspace and the other for the remote one. 
                //
                // When this exception is thrown by a test, it typically means one of the following occurred:
                //
                // * A test failed to pass an ExportProvider via an optional argument to a method, resulting in the
                //   method attempting to create a default ExportProvider which did not match the one assigned to
                //   the test.
                // * A test attempted to perform multiple test sequences in the context of a single test method,
                //   rather than break up the test into distinct tests for each case.
                // * A test referenced different predefined ExportProvider instances within the context of a test.
                //   Each test is expected to use the same ExportProvider throughout the test.
                throw new InvalidOperationException($"Only one {_scope.Name} {nameof(ExportProvider)} can be created in the context of a single test.");
            }
        }
    }
 
    private sealed class Scope
    {
        public readonly string Name;
        public ExportProvider? CurrentExportProvider;
        public ComposableCatalog? ExpectedCatalog;
        public ExportProvider? ExpectedProviderForCatalog;
 
        public Scope(string name)
        {
            Name = name;
        }
 
        public void Clear()
        {
            CurrentExportProvider = null;
            ExpectedCatalog = null;
            ExpectedProviderForCatalog = null;
        }
    }
 
    private sealed class SimpleAssemblyLoader : IAssemblyLoader
    {
        public static readonly IAssemblyLoader Instance = new SimpleAssemblyLoader();
 
        public Assembly LoadAssembly(AssemblyName assemblyName)
            => Assembly.Load(assemblyName);
 
        public Assembly LoadAssembly(string assemblyFullName, string? codeBasePath)
        {
            var assemblyName = new AssemblyName(assemblyFullName);
            if (!string.IsNullOrEmpty(codeBasePath))
            {
#pragma warning disable SYSLIB0044
                assemblyName.CodeBase = codeBasePath;
#pragma warning restore SYSLIB0044
            }
 
            return this.LoadAssembly(assemblyName);
        }
    }
}