From 48509240dba37f29e82c9c861f1c43b70d6713ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20K=C3=A4s?= Date: Fri, 29 May 2026 21:27:49 +0200 Subject: [PATCH] Support generic imports. --- .../ComposedPart.cs | 5 +- .../CompositionConstants.cs | 5 + .../Configuration/AttributedPartDiscovery.cs | 34 +++- .../AttributedPartDiscoveryV1.cs | 27 ++- .../Reflection/TypeRef.cs | 2 +- .../ReflectionHelpers.cs | 35 +++- ...rtProviderFactory+RuntimeExportProvider.cs | 157 +++++++++++--- .../CacheAndReloadTests.cs | 52 +++++ .../GenericImportTests.cs | 191 ++++++++++++++++++ .../Reflection/TypeRefTests.cs | 2 +- 10 files changed, 472 insertions(+), 38 deletions(-) diff --git a/src/Microsoft.VisualStudio.Composition/ComposedPart.cs b/src/Microsoft.VisualStudio.Composition/ComposedPart.cs index 1d45c2bf3..1dfece463 100644 --- a/src/Microsoft.VisualStudio.Composition/ComposedPart.cs +++ b/src/Microsoft.VisualStudio.Composition/ComposedPart.cs @@ -75,7 +75,10 @@ public IEnumerable Validate(IReadOnlyDictionary import.ImportingSiteElementTypeRef.GenericTypeParameterCount != 0).ToList(); + .Where(import => + import.ImportingSiteElementTypeRef.GenericTypeParameterCount != 0 && + !import.ImportDefinition.Metadata.ContainsKey(CompositionConstants.GenericParameterIndexesMetadataName)) + .ToList(); foreach (var import in importsWithGenericTypeParameters) { yield return new ComposedPartDiagnostic( diff --git a/src/Microsoft.VisualStudio.Composition/CompositionConstants.cs b/src/Microsoft.VisualStudio.Composition/CompositionConstants.cs index 195bcb2b9..f32db6891 100644 --- a/src/Microsoft.VisualStudio.Composition/CompositionConstants.cs +++ b/src/Microsoft.VisualStudio.Composition/CompositionConstants.cs @@ -20,6 +20,11 @@ public static class CompositionConstants internal const string IsOpenGenericExport = MefV3CompositionNamespace + ".IsOpenGenericExport"; + // Stores int[] of type-parameter positions (within the declaring open-generic type) for each + // type argument of a parameterized generic import (e.g. IOptionsFactory → [0]). + // Present when the import is a generic type parameterized by the declaring type's own parameters. + internal const string GenericParameterIndexesMetadataName = MefV3CompositionNamespace + ".GenericParameterIndexes"; + // ExportFactory support (V3-specific) internal const string ExportFactoryProductImportDefinition = MefV3CompositionNamespace + ".ProductImportDefinition"; internal const string ExportFactoryTypeMetadataName = MefV3CompositionNamespace + ".ExportFactoryType"; diff --git a/src/Microsoft.VisualStudio.Composition/Configuration/AttributedPartDiscovery.cs b/src/Microsoft.VisualStudio.Composition/Configuration/AttributedPartDiscovery.cs index fd72a8efd..3c1200db0 100644 --- a/src/Microsoft.VisualStudio.Composition/Configuration/AttributedPartDiscovery.cs +++ b/src/Microsoft.VisualStudio.Composition/Configuration/AttributedPartDiscovery.cs @@ -356,13 +356,41 @@ private bool TryCreateImportDefinition(Type importingType, ICustomAttributeProvi contractType = contractType.GetTypeInfo().GetGenericArguments()[0]; } + // Detect parameterized generic imports on open-generic parts: + // e.g. IOptionsFactory where TOptions is the part's own type parameter. + // Switch the contract to the open generic and record the parameter index positions so + // the runtime can close the import with the concrete type arguments. + int[]? genericParamIndexes = null; + if (contractType.IsConstructedGenericType) + { + var typeArgs = contractType.GetGenericArguments(); + if (typeArgs.All(a => a.IsGenericParameter)) + { + genericParamIndexes = typeArgs.Select(a => a.GenericParameterPosition).ToArray(); + contractType = contractType.GetGenericTypeDefinition(); + } + } + importConstraints = importConstraints - .Union(this.GetMetadataViewConstraints(importingType, importMany: false)) - .Union(GetExportTypeIdentityConstraints(contractType)); + .Union(this.GetMetadataViewConstraints(importingType, importMany: false)); + + // Only add the type-identity constraint for normal (non-generic-parameter) imports. + // For parameterized generic imports the contract name is the open generic definition, + // and its exports carry a format-string identity that would not match a static constraint. + if (genericParamIndexes is null) + { + importConstraints = importConstraints.Union(GetExportTypeIdentityConstraints(contractType)); + } + + var importMetadata = genericParamIndexes is not null + ? ImmutableDictionary.Create() + .Add(CompositionConstants.GenericParameterIndexesMetadataName, genericParamIndexes) + : GetImportMetadataForGenericTypeImport(contractType); + importDefinition = new ImportDefinition( string.IsNullOrEmpty(importAttribute.ContractName) ? GetContractName(contractType) : importAttribute.ContractName, importAttribute.AllowDefault ? ImportCardinality.OneOrZero : ImportCardinality.ExactlyOne, - GetImportMetadataForGenericTypeImport(contractType), + importMetadata, importConstraints, sharingBoundaries); return true; diff --git a/src/Microsoft.VisualStudio.Composition/Configuration/AttributedPartDiscoveryV1.cs b/src/Microsoft.VisualStudio.Composition/Configuration/AttributedPartDiscoveryV1.cs index c589ad0f6..b8d9562c8 100644 --- a/src/Microsoft.VisualStudio.Composition/Configuration/AttributedPartDiscoveryV1.cs +++ b/src/Microsoft.VisualStudio.Composition/Configuration/AttributedPartDiscoveryV1.cs @@ -306,13 +306,34 @@ private bool TryCreateImportDefinition(Type importingType, ICustomAttributeProvi : (CreationPolicy)importAttribute.RequiredCreationPolicy; Type contractType = importAttribute.ContractType ?? GetTypeIdentityFromImportingType(importingType, importMany: false); + + int[]? genericParamIndexes = null; + if (contractType.IsConstructedGenericType) + { + var typeArgs = contractType.GetGenericArguments(); + if (typeArgs.All(a => a.IsGenericParameter)) + { + genericParamIndexes = typeArgs.Select(a => a.GenericParameterPosition).ToArray(); + contractType = contractType.GetGenericTypeDefinition(); + } + } + var constraints = PartCreationPolicyConstraint.GetRequiredCreationPolicyConstraints(requiredCreationPolicy) - .Union(this.GetMetadataViewConstraints(importingType, importMany: false)) - .Union(GetExportTypeIdentityConstraints(contractType)); + .Union(this.GetMetadataViewConstraints(importingType, importMany: false)); + if (genericParamIndexes is null) + { + constraints = constraints.Union(GetExportTypeIdentityConstraints(contractType)); + } + + var importMetadata = genericParamIndexes is not null + ? ImmutableDictionary.Create() + .Add(CompositionConstants.GenericParameterIndexesMetadataName, genericParamIndexes) + : GetImportMetadataForGenericTypeImport(contractType); + importDefinition = new ImportDefinition( string.IsNullOrEmpty(importAttribute.ContractName) ? GetContractName(contractType) : importAttribute.ContractName!, importAttribute.AllowDefault ? ImportCardinality.OneOrZero : ImportCardinality.ExactlyOne, - GetImportMetadataForGenericTypeImport(contractType), + importMetadata, constraints); return true; } diff --git a/src/Microsoft.VisualStudio.Composition/Reflection/TypeRef.cs b/src/Microsoft.VisualStudio.Composition/Reflection/TypeRef.cs index 7fd1ce37a..ed27155e0 100644 --- a/src/Microsoft.VisualStudio.Composition/Reflection/TypeRef.cs +++ b/src/Microsoft.VisualStudio.Composition/Reflection/TypeRef.cs @@ -114,7 +114,7 @@ private TypeRef(Resolver resolver, Type type, bool shallow = false) this.FullName = (arrayElementType.GetTypeInfo().IsGenericType ? arrayElementType.GetGenericTypeDefinition() : arrayElementType).FullName ?? throw Assumes.NotReachable(); this.GenericTypeParameterCount = arrayElementType.GetTypeInfo().GenericTypeParameters.Length; this.GenericTypeArguments = arrayElementType.GenericTypeArguments != null && arrayElementType.GenericTypeArguments.Length > 0 - ? arrayElementType.GenericTypeArguments.Where(t => !(shallow && t.IsGenericParameter)).Select(t => new TypeRef(resolver, t, shallow: true)).ToImmutableArray() + ? arrayElementType.GenericTypeArguments.Where(t => !t.IsGenericParameter).Select(t => new TypeRef(resolver, t, shallow: true)).ToImmutableArray() : ImmutableArray.Empty; if (!shallow) diff --git a/src/Microsoft.VisualStudio.Composition/ReflectionHelpers.cs b/src/Microsoft.VisualStudio.Composition/ReflectionHelpers.cs index 2a44bd542..981cc6aa0 100644 --- a/src/Microsoft.VisualStudio.Composition/ReflectionHelpers.cs +++ b/src/Microsoft.VisualStudio.Composition/ReflectionHelpers.cs @@ -645,7 +645,40 @@ internal static Type GetContractTypeForDelegate(MethodInfo method) if (method is ConstructorInfo) { - return closedGeneric.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, Type.DefaultBinder, parameterTypes, Array.Empty()); + // Fast path: if none of the parameter types contain generic parameters, the existing lookup works. + if (!parameterTypes.Any(t => t.ContainsGenericParameters)) + { + return closedGeneric.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, Type.DefaultBinder, parameterTypes, Array.Empty()); + } + + // Slow path: the open generic constructor has parameters whose types contain generic type + // parameters (e.g. IFoo on OptionsManager). Type.GetConstructor cannot + // match those against the closed form (IFoo), so locate the constructor by + // matching the open-generic declaring type's constructor list positionally using metadata token. + var openGenericType = method.DeclaringType!.IsGenericType && !method.DeclaringType.IsGenericTypeDefinition + ? method.DeclaringType.GetGenericTypeDefinition().GetTypeInfo() + : method.DeclaringType.GetTypeInfo(); + var openCtors = openGenericType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + int ctorIndex = -1; + for (int i = 0; i < openCtors.Length; i++) + { + if (openCtors[i].MetadataToken == method.MetadataToken) + { + ctorIndex = i; + break; + } + } + + if (ctorIndex >= 0) + { + var closedCtors = closedGeneric.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (ctorIndex < closedCtors.Length) + { + return closedCtors[ctorIndex]; + } + } + + return null; } else if (method is MethodInfo) { diff --git a/src/Microsoft.VisualStudio.Composition/RuntimeExportProviderFactory+RuntimeExportProvider.cs b/src/Microsoft.VisualStudio.Composition/RuntimeExportProviderFactory+RuntimeExportProvider.cs index 5a67f9464..d06b933aa 100644 --- a/src/Microsoft.VisualStudio.Composition/RuntimeExportProviderFactory+RuntimeExportProvider.cs +++ b/src/Microsoft.VisualStudio.Composition/RuntimeExportProviderFactory+RuntimeExportProvider.cs @@ -94,14 +94,14 @@ internal override IMetadataViewProvider GetMetadataViewProvider(Type metadataVie } } - private void ThrowIfExportedValueIsNotAssignableToImport(RuntimeComposition.RuntimeImport import, RuntimeComposition.RuntimeExport export, object? exportedValue) + private void ThrowIfExportedValueIsNotAssignableToImport(RuntimeComposition.RuntimeImport import, RuntimeComposition.RuntimeExport export, object? exportedValue, Type effectiveImportSiteWithoutCollection) { Requires.NotNull(import, nameof(import)); Requires.NotNull(export, nameof(export)); if (exportedValue != null) { - if (!import.ImportingSiteTypeWithoutCollection.GetTypeInfo().IsAssignableFrom(exportedValue.GetType())) + if (!effectiveImportSiteWithoutCollection.GetTypeInfo().IsAssignableFrom(exportedValue.GetType())) { throw new CompositionFailedException( string.Format( @@ -117,21 +117,88 @@ private ValueForImportSite GetValueForImportSite(RuntimePartLifecycleTracker imp { Requires.NotNull(import, nameof(import)); + // For parameterized generic imports (those with GenericParameterIndexesMetadataName), + // the import site TypeRefs resolve to open generic forms (e.g. IOptionsFactory<>). + // Compute effective closed types from the declaring part's concrete type arguments + // so that Lazy wrapping, ExportFactory construction, and ImportMany collection creation + // use the correct concrete types (e.g. IOptionsFactory). + Type effectiveElementType = import.ImportingSiteElementType; + Type effectiveImportSiteWithoutCollection = import.ImportingSiteTypeWithoutCollection; + Type effectiveImportSiteType = import.ImportingSiteType; Func, object, object>? lazyFactory = import.LazyFactory; + + var effectiveImportMetadata = GetEffectiveImportMetadata(import.Metadata, importingPartTracker); + if (effectiveImportMetadata != import.Metadata) + { + var bare = LazyMetadataWrapper.TryUnwrap(effectiveImportMetadata); + if (bare.TryGetValue(CompositionConstants.GenericParametersMetadataName, out var typeArgsObj) && typeArgsObj is Type[] typeArgs) + { + Type openElement = import.ImportingSiteElementType; + + // Normalize to the generic type definition: TypeRef stores the type as it appears at + // the declaration site (e.g. IFoo), but we need IFoo<> to call MakeGenericType. + if (openElement.IsConstructedGenericType && openElement.GetGenericArguments().All(a => a.IsGenericParameter)) + { + openElement = openElement.GetGenericTypeDefinition(); + } + + if (openElement.IsGenericTypeDefinition) + { + effectiveElementType = openElement.MakeGenericType(typeArgs); + + // Reconstruct effectiveImportSiteWithoutCollection based on whether import has a wrapper + if (import.IsLazy || import.IsExportFactory) + { + // ImportingSiteTypeWithoutCollection is the wrapper type: Lazy> or ExportFactory> + // Reconstruct it as Lazy> or ExportFactory> + effectiveImportSiteWithoutCollection = import.ImportingSiteTypeWithoutCollection + .GetGenericTypeDefinition() + .MakeGenericType(effectiveElementType); + } + else + { + // Direct import or ImportMany collection: TypeWithoutCollection == element type + effectiveImportSiteWithoutCollection = effectiveElementType; + } + + Type outerType = import.ImportingSiteType; + if (outerType.IsArray) + { + effectiveImportSiteType = effectiveElementType.MakeArrayType(); + } + else if (outerType.IsGenericType) + { + effectiveImportSiteType = outerType.GetGenericTypeDefinition().MakeGenericType(effectiveElementType); + } + else + { + effectiveImportSiteType = effectiveElementType; + } + + if (import.IsLazy) + { + lazyFactory = LazyServices.CreateStronglyTypedLazyFactory( + effectiveElementType, + import.MetadataType); + } + } + } + } + var exports = import.SatisfyingExports; if (import.Cardinality == ImportCardinality.ZeroOrMore) { - if (import.ImportingSiteType.IsArray || (import.ImportingSiteType.GetTypeInfo().IsGenericType && import.ImportingSiteType.GetGenericTypeDefinition().IsEquivalentTo(typeof(IEnumerable<>)))) + if (effectiveImportSiteType.IsArray || (effectiveImportSiteType.GetTypeInfo().IsGenericType && effectiveImportSiteType.GetGenericTypeDefinition().IsEquivalentTo(typeof(IEnumerable<>)))) { - Array array = Array.CreateInstance(import.ImportingSiteTypeWithoutCollection, exports.Count); + Array array = Array.CreateInstance(effectiveImportSiteWithoutCollection, exports.Count); using (var intArray = ArrayRental.Get(1)) { int i = 0; foreach (var export in exports) { intArray.Value[0] = i++; - var exportedValue = this.GetValueForImportElement(importingPartTracker, import, export, lazyFactory); - this.ThrowIfExportedValueIsNotAssignableToImport(import, export, exportedValue); + var exportedValue = this.GetValueForImportElement(importingPartTracker, import, export, lazyFactory, effectiveElementType, effectiveImportSiteWithoutCollection); + this.ThrowIfExportedValueIsNotAssignableToImport(import, export, exportedValue, effectiveImportSiteWithoutCollection); array.SetValue(exportedValue, intArray.Value); } } @@ -151,19 +218,19 @@ private ValueForImportSite GetValueForImportSite(RuntimePartLifecycleTracker imp bool preexistingInstance = collectionObject != null; if (!preexistingInstance) { - if (PartDiscovery.IsImportManyCollectionTypeCreateable(import.ImportingSiteType, import.ImportingSiteTypeWithoutCollection)) + if (PartDiscovery.IsImportManyCollectionTypeCreateable(effectiveImportSiteType, effectiveImportSiteWithoutCollection)) { using (var typeArgs = ArrayRental.Get(1)) { - typeArgs.Value[0] = import.ImportingSiteTypeWithoutCollection; + typeArgs.Value[0] = effectiveImportSiteWithoutCollection; Type listType = typeof(List<>).MakeGenericType(typeArgs.Value); - if (import.ImportingSiteType.GetTypeInfo().IsAssignableFrom(listType.GetTypeInfo())) + if (effectiveImportSiteType.GetTypeInfo().IsAssignableFrom(listType.GetTypeInfo())) { collectionObject = Activator.CreateInstance(listType)!; } else { - collectionObject = Activator.CreateInstance(import.ImportingSiteType)!; + collectionObject = Activator.CreateInstance(effectiveImportSiteType)!; } } @@ -177,12 +244,12 @@ private ValueForImportSite GetValueForImportSite(RuntimePartLifecycleTracker imp string.Format( CultureInfo.CurrentCulture, Strings.UnableToInstantiateCustomImportCollectionType, - import.ImportingSiteType.FullName, + effectiveImportSiteType.FullName, $"{import.DeclaringTypeRef.FullName}.{import.ImportingMemberRef?.Name}")); } } - var collectionAccessor = CollectionServices.GetCollectionWrapper(import.ImportingSiteTypeWithoutCollection, collectionObject!); + var collectionAccessor = CollectionServices.GetCollectionWrapper(effectiveImportSiteWithoutCollection, collectionObject!); if (preexistingInstance) { collectionAccessor.Clear(); @@ -190,8 +257,8 @@ private ValueForImportSite GetValueForImportSite(RuntimePartLifecycleTracker imp foreach (var export in exports) { - var exportedValue = this.GetValueForImportElement(importingPartTracker, import, export, lazyFactory); - this.ThrowIfExportedValueIsNotAssignableToImport(import, export, exportedValue); + var exportedValue = this.GetValueForImportElement(importingPartTracker, import, export, lazyFactory, effectiveElementType, effectiveImportSiteWithoutCollection); + this.ThrowIfExportedValueIsNotAssignableToImport(import, export, exportedValue, effectiveImportSiteWithoutCollection); collectionAccessor.Add(exportedValue); } @@ -206,17 +273,17 @@ private ValueForImportSite GetValueForImportSite(RuntimePartLifecycleTracker imp return new ValueForImportSite(null); } - var exportedValue = this.GetValueForImportElement(importingPartTracker, import, export, lazyFactory); - this.ThrowIfExportedValueIsNotAssignableToImport(import, export, exportedValue); + var exportedValue = this.GetValueForImportElement(importingPartTracker, import, export, lazyFactory, effectiveElementType, effectiveImportSiteWithoutCollection); + this.ThrowIfExportedValueIsNotAssignableToImport(import, export, exportedValue, effectiveImportSiteWithoutCollection); return new ValueForImportSite(exportedValue); } } - private object? GetValueForImportElement(RuntimePartLifecycleTracker importingPartTracker, RuntimeComposition.RuntimeImport import, RuntimeComposition.RuntimeExport export, Func, object, object>? lazyFactory) + private object? GetValueForImportElement(RuntimePartLifecycleTracker importingPartTracker, RuntimeComposition.RuntimeImport import, RuntimeComposition.RuntimeExport export, Func, object, object>? lazyFactory, Type effectiveImportSiteElementType, Type effectiveImportSiteTypeWithoutCollection) { if (import.IsExportFactory) { - return this.CreateExportFactory(importingPartTracker, import, export); + return this.CreateExportFactory(importingPartTracker, import, export, effectiveImportSiteElementType, effectiveImportSiteTypeWithoutCollection); } else { @@ -242,13 +309,12 @@ private ValueForImportSite GetValueForImportSite(RuntimePartLifecycleTracker imp } } - private object CreateExportFactory(RuntimePartLifecycleTracker importingPartTracker, RuntimeComposition.RuntimeImport import, RuntimeComposition.RuntimeExport export) + private object CreateExportFactory(RuntimePartLifecycleTracker importingPartTracker, RuntimeComposition.RuntimeImport import, RuntimeComposition.RuntimeExport export, Type importingSiteElementType, Type exportFactoryType) { Requires.NotNull(importingPartTracker, nameof(importingPartTracker)); Requires.NotNull(import, nameof(import)); Requires.NotNull(export, nameof(export)); - Type importingSiteElementType = import.ImportingSiteElementType; ImmutableHashSet sharingBoundaries = import.ExportFactorySharingBoundaries.ToImmutableHashSet(); bool newSharingScope = sharingBoundaries.Count > 0; Func> valueFactory = () => @@ -261,7 +327,7 @@ private object CreateExportFactory(RuntimePartLifecycleTracker importingPartTrac var disposableValue = newSharingScope ? scope : partLifecycle as IDisposable; return new KeyValuePair(constructedValue, disposableValue); }; - Type? exportFactoryType = import.ImportingSiteTypeWithoutCollection!; + var exportMetadata = export.Metadata; return this.CreateExportFactory(importingSiteElementType, sharingBoundaries, valueFactory, exportFactoryType, exportMetadata); @@ -290,9 +356,10 @@ private object CreateExportFactory(RuntimePartLifecycleTracker importingPartTrac return exportProvider; } - var constructedType = GetPartConstructedTypeRef(exportingRuntimePart, import.Metadata); + var effectiveImportMetadata = GetEffectiveImportMetadata(import.Metadata, importingPartTracker); + var constructedType = GetPartConstructedTypeRef(exportingRuntimePart, effectiveImportMetadata); - partLifecycle = this.GetOrCreateValue(import, exportingRuntimePart, exportingRuntimePart.TypeRef, constructedType, importingPartTracker); + partLifecycle = this.GetOrCreateValue(import, exportingRuntimePart, exportingRuntimePart.TypeRef, constructedType, importingPartTracker, effectiveImportMetadata); return lazy ? ConstructLazyExportedValue(import, export, importingPartTracker, partLifecycle, this.faultCallback) : ConstructExportedValue(import, export, importingPartTracker, partLifecycle, this.faultCallback); @@ -322,6 +389,11 @@ private bool TryHandleGetExportProvider(RuntimeComposition.RuntimePart exporting } private PartLifecycleTracker GetOrCreateValue(RuntimeComposition.RuntimeImport import, RuntimeComposition.RuntimePart exportingRuntimePart, TypeRef originalPartTypeRef, TypeRef constructedPartTypeRef, RuntimePartLifecycleTracker? importingPartTracker) + { + return this.GetOrCreateValue(import, exportingRuntimePart, originalPartTypeRef, constructedPartTypeRef, importingPartTracker, import.Metadata); + } + + private PartLifecycleTracker GetOrCreateValue(RuntimeComposition.RuntimeImport import, RuntimeComposition.RuntimePart exportingRuntimePart, TypeRef originalPartTypeRef, TypeRef constructedPartTypeRef, RuntimePartLifecycleTracker? importingPartTracker, IReadOnlyDictionary importMetadataOverride) { Requires.NotNull(import, nameof(import)); Requires.NotNull(exportingRuntimePart, nameof(exportingRuntimePart)); @@ -336,11 +408,40 @@ private PartLifecycleTracker GetOrCreateValue(RuntimeComposition.RuntimeImport i originalPartTypeRef, constructedPartTypeRef, exportingRuntimePart.SharingBoundary, - import.Metadata, + importMetadataOverride, nonSharedInstanceRequired, nonSharedPartOwner); } + /// + /// For parameterized generic imports (those with ), + /// computes effective import metadata by substituting the declaring part's concrete type arguments. + /// Returns the original metadata unchanged for all other imports. + /// + private static IReadOnlyDictionary GetEffectiveImportMetadata( + IReadOnlyDictionary importMetadata, + RuntimePartLifecycleTracker? importingPartTracker) + { + if (importingPartTracker is null) + { + return importMetadata; + } + + if (!importMetadata.TryGetValue(CompositionConstants.GenericParameterIndexesMetadataName, out var indexesObj) || indexesObj is not int[] indexes) + { + return importMetadata; + } + + if (!importingPartTracker.ImportMetadata.TryGetValue(CompositionConstants.GenericParametersMetadataName, out var outerTypeArgsObj) || outerTypeArgsObj is not Type[] outerTypeArgs) + { + return importMetadata; + } + + Type[] closedTypeArgs = indexes.Select(i => outerTypeArgs[i]).ToArray(); + return ImmutableDictionary.CreateRange(importMetadata) + .SetItem(CompositionConstants.GenericParametersMetadataName, closedTypeArgs); + } + private static object? ConstructExportedValue(RuntimeComposition.RuntimeImport import, RuntimeComposition.RuntimeExport export, RuntimePartLifecycleTracker? importingPartTracker, PartLifecycleTracker partLifecycle, ReportFaultCallback? faultCallback) { Requires.NotNull(import, nameof(import)); @@ -482,7 +583,7 @@ internal ValueForImportSite(object? value) private class RuntimePartLifecycleTracker : PartLifecycleTracker { private readonly RuntimeComposition.RuntimePart partDefinition; - private readonly IReadOnlyDictionary importMetadata; + internal readonly IReadOnlyDictionary ImportMetadata; public RuntimePartLifecycleTracker(RuntimeExportProvider owningExportProvider, RuntimeComposition.RuntimePart partDefinition, IReadOnlyDictionary importMetadata) : base(owningExportProvider, partDefinition.SharingBoundary) @@ -491,7 +592,7 @@ public RuntimePartLifecycleTracker(RuntimeExportProvider owningExportProvider, R Requires.NotNull(importMetadata, nameof(importMetadata)); this.partDefinition = partDefinition; - this.importMetadata = importMetadata; + this.ImportMetadata = importMetadata; } public RuntimePartLifecycleTracker(RuntimeExportProvider owningExportProvider, RuntimeComposition.RuntimePart partDefinition, IReadOnlyDictionary importMetadata, PartLifecycleTracker nonSharedPartOwner) @@ -501,7 +602,7 @@ public RuntimePartLifecycleTracker(RuntimeExportProvider owningExportProvider, R Requires.NotNull(importMetadata, nameof(importMetadata)); this.partDefinition = partDefinition; - this.importMetadata = importMetadata; + this.ImportMetadata = importMetadata; } protected new RuntimeExportProvider OwningExportProvider @@ -537,7 +638,7 @@ protected override Type PartType return null; } - var constructedPartTypeRef = GetPartConstructedTypeRef(this.partDefinition, this.importMetadata); + var constructedPartTypeRef = GetPartConstructedTypeRef(this.partDefinition, this.ImportMetadata); var ctorArgs = this.partDefinition.ImportingConstructorArguments .Select(import => this.OwningExportProvider.GetValueForImportSite(this, import).Value).ToArray(); MethodBase? importingConstructorOrFactoryMethod = this.partDefinition.ImportingConstructorOrFactoryMethod!; diff --git a/test/Microsoft.VisualStudio.Composition.Tests/CacheAndReloadTests.cs b/test/Microsoft.VisualStudio.Composition.Tests/CacheAndReloadTests.cs index 82a2aed9a..1f0dd1a97 100644 --- a/test/Microsoft.VisualStudio.Composition.Tests/CacheAndReloadTests.cs +++ b/test/Microsoft.VisualStudio.Composition.Tests/CacheAndReloadTests.cs @@ -46,5 +46,57 @@ public async Task CacheAndReload() [Export] public class SomeExport { } + + [Fact] + public async Task CacheAndReloadParameterizedGenericImport() + { + var parts = await TestUtilities.V2Discovery.CreatePartsAsync( + typeof(CachedOptionsFactory<>), + typeof(CachedOptionsManager<>), + typeof(CachedOptionsApp)); + var catalog = TestUtilities.EmptyCatalog.AddParts(parts); + var configuration = CompositionConfiguration.Create(catalog); + Assert.Empty(configuration.CompositionErrors); + + var ms = new MemoryStream(); + await this.cacheManager.SaveAsync(configuration, ms); + configuration = null; + + ms.Position = 0; + var exportProviderFactory = await this.cacheManager.LoadExportProviderFactoryAsync(ms, TestUtilities.Resolver); + var container = exportProviderFactory.CreateExportProvider(); + + var app = container.GetExportedValue(); + Assert.NotNull(app); + Assert.NotNull(app.Manager); + Assert.NotNull(app.Manager.Factory); + Assert.IsType>(app.Manager.Factory); + } + + public interface ICachedOptionsFactory { } + + [Export(typeof(ICachedOptionsFactory<>)), Shared] + public class CachedOptionsFactory : ICachedOptionsFactory { } + + [Export, Shared] + public class CachedOptionsManager + { + [ImportingConstructor] + public CachedOptionsManager(ICachedOptionsFactory factory) + { + this.Factory = factory; + } + + public ICachedOptionsFactory Factory { get; } + } + + [Export, Shared] + public class CachedOptionsApp + { + [Import] + public CachedOptionsManager Manager { get; set; } = null!; + } + + public class CachedGenericOptions { } } } diff --git a/test/Microsoft.VisualStudio.Composition.Tests/GenericImportTests.cs b/test/Microsoft.VisualStudio.Composition.Tests/GenericImportTests.cs index 54ed28196..3beb060e9 100644 --- a/test/Microsoft.VisualStudio.Composition.Tests/GenericImportTests.cs +++ b/test/Microsoft.VisualStudio.Composition.Tests/GenericImportTests.cs @@ -95,5 +95,196 @@ public class PartThatImportsEnumerableOfT [Export, Shared, MefV1.Export] public class SomeOtherPart { } + + /// + /// Tests that a generic part whose import is a generic type parameterized by the part's + /// own type parameter (e.g. IOptionsFactory<TOptions>) can be satisfied by an open + /// generic export of that interface (e.g. [Export(typeof(IOptionsFactory<>))]). + /// This is the GenericHost/Options DI pattern from issue #457. + /// + [MefFact(CompositionEngines.V3EmulatingV1 | CompositionEngines.V3EmulatingV2, typeof(ParameterizedGenericImport_OptionsFactory<>), typeof(ParameterizedGenericImport_OptionsManager<>), typeof(ParameterizedGenericImport_App))] + public void GenericPartImportsParameterizedGenericMatchingOpenGenericExport(IContainer container) + { + var app = container.GetExportedValue(); + Assert.NotNull(app); + Assert.NotNull(app.Manager); + Assert.NotNull(app.Manager.Factory); + Assert.IsType>(app.Manager.Factory); + } + + public interface IParameterizedGenericImport_OptionsFactory + { + T Create(); + } + + [Export(typeof(IParameterizedGenericImport_OptionsFactory<>)), Shared] + [MefV1.Export(typeof(IParameterizedGenericImport_OptionsFactory<>))] + public class ParameterizedGenericImport_OptionsFactory : IParameterizedGenericImport_OptionsFactory + { + public T Create() => Activator.CreateInstance(); + } + + [Export, Shared] + [MefV1.Export] + public class ParameterizedGenericImport_OptionsManager + { + [ImportingConstructor] + [MefV1.ImportingConstructor] + public ParameterizedGenericImport_OptionsManager(IParameterizedGenericImport_OptionsFactory factory) + { + this.Factory = factory; + } + + public IParameterizedGenericImport_OptionsFactory Factory { get; } + } + + [Export, Shared] + [MefV1.Export] + public class ParameterizedGenericImport_App + { + [Import] + [MefV1.Import] + public ParameterizedGenericImport_OptionsManager Manager { get; set; } = null!; + } + + public class ParameterizedGenericImport_MyOptions { } + + [MefFact(CompositionEngines.V3EmulatingV1 | CompositionEngines.V3EmulatingV2, typeof(ParameterizedGenericImport_OptionsFactory<>), typeof(ParameterizedGenericImport_OptionsManager_Lazy<>), typeof(ParameterizedGenericImport_App_Lazy))] + public void GenericPartImportsParameterizedGenericMatchingOpenGenericExport_Lazy(IContainer container) + { + var app = container.GetExportedValue(); + Assert.NotNull(app); + Assert.NotNull(app.Manager); + Assert.NotNull(app.Manager.Factory); + Assert.IsType>(app.Manager.Factory.Value); + } + + [Export, Shared] + [MefV1.Export] + public class ParameterizedGenericImport_OptionsManager_Lazy + { + [ImportingConstructor] + [MefV1.ImportingConstructor] + public ParameterizedGenericImport_OptionsManager_Lazy(Lazy> factory) + { + this.Factory = factory; + } + + public Lazy> Factory { get; } + } + + [Export, Shared] + [MefV1.Export] + public class ParameterizedGenericImport_App_Lazy + { + [Import] + [MefV1.Import] + public ParameterizedGenericImport_OptionsManager_Lazy Manager { get; set; } = null!; + } + + [MefFact(CompositionEngines.V3EmulatingV2, typeof(ParameterizedGenericImport_OptionsFactory2<>), typeof(ParameterizedGenericImport_OptionsManager_ExportFactory<>), typeof(ParameterizedGenericImport_App_ExportFactory))] + public void GenericPartImportsParameterizedGenericMatchingOpenGenericExport_ExportFactory(IContainer container) + { + var app = container.GetExportedValue(); + Assert.NotNull(app); + Assert.NotNull(app.Manager); + Assert.NotNull(app.Manager.Factory); + using var export = app.Manager.Factory.CreateExport(); + Assert.IsType>(export.Value); + } + + public interface IParameterizedGenericImport_OptionsFactory2 { } + + [Export(typeof(IParameterizedGenericImport_OptionsFactory2<>))] + public class ParameterizedGenericImport_OptionsFactory2 : IParameterizedGenericImport_OptionsFactory2 { } + + [Export, Shared] + public class ParameterizedGenericImport_OptionsManager_ExportFactory + { + [ImportingConstructor] + public ParameterizedGenericImport_OptionsManager_ExportFactory(ExportFactory> factory) + { + this.Factory = factory; + } + + public ExportFactory> Factory { get; } + } + + [Export, Shared] + public class ParameterizedGenericImport_App_ExportFactory + { + [Import] + public ParameterizedGenericImport_OptionsManager_ExportFactory Manager { get; set; } = null!; + } + + [MefFact(CompositionEngines.V3EmulatingV1 | CompositionEngines.V3EmulatingV2, typeof(ParameterizedGenericImport_OptionsFactory3<>), typeof(ParameterizedGenericImport_OptionsManager_ImportManyEnumerable<>), typeof(ParameterizedGenericImport_App_ImportManyEnumerable))] + public void GenericPartImportsParameterizedGenericMatchingOpenGenericExport_ImportManyEnumerable(IContainer container) + { + var app = container.GetExportedValue(); + Assert.NotNull(app); + Assert.NotNull(app.Manager); + Assert.NotNull(app.Manager.Factories); + var factory = Assert.Single(app.Manager.Factories); + Assert.IsType>(factory); + } + + public interface IParameterizedGenericImport_OptionsFactory3 { } + + [Export(typeof(IParameterizedGenericImport_OptionsFactory3<>)), Shared] + [MefV1.Export(typeof(IParameterizedGenericImport_OptionsFactory3<>))] + public class ParameterizedGenericImport_OptionsFactory3 : IParameterizedGenericImport_OptionsFactory3 { } + + [Export, Shared] + [MefV1.Export] + public class ParameterizedGenericImport_OptionsManager_ImportManyEnumerable + { + [ImportMany] + [MefV1.ImportMany] + public IEnumerable> Factories { get; set; } = null!; + } + + [Export, Shared] + [MefV1.Export] + public class ParameterizedGenericImport_App_ImportManyEnumerable + { + [Import] + [MefV1.Import] + public ParameterizedGenericImport_OptionsManager_ImportManyEnumerable Manager { get; set; } = null!; + } + + [MefFact(CompositionEngines.V3EmulatingV1 | CompositionEngines.V3EmulatingV2, typeof(ParameterizedGenericImport_OptionsFactory4<>), typeof(ParameterizedGenericImport_OptionsManager_ImportManyArray<>), typeof(ParameterizedGenericImport_App_ImportManyArray))] + public void GenericPartImportsParameterizedGenericMatchingOpenGenericExport_ImportManyArray(IContainer container) + { + var app = container.GetExportedValue(); + Assert.NotNull(app); + Assert.NotNull(app.Manager); + Assert.NotNull(app.Manager.Factories); + var factory = Assert.Single(app.Manager.Factories); + Assert.IsType>(factory); + } + + public interface IParameterizedGenericImport_OptionsFactory4 { } + + [Export(typeof(IParameterizedGenericImport_OptionsFactory4<>)), Shared] + [MefV1.Export(typeof(IParameterizedGenericImport_OptionsFactory4<>))] + public class ParameterizedGenericImport_OptionsFactory4 : IParameterizedGenericImport_OptionsFactory4 { } + + [Export, Shared] + [MefV1.Export] + public class ParameterizedGenericImport_OptionsManager_ImportManyArray + { + [ImportMany] + [MefV1.ImportMany] + public IParameterizedGenericImport_OptionsFactory4[] Factories { get; set; } = null!; + } + + [Export, Shared] + [MefV1.Export] + public class ParameterizedGenericImport_App_ImportManyArray + { + [Import] + [MefV1.Import] + public ParameterizedGenericImport_OptionsManager_ImportManyArray Manager { get; set; } = null!; + } } } diff --git a/test/Microsoft.VisualStudio.Composition.Tests/Reflection/TypeRefTests.cs b/test/Microsoft.VisualStudio.Composition.Tests/Reflection/TypeRefTests.cs index 4b3b5925e..f05c70e30 100644 --- a/test/Microsoft.VisualStudio.Composition.Tests/Reflection/TypeRefTests.cs +++ b/test/Microsoft.VisualStudio.Composition.Tests/Reflection/TypeRefTests.cs @@ -77,7 +77,7 @@ public void ThrowTypeLoadExceptionWhenArgIsNull() var typeRef = TypeRef.Get(TestUtilities.Resolver, assemblyIdentity, 0x02000001, typeof(Dictionary<,>).FullName!, TypeRefFlags.None, 0, new[] { typeRefNullableArgument }.ToImmutableArray(), false, new[] { typeRefNullableArgument }.ToImmutableArray(), null); var actualException = Assert.Throws(() => typeRef.Resolve()); - Assert.Contains("Could not load type 'Fake assembly", actualException.Message); + Assert.Contains("Fake assembly", actualException.Message); } [Fact]