diff --git a/src/Autofac/Features/Scanning/OpenGenericScanningRegistrationExtensions.cs b/src/Autofac/Features/Scanning/OpenGenericScanningRegistrationExtensions.cs index c70bb9902..ea68d35e1 100644 --- a/src/Autofac/Features/Scanning/OpenGenericScanningRegistrationExtensions.cs +++ b/src/Autofac/Features/Scanning/OpenGenericScanningRegistrationExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using Autofac.Builder; @@ -169,5 +170,135 @@ private static void ConfigureFrom + /// Filters the scanned types to include only those assignable to the provided. + /// + /// The limit type. + /// The registration style. + /// The registration builder. + /// The type or interface which all classes must be assignable from. + /// The registration builder. + public static IRegistrationBuilder + AssignableTo( + IRegistrationBuilder registration, + Type openGenericServiceType) + { + if (openGenericServiceType == null) + { + throw new ArgumentNullException(nameof(openGenericServiceType)); + } + + return registration + .Where(candidateType => candidateType.IsOpenGenericTypeOf(openGenericServiceType)) + .As(candidateType => (Service)new TypedService(candidateType)); + } + + /// + /// Filters the scanned types to include only those assignable to the provided. + /// + /// The limit type. + /// The registration style. + /// The registration builder. + /// The type or interface which all classes must be assignable from. + /// The service key. + /// The registration builder. + public static IRegistrationBuilder + AssignableTo( + IRegistrationBuilder registration, + Type openGenericServiceType, + object serviceKey) + { + if (openGenericServiceType == null) + { + throw new ArgumentNullException(nameof(openGenericServiceType)); + } + + if (serviceKey == null) + { + throw new ArgumentNullException(nameof(serviceKey)); + } + + return AssignableTo(registration, openGenericServiceType, t => serviceKey); + } + + /// + /// Filters the scanned types to include only those assignable to the provided. + /// + /// The limit type. + /// The registration style. + /// The registration builder. + /// The type or interface which all classes must be assignable from. + /// A function to determine the service key for a given type. + /// The registration builder. + public static IRegistrationBuilder + AssignableTo( + IRegistrationBuilder registration, + Type openGenericServiceType, + Func serviceKeyMapping) + { + if (openGenericServiceType == null) + { + throw new ArgumentNullException(nameof(openGenericServiceType)); + } + + return registration + .Where(candidateType => candidateType.IsOpenGenericTypeOf(openGenericServiceType)) + .As(candidateType => (Service)new KeyedService(serviceKeyMapping(candidateType), candidateType)); + } + + /// + /// Specify how an open generic type from a scanned assembly provides metadata. + /// + /// Registration limit type. + /// Registration style. + /// Registration to set metadata on. + /// A function mapping the type to a list of metadata items. + /// Registration builder allowing the registration to be configured. + public static IRegistrationBuilder + WithMetadata( + this IRegistrationBuilder registration, + Func>> metadataMapping) + { + registration.ActivatorData.ConfigurationActions.Add((t, rb) => rb.WithMetadata(metadataMapping(t))); + return registration; + } + + /// + /// Use the properties of an attribute (or interface implemented by an attribute) on the scanned type + /// to provide metadata values. + /// + /// Inherited attributes are supported; however, there must be at most one matching attribute + /// in the inheritance chain. + /// The attribute applied to the scanned type. + /// Registration to set metadata on. + /// Registration builder allowing the registration to be configured. + public static IRegistrationBuilder + WithMetadataFrom( + this IRegistrationBuilder registration) + { + var attrType = typeof(TAttribute); + var metadataProperties = attrType + .GetRuntimeProperties() + .Where(pi => pi.CanRead); + + return registration.WithMetadata(t => + { + var attrs = t.GetCustomAttributes(true).OfType().ToList(); + + if (attrs.Count == 0) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, RegistrationExtensionsResources.MetadataAttributeNotFound, typeof(TAttribute), t)); + } + + if (attrs.Count != 1) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, RegistrationExtensionsResources.MultipleMetadataAttributesSameType, typeof(TAttribute), t)); + } + + var attr = attrs[0]; + return metadataProperties.Select(p => new KeyValuePair(p.Name, p.GetValue(attr, null))); + }); + } } } diff --git a/src/Autofac/RegistrationExtensions.AssemblyScanning.cs b/src/Autofac/RegistrationExtensions.AssemblyScanning.cs index 2e4112ffc..f7efe15e5 100644 --- a/src/Autofac/RegistrationExtensions.AssemblyScanning.cs +++ b/src/Autofac/RegistrationExtensions.AssemblyScanning.cs @@ -245,6 +245,14 @@ private static Type[] GetImplementedInterfaces(Type type) return type.IsInterface ? interfaces.AppendItem(type).ToArray() : interfaces.ToArray(); } + private static Type[] GetOpenGenericImplementedInterfaces(this Type @this) + { + return @this.GetInterfaces() + .Where(it => it.IsGenericType) + .Select(it => it.GetGenericTypeDefinition()) + .ToArray(); + } + /// /// Specifies that the components being registered should only be made the default for services /// that have not already been registered. diff --git a/src/Autofac/RegistrationExtensions.OpenGenericAssemblyScanning.cs b/src/Autofac/RegistrationExtensions.OpenGenericAssemblyScanning.cs index 7ce130fed..947f3ff51 100644 --- a/src/Autofac/RegistrationExtensions.OpenGenericAssemblyScanning.cs +++ b/src/Autofac/RegistrationExtensions.OpenGenericAssemblyScanning.cs @@ -202,5 +202,101 @@ public static IRegistrationBuilder + /// Filters the scanned types to include only those assignable to the provided. + /// + /// Registration limit type. + /// Registration style. + /// Registration to set service mapping on. + /// The type or interface which all classes must be assignable from. + /// Registration builder allowing the registration to be configured. + public static IRegistrationBuilder + AssignableTo( + this IRegistrationBuilder registration, Type openGenericServiceType) + { + return ScanningRegistrationExtensions.AssignableTo(registration, openGenericServiceType); + } + + /// + /// Filters the scanned types to include only those assignable to the provided. + /// + /// Registration limit type. + /// Registration style. + /// Registration to set service mapping on. + /// The type or interface which all classes must be assignable from. + /// Key of the service. + /// Registration builder allowing the registration to be configured. + public static IRegistrationBuilder + AssignableTo( + this IRegistrationBuilder registration, Type openGenericServiceType, object serviceKey) + { + return ScanningRegistrationExtensions.AssignableTo(registration, openGenericServiceType, serviceKey); + } + + /// + /// Filters the scanned types to include only those assignable to the provided. + /// + /// Registration limit type. + /// Registration style. + /// Registration to set service mapping on. + /// The type or interface which all classes must be assignable from. + /// Function mapping types to service keys. + /// Registration builder allowing the registration to be configured. + public static IRegistrationBuilder + AssignableTo( + this IRegistrationBuilder registration, Type openGenericServiceType, Func serviceKeyMapping) + { + return ScanningRegistrationExtensions.AssignableTo(registration, openGenericServiceType, serviceKeyMapping); + } + + /// + /// Filters the scanned open generic types to include only those in the namespace of the provided type + /// or one of its sub-namespaces. + /// + /// Registration to filter types from. + /// A type in the target namespace. + /// Registration builder allowing the registration to be configured. + public static IRegistrationBuilder + InNamespaceOf(this IRegistrationBuilder registration) + { + // Namespace is always non-null for concrete type parameters. + return registration.InNamespace(typeof(T).Namespace!); + } + + /// + /// Filters the scanned types to include only those in the provided namespace + /// or one of its sub-namespaces. + /// + /// Registration limit type. + /// Registration style. + /// Registration to filter types from. + /// The namespace from which types will be selected. + /// Registration builder allowing the registration to be configured. + public static IRegistrationBuilder + InNamespace( + this IRegistrationBuilder registration, + string ns) + { + if (string.IsNullOrEmpty(ns)) + { + throw new ArgumentNullException(nameof(ns)); + } + + return registration.Where(t => t.IsInNamespace(ns)); + } + + /// + /// Specifies that an open generic type from a scanned assembly is registered as providing all of its + /// implemented interfaces. + /// + /// Registration limit type. + /// Registration to set service mapping on. + /// Registration builder allowing the registration to be configured. + public static IRegistrationBuilder + AsImplementedInterfaces(this IRegistrationBuilder registration) + { + return registration.As(t => t.GetOpenGenericImplementedInterfaces()); + } } } diff --git a/src/Autofac/Util/TypeExtensions.cs b/src/Autofac/Util/TypeExtensions.cs index ac2b9d3e6..795ddcced 100644 --- a/src/Autofac/Util/TypeExtensions.cs +++ b/src/Autofac/Util/TypeExtensions.cs @@ -223,7 +223,6 @@ private static bool CheckBaseTypeIsOpenGenericTypeOf(this Type @this, Type type) private static bool CheckInterfacesAreOpenGenericTypeOf(this Type @this, Type type) { - var interfaces = @this.GetInterfaces().ToList(); return @this.GetInterfaces() .Any(it => it.IsGenericType ? it.GetGenericTypeDefinition().IsOpenGenericTypeOf(type) diff --git a/test/Autofac.Test.Scenarios.ScannedAssembly/MetadataAttributeScanningScenario/DuplicatedNameAttribute.cs b/test/Autofac.Test.Scenarios.ScannedAssembly/MetadataAttributeScanningScenario/DuplicatedNameAttribute.cs new file mode 100644 index 000000000..2c6c9cf98 --- /dev/null +++ b/test/Autofac.Test.Scenarios.ScannedAssembly/MetadataAttributeScanningScenario/DuplicatedNameAttribute.cs @@ -0,0 +1,17 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; + +namespace Autofac.Test.Scenarios.ScannedAssembly.MetadataAttributeScanningScenario +{ + public class DuplicatedNameAttribute : Attribute, IHaveName + { + public DuplicatedNameAttribute(string name) + { + Name = name ?? throw new ArgumentNullException("name"); + } + + public string Name { get; } + } +} diff --git a/test/Autofac.Test.Scenarios.ScannedAssembly/MetadataAttributeScanningScenario/OpenGenericScannedComponentWithMultipleNames.cs b/test/Autofac.Test.Scenarios.ScannedAssembly/MetadataAttributeScanningScenario/OpenGenericScannedComponentWithMultipleNames.cs new file mode 100644 index 000000000..47f50a06b --- /dev/null +++ b/test/Autofac.Test.Scenarios.ScannedAssembly/MetadataAttributeScanningScenario/OpenGenericScannedComponentWithMultipleNames.cs @@ -0,0 +1,11 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Test.Scenarios.ScannedAssembly.MetadataAttributeScanningScenario +{ + [Name("My Name")] + [DuplicatedName("My Name 2")] + public class OpenGenericScannedComponentWithMultipleNames + { + } +} diff --git a/test/Autofac.Test.Scenarios.ScannedAssembly/MetadataAttributeScanningScenario/OpenGenericScannedComponentWithName.cs b/test/Autofac.Test.Scenarios.ScannedAssembly/MetadataAttributeScanningScenario/OpenGenericScannedComponentWithName.cs new file mode 100644 index 000000000..db83d64f4 --- /dev/null +++ b/test/Autofac.Test.Scenarios.ScannedAssembly/MetadataAttributeScanningScenario/OpenGenericScannedComponentWithName.cs @@ -0,0 +1,10 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Test.Scenarios.ScannedAssembly.MetadataAttributeScanningScenario +{ + [Name("My Name")] + public class OpenGenericScannedComponentWithName + { + } +} diff --git a/test/Autofac.Test.Scenarios.ScannedAssembly/OpenGenericAComponent.cs b/test/Autofac.Test.Scenarios.ScannedAssembly/OpenGenericAComponent.cs new file mode 100644 index 000000000..6f7b47fdc --- /dev/null +++ b/test/Autofac.Test.Scenarios.ScannedAssembly/OpenGenericAComponent.cs @@ -0,0 +1,9 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Test.Scenarios.ScannedAssembly +{ + public class OpenGenericAComponent + { + } +} diff --git a/test/Autofac.Test/Assertions.cs b/test/Autofac.Test/Assertions.cs index 6c3d69b7a..2104c0aa5 100644 --- a/test/Autofac.Test/Assertions.cs +++ b/test/Autofac.Test/Assertions.cs @@ -4,7 +4,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using Autofac.Builder; using Autofac.Core; +using Autofac.Features.Indexed; +using Autofac.Features.OpenGenerics; using Xunit; namespace Autofac.Test @@ -92,6 +96,24 @@ public static void AssertComponentRegistrationOrder + { + if (source is OpenGenericRegistrationSource) + { + var activatorData = typeof(OpenGenericRegistrationSource) + .GetField("_activatorData", BindingFlags.NonPublic | BindingFlags.Instance) + .GetValue(source) as ReflectionActivatorData; + return activatorData.ImplementationType != typeof(KeyedServiceIndex<,>); + } + else + { + return false; + } + }); + } + private static IEnumerable LookForComponents(this IEnumerable registrations, IEnumerable types) { return registrations diff --git a/test/Autofac.Test/Features/Scanning/OpenGenericScanningRegistrationTests.cs b/test/Autofac.Test/Features/Scanning/OpenGenericScanningRegistrationTests.cs index 620e3f723..aad90c9cc 100644 --- a/test/Autofac.Test/Features/Scanning/OpenGenericScanningRegistrationTests.cs +++ b/test/Autofac.Test/Features/Scanning/OpenGenericScanningRegistrationTests.cs @@ -1,14 +1,19 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using Autofac.Core; using Autofac.Core.Lifetime; using Autofac.Core.Registration; +using Autofac.Features.Metadata; +using Autofac.Features.OpenGenerics; using Autofac.Features.Scanning; using Autofac.Test.Scenarios.ScannedAssembly; +using Autofac.Test.Scenarios.ScannedAssembly.MetadataAttributeScanningScenario; using Xunit; namespace Autofac.Test.Features.Scanning @@ -97,5 +102,221 @@ public void WhenExceptionsProvideConfigurationComponentConfiguredAppropriately() var a2 = c.Resolve>(); Assert.Same(a1, a2); } + + [Fact] + public void AssignableToNullTypeProvidedThrowsException() + { + var cb = new ContainerBuilder(); + Assert.Throws(() => cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AssignableTo(null)); + + Assert.Throws(() => cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AssignableTo(typeof(RedoOpenGenericCommand<>), (object)null)); + + Assert.Throws(() => cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AssignableTo(null, "serviceKey")); + + Assert.Throws(() => cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AssignableTo(null, t => t)); + } + + [Theory] + [InlineData(typeof(ICloseCommand))] + [InlineData(typeof(CloseCommand))] + public void AssignableToClosedTypeProvidedNoneOpenGenericSourceRegistered(Type closedType) + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AssignableTo(closedType); + var c = cb.Build(); + + Assert.False(c.RegisteredAnyOpenGenericTypeFromScanningAssembly()); + } + + [Fact] + public void ServiceIsNotAssignableToIsNotRegistered() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AssignableTo(typeof(RedoOpenGenericCommand<>)); + var c = cb.Build(); + + Assert.Throws(() => c.Resolve>()); + } + + [Fact] + public void AssignableToOpenGenericInterfaceTypeProvidedOpenGenericTypesRegistered() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AssignableTo(typeof(ICommand<>)); + var c = cb.Build(); + + Assert.NotNull(c.Resolve>()); + } + + [Fact] + public void AssignableToOpenGenericAbstractClassTypeProvidedOpenGenericTypesRegistered() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AssignableTo(typeof(CommandBase<>)); + var c = cb.Build(); + + Assert.NotNull(c.Resolve>()); + } + + [Fact] + public void AssignableToWithServiceKeyShouldAssignKeyToAllRegistrations() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AssignableTo(typeof(ICommand<>), "command"); + var c = cb.Build(); + + Assert.Throws(() => c.Resolve>()); + Assert.NotNull(c.ResolveKeyed>("command")); + } + + [Fact] + public void AssignableToWithServiceKeyMappingShouldAssignKeyResultToEachRegistration() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AssignableTo(typeof(ICommand<>), t => t); + var c = cb.Build(); + + Assert.NotNull(c.ResolveKeyed>(typeof(RedoOpenGenericCommand<>))); + } + + [Fact] + public void AsImplementedInterfacesRegistersImplementedInterfaces() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AsImplementedInterfaces(); + var c = cb.Build(); + + Assert.NotNull(c.Resolve>()); + } + + [Fact] + public void WhenFilterAppliedDefaultSelfRegistrationOmitted() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AsImplementedInterfaces(); + var c = cb.Build(); + + Assert.Throws(() => c.Resolve>()); + } + + [Fact] + public void AsSelfExposesConcreteTypeAsService() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .AsImplementedInterfaces() + .AsSelf(); + var c = cb.Build(); + + Assert.NotNull(c.Resolve>()); + } + + [Fact] + public void WhenMetadataMappingAppliedValuesCalculatedFromType() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .WithMetadata(t => t.GetMethods().ToDictionary(m => m.Name, m => (object)m.ReturnType)); + + var c = cb.Build(); + var s = c.Resolve>>(); + + Assert.True(s.Metadata.ContainsKey("Execute")); + } + + [Fact] + public void WhenMetadataNotFoundThrowException() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .Where(t => t == typeof(OpenGenericScannedComponentWithName<>)) + .WithMetadataFrom(); + + var ex = Assert.Throws(() => cb.Build()); + + Assert.Equal( + string.Format(CultureInfo.CurrentCulture, RegistrationExtensionsResources.MetadataAttributeNotFound, typeof(ICloseCommand), typeof(OpenGenericScannedComponentWithName<>)), + ex.Message); + } + + [Fact] + public void WhenMultipleMetadataAttributesSameTypeThrowException() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .Where(t => t == typeof(OpenGenericScannedComponentWithMultipleNames<>)) + .WithMetadataFrom(); + + var ex = Assert.Throws(() => cb.Build()); + + Assert.Equal( + string.Format(CultureInfo.CurrentCulture, RegistrationExtensionsResources.MultipleMetadataAttributesSameType, typeof(IHaveName), typeof(OpenGenericScannedComponentWithMultipleNames<>)), + ex.Message); + } + + [Fact] + public void MetadataCanBeScannedFromAMatchingAttributeInterface() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .Where(t => t == typeof(OpenGenericScannedComponentWithName<>)) + .WithMetadataFrom(); + + var c = cb.Build(); + + c.ComponentRegistry.TryGetRegistration(new TypedService(typeof(OpenGenericScannedComponentWithName)), out IComponentRegistration r); + + r.Metadata.TryGetValue("Name", out object name); + + Assert.Equal("My Name", name); + } + + [Fact] + public void InNamespaceNullProvidedThrowException() + { + var cb = new ContainerBuilder(); + var ex = Assert.Throws(() => + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly).InNamespace(ns: "")); + + Assert.Equal("ns", ex.ParamName); + } + + [Fact] + public void InNamespaceLimitsServicesToBeRegistered() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .InNamespace("Autofac.Test.Scenarios.ScannedAssembly.MetadataAttributeScanningScenario"); + + var c = cb.Build(); + + Assert.NotNull(c.Resolve>()); + Assert.Throws(() => c.Resolve>()); + } + + [Fact] + public void InNamespaceOfLimitsServicesToBeRegistered() + { + var cb = new ContainerBuilder(); + cb.RegisterAssemblyOpenGenericTypes(typeof(ICommand<>).GetTypeInfo().Assembly) + .InNamespaceOf(); + + var c = cb.Build(); + + Assert.NotNull(c.Resolve>()); + Assert.Throws(() => c.Resolve>()); + } } }