diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1fae5888b..1bb960f05 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: "cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b" # v5.0.0 + rev: "3e8a8703264a2f4a69428a0aa4dcb512790b2c8c" # frozen: v6.0.0 hooks: - id: check-json - id: check-yaml @@ -8,13 +8,13 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/igorshubovych/markdownlint-cli - rev: "192ad822316c3a22fb3d3cc8aa6eafa0b8488360" # v0.45.0 + rev: "76b3d32d3f4b965e1d6425253c59407420ae2c43" # frozen: v0.47.0 hooks: - id: markdownlint args: - --fix - repo: https://github.com/tillig/json-sort-cli - rev: "009ab2ab49e1f2fa9d6b9dfc31009ceeca055204" # v3.0.0 + rev: "009ab2ab49e1f2fa9d6b9dfc31009ceeca055204" # frozen: v3.0.0 hooks: - id: json-sort args: diff --git a/.vscode/settings.json b/.vscode/settings.json index 2ea49d18f..b0c043330 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "finalizers", "inheritdoc", "langword", + "medi", "netcoreapp", "netstandard", "notnull", @@ -20,6 +21,7 @@ "subclassing", "typeparam", "unconfigured", + "unkeyed", "xunit" ], "coverage-gutters.coverageBaseDir": "artifacts/logs", diff --git a/bench/Autofac.Benchmarks/RequiredPropertyBenchmark.cs b/bench/Autofac.Benchmarks/RequiredPropertyBenchmark.cs index bff85ee83..a228e2e2a 100644 --- a/bench/Autofac.Benchmarks/RequiredPropertyBenchmark.cs +++ b/bench/Autofac.Benchmarks/RequiredPropertyBenchmark.cs @@ -1,8 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using Autofac.Core; - namespace Autofac.Benchmarks; public class RequiredPropertyBenchmark diff --git a/default.proj b/default.proj index ba7ce55ad..49be97cd7 100644 --- a/default.proj +++ b/default.proj @@ -2,7 +2,7 @@ - 9.0.0 + 9.1.0 Autofac Release $([System.IO.Path]::Combine($(MSBuildProjectDirectory),"artifacts")) diff --git a/src/Autofac/Builder/MetadataKeys.cs b/src/Autofac/Builder/MetadataKeys.cs index 049d5fe8b..3eb9dbc8f 100644 --- a/src/Autofac/Builder/MetadataKeys.cs +++ b/src/Autofac/Builder/MetadataKeys.cs @@ -37,4 +37,9 @@ internal static class MetadataKeys /// Event handler for . /// internal const string RegistrationSourceAddedPropertyKey = "__RegistrationSourceAddedKey"; + + /// + /// Marks registrations generated by the AnyKey adapter. + /// + internal const string AnyKeyAdapter = "__AnyKeyAdapter"; } diff --git a/src/Autofac/ContainerBuilder.cs b/src/Autofac/ContainerBuilder.cs index 501c02cc3..f50340a06 100644 --- a/src/Autofac/ContainerBuilder.cs +++ b/src/Autofac/ContainerBuilder.cs @@ -7,6 +7,7 @@ using Autofac.Features.Collections; using Autofac.Features.GeneratedFactories; using Autofac.Features.Indexed; +using Autofac.Features.KeyedServices; using Autofac.Features.LazyDependencies; using Autofac.Features.Metadata; using Autofac.Features.OwnedInstances; @@ -233,6 +234,7 @@ private void RegisterDefaultAdapters(IComponentRegistryBuilder componentRegistry { this.RegisterGeneric(typeof(KeyedServiceIndex<,>)).As(typeof(IIndex<,>)).InstancePerLifetimeScope(); componentRegistry.AddRegistrationSource(new CollectionRegistrationSource()); + componentRegistry.AddRegistrationSource(new AnyKeyRegistrationSource()); componentRegistry.AddRegistrationSource(new OwnedInstanceRegistrationSource()); componentRegistry.AddRegistrationSource(new MetaRegistrationSource()); componentRegistry.AddRegistrationSource(new LazyRegistrationSource()); diff --git a/src/Autofac/Core/Activators/DefaultPropertySelector.cs b/src/Autofac/Core/Activators/DefaultPropertySelector.cs index 8365f22ff..21fb65837 100644 --- a/src/Autofac/Core/Activators/DefaultPropertySelector.cs +++ b/src/Autofac/Core/Activators/DefaultPropertySelector.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Reflection; -using System.Runtime.CompilerServices; using Autofac.Util; diff --git a/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs b/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs index 66e87932a..ef1a9160c 100644 --- a/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs +++ b/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Collections.Concurrent; using System.Reflection; using Autofac.Util; diff --git a/src/Autofac/Core/Activators/Reflection/DefaultConstructorFinder.cs b/src/Autofac/Core/Activators/Reflection/DefaultConstructorFinder.cs index dbb1eba63..867cc4ed1 100644 --- a/src/Autofac/Core/Activators/Reflection/DefaultConstructorFinder.cs +++ b/src/Autofac/Core/Activators/Reflection/DefaultConstructorFinder.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Collections.Concurrent; using System.Reflection; namespace Autofac.Core.Activators.Reflection; diff --git a/src/Autofac/Core/Activators/Reflection/InjectableProperty.cs b/src/Autofac/Core/Activators/Reflection/InjectableProperty.cs index de3ca9365..8da02e237 100644 --- a/src/Autofac/Core/Activators/Reflection/InjectableProperty.cs +++ b/src/Autofac/Core/Activators/Reflection/InjectableProperty.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Reflection; -using System.Runtime.CompilerServices; using Autofac.Util; diff --git a/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs b/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs index a957ba501..013723ad5 100644 --- a/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs +++ b/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.Reflection; -using System.Runtime.CompilerServices; using System.Text; using Autofac.Core.Resolving.Pipeline; using Autofac.Util; @@ -18,6 +17,7 @@ public class ReflectionActivator : InstanceActivator, IInstanceActivator private readonly Type _implementationType; private readonly Parameter[] _configuredProperties; private readonly Parameter[] _defaultParameters; + private readonly bool _requiresServiceKeyParameter; private ConstructorBinder[]? _constructorBinders; private bool _anyRequiredMembers; @@ -50,6 +50,9 @@ public ReflectionActivator( } _implementationType = implementationType; + _requiresServiceKeyParameter = ReflectionCacheSet.Shared.Internal.ServiceKeyUsageByType.GetOrAdd( + _implementationType, + static t => UsesServiceKeyAttribute(t)); ConstructorFinder = constructorFinder ?? throw new ArgumentNullException(nameof(constructorFinder)); ConstructorSelector = constructorSelector ?? throw new ArgumentNullException(nameof(constructorSelector)); _configuredProperties = configuredProperties.ToArray(); @@ -66,6 +69,11 @@ public ReflectionActivator( /// public IConstructorSelector ConstructorSelector { get; } + /// + /// Gets a value indicating whether the activation pipeline needs a keyed service parameter for this type. + /// + internal bool RequiresServiceKeyParameter => _requiresServiceKeyParameter; + /// public void ConfigurePipeline(IComponentRegistryServices componentRegistryServices, IResolvePipelineBuilder pipelineBuilder) { @@ -156,6 +164,43 @@ public void ConfigurePipeline(IComponentRegistryServices componentRegistryServic }); } + /// + /// Determines if any constructor parameters or settable properties on the + /// type have a , which would require + /// special handling in the activation pipeline. + /// + /// The type to inspect. + /// if the type uses the ; otherwise, . + private static bool UsesServiceKeyAttribute(Type implementationType) + { + // Intentionally not picky about _which_ constructor or property has the + // attribute. If a different constructor is picked via constructor + // binder/selector, it's fine; we just want to try to shortcut the case + // where we "may or may not need it." If you mark a property with the + // attribute but never inject properties, we'll still provide the + // parameter "just in case" you change your mind at runtime. + foreach (var constructor in implementationType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + foreach (var parameter in constructor.GetParameters()) + { + if (ServiceKeyAttributeCache.ParameterHasServiceKey(parameter)) + { + return true; + } + } + } + + foreach (var property in implementationType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (property.CanWrite && ServiceKeyAttributeCache.PropertyHasServiceKey(property)) + { + return true; + } + } + + return false; + } + private void UseSingleConstructorActivation(IResolvePipelineBuilder pipelineBuilder, ConstructorBinder singleConstructor) { if (singleConstructor.ParameterCount == 0) diff --git a/src/Autofac/Core/ImplicitRegistrationSource.cs b/src/Autofac/Core/ImplicitRegistrationSource.cs index f676c1b81..66bda63dd 100644 --- a/src/Autofac/Core/ImplicitRegistrationSource.cs +++ b/src/Autofac/Core/ImplicitRegistrationSource.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Collections.Concurrent; using System.Reflection; using Autofac.Builder; using Autofac.Util; diff --git a/src/Autofac/Core/InternalReflectionCaches.cs b/src/Autofac/Core/InternalReflectionCaches.cs index 003487112..0988cafa2 100644 --- a/src/Autofac/Core/InternalReflectionCaches.cs +++ b/src/Autofac/Core/InternalReflectionCaches.cs @@ -35,6 +35,9 @@ public InternalReflectionCaches(ReflectionCacheSet set) DefaultPublicConstructors = set.GetOrCreateCache>(nameof(DefaultPublicConstructors)); GenericTypeDefinitionByType = set.GetOrCreateCache>(nameof(GenericTypeDefinitionByType)); HasRequiredMemberAttribute = set.GetOrCreateCache>(nameof(HasRequiredMemberAttribute)); + ServiceKeyParameterAttributes = set.GetOrCreateCache>(nameof(ServiceKeyParameterAttributes)); + ServiceKeyPropertyAttributes = set.GetOrCreateCache>(nameof(ServiceKeyPropertyAttributes)); + ServiceKeyUsageByType = set.GetOrCreateCache>(nameof(ServiceKeyUsageByType)); } /// @@ -91,4 +94,19 @@ public InternalReflectionCaches(ReflectionCacheSet set) /// Gets a cache used by . /// public ReflectionCacheDictionary HasRequiredMemberAttribute { get; } + + /// + /// Gets a cache used to track usage on parameters. + /// + public ReflectionCacheParameterDictionary ServiceKeyParameterAttributes { get; } + + /// + /// Gets a cache used to track usage on properties. + /// + public ReflectionCacheDictionary ServiceKeyPropertyAttributes { get; } + + /// + /// Gets a cache used to determine if a type uses . + /// + public ReflectionCacheDictionary ServiceKeyUsageByType { get; } } diff --git a/src/Autofac/Core/KeyedService.cs b/src/Autofac/Core/KeyedService.cs index 7fe449197..cf34ba634 100644 --- a/src/Autofac/Core/KeyedService.cs +++ b/src/Autofac/Core/KeyedService.cs @@ -19,6 +19,18 @@ public KeyedService(object serviceKey, Type serviceType) ServiceType = serviceType ?? throw new ArgumentNullException(nameof(serviceType)); } + /// + /// Gets a sentinel key representing a wildcard keyed registration. + /// + /// + /// A singleton object that can be used as a key to match any keyed registration. + /// + /// + /// Registering a service with this key will allow it to be resolved by any + /// keyed service request for the same service type. + /// + public static object AnyKey { get; } = new object(); + /// /// Gets the key of the service. /// @@ -37,6 +49,23 @@ public KeyedService(object serviceKey, Type serviceType) /// The description. public override string Description => ServiceKey + " (" + ServiceType.FullName + ")"; + /// + /// Determines whether the provided key is the sentinel. + /// + /// The key to test. + /// + /// when the key is ; otherwise, . + /// + public static bool IsAnyKey(object serviceKey) + { + if (serviceKey == null) + { + throw new ArgumentNullException(nameof(serviceKey)); + } + + return ReferenceEquals(serviceKey, AnyKey); + } + /// /// Indicates whether the current object is equal to another object of the same type. /// diff --git a/src/Autofac/Core/KeyedServiceParameterInjector.cs b/src/Autofac/Core/KeyedServiceParameterInjector.cs new file mode 100644 index 000000000..421b56537 --- /dev/null +++ b/src/Autofac/Core/KeyedServiceParameterInjector.cs @@ -0,0 +1,137 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Core; + +/// +/// Helper methods for ensuring keyed resolve requests carry the requested key as a parameter. +/// +internal static class KeyedServiceParameterInjector +{ + /// + /// Ensures keyed service requests carry their associated key with the parameter sequence. + /// + /// The service being resolved. + /// The parameters supplied by the caller. + /// An enumerable that exposes the keyed service key when appropriate. + public static IEnumerable AddKeyedServiceParameter(Service service, IEnumerable parameters) + { + if (service == null) + { + throw new ArgumentNullException(nameof(service)); + } + + if (service is not KeyedService keyedService) + { + return parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + return AddKeyedServiceParameter(keyedService.ServiceKey, parameters); + } + + /// + /// Ensures keyed service requests carry their associated key with the parameter sequence. + /// + /// The service being resolved. + /// The parameters supplied by the caller. + /// The component registration (if known). + /// An enumerable that exposes the keyed service key when appropriate. + public static IEnumerable AddKeyedServiceParameter(Service service, IEnumerable parameters, IComponentRegistration? registration) + { + if (service == null) + { + throw new ArgumentNullException(nameof(service)); + } + + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + if (service is not KeyedService keyedService || KeyedService.IsAnyKey(keyedService.ServiceKey)) + { + // It's not a keyed service OR it's registered with AnyKey. + return parameters; + } + + if (registration?.Activator is Autofac.Core.Activators.Reflection.ReflectionActivator reflectionActivator && + !reflectionActivator.RequiresServiceKeyParameter) + { + return parameters; + } + + return AddKeyedServiceParameter(keyedService.ServiceKey, parameters); + } + + /// + /// Ensures keyed service requests carry their associated key with the parameter sequence. + /// + /// The keyed service key. + /// The parameters supplied by the caller. + /// An enumerable that exposes the keyed service key when appropriate. + public static IEnumerable AddKeyedServiceParameter(object serviceKey, IEnumerable parameters) + { + if (serviceKey == null) + { + throw new ArgumentNullException(nameof(serviceKey)); + } + + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + if (KeyedService.IsAnyKey(serviceKey)) + { + return parameters; + } + + if (HasKeyParameter(parameters, serviceKey)) + { + return parameters; + } + + return AppendKeyParameter(parameters, serviceKey); + } + + private static bool HasKeyParameter(IEnumerable parameters, object serviceKey) + { + if (parameters is IReadOnlyList readOnlyList) + { + if (readOnlyList.Count == 0) + { + return false; + } + + // Fast path for the common case where the key parameter is last. + var lastIndex = readOnlyList.Count - 1; + if (readOnlyList[lastIndex] is KeyedServiceKeyParameter lastKey && + Equals(lastKey.ServiceKey, serviceKey)) + { + return true; + } + } + + foreach (var parameter in parameters) + { + if (parameter is KeyedServiceKeyParameter keyParameter && Equals(keyParameter.ServiceKey, serviceKey)) + { + return true; + } + } + + return false; + } + + private static IEnumerable AppendKeyParameter(IEnumerable parameters, object serviceKey) + { + var keyParameter = new KeyedServiceKeyParameter(serviceKey); + + if (ReferenceEquals(parameters, ResolveRequest.NoParameters)) + { + return new Parameter[] { keyParameter }; + } + + return parameters.Append(keyParameter); + } +} diff --git a/src/Autofac/Core/Lifetime/LifetimeScope.cs b/src/Autofac/Core/Lifetime/LifetimeScope.cs index cce522510..fd676c46c 100644 --- a/src/Autofac/Core/Lifetime/LifetimeScope.cs +++ b/src/Autofac/Core/Lifetime/LifetimeScope.cs @@ -11,7 +11,6 @@ using Autofac.Builder; using Autofac.Core.Registration; using Autofac.Core.Resolving; -using Autofac.Features.Collections; using Autofac.Util; namespace Autofac.Core.Lifetime; diff --git a/src/Autofac/Core/ReflectionCacheSet.cs b/src/Autofac/Core/ReflectionCacheSet.cs index 49d7b131e..63020f146 100644 --- a/src/Autofac/Core/ReflectionCacheSet.cs +++ b/src/Autofac/Core/ReflectionCacheSet.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Collections.Concurrent; -using System.Reflection; using Autofac.Util.Cache; namespace Autofac.Core; diff --git a/src/Autofac/Core/Resolving/Pipeline/DefaultResolveRequestContext.cs b/src/Autofac/Core/Resolving/Pipeline/DefaultResolveRequestContext.cs index 8efc3892a..d0badb5b4 100644 --- a/src/Autofac/Core/Resolving/Pipeline/DefaultResolveRequestContext.cs +++ b/src/Autofac/Core/Resolving/Pipeline/DefaultResolveRequestContext.cs @@ -31,7 +31,7 @@ internal DefaultResolveRequestContext( { Operation = owningOperation; ActivationScope = scope; - Parameters = request.Parameters; + Parameters = KeyedServiceParameterInjector.AddKeyedServiceParameter(request.Service, request.Parameters, request.Registration); _resolveRequest = request; PhaseReached = PipelinePhase.ResolveRequestStart; DiagnosticSource = diagnosticSource; @@ -87,7 +87,7 @@ public override void ChangeScope(ISharingLifetimeScope newScope) => /// public override void ChangeParameters(IEnumerable newParameters) => - Parameters = newParameters ?? throw new ArgumentNullException(nameof(newParameters)); + Parameters = KeyedServiceParameterInjector.AddKeyedServiceParameter(Service, newParameters ?? throw new ArgumentNullException(nameof(newParameters)), Registration); /// public override object ResolveComponent(in ResolveRequest request) => diff --git a/src/Autofac/Core/ServiceKeyAttributeCache.cs b/src/Autofac/Core/ServiceKeyAttributeCache.cs new file mode 100644 index 000000000..202b24a7a --- /dev/null +++ b/src/Autofac/Core/ServiceKeyAttributeCache.cs @@ -0,0 +1,61 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Reflection; +using Autofac.Util; + +namespace Autofac.Core; + +/// +/// Caches lookups for to avoid repeated reflection scans. +/// +/// +/// This class doesn't actually do the caching itself, but provides a single +/// point of access to the cache for both parameters and properties. This is an +/// important distinction because this class is not responsible for flushing +/// reflection info - that's handled by the . +/// +internal static class ServiceKeyAttributeCache +{ + /// + /// Determines whether a parameter is decorated with . + /// + /// The parameter to inspect. + /// when the attribute is present; otherwise . + public static bool ParameterHasServiceKey(ParameterInfo parameter) + { + if (parameter == null) + { + throw new ArgumentNullException(nameof(parameter)); + } + + return ReflectionCacheSet.Shared.Internal.ServiceKeyParameterAttributes.GetOrAdd( + parameter, + static p => + { + if (p.IsDefined(typeof(ServiceKeyAttribute), inherit: true)) + { + return true; + } + + return p.TryGetDeclaringProperty(out PropertyInfo? property) && PropertyHasServiceKey(property); + }); + } + + /// + /// Determines whether a property is decorated with . + /// + /// The property to inspect. + /// when the attribute is present; otherwise . + public static bool PropertyHasServiceKey(PropertyInfo property) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + return ReflectionCacheSet.Shared.Internal.ServiceKeyPropertyAttributes.GetOrAdd( + property, + static p => p.IsDefined(typeof(ServiceKeyAttribute), inherit: true)); + } +} diff --git a/src/Autofac/Features/AttributeFilters/KeyFilterAttribute.cs b/src/Autofac/Features/AttributeFilters/KeyFilterAttribute.cs index 9874d0d16..693a8ffa5 100644 --- a/src/Autofac/Features/AttributeFilters/KeyFilterAttribute.cs +++ b/src/Autofac/Features/AttributeFilters/KeyFilterAttribute.cs @@ -138,6 +138,13 @@ public override bool CanResolveParameter(ParameterInfo parameter, IComponentCont throw new ArgumentNullException(nameof(context)); } + // Note that the design of attribute filtering is that the attribute + // doesn't REQUIRE the presence of the keyed service. If the injection + // can fall back to an unkeyed/typed service, that is allowed. Changing + // that behavior will be breaking... and possibly expensive because we + // would have to assume the attribute can supply the value and, later, + // actually try to resolve the dependency and fail. No short-circuit + // checks. return context.ComponentRegistry.IsRegistered(new KeyedService(Key, parameter.ParameterType)); } } diff --git a/src/Autofac/Features/Collections/CollectionRegistrationSource.cs b/src/Autofac/Features/Collections/CollectionRegistrationSource.cs index de0da9429..c51054a9e 100644 --- a/src/Autofac/Features/Collections/CollectionRegistrationSource.cs +++ b/src/Autofac/Features/Collections/CollectionRegistrationSource.cs @@ -113,16 +113,27 @@ public IEnumerable RegistrationsFor(Service service, Fun } var elementTypeService = swt.ChangeType(elementType); + var isAnyKeyQuery = service is KeyedService keyedService && KeyedService.IsAnyKey(keyedService.ServiceKey); var activator = new DelegateActivator( limitType, (c, p) => { - var itemRegistrations = c.ComponentRegistry - .ServiceRegistrationsFor(elementTypeService) - .Where(cr => !cr.Registration.Options.HasOption(RegistrationOptions.ExcludeFromCollections)) - .OrderBy(cr => cr.Registration.GetRegistrationOrder()) - .ToList(); + List itemRegistrations; + + if (isAnyKeyQuery) + { + // AnyKey queries for collections return _all_ keyed services. + itemRegistrations = GetAllSpecificKeyedRegistrations(c.ComponentRegistry, elementType); + } + else + { + itemRegistrations = c.ComponentRegistry + .ServiceRegistrationsFor(elementTypeService) + .Where(cr => !cr.Registration.Options.HasOption(RegistrationOptions.ExcludeFromCollections)) + .OrderBy(cr => cr.Registration.GetRegistrationOrder()) + .ToList(); + } var output = factory(itemRegistrations.Count); var isFixedSize = output.IsFixedSize; @@ -149,8 +160,8 @@ public IEnumerable RegistrationsFor(Service service, Fun Guid.NewGuid(), activator, CurrentScopeLifetime.Instance, - InstanceSharing.None, - InstanceOwnership.ExternallyOwned, + isAnyKeyQuery ? InstanceSharing.Shared : InstanceSharing.None, + isAnyKeyQuery ? InstanceOwnership.OwnedByLifetimeScope : InstanceOwnership.ExternallyOwned, new[] { service }, new Dictionary()); @@ -178,4 +189,43 @@ private static Func GenerateArrayFactory(Type elementType) var newArray = Expression.NewArrayBounds(elementType, parameter); return Expression.Lambda>(newArray, parameter).Compile(); } + + private static List GetAllSpecificKeyedRegistrations(IComponentRegistry registry, Type elementType) + { + var result = new List(); + var processedServices = new HashSet(); + + foreach (var registration in registry.Registrations) + { + if (registration.Metadata.ContainsKey(MetadataKeys.AnyKeyAdapter)) + { + continue; + } + + foreach (var keyed in registration.Services.OfType()) + { + if (keyed.ServiceType != elementType || KeyedService.IsAnyKey(keyed.ServiceKey)) + { + continue; + } + + if (!processedServices.Add(keyed)) + { + continue; + } + + var serviceRegistrations = registry + .ServiceRegistrationsFor(keyed) + .Where(cr => + !cr.Registration.Options.HasOption(RegistrationOptions.ExcludeFromCollections) && + !cr.Registration.Metadata.ContainsKey(MetadataKeys.AnyKeyAdapter)); + + result.AddRange(serviceRegistrations); + } + } + + return result + .OrderBy(cr => cr.Registration.GetRegistrationOrder()) + .ToList(); + } } diff --git a/src/Autofac/Features/KeyedServices/AnyKeyRegistrationSource.cs b/src/Autofac/Features/KeyedServices/AnyKeyRegistrationSource.cs new file mode 100644 index 000000000..83ebd9efb --- /dev/null +++ b/src/Autofac/Features/KeyedServices/AnyKeyRegistrationSource.cs @@ -0,0 +1,91 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Builder; +using Autofac.Core; +using Autofac.Core.Activators.Delegate; +using Autofac.Core.Registration; +using Autofac.Util; + +namespace Autofac.Features.KeyedServices; + +/// +/// Provides fallback registrations for keyed services that can be satisfied by . +/// +internal sealed class AnyKeyRegistrationSource : IRegistrationSource +{ + /// + public bool IsAdapterForIndividualComponents => true; + + /// + public IEnumerable RegistrationsFor(Service service, Func> registrationAccessor) + { + if (service == null) + { + throw new ArgumentNullException(nameof(service)); + } + + if (registrationAccessor == null) + { + throw new ArgumentNullException(nameof(registrationAccessor)); + } + + if (service is not KeyedService keyedService || + KeyedService.IsAnyKey(keyedService.ServiceKey) || + keyedService.ServiceType.IsCollectionServiceType()) + { + return Enumerable.Empty(); + } + + // If there are already specific registrations for this key, do nothing. + if (registrationAccessor(service).Any()) + { + return Enumerable.Empty(); + } + + var anyKeyService = new KeyedService(KeyedService.AnyKey, keyedService.ServiceType); + var anyKeyRegistrations = registrationAccessor(anyKeyService).ToArray(); + + if (anyKeyRegistrations.Length == 0) + { + return Enumerable.Empty(); + } + + return anyKeyRegistrations.Select(r => CreateAdapterRegistration(r, keyedService)); + } + + private static ComponentRegistration CreateAdapterRegistration(ServiceRegistration anyKeyRegistration, KeyedService requestedService) + { + var metadata = new Dictionary(anyKeyRegistration.Registration.Metadata) + { + [MetadataKeys.AnyKeyAdapter] = true, + }; + + ComponentRegistration? adapterRegistration = null; + + var activator = new DelegateActivator( + requestedService.ServiceType, + (c, p) => + { + var request = new ResolveRequest( + new KeyedService(KeyedService.AnyKey, requestedService.ServiceType), + anyKeyRegistration, + p, + adapterRegistration); + + return c.ResolveComponent(request); + }); + + adapterRegistration = new ComponentRegistration( + Guid.NewGuid(), + activator, + anyKeyRegistration.Registration.Lifetime, + anyKeyRegistration.Registration.Sharing, + anyKeyRegistration.Registration.Ownership, + new[] { requestedService }, + metadata, + anyKeyRegistration.Registration); + + return adapterRegistration; + } +} diff --git a/src/Autofac/Features/Scanning/ScanningRegistrationExtensions.cs b/src/Autofac/Features/Scanning/ScanningRegistrationExtensions.cs index 23bd5c56b..46067df7c 100644 --- a/src/Autofac/Features/Scanning/ScanningRegistrationExtensions.cs +++ b/src/Autofac/Features/Scanning/ScanningRegistrationExtensions.cs @@ -4,7 +4,6 @@ using System.Reflection; using Autofac.Builder; using Autofac.Core; -using Autofac.Core.Registration; using Autofac.Util; namespace Autofac.Features.Scanning; diff --git a/src/Autofac/KeyedServiceKeyParameter.cs b/src/Autofac/KeyedServiceKeyParameter.cs new file mode 100644 index 000000000..43226d9b3 --- /dev/null +++ b/src/Autofac/KeyedServiceKeyParameter.cs @@ -0,0 +1,55 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Reflection; +using Autofac.Core; + +namespace Autofac; + +/// +/// Parameter that exposes the keyed service key for the current resolve operation. +/// +internal sealed class KeyedServiceKeyParameter : Parameter +{ + /// + /// Initializes a new instance of the class. + /// + /// The keyed service key associated with the resolve operation. + public KeyedServiceKeyParameter(object serviceKey) + { + ServiceKey = serviceKey ?? throw new ArgumentNullException(nameof(serviceKey)); + } + + /// + /// Gets the keyed service key value. + /// + public object ServiceKey { get; } + + /// + public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, [NotNullWhen(returnValue: true)] out Func? valueProvider) + { + if (pi == null) + { + throw new ArgumentNullException(nameof(pi)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!ShouldInject(pi)) + { + valueProvider = null; + return false; + } + + valueProvider = () => ServiceKey; + return true; + } + + private static bool ShouldInject(ParameterInfo parameter) + { + return ServiceKeyAttributeCache.ParameterHasServiceKey(parameter); + } +} diff --git a/src/Autofac/ParameterExtensions.cs b/src/Autofac/ParameterExtensions.cs index b4f19be8c..448d0479c 100644 --- a/src/Autofac/ParameterExtensions.cs +++ b/src/Autofac/ParameterExtensions.cs @@ -89,6 +89,31 @@ public static T TypedAs(this IEnumerable parameters) return ConstantValue(parameters, c => c.Type == typeof(T)); } + /// + /// Retrieve the keyed service key value associated with the current resolve operation. + /// + /// The type to which the returned value will be cast. + /// The available parameters to choose from. + /// The value of the keyed service key. + /// + public static T KeyedServiceKey(this IEnumerable parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + foreach (var parameter in parameters) + { + if (parameter is KeyedServiceKeyParameter keyParameter) + { + return (T)keyParameter.ServiceKey; + } + } + + throw new InvalidOperationException(ResolutionExtensionsResources.KeyedServiceKeyUnavailable); + } + private static TValue ConstantValue(IEnumerable parameters, Func predicate) where TParameter : ConstantParameter { diff --git a/src/Autofac/RegistrationExtensions.cs b/src/Autofac/RegistrationExtensions.cs index 646c86ba6..852ffeb63 100644 --- a/src/Autofac/RegistrationExtensions.cs +++ b/src/Autofac/RegistrationExtensions.cs @@ -6,11 +6,9 @@ using System.Reflection; using Autofac.Builder; using Autofac.Core; -using Autofac.Core.Activators.Delegate; using Autofac.Core.Activators.ProvidedInstance; using Autofac.Core.Activators.Reflection; using Autofac.Core.Lifetime; -using Autofac.Core.Resolving.Pipeline; using Autofac.Features.Scanning; using Autofac.Util; diff --git a/src/Autofac/ResolutionExtensions.cs b/src/Autofac/ResolutionExtensions.cs index 62bfeb10a..9008eeb75 100644 --- a/src/Autofac/ResolutionExtensions.cs +++ b/src/Autofac/ResolutionExtensions.cs @@ -6,6 +6,7 @@ using Autofac.Core; using Autofac.Core.Activators.Reflection; using Autofac.Core.Registration; +using Autofac.Util; namespace Autofac; @@ -416,6 +417,7 @@ public static TService ResolveKeyed(this IComponentContext context, ob public static TService ResolveKeyed(this IComponentContext context, object serviceKey, IEnumerable parameters) where TService : notnull { + EnsureAnyKeyUsageIsValid(serviceKey, typeof(TService)); return CastInstance(ResolveService(context, new KeyedService(serviceKey, typeof(TService)), parameters)); } @@ -479,6 +481,7 @@ public static object ResolveKeyed(this IComponentContext context, object service /// public static object ResolveKeyed(this IComponentContext context, object serviceKey, Type serviceType, IEnumerable parameters) { + EnsureAnyKeyUsageIsValid(serviceKey, serviceType); return ResolveService(context, new KeyedService(serviceKey, serviceType), parameters); } @@ -1092,6 +1095,7 @@ public static bool TryResolveKeyed(this IComponentContext context, object ser /// public static bool TryResolveKeyed(this IComponentContext context, object serviceKey, Type serviceType, [NotNullWhen(returnValue: true)] out object? instance) { + EnsureAnyKeyUsageIsValid(serviceKey, serviceType); return context.TryResolveService(new KeyedService(serviceKey, serviceType), ResolveRequest.NoParameters, out instance); } @@ -1193,6 +1197,33 @@ public static bool TryResolveService(this IComponentContext context, Service ser return true; } + private static void EnsureAnyKeyUsageIsValid(object serviceKey, Type serviceType) + { + if (serviceKey == null) + { + throw new ArgumentNullException(nameof(serviceKey)); + } + + if (serviceType == null) + { + throw new ArgumentNullException(nameof(serviceType)); + } + + if (!KeyedService.IsAnyKey(serviceKey)) + { + return; + } + + if (!serviceType.IsCollectionServiceType()) + { + throw new DependencyResolutionException( + string.Format( + CultureInfo.CurrentCulture, + ResolutionExtensionsResources.AnyKeyRequiresEnumerable, + serviceType.FullName)); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static TService CastInstance(object? instance) { diff --git a/src/Autofac/ResolutionExtensionsResources.resx b/src/Autofac/ResolutionExtensionsResources.resx index 98affed57..20f0ba049 100644 --- a/src/Autofac/ResolutionExtensionsResources.resx +++ b/src/Autofac/ResolutionExtensionsResources.resx @@ -117,7 +117,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + AnyKey can only be used when resolving collection services. Service type {0} is not a collection. + Resolved instance of type '{0}' could not be cast to the requested service type '{1}'. + + No keyed service key is available for the current resolve operation. + diff --git a/src/Autofac/ServiceKeyAttribute.cs b/src/Autofac/ServiceKeyAttribute.cs new file mode 100644 index 000000000..b86f3a84d --- /dev/null +++ b/src/Autofac/ServiceKeyAttribute.cs @@ -0,0 +1,12 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac; + +/// +/// Marks a constructor parameter or property as a target for service key injection. +/// +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public sealed class ServiceKeyAttribute : Attribute +{ +} diff --git a/src/Autofac/Util/AssemblyExtensions.cs b/src/Autofac/Util/AssemblyExtensions.cs index 0a39b0be7..ede8346f7 100644 --- a/src/Autofac/Util/AssemblyExtensions.cs +++ b/src/Autofac/Util/AssemblyExtensions.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Reflection; -using Autofac.Core; namespace Autofac.Util; diff --git a/src/Autofac/Util/Cache/ReflectionCacheAssemblyDictionary.cs b/src/Autofac/Util/Cache/ReflectionCacheAssemblyDictionary.cs index f632cd5f6..5afdcc8aa 100644 --- a/src/Autofac/Util/Cache/ReflectionCacheAssemblyDictionary.cs +++ b/src/Autofac/Util/Cache/ReflectionCacheAssemblyDictionary.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Collections.Concurrent; -using System.Linq; using System.Reflection; using Autofac.Core; diff --git a/src/Autofac/Util/Cache/ReflectionCacheParameterDictionary.cs b/src/Autofac/Util/Cache/ReflectionCacheParameterDictionary.cs new file mode 100644 index 000000000..a510fc266 --- /dev/null +++ b/src/Autofac/Util/Cache/ReflectionCacheParameterDictionary.cs @@ -0,0 +1,44 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Collections.Concurrent; +using System.Reflection; +using Autofac.Core; + +namespace Autofac.Util.Cache; + +/// +/// A reflection cache dictionary, keyed on a . +/// +/// The value type. +public sealed class ReflectionCacheParameterDictionary + : ConcurrentDictionary, IReflectionCache +{ + /// + public ReflectionCacheUsage Usage { get; set; } = ReflectionCacheUsage.All; + + /// + public void Clear(ReflectionCacheClearPredicate predicate) + { + if (predicate is null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + if (Count == 0) + { + return; + } + + var reusableAssemblySet = new HashSet(); + + foreach (var kvp in this) + { + var member = kvp.Key.Member; + if (predicate(member, TypeAssemblyReferenceProvider.GetAllReferencedAssemblies(member, reusableAssemblySet))) + { + TryRemove(kvp.Key, out _); + } + } + } +} diff --git a/src/Autofac/Util/Cache/TypeAssemblyReferenceProvider.cs b/src/Autofac/Util/Cache/TypeAssemblyReferenceProvider.cs index fec0cac8c..67491cf5c 100644 --- a/src/Autofac/Util/Cache/TypeAssemblyReferenceProvider.cs +++ b/src/Autofac/Util/Cache/TypeAssemblyReferenceProvider.cs @@ -1,9 +1,7 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Collections.Concurrent; using System.Reflection; -using Autofac.Core; namespace Autofac.Util.Cache; diff --git a/src/Autofac/Util/InternalTypeExtensions.cs b/src/Autofac/Util/InternalTypeExtensions.cs index 67449add3..72c8fbb5d 100644 --- a/src/Autofac/Util/InternalTypeExtensions.cs +++ b/src/Autofac/Util/InternalTypeExtensions.cs @@ -111,6 +111,17 @@ public static bool IsCompatibleWithGenericParameterConstraints(this Type generic return true; } + /// + /// Checks whether a type is any sort of a collection - array, generic enumerable, or generic list/collection interface. + /// + /// The type to check. + /// True if the type is a collection type; false otherwise. + public static bool IsCollectionServiceType(this Type serviceType) + { + return serviceType.IsArray || + serviceType.IsGenericEnumerableInterfaceType(); + } + /// /// Checks whether a type is compiler generated. /// diff --git a/test/Autofac.Specification.Test/Features/KeyedServiceTests.cs b/test/Autofac.Specification.Test/Features/KeyedServiceTests.cs new file mode 100644 index 000000000..3882f7efc --- /dev/null +++ b/test/Autofac.Specification.Test/Features/KeyedServiceTests.cs @@ -0,0 +1,1037 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Core; +using Autofac.Core.Registration; +using Autofac.Features.AttributeFilters; + +namespace Autofac.Specification.Test.Features; + +// Tests here mimic the keyed service tests from +// Microsoft.Extensions.DependencyInjection.Specification.Tests. +// +// https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs +// +// Some of the tests have been modified or omitted based on what core Autofac +// actually supports. +// +// - Autofac does not support null keys (which is the same as register-by-type). +// - Autofac injects the keyed value into constructors or properties that are +// explicitly decorated with the ServiceKeyAttribute from Autofac. This is the +// same concept as the MEDI attribute but not the same literal type. +// - Autofac scopes and injected ILifetimeScope behave slightly differently than +// MEDI's IServiceScope and injected IServiceProvider, so some of the scope +// related tests have been omitted or modified. +// - Autofac KeyFilterAttribute/WithAttributeFiltering does not REQUIRE the +// presence of the keyed service. If the injection can fall back to an +// unkeyed/typed service, that is allowed. +public class KeyedServiceTests +{ + [Fact] + public void CombinationalRegistration() + { + Service service1 = new(); + Service service2 = new(); + Service keyedService1 = new(); + Service keyedService2 = new(); + Service anyKeyService1 = new(); + Service anyKeyService2 = new(); + + var builder = new ContainerBuilder(); + builder.RegisterInstance(service1); + builder.RegisterInstance(service2); + builder.RegisterInstance(anyKeyService1).Keyed(KeyedService.AnyKey); + builder.RegisterInstance(anyKeyService2).Keyed(KeyedService.AnyKey); + builder.RegisterInstance(keyedService1).Keyed("keyedService"); + builder.RegisterInstance(keyedService2).Keyed("keyedService"); + + var provider = builder.Build(); + + /* + * Table for what results are included: + * + * Query | Keyed? | Unkeyed? | AnyKey? | + * ----------------------------------------------------------------------- + * Resolve> | no | yes | no | + * Resolve | no | yes | no | + * + * ResolveKeyed>(AnyKey) | yes | no | no | + * ResolveKeyed(AnyKey) | throw | throw | throw | + * + * ResolveKeyed>(key) | yes | no | no | + * ResolveKeyed(key) | yes | no | yes | + * + * Summary: + * - Autofac does not support null keys, so there is no concept of differentiating between null key and unkeyed. + * - In MEDI, a null key is the same as unkeyed. This allows their KeyedServices APIs to support both keyed and unkeyed. + * - AnyKey is a special case of Keyed. + * - AnyKey registrations are not returned with GetKeyedServices(AnyKey) and GetKeyedService(AnyKey) always throws. + * - For IEnumerable, the ordering of the results are in registration order. + * - For a singleton resolve, the last match wins. + */ + + // Unkeyed (register by type). + Assert.Equal( + new[] { service1, service2 }, + provider.Resolve>()); + + Assert.Equal(service2, provider.Resolve()); + + // AnyKey. + Assert.Equal( + new[] { keyedService1, keyedService2 }, + provider.ResolveKeyed>(KeyedService.AnyKey)); + + Assert.Throws(() => provider.ResolveKeyed(KeyedService.AnyKey)); + + // Keyed. + Assert.Equal( + new[] { keyedService1, keyedService2 }, + provider.ResolveKeyed>("keyedService")); + + Assert.Equal(keyedService2, provider.ResolveKeyed("keyedService")); + } + + [Fact] + public void ResolveKeyedService() + { + var service1 = new Service(); + var service2 = new Service(); + var builder = new ContainerBuilder(); + builder.RegisterInstance(service1).Keyed("service1"); + builder.RegisterInstance(service2).Keyed("service2"); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + Assert.Same(service1, provider.ResolveKeyed("service1")); + Assert.Same(service2, provider.ResolveKeyed("service2")); + + Assert.Throws(() => provider.Resolve(typeof(IService))); + Assert.Same(service1, provider.ResolveKeyed("service1", typeof(IService))); + Assert.Same(service2, provider.ResolveKeyed("service2", typeof(IService))); + } + + [Fact] + public void ResolveKeyedOpenGenericService() + { + var builder = new ContainerBuilder(); + builder.RegisterGeneric(typeof(FakeOpenGenericService<>)).Keyed("my-service", typeof(IFakeOpenGenericService<>)); + builder.RegisterType().As().SingleInstance(); + var provider = builder.Build(); + + // Act + var genericService = provider.ResolveKeyed>("my-service"); + var singletonService = provider.Resolve(); + + // Assert + Assert.Same(singletonService, genericService.Value); + } + + [Fact] + public void ResolveKeyedServices() + { + var service1 = new Service(); + var service2 = new Service(); + var service3 = new Service(); + var service4 = new Service(); + var builder = new ContainerBuilder(); + builder.RegisterInstance(service1).Keyed("first-service"); + builder.RegisterInstance(service2).Keyed("service"); + builder.RegisterInstance(service3).Keyed("service"); + builder.RegisterInstance(service4).Keyed("service"); + + var provider = builder.Build(); + + var firstSvc = provider.ResolveKeyed>("first-service").ToList(); + Assert.Single(firstSvc); + Assert.Same(service1, firstSvc[0]); + + var services = provider.ResolveKeyed>("service").ToList(); + Assert.Equal(new[] { service2, service3, service4 }, services); + } + + [Fact] + public void ResolveKeyedServicesAnyKey() + { + var service1 = new Service(); + var service2 = new Service(); + var service3 = new Service(); + var service4 = new Service(); + var service5 = new Service(); + var service6 = new Service(); + var builder = new ContainerBuilder(); + builder.RegisterInstance(service1).Keyed("first-service"); + builder.RegisterInstance(service2).Keyed("service"); + builder.RegisterInstance(service3).Keyed("service"); + builder.RegisterInstance(service4).Keyed("service"); + builder.RegisterInstance(service5); + builder.RegisterInstance(service6); + + var provider = builder.Build(); + + // Return all services registered with a non null key + var allServices = provider.ResolveKeyed>(KeyedService.AnyKey).ToList(); + Assert.Equal(4, allServices.Count); + Assert.Equal(new[] { service1, service2, service3, service4 }, allServices); + + // Check again (caching) + var allServices2 = provider.ResolveKeyed>(KeyedService.AnyKey).ToList(); + Assert.Equal(allServices, allServices2); + } + + [Fact] + public void ResolveKeyedServicesAnyKeyWithAnyKeyRegistration() + { + var service1 = new Service(); + var service2 = new Service(); + var service3 = new Service(); + var service4 = new Service(); + var service5 = new Service(); + var service6 = new Service(); + var builder = new ContainerBuilder(); + builder.Register(ctx => new Service()).Keyed(KeyedService.AnyKey); + builder.RegisterInstance(service1).Keyed("first-service"); + builder.RegisterInstance(service2).Keyed("service"); + builder.RegisterInstance(service3).Keyed("service"); + builder.RegisterInstance(service4).Keyed("service"); + builder.RegisterInstance(service5); + builder.RegisterInstance(service6); + + var provider = builder.Build(); + + _ = provider.ResolveKeyed("something-else"); + _ = provider.ResolveKeyed("something-else-again"); + + // Return all services registered with a non null key, but not the one "created" with KeyedService.AnyKey, + // nor the KeyedService.AnyKey registration + var allServices = provider.ResolveKeyed>(KeyedService.AnyKey).ToList(); + Assert.Equal(4, allServices.Count); + Assert.Equal(new[] { service1, service2, service3, service4 }, allServices); + + var someKeyedServices = provider.ResolveKeyed>("service").ToList(); + Assert.Equal(new[] { service2, service3, service4 }, someKeyedServices); + + var unkeyedServices = provider.Resolve>().ToList(); + Assert.Equal(new[] { service5, service6 }, unkeyedServices); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResolveWithAnyKeyQuery_Constructor(bool anyKeyQueryBeforeSingletonQueries) + { + // Test ordering and slot assignments when DI calls the service's constructor + // across keyed services with different service types and keys. + var builder = new ContainerBuilder(); + + // Interweave these to check that the slot \ ordering logic is correct. + // Each unique key + its service Type maintains their own slot in a AnyKey query. + builder.RegisterType().Keyed("key1").SingleInstance(); + builder.RegisterType().Keyed("key1").SingleInstance(); + builder.RegisterType().Keyed("key2").SingleInstance(); + builder.RegisterType().Keyed("key2").SingleInstance(); + builder.RegisterType().Keyed("key3").SingleInstance(); + builder.RegisterType().Keyed("key3").SingleInstance(); + + var provider = builder.Build(); + + TestServiceA[] allInstancesA = null; + TestServiceB[] allInstancesB = null; + + if (anyKeyQueryBeforeSingletonQueries) + { + DoAnyKeyQuery(); + } + + var serviceA1 = provider.ResolveKeyed("key1"); + var serviceB1 = provider.ResolveKeyed("key1"); + var serviceA2 = provider.ResolveKeyed("key2"); + var serviceB2 = provider.ResolveKeyed("key2"); + var serviceA3 = provider.ResolveKeyed("key3"); + var serviceB3 = provider.ResolveKeyed("key3"); + + if (!anyKeyQueryBeforeSingletonQueries) + { + DoAnyKeyQuery(); + } + + Assert.Equal( + new[] { serviceA1, serviceA2, serviceA3 }, + allInstancesA); + + Assert.Equal( + new[] { serviceB1, serviceB2, serviceB3 }, + allInstancesB); + + void DoAnyKeyQuery() + { + IEnumerable allA = provider.ResolveKeyed>(KeyedService.AnyKey); + IEnumerable allB = provider.ResolveKeyed>(KeyedService.AnyKey); + + // Verify caching returns the same IEnumerable<> instance. + Assert.Same(allA, provider.ResolveKeyed>(KeyedService.AnyKey)); + Assert.Same(allB, provider.ResolveKeyed>(KeyedService.AnyKey)); + + allInstancesA = allA.ToArray(); + allInstancesB = allB.ToArray(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResolveWithAnyKeyQuery_Constructor_Duplicates(bool anyKeyQueryBeforeSingletonQueries) + { + // Test ordering and slot assignments when DI calls the service's constructor + // across keyed services with different service types with duplicate keys. + var builder = new ContainerBuilder(); + + // Interweave these to check that the slot \ ordering logic is correct. + // Each unique key + its service Type maintains their own slot in a AnyKey query. + builder.RegisterType().Keyed("key").SingleInstance(); + builder.RegisterType().Keyed("key").SingleInstance(); + builder.RegisterType().Keyed("key").SingleInstance(); + builder.RegisterType().Keyed("key").SingleInstance(); + builder.RegisterType().Keyed("key").SingleInstance(); + builder.RegisterType().Keyed("key").SingleInstance(); + + var provider = builder.Build(); + + TestServiceA[] allInstancesA = null; + TestServiceB[] allInstancesB = null; + + if (anyKeyQueryBeforeSingletonQueries) + { + DoAnyKeyQuery(); + } + + var serviceA = provider.ResolveKeyed("key"); + Assert.Same(serviceA, provider.ResolveKeyed("key")); + + var serviceB = provider.ResolveKeyed("key"); + Assert.Same(serviceB, provider.ResolveKeyed("key")); + + if (!anyKeyQueryBeforeSingletonQueries) + { + DoAnyKeyQuery(); + } + + // An AnyKey query we get back the last registered service for duplicates. + // The first and second services are effectively hidden unless we query all. + Assert.Equal(3, allInstancesA.Length); + Assert.Same(serviceA, allInstancesA[2]); + Assert.NotSame(serviceA, allInstancesA[1]); + Assert.NotSame(serviceA, allInstancesA[0]); + Assert.NotSame(allInstancesA[0], allInstancesA[1]); + + Assert.Equal(3, allInstancesB.Length); + Assert.Same(serviceB, allInstancesB[2]); + Assert.NotSame(serviceB, allInstancesB[1]); + Assert.NotSame(serviceB, allInstancesB[0]); + Assert.NotSame(allInstancesB[0], allInstancesB[1]); + + void DoAnyKeyQuery() + { + IEnumerable allA = provider.ResolveKeyed>(KeyedService.AnyKey); + IEnumerable allB = provider.ResolveKeyed>(KeyedService.AnyKey); + + // Verify caching returns the same IEnumerable<> instances. + Assert.Same(allA, provider.ResolveKeyed>(KeyedService.AnyKey)); + Assert.Same(allB, provider.ResolveKeyed>(KeyedService.AnyKey)); + + allInstancesA = allA.ToArray(); + allInstancesB = allB.ToArray(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResolveWithAnyKeyQuery_InstanceProvided(bool anyKeyQueryBeforeSingletonQueries) + { + // Test ordering and slot assignments when service is provided + // across keyed services with different service types and keys. + var builder = new ContainerBuilder(); + + TestServiceA serviceA1 = new(); + TestServiceA serviceA2 = new(); + TestServiceA serviceA3 = new(); + TestServiceB serviceB1 = new(); + TestServiceB serviceB2 = new(); + TestServiceB serviceB3 = new(); + + // Interweave these to check that the slot \ ordering logic is correct. + // Each unique key + its service Type maintains their own slot in a AnyKey query. + builder.RegisterInstance(serviceA1).Keyed("key1"); + builder.RegisterInstance(serviceB1).Keyed("key1"); + builder.RegisterInstance(serviceA2).Keyed("key2"); + builder.RegisterInstance(serviceB2).Keyed("key2"); + builder.RegisterInstance(serviceA3).Keyed("key3"); + builder.RegisterInstance(serviceB3).Keyed("key3"); + + var provider = builder.Build(); + + TestServiceA[] allInstancesA = null; + TestServiceB[] allInstancesB = null; + + if (anyKeyQueryBeforeSingletonQueries) + { + DoAnyKeyQuery(); + } + + var fromServiceA1 = provider.ResolveKeyed("key1"); + var fromServiceA2 = provider.ResolveKeyed("key2"); + var fromServiceA3 = provider.ResolveKeyed("key3"); + Assert.Same(serviceA1, fromServiceA1); + Assert.Same(serviceA2, fromServiceA2); + Assert.Same(serviceA3, fromServiceA3); + + var fromServiceB1 = provider.ResolveKeyed("key1"); + var fromServiceB2 = provider.ResolveKeyed("key2"); + var fromServiceB3 = provider.ResolveKeyed("key3"); + Assert.Same(serviceB1, fromServiceB1); + Assert.Same(serviceB2, fromServiceB2); + Assert.Same(serviceB3, fromServiceB3); + + if (!anyKeyQueryBeforeSingletonQueries) + { + DoAnyKeyQuery(); + } + + Assert.Equal( + new[] { serviceA1, serviceA2, serviceA3 }, + allInstancesA); + + Assert.Equal( + new[] { serviceB1, serviceB2, serviceB3 }, + allInstancesB); + + void DoAnyKeyQuery() + { + IEnumerable allA = provider.ResolveKeyed>(KeyedService.AnyKey); + IEnumerable allB = provider.ResolveKeyed>(KeyedService.AnyKey); + + // Verify caching returns the same items. + Assert.Equal(allA, provider.ResolveKeyed>(KeyedService.AnyKey)); + Assert.Equal(allB, provider.ResolveKeyed>(KeyedService.AnyKey)); + + allInstancesA = allA.ToArray(); + allInstancesB = allB.ToArray(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResolveWithAnyKeyQuery_InstanceProvided_Duplicates(bool anyKeyQueryBeforeSingletonQueries) + { + // Test ordering and slot assignments when service is provided + // across keyed services with different service types with duplicate keys. + var builder = new ContainerBuilder(); + + TestServiceA serviceA1 = new(); + TestServiceA serviceA2 = new(); + TestServiceA serviceA3 = new(); + TestServiceB serviceB1 = new(); + TestServiceB serviceB2 = new(); + TestServiceB serviceB3 = new(); + + // Interweave these to check that the slot \ ordering logic is correct. + // Each unique key + its service Type maintains their own slot in a AnyKey query. + builder.RegisterInstance(serviceA1).Keyed("key"); + builder.RegisterInstance(serviceB1).Keyed("key"); + builder.RegisterInstance(serviceA2).Keyed("key"); + builder.RegisterInstance(serviceB2).Keyed("key"); + builder.RegisterInstance(serviceA3).Keyed("key"); + builder.RegisterInstance(serviceB3).Keyed("key"); + + var provider = builder.Build(); + + TestServiceA[] allInstancesA = null; + TestServiceB[] allInstancesB = null; + + if (anyKeyQueryBeforeSingletonQueries) + { + DoAnyKeyQuery(); + } + + // We get back the last registered service for duplicates. + Assert.Same(serviceA3, provider.ResolveKeyed("key")); + Assert.Same(serviceB3, provider.ResolveKeyed("key")); + + if (!anyKeyQueryBeforeSingletonQueries) + { + DoAnyKeyQuery(); + } + + Assert.Equal( + new[] { serviceA1, serviceA2, serviceA3 }, + allInstancesA); + + Assert.Equal( + new[] { serviceB1, serviceB2, serviceB3 }, + allInstancesB); + + void DoAnyKeyQuery() + { + IEnumerable allA = provider.ResolveKeyed>(KeyedService.AnyKey); + IEnumerable allB = provider.ResolveKeyed>(KeyedService.AnyKey); + + // Verify caching returns the same items. + Assert.Equal(allA, provider.ResolveKeyed>(KeyedService.AnyKey)); + Assert.Equal(allB, provider.ResolveKeyed>(KeyedService.AnyKey)); + + allInstancesA = allA.ToArray(); + allInstancesB = allB.ToArray(); + } + } + + private class TestServiceA + { + } + + private class TestServiceB + { + } + + [Fact] + public void ResolveKeyedServicesAnyKeyOrdering() + { + var builder = new ContainerBuilder(); + var service1 = new Service(); + var service2 = new Service(); + var service3 = new Service(); + + builder.RegisterInstance(service1).Keyed("A-service"); + builder.RegisterInstance(service2).Keyed("B-service"); + builder.RegisterInstance(service3).Keyed("A-service"); + + var provider = builder.Build(); + + // The order should be in registration order, and not grouped by key for example. + // Although this isn't necessarily a requirement, it is the current behavior. + Assert.Equal( + new[] { service1, service2, service3 }, + provider.ResolveKeyed>(KeyedService.AnyKey)); + } + + [Fact] + public void ResolveKeyedGenericServices() + { + var service1 = new FakeService(); + var service2 = new FakeService(); + var service3 = new FakeService(); + var service4 = new FakeService(); + var builder = new ContainerBuilder(); + builder.RegisterInstance>(service1).Keyed>("first-service"); + builder.RegisterInstance>(service2).Keyed>("service"); + builder.RegisterInstance>(service3).Keyed>("service"); + builder.RegisterInstance>(service4).Keyed>("service"); + + var provider = builder.Build(); + + var firstSvc = provider.ResolveKeyed>>("first-service").ToList(); + Assert.Single(firstSvc); + Assert.Same(service1, firstSvc[0]); + + var services = provider.ResolveKeyed>>("service").ToList(); + Assert.Equal(new[] { service2, service3, service4 }, services); + } + + [Fact] + public void ResolveKeyedServiceSingletonInstance() + { + var service = new Service(); + var builder = new ContainerBuilder(); + builder.RegisterInstance(service).Keyed("service1"); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + Assert.Same(service, provider.ResolveKeyed("service1")); + Assert.Same(service, provider.ResolveKeyed("service1", typeof(IService))); + } + + [Fact] + public void ResolveKeyedServiceSingletonInstanceWithKeyInjection() + { + var serviceKey = "this-is-my-service"; + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed(serviceKey).SingleInstance(); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + var svc = provider.ResolveKeyed(serviceKey); + Assert.NotNull(svc); + Assert.Equal(serviceKey, svc.ToString()); + } + + [Fact] + public void ResolveKeyedServiceOpenGenericWithKeyInjection() + { + var serviceKey = "this-is-my-service"; + var builder = new ContainerBuilder(); + builder.RegisterGeneric(typeof(KeyAwareGenericService<>)).Keyed(serviceKey, typeof(KeyAwareGenericService<>)); + builder.RegisterType(); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve>()); + var svc = provider.ResolveKeyed>(serviceKey); + Assert.NotNull(svc); + Assert.Equal(serviceKey, svc.Key); + } + + [Fact] + public void ResolveKeyedServiceSingletonInstanceWithKeyPropertyInjection() + { + var serviceKey = "this-is-my-service"; + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed(serviceKey).PropertiesAutowired().SingleInstance(); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + var svc = provider.ResolveKeyed(serviceKey); + Assert.NotNull(svc); + Assert.Equal(serviceKey, svc.Key); + } + + [Fact] + public void ResolveKeyedServiceSingletonInstanceWithAnyKey() + { + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed(KeyedService.AnyKey).SingleInstance(); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + + var serviceKey1 = "some-key"; + var svc1 = provider.ResolveKeyed(serviceKey1); + Assert.NotNull(svc1); + Assert.Equal(serviceKey1, svc1.ToString()); + + var serviceKey2 = "some-other-key"; + var svc2 = provider.ResolveKeyed(serviceKey2); + Assert.NotNull(svc2); + Assert.Equal(serviceKey2, svc2.ToString()); + } + + [Fact] + public void ResolveKeyedServicesSingletonInstanceWithAnyKey() + { + var service1 = new FakeService(); + var service2 = new FakeService(); + + var builder = new ContainerBuilder(); + builder.RegisterInstance>(service1).Keyed>(KeyedService.AnyKey); + builder.RegisterInstance>(service2).Keyed>("some-key"); + + var provider = builder.Build(); + + var services = provider.ResolveKeyed>>("some-key").ToList(); + Assert.Equal(new[] { service2 }, services); + } + + [Fact] + public void ResolveKeyedServiceSingletonInstanceWithKeyedParameter() + { + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed("service1").SingleInstance(); + builder.RegisterType().Keyed("service2").SingleInstance(); + builder.RegisterType().SingleInstance().WithAttributeFiltering(); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + var svc = provider.Resolve(); + Assert.NotNull(svc); + Assert.Equal("service1", svc.Service1.ToString()); + Assert.Equal("service2", svc.Service2.ToString()); + } + + [Fact] + public void ResolveKeyedServiceWithKeyedParameter_MissingRegistration_SecondParameter() + { + var builder = new ContainerBuilder(); + + builder.RegisterType().Keyed("service1").SingleInstance(); + + // We are missing the registration for "service2" here and OtherService requires it. + builder.RegisterType().SingleInstance().WithAttributeFiltering(); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + Assert.Throws(() => provider.Resolve()); + } + + [Fact] + public void ResolveKeyedServiceWithKeyedParameter_MissingRegistration_FirstParameter() + { + var builder = new ContainerBuilder(); + + // We are not registering "service1" and "service2" keyed IService services and OtherService requires them. + builder.RegisterType().SingleInstance().WithAttributeFiltering(); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + Assert.Throws(() => provider.Resolve()); + } + + [Fact] + public void ResolveKeyedServiceWithKeyedParameter_MissingRegistrationButWithDefaults() + { + var builder = new ContainerBuilder(); + + // We are not registering "service1" and "service2" keyed IService services and OtherServiceWithDefaultCtorArgs + // specifies them but has argument defaults if missing. + builder.RegisterType().SingleInstance().WithAttributeFiltering(); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + Assert.NotNull(provider.Resolve()); + } + + [Fact] + public void ResolveKeyedServiceSingletonFactory() + { + var service = new Service(); + var builder = new ContainerBuilder(); + builder.Register(ctx => service).Keyed("service1").SingleInstance(); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + Assert.Same(service, provider.ResolveKeyed("service1")); + Assert.Same(service, provider.ResolveKeyed("service1", typeof(IService))); + } + + [Fact] + public void ResolveKeyedServiceSingletonFactoryWithAnyKey() + { + var builder = new ContainerBuilder(); + builder + .Register((ctx, p) => + { + var key = p.KeyedServiceKey(); + return new Service(key); + }) + .Keyed(KeyedService.AnyKey) + .SingleInstance(); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + + for (int i = 0; i < 3; i++) + { + var key = "service" + i; + var s1 = provider.ResolveKeyed(key); + var s2 = provider.ResolveKeyed(key); + Assert.Same(s1, s2); + Assert.Equal(key, s1.ToString()); + } + } + + [Fact] + public void ResolveKeyedServiceSingletonType() + { + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed("service1").SingleInstance(); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + Assert.Equal(typeof(Service), provider.ResolveKeyed("service1").GetType()); + } + + [Fact] + public void ResolveKeyedServiceTransientFactory() + { + var builder = new ContainerBuilder(); + builder + .Register((ctx, p) => + { + var key = p.KeyedServiceKey(); + return new Service(key); + }) + .Keyed("service1"); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + var first = provider.ResolveKeyed("service1"); + var second = provider.ResolveKeyed("service1"); + Assert.NotSame(first, second); + Assert.Equal("service1", first.ToString()); + Assert.Equal("service1", second.ToString()); + } + + [Fact] + public void ResolveKeyedServiceTransientType() + { + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed("service1"); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + var first = provider.ResolveKeyed("service1"); + var second = provider.ResolveKeyed("service1"); + Assert.NotSame(first, second); + } + + [Fact] + public void ResolveKeyedServiceTransientTypeWithAnyKey() + { + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed(KeyedService.AnyKey); + + var provider = builder.Build(); + + Assert.Throws(() => provider.Resolve()); + var first = provider.ResolveKeyed("service1"); + var second = provider.ResolveKeyed("service1"); + Assert.NotSame(first, second); + } + + [Fact] + public void ResolveKeyedSingletonFromScopeServiceProvider() + { + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed("key").SingleInstance(); + + var provider = builder.Build(); + var scopeA = provider.BeginLifetimeScope(); + var scopeB = provider.BeginLifetimeScope(); + + Assert.Throws(() => scopeA.Resolve()); + Assert.Throws(() => scopeB.Resolve()); + + Assert.Throws(() => scopeA.ResolveKeyed(KeyedService.AnyKey)); + Assert.Throws(() => scopeB.ResolveKeyed(KeyedService.AnyKey)); + + var serviceA1 = scopeA.ResolveKeyed("key"); + var serviceA2 = scopeA.ResolveKeyed("key"); + + var serviceB1 = scopeB.ResolveKeyed("key"); + var serviceB2 = scopeB.ResolveKeyed("key"); + + Assert.Same(serviceA1, serviceA2); + Assert.Same(serviceB1, serviceB2); + Assert.Same(serviceA1, serviceB1); + } + + [Fact] + public void ResolveKeyedScopedFromScopeServiceProvider() + { + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed("key").InstancePerLifetimeScope(); + + var provider = builder.Build(); + var scopeA = provider.BeginLifetimeScope(); + var scopeB = provider.BeginLifetimeScope(); + + Assert.Throws(() => scopeA.Resolve()); + Assert.Throws(() => scopeB.Resolve()); + + Assert.Throws(() => scopeA.ResolveKeyed(KeyedService.AnyKey)); + Assert.Throws(() => scopeB.ResolveKeyed(KeyedService.AnyKey)); + + var serviceA1 = scopeA.ResolveKeyed("key"); + var serviceA2 = scopeA.ResolveKeyed("key"); + + var serviceB1 = scopeB.ResolveKeyed("key"); + var serviceB2 = scopeB.ResolveKeyed("key"); + + Assert.Same(serviceA1, serviceA2); + Assert.Same(serviceB1, serviceB2); + Assert.NotSame(serviceA1, serviceB1); + } + + [Fact] + public void ResolveKeyedTransientFromScopeServiceProvider() + { + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed("key"); + + var provider = builder.Build(); + var scopeA = provider.BeginLifetimeScope(); + var scopeB = provider.BeginLifetimeScope(); + + Assert.Throws(() => scopeA.Resolve()); + Assert.Throws(() => scopeB.Resolve()); + + var serviceA1 = scopeA.ResolveKeyed("key"); + var serviceA2 = scopeA.ResolveKeyed("key"); + + var serviceB1 = scopeB.ResolveKeyed("key"); + var serviceB2 = scopeB.ResolveKeyed("key"); + + Assert.NotSame(serviceA1, serviceA2); + Assert.NotSame(serviceB1, serviceB2); + Assert.NotSame(serviceA1, serviceB1); + } + + [Fact] + public void ResolveRequiredKeyedServiceThrowsIfNotFound() + { + var builder = new ContainerBuilder(); + var provider = builder.Build(); + var serviceKey = new object(); + + ComponentNotRegisteredException e; + + e = Assert.Throws(() => provider.ResolveKeyed(serviceKey)); + VerifyException(); + + e = Assert.Throws(() => provider.ResolveKeyed(serviceKey, typeof(IService))); + VerifyException(); + + void VerifyException() + { + Assert.Contains(nameof(IService), e.Message, StringComparison.Ordinal); + Assert.Contains(serviceKey.GetType().FullName, e.Message, StringComparison.Ordinal); + } + } + + private interface IService + { + } + + private class Service : IService + { + private readonly string _id; + + public Service() => _id = Guid.NewGuid().ToString(); + + public Service([ServiceKey] string id) => _id = id; + + public override string ToString() => _id; + } + + private class OtherService + { + public OtherService( + [KeyFilter("service1")] IService service1, + [KeyFilter("service2")] IService service2) + { + Service1 = service1; + Service2 = service2; + } + + public IService Service1 { get; } + + public IService Service2 { get; } + } + + private class OtherServiceWithDefaultCtorArgs + { + public OtherServiceWithDefaultCtorArgs( + [KeyFilter("service1")] IService service1 = null, + [KeyFilter("service2")] IService service2 = null) + { + Service1 = service1; + Service2 = service2; + } + + public IService Service1 { get; } + + public IService Service2 { get; } + } + + [Fact] + public void SimpleServiceKeyedResolution() + { + // Arrange + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed("simple"); + builder.RegisterType().Keyed("another"); + builder.RegisterType(); + var provider = builder.Build(); + var sut = provider.Resolve(); + + // Act + var result = sut.GetService("simple"); + + // Assert + Assert.True(result.GetType() == typeof(SimpleService)); + } + + private class KeyAwarePropertyService + { + [ServiceKey] + public object Key { get; set; } = default!; + } + + private class KeyAwareGenericService + { + public KeyAwareGenericService([ServiceKey] string key, T value) + { + Key = key; + Value = value; + } + + public string Key { get; set; } = default!; + + public T Value { get; set; } = default!; + } + + private class SimpleParentWithDynamicKeyedService + { + private readonly ILifetimeScope _lifetimeScope; + + public SimpleParentWithDynamicKeyedService(ILifetimeScope lifetimeScope) + { + _lifetimeScope = lifetimeScope; + } + + public ISimpleService GetService(string name) => _lifetimeScope.ResolveKeyed(name); + } + + private interface ISimpleService + { + } + + private class SimpleService : ISimpleService + { + } + + private class AnotherSimpleService : ISimpleService + { + } + + private class FakeService : IFakeSingletonService, IFakeOpenGenericService + { + public PocoClass Value { get; set; } + } + + private interface IFakeSingletonService + { + } + + private interface IFakeOpenGenericService + { + TValue Value { get; } + } + + private class PocoClass + { + } + + private class FakeOpenGenericService : IFakeOpenGenericService + { + public FakeOpenGenericService(TVal value) + { + Value = value; + } + + public TVal Value { get; } + } +} diff --git a/test/Autofac.Specification.Test/Features/PropertyInjectionTests.cs b/test/Autofac.Specification.Test/Features/PropertyInjectionTests.cs index f8e544294..5191cfb99 100644 --- a/test/Autofac.Specification.Test/Features/PropertyInjectionTests.cs +++ b/test/Autofac.Specification.Test/Features/PropertyInjectionTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics.CodeAnalysis; using System.Reflection; using Autofac.Core; using Autofac.Specification.Test.Features.PropertyInjection; diff --git a/test/Autofac.Specification.Test/Features/RequiredPropertyTests.cs b/test/Autofac.Specification.Test/Features/RequiredPropertyTests.cs index 5f5bc70f9..5248c3fc0 100644 --- a/test/Autofac.Specification.Test/Features/RequiredPropertyTests.cs +++ b/test/Autofac.Specification.Test/Features/RequiredPropertyTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics.CodeAnalysis; using Autofac.Core; namespace Autofac.Specification.Test.Features; diff --git a/test/Autofac.Specification.Test/LoadContextScopeTests.cs b/test/Autofac.Specification.Test/LoadContextScopeTests.cs index ca81f36d6..0f05a9bfa 100644 --- a/test/Autofac.Specification.Test/LoadContextScopeTests.cs +++ b/test/Autofac.Specification.Test/LoadContextScopeTests.cs @@ -1,11 +1,7 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Collections; -using System.Diagnostics.SymbolStore; -using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.Loader; using Autofac.Core; using Autofac.Features.ResolveAnything; diff --git a/test/Autofac.Specification.Test/Resolution/ConstructorFinderTests.cs b/test/Autofac.Specification.Test/Resolution/ConstructorFinderTests.cs index 697369427..14eccfd3d 100644 --- a/test/Autofac.Specification.Test/Resolution/ConstructorFinderTests.cs +++ b/test/Autofac.Specification.Test/Resolution/ConstructorFinderTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Reflection; -using Autofac.Core; using Autofac.Core.Activators.Reflection; namespace Autofac.Specification.Test.Resolution; diff --git a/test/Autofac.Test/ActivatorPipelineExtensions.cs b/test/Autofac.Test/ActivatorPipelineExtensions.cs index 674469103..8ae069161 100644 --- a/test/Autofac.Test/ActivatorPipelineExtensions.cs +++ b/test/Autofac.Test/ActivatorPipelineExtensions.cs @@ -1,12 +1,10 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics.CodeAnalysis; using Autofac.Core; using Autofac.Core.Lifetime; using Autofac.Core.Resolving; using Autofac.Core.Resolving.Pipeline; -using Autofac.Util.Cache; namespace Autofac.Test; diff --git a/test/Autofac.Test/Core/DependencyResolutionExceptionTests.cs b/test/Autofac.Test/Core/DependencyResolutionExceptionTests.cs index f30e01be8..16e54e0e2 100644 --- a/test/Autofac.Test/Core/DependencyResolutionExceptionTests.cs +++ b/test/Autofac.Test/Core/DependencyResolutionExceptionTests.cs @@ -1,9 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; -using System.Security.Permissions; using Autofac.Core; namespace Autofac.Test.Core; diff --git a/test/Autofac.Test/Core/KeyedServiceKeyParameterTests.cs b/test/Autofac.Test/Core/KeyedServiceKeyParameterTests.cs new file mode 100644 index 000000000..f0a91efed --- /dev/null +++ b/test/Autofac.Test/Core/KeyedServiceKeyParameterTests.cs @@ -0,0 +1,84 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Test.Core; + +public class KeyedServiceKeyParameterTests +{ + [Fact] + public void Ctor_NullServiceKey() + { + Assert.Throws(() => new KeyedServiceKeyParameter(null!)); + } + + [Fact] + public void ServiceKey_ReturnsCtorValue() + { + var parameter = new KeyedServiceKeyParameter("expected"); + + Assert.Equal("expected", parameter.ServiceKey); + } + + [Fact] + public void CanSupplyValue_NullParameter() + { + var parameter = new KeyedServiceKeyParameter("key"); + + Assert.Throws( + () => parameter.CanSupplyValue(null!, new ContainerBuilder().Build(), out _)); + } + + [Fact] + public void CanSupplyValue_NullContext() + { + var parameterInfo = typeof(NeedsConstructorKey).GetConstructors().Single().GetParameters().Single(); + var parameter = new KeyedServiceKeyParameter("key"); + + Assert.Throws( + () => parameter.CanSupplyValue(parameterInfo, null!, out _)); + } + + [Fact] + public void CanSupplyValue_AttributeMissing() + { + var parameterInfo = typeof(PlainService).GetConstructors().Single().GetParameters().Single(); + var parameter = new KeyedServiceKeyParameter("key"); + using var context = new ContainerBuilder().Build(); + + var result = parameter.CanSupplyValue(parameterInfo, context, out var valueProvider); + + Assert.False(result); + Assert.Null(valueProvider); + } + + [Fact] + public void CanSupplyValue_AttributePresentReturnsValue() + { + var parameterInfo = typeof(NeedsConstructorKey).GetConstructors().Single().GetParameters().Single(); + var parameter = new KeyedServiceKeyParameter("expected"); + using var context = new ContainerBuilder().Build(); + + var result = parameter.CanSupplyValue(parameterInfo, context, out var valueProvider); + + Assert.True(result); + Assert.NotNull(valueProvider); + Assert.Equal("expected", valueProvider()); + } + + private sealed class PlainService + { + public PlainService(object value) + { + } + } + + private sealed class NeedsConstructorKey + { + public NeedsConstructorKey([ServiceKey] object key) + { + Key = key; + } + + public object Key { get; } + } +} diff --git a/test/Autofac.Test/Core/KeyedServiceParameterInjectorTests.cs b/test/Autofac.Test/Core/KeyedServiceParameterInjectorTests.cs new file mode 100644 index 000000000..e23fb1d92 --- /dev/null +++ b/test/Autofac.Test/Core/KeyedServiceParameterInjectorTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Builder; +using Autofac.Core; + +namespace Autofac.Test.Core; + +public class KeyedServiceParameterInjectorTests +{ + [Fact] + public void AddKeyedServiceParameter_ServiceOverload_NullService() + { + Assert.Throws( + () => KeyedServiceParameterInjector.AddKeyedServiceParameter( + service: null!, + parameters: ResolveRequest.NoParameters)); + } + + [Fact] + public void AddKeyedServiceParameter_ServiceOverload_NullNonKeyedServiceParameters() + { + Assert.Throws( + () => KeyedServiceParameterInjector.AddKeyedServiceParameter( + service: new TypedService(typeof(object)), + parameters: null!)); + } + + [Fact] + public void AddKeyedServiceParameter_ServiceRegistrationOverload_NullService() + { + Assert.Throws( + () => KeyedServiceParameterInjector.AddKeyedServiceParameter( + service: null!, + parameters: ResolveRequest.NoParameters, + registration: null)); + } + + [Fact] + public void AddKeyedServiceParameter_ServiceRegistrationOverload_NullParameters() + { + Assert.Throws( + () => KeyedServiceParameterInjector.AddKeyedServiceParameter( + service: new KeyedService("key", typeof(object)), + parameters: null!, + registration: null)); + } + + [Fact] + public void AddKeyedServiceParameter_ServiceKeyOverload_NullServiceKey() + { + Assert.Throws( + () => KeyedServiceParameterInjector.AddKeyedServiceParameter( + serviceKey: null!, + parameters: ResolveRequest.NoParameters)); + } + + [Fact] + public void AddKeyedServiceParameter_ServiceKeyOverload_NullParameters() + { + Assert.Throws( + () => KeyedServiceParameterInjector.AddKeyedServiceParameter( + serviceKey: "key", + parameters: null!)); + } + + [Fact] + public void AddKeyedServiceParameter_SkipsWhenServiceIsAnyKey() + { + var parameters = KeyedServiceParameterInjector.AddKeyedServiceParameter( + new KeyedService(KeyedService.AnyKey, typeof(object)), + ResolveRequest.NoParameters); + + Assert.Same(ResolveRequest.NoParameters, parameters); + } + + [Fact] + public void AddKeyedServiceParameter_SkipsWhenActivatorDoesNotNeedKey() + { + using var registration = CreateRegistration(); + var parameters = KeyedServiceParameterInjector.AddKeyedServiceParameter( + new KeyedService("key", typeof(PlainService)), + ResolveRequest.NoParameters, + registration); + + Assert.Same(ResolveRequest.NoParameters, parameters); + } + + [Fact] + public void AddKeyedServiceParameter_AppendsWhenActivatorNeedsKey() + { + using var registration = CreateRegistration(); + var parameters = KeyedServiceParameterInjector.AddKeyedServiceParameter( + new KeyedService("key", typeof(NeedsConstructorKey)), + ResolveRequest.NoParameters, + registration).ToArray(); + + var keyParameter = Assert.Single(parameters); + Assert.IsType(keyParameter); + Assert.Equal("key", ((KeyedServiceKeyParameter)keyParameter).ServiceKey); + } + + private static IComponentRegistration CreateRegistration() + => RegistrationBuilder + .ForType() + .CreateRegistration(); + + private sealed class PlainService + { + } + + private sealed class NeedsConstructorKey + { + public NeedsConstructorKey([ServiceKey] object key) + { + Key = key; + } + + public object Key { get; } + } +} diff --git a/test/Autofac.Test/Core/KeyedServiceTests.cs b/test/Autofac.Test/Core/KeyedServiceTests.cs index 4972c77e3..8cf54a535 100644 --- a/test/Autofac.Test/Core/KeyedServiceTests.cs +++ b/test/Autofac.Test/Core/KeyedServiceTests.cs @@ -8,7 +8,7 @@ namespace Autofac.Test.Core; public class KeyedServiceTests { [Fact] - public void KeyedServicesForTheSameName_AreEqual() + public void Equals_SameKeyAndType() { var key = new object(); var type = typeof(object); @@ -16,19 +16,19 @@ public void KeyedServicesForTheSameName_AreEqual() } [Fact] - public void ConstructorRequires_KeyNotNull() + public void Ctor_NullKey() { Assert.Throws(() => new KeyedService(null, typeof(object))); } [Fact] - public void ConstructorRequires_TypeNotNull() + public void Ctor_NullType() { Assert.Throws(() => new KeyedService("name", null)); } [Fact] - public void KeyedServicesForDifferentKeys_AreNotEqual() + public void Equals_DifferentKeys() { var key1 = new object(); var key2 = new object(); @@ -38,7 +38,7 @@ public void KeyedServicesForDifferentKeys_AreNotEqual() } [Fact] - public void KeyedServicesForDifferentTypes_AreNotEqual() + public void Equals_DifferentTypes() { var key = new object(); @@ -47,19 +47,19 @@ public void KeyedServicesForDifferentTypes_AreNotEqual() } [Fact] - public void KeyedServices_AreNotEqualToOtherServiceTypes() + public void Equals_DifferentServiceType() { Assert.False(new KeyedService(new object(), typeof(object)).Equals(new TypedService(typeof(object)))); } [Fact] - public void AKeyedService_IsNotEqualToNull() + public void Equals_Null() { Assert.False(new KeyedService(new object(), typeof(object)).Equals(null)); } [Fact] - public void ChangeType_ProvidesKeyedServiceWithNewTypeAndSameKey() + public void ChangeType_KeepsKeyAndAppliesNewType() { var newType = typeof(string); var key = new object(); @@ -67,4 +67,22 @@ public void ChangeType_ProvidesKeyedServiceWithNewTypeAndSameKey() var changedService = service.ChangeType(newType); Assert.Equal(new KeyedService(key, newType), changedService); } + + [Fact] + public void IsAnyKey_Sentinel() + { + Assert.True(KeyedService.IsAnyKey(KeyedService.AnyKey)); + } + + [Fact] + public void IsAnyKey_NonSentinel() + { + Assert.False(KeyedService.IsAnyKey(new object())); + } + + [Fact] + public void IsAnyKey_NullValue() + { + Assert.Throws(() => KeyedService.IsAnyKey(null!)); + } } diff --git a/test/Autofac.Test/Core/ParameterExtensionsTests.cs b/test/Autofac.Test/Core/ParameterExtensionsTests.cs new file mode 100644 index 000000000..384a51737 --- /dev/null +++ b/test/Autofac.Test/Core/ParameterExtensionsTests.cs @@ -0,0 +1,25 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Core; + +namespace Autofac.Test.Core; + +public class ParameterExtensionsTests +{ + [Fact] + public void KeyedServiceKey_ReturnsValue() + { + var result = new Parameter[] { new KeyedServiceKeyParameter("expected") } + .KeyedServiceKey(); + + Assert.Equal("expected", result); + } + + [Fact] + public void KeyedServiceKey_NotFound() + { + Assert.Throws( + () => Array.Empty().KeyedServiceKey()); + } +} diff --git a/test/Autofac.Test/Core/ServiceKeyAttributeCacheTests.cs b/test/Autofac.Test/Core/ServiceKeyAttributeCacheTests.cs new file mode 100644 index 000000000..525fec9fa --- /dev/null +++ b/test/Autofac.Test/Core/ServiceKeyAttributeCacheTests.cs @@ -0,0 +1,56 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Core; + +namespace Autofac.Test.Core; + +public class ServiceKeyAttributeCacheTests +{ + [Fact] + public void ParameterHasServiceKey_FindsConstructorParameter() + { + var constructor = typeof(NeedsConstructorKey).GetConstructors().Single(); + var parameter = constructor.GetParameters().Single(); + + Assert.True(ServiceKeyAttributeCache.ParameterHasServiceKey(parameter)); + } + + [Fact] + public void ParameterHasServiceKey_NullParameter() + { + Assert.Throws(() => ServiceKeyAttributeCache.ParameterHasServiceKey(null!)); + } + + [Fact] + public void PropertyHasServiceKey_FindsProperty() + { + var property = typeof(NeedsPropertyKey).GetProperty(nameof(NeedsPropertyKey.Dependency))!; + var setterParameter = property.SetMethod!.GetParameters().Single(); + + Assert.True(ServiceKeyAttributeCache.PropertyHasServiceKey(property)); + Assert.True(ServiceKeyAttributeCache.ParameterHasServiceKey(setterParameter)); + } + + [Fact] + public void PropertyHasServiceKey_NullProperty() + { + Assert.Throws(() => ServiceKeyAttributeCache.PropertyHasServiceKey(null!)); + } + + private sealed class NeedsConstructorKey + { + public NeedsConstructorKey([ServiceKey] object key) + { + Key = key; + } + + public object Key { get; } + } + + private sealed class NeedsPropertyKey + { + [ServiceKey] + public object Dependency { get; set; } = default!; + } +} diff --git a/test/Autofac.Test/Features/KeyedServices/AnyKeyRegistrationSourceTests.cs b/test/Autofac.Test/Features/KeyedServices/AnyKeyRegistrationSourceTests.cs new file mode 100644 index 000000000..662041bb0 --- /dev/null +++ b/test/Autofac.Test/Features/KeyedServices/AnyKeyRegistrationSourceTests.cs @@ -0,0 +1,144 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Builder; +using Autofac.Core; +using Autofac.Core.Resolving.Pipeline; +using Autofac.Features.KeyedServices; + +namespace Autofac.Test.Features.KeyedServices; + +public class AnyKeyRegistrationSourceTests +{ + private readonly AnyKeyRegistrationSource _source = new(); + + [Fact] + public void RegistrationsFor_NullService() + { + Assert.Throws( + () => _source.RegistrationsFor( + service: null!, + registrationAccessor: _ => Enumerable.Empty())); + } + + [Fact] + public void RegistrationsFor_NullRegistrationAccessor() + { + var service = new KeyedService("key", typeof(object)); + + Assert.Throws( + () => _source.RegistrationsFor(service, null!)); + } + + [Fact] + public void RegistrationsFor_NonKeyedService() + { + var registrations = _source.RegistrationsFor( + new TypedService(typeof(object)), + _ => Enumerable.Empty()); + + Assert.Empty(registrations); + } + + [Fact] + public void RegistrationsFor_ServiceKeyIsAnyKey() + { + var registrations = _source.RegistrationsFor( + new KeyedService(KeyedService.AnyKey, typeof(object)), + _ => Enumerable.Empty()); + + Assert.Empty(registrations); + } + + [Fact] + public void RegistrationsFor_ServiceTypeIsCollection() + { + var registrations = _source.RegistrationsFor( + new KeyedService("key", typeof(IEnumerable)), + _ => Enumerable.Empty()); + + Assert.Empty(registrations); + } + + [Fact] + public void RegistrationsFor_ServiceHasSpecificRegistration() + { + var service = new KeyedService("key", typeof(DummyService)); + var anyKeyService = new KeyedService(KeyedService.AnyKey, typeof(DummyService)); + + using var registration = CreateComponentRegistration(); + var serviceRegistration = CreateServiceRegistration(registration); + + var registrations = _source.RegistrationsFor( + service, + requested => + { + if (requested.Equals(service)) + { + return new[] { serviceRegistration }; + } + + return Array.Empty(); + }); + + Assert.Empty(registrations); + } + + [Fact] + public void RegistrationsFor_NoAnyKeyRegistrations() + { + var service = new KeyedService("key", typeof(DummyService)); + var anyKeyService = new KeyedService(KeyedService.AnyKey, typeof(DummyService)); + + var registrations = _source.RegistrationsFor( + service, + requested => Enumerable.Empty()); + + Assert.Empty(registrations); + } + + [Fact] + public void RegistrationsFor_AnyKeyRegistrationCreatesAdapter() + { + var service = new KeyedService("key", typeof(DummyService)); + var anyKeyService = new KeyedService(KeyedService.AnyKey, typeof(DummyService)); + + using var registration = CreateComponentRegistration(); + var serviceRegistration = CreateServiceRegistration(registration); + + var registrations = _source.RegistrationsFor( + service, + requested => + { + if (requested.Equals(anyKeyService)) + { + return new[] { serviceRegistration }; + } + + return Enumerable.Empty(); + }).ToArray(); + + var adapter = Assert.Single(registrations); + Assert.Contains(service, adapter.Services); + Assert.True( + adapter.Metadata.TryGetValue(MetadataKeys.AnyKeyAdapter, out var marker) && + marker is true); + Assert.Same(registration, adapter.Target); + } + + private static IComponentRegistration CreateComponentRegistration() + { + return RegistrationBuilder + .ForType() + .CreateRegistration(); + } + + private static ServiceRegistration CreateServiceRegistration(IComponentRegistration registration) + { + return new ServiceRegistration(ServicePipelines.DefaultServicePipeline, registration); + } + + private sealed class DummyService + { + } +} diff --git a/test/Autofac.Test/Features/LightweightAdapters/LightweightAdapterRegistrationExtensionsTests.cs b/test/Autofac.Test/Features/LightweightAdapters/LightweightAdapterRegistrationExtensionsTests.cs index 252a6724e..ea756ee7e 100644 --- a/test/Autofac.Test/Features/LightweightAdapters/LightweightAdapterRegistrationExtensionsTests.cs +++ b/test/Autofac.Test/Features/LightweightAdapters/LightweightAdapterRegistrationExtensionsTests.cs @@ -129,11 +129,11 @@ public void ParametersGoToTheDecoratedInstance() { var resolved = _container.Resolve(TypedParameter.From(new Implementer1())); var dec2 = Assert.IsType(resolved); - Assert.Empty(dec2.Parameters); + Assert.Empty(dec2.Parameters.OfType()); var dec1 = Assert.IsType(dec2.Implementer); - Assert.Empty(dec1.Parameters); + Assert.Empty(dec1.Parameters.OfType()); var imp = Assert.IsType(dec1.Implementer); - Assert.Single(imp.Parameters); + Assert.Single(imp.Parameters.OfType()); } public interface IParameterizedService diff --git a/test/Autofac.Test/ResolutionExtensionsTests.cs b/test/Autofac.Test/ResolutionExtensionsTests.cs index b94621aff..23b3e1562 100644 --- a/test/Autofac.Test/ResolutionExtensionsTests.cs +++ b/test/Autofac.Test/ResolutionExtensionsTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Collections.Generic; +using System.Linq; using Autofac.Core; using Autofac.Core.Activators.ProvidedInstance; using Autofac.Core.Registration; @@ -171,4 +173,53 @@ public void WhenServiceIsNotRegistered_TryResolveKeyedReturnsFalse() Assert.False(container.TryResolveKeyed("name", out var o)); Assert.Null(o); } + + [Fact] + public void ResolveKeyed_AnyKeyNonCollection() + { + using var container = Factory.CreateEmptyContainer(); + + Assert.Throws( + () => container.ResolveKeyed(KeyedService.AnyKey)); + } + + [Fact] + public void ResolveKeyed_AnyKeyEnumerable() + { + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed("first"); + builder.RegisterType().Keyed("second"); + using var container = builder.Build(); + + var services = container.ResolveKeyed>(KeyedService.AnyKey).ToList(); + + Assert.Equal(2, services.Count); + } + + [Fact] + public void TryResolveKeyed_AnyKeyNonCollection() + { + using var container = Factory.CreateEmptyContainer(); + + Assert.Throws( + () => container.TryResolveKeyed(KeyedService.AnyKey, typeof(object), out _)); + } + + [Fact] + public void TryResolveKeyed_AnyKeyEnumerable() + { + var builder = new ContainerBuilder(); + builder.RegisterType().Keyed("only"); + using var container = builder.Build(); + + var result = container.TryResolveKeyed>(KeyedService.AnyKey, out var services); + + Assert.True(result); + Assert.NotNull(services); + Assert.Single(services!); + } + + private sealed class AnyKeyService + { + } } diff --git a/test/Autofac.Test/Util/Cache/ReflectionCacheParameterDictionaryTests.cs b/test/Autofac.Test/Util/Cache/ReflectionCacheParameterDictionaryTests.cs new file mode 100644 index 000000000..50bd4ed13 --- /dev/null +++ b/test/Autofac.Test/Util/Cache/ReflectionCacheParameterDictionaryTests.cs @@ -0,0 +1,76 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Reflection; +using Autofac.Core; +using Autofac.Util.Cache; + +namespace Autofac.Test.Util.Cache; + +public class ReflectionCacheParameterDictionaryTests +{ + private static readonly ParameterInfo SampleParameter = typeof(ParameterOwner) + .GetMethod(nameof(ParameterOwner.Method), BindingFlags.Public | BindingFlags.Static)! + .GetParameters() + .Single(); + + [Fact] + public void Usage_DefaultsToAll() + { + var cache = new ReflectionCacheParameterDictionary(); + + Assert.Equal(ReflectionCacheUsage.All, cache.Usage); + } + + [Fact] + public void Usage_CanBeUpdated() + { + var cache = new ReflectionCacheParameterDictionary + { + Usage = ReflectionCacheUsage.None, + }; + + Assert.Equal(ReflectionCacheUsage.None, cache.Usage); + } + + [Fact] + public void Clear_NullPredicate() + { + var cache = new ReflectionCacheParameterDictionary(); + + Assert.Throws(() => cache.Clear(null!)); + } + + [Fact] + public void Clear_PredicateDoesNotMatch() + { + var cache = new ReflectionCacheParameterDictionary + { + [SampleParameter] = true, + }; + + cache.Clear((_, _) => false); + + Assert.True(cache.ContainsKey(SampleParameter)); + } + + [Fact] + public void Clear_PredicateMatchesMember() + { + var cache = new ReflectionCacheParameterDictionary + { + [SampleParameter] = true, + }; + + cache.Clear((member, _) => member == SampleParameter.Member); + + Assert.False(cache.ContainsKey(SampleParameter)); + } + + private static class ParameterOwner + { + public static void Method(object value) + { + } + } +}