sources/Google.Solutions.IapDesktop.Core/ObjectModel/ServiceRegistry.cs (339 lines of code) (raw):

// // Copyright 2020 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Solutions.Common.Linq; using Google.Solutions.Common.Runtime; using Google.Solutions.Common.Util; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Threading; namespace Google.Solutions.IapDesktop.Core.ObjectModel { /// <summary> /// Registry that allows lookup and creation of object by type, similar to a /// "Kernel" in some IoC frameworks. /// /// ServiceRegistries can be nested. In this case, a registry first queries its /// own services before delegating to the parent registry. /// /// Service registration is not thread-safe and expected to happen during /// startup. Once all services have been registered, service lookup is /// thread-safe. /// </summary> public class ServiceRegistry : IServiceCategoryProvider, IServiceProvider { private readonly ServiceRegistry? parent; private readonly IDictionary<Type, SingletonStub> singletons = new Dictionary<Type, SingletonStub>(); private readonly IDictionary<Type, Func<object>> transients = new Dictionary<Type, Func<object>>(); // Categories map a category type to a list of service types. They can be used if there // are multiple implementations for a common function. private readonly IDictionary<Type, IList<Type>> categories = new Dictionary<Type, IList<Type>>(); public ServiceRegistry() { this.parent = null; } public ServiceRegistry(ServiceRegistry parent) { this.parent = parent; } internal ServiceRegistry RootRegistry => this.parent != null ? this.parent.RootRegistry : this; //--------------------------------------------------------------------- // Service registration and instantiation. //--------------------------------------------------------------------- private object CreateInstance(Type serviceType) { // // Check for ctor(IServiceProvider). // var constructorWithServiceProvider = serviceType.GetConstructor( BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(IServiceProvider) }, null); if (constructorWithServiceProvider != null) { return Activator.CreateInstance(serviceType, (IServiceProvider)this); } // // Check for ctor(IServiceCategoryProvider). // var constructorWithServiceCategoryProvider = serviceType.GetConstructor( BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(IServiceCategoryProvider) }, null); if (constructorWithServiceCategoryProvider != null) { return Activator.CreateInstance(serviceType, (IServiceCategoryProvider)this); } // // Check other constructors and see if there is one for which all // parameters can be bound to a service. Analyze the one with the most // parameters first. // bool IsSupportedConstructorArgumentType(Type t) { if (t == serviceType) { // Don't allow recursion return false; } if (IsServiceRegistered(t)) { return true; } else if (IsDecorator(t) && t.GetGenericArguments().All(IsServiceRegistered)) { return true; } else { return false; } } foreach (var constructor in serviceType.GetConstructors() .OrderByDescending(c => c.GetParameters().Length)) { if (constructor .GetParameters() .Select(p => p.ParameterType) .All(t => IsSupportedConstructorArgumentType(t))) { // We have services for all parameters. var parameterValues = constructor .GetParameters() .Select(p => GetService(p.ParameterType)) .ToArray(); return Activator.CreateInstance( serviceType, parameterValues); } else { #if DEBUG Debug.WriteLine($"ServiceRegistry: {serviceType} has an unsuitable ctor: {constructor}"); foreach (var t in constructor .GetParameters() .Select(p => p.ParameterType) .Where(t => !IsSupportedConstructorArgumentType(t)) .ToList()) { Debug.WriteLine($" > Unsupported argument type: {t}"); } #endif // // There is at least one parameter that we do not // have a suitable service for. // } } throw new UnknownServiceException( $"Class {serviceType.Name} lacks a suitable constructor to serve as service"); } private TService CreateInstance<TService>() where TService : class { return (TService)CreateInstance(typeof(TService)); } public IDictionary<Type, ServiceLifetime> Registrations { get { var registrations = this.parent != null ? this.parent.Registrations : new Dictionary<Type, ServiceLifetime>(); foreach (var singleton in this.singletons) { registrations[singleton.Key] = ServiceLifetime.Singleton; } foreach (var transient in this.transients) { registrations[transient.Key] = ServiceLifetime.Transient; } return registrations; } } //--------------------------------------------------------------------- // Singleton registration. //--------------------------------------------------------------------- private void AddSingleton(Type singletonType, SingletonStub stub) { this.singletons[singletonType] = stub; } public void AddSingleton<TService>(TService singleton) where TService : class { singleton.ExpectNotNull(nameof(singleton)); AddSingleton(typeof(TService), new SingletonStub(singleton)); } public void AddSingleton<TService, TServiceClass>() where TService : class where TServiceClass : class { AddSingleton(typeof(TService), new SingletonStub(() => CreateInstance<TServiceClass>())); } public void AddSingleton<TService>() where TService : class { AddSingleton<TService, TService>(); } //--------------------------------------------------------------------- // Transient registration. //--------------------------------------------------------------------- private void AddTransient(Type serviceType, Type implementationType) { this.transients[serviceType] = () => CreateInstance(implementationType); } public void AddTransient<TService>() { AddTransient(typeof(TService), typeof(TService)); } public void AddTransient<TService, TServiceClass>() { AddTransient(typeof(TService), typeof(TServiceClass)); } //--------------------------------------------------------------------- // Lookup. //--------------------------------------------------------------------- private bool IsServiceRegistered(Type serviceType) { return this.singletons.ContainsKey(serviceType) || this.transients.ContainsKey(serviceType) || (this.parent != null && this.parent.IsServiceRegistered(serviceType)); } /// <summary> /// Check if the type is derived from IActivator<>. /// </summary> private static bool IsDecorator(Type serviceType) { return serviceType.IsGenericType && (serviceType.GetGenericTypeDefinition() == typeof(IActivator<>) || serviceType.GetInterfaces().EnsureNotNull().Any(IsDecorator)); } public object GetService(Type serviceType) { if (IsDecorator(serviceType)) { // // This is a decorator. // // Verify that the services referenced by type arguments exist // so that we don't get any surprises later. // foreach (var referencedType in serviceType.GetGenericArguments()) { if (!IsServiceRegistered(referencedType)) { throw new UnknownServiceException( $"The decorator {serviceType.Name} references " + $"an unknown service: {referencedType.Name}"); } } if (serviceType.GetGenericTypeDefinition() == typeof(IActivator<>)) { // // Use Service<> as default implementation for an // IActviator<> decorator. // // This doesn't make a real difference for singletons, but // for transients it lets clients delay object creation // (and hence, lookup of dependencies). // return Activator.CreateInstance(typeof(Service<>) .MakeGenericType(serviceType.GetGenericArguments()), this); } else { // // Custom decorator. // return CreateInstance(serviceType); } } else if (this.singletons.TryGetValue(serviceType, out var singletonStub)) { return singletonStub.Object; } else if (this.transients.TryGetValue(serviceType, out var transientFactory)) { return transientFactory(); } else if (this.parent != null) { return this.parent.GetService(serviceType); } else { throw new UnknownServiceException( $"Unknown service: {serviceType.Name}"); } } //--------------------------------------------------------------------- // Categories. //--------------------------------------------------------------------- public void AddServiceToCategory(Type categoryType, Type serviceType) { if (!categoryType.IsInterface) { throw new ArgumentException("Category must be an interface"); } if (!this.singletons.ContainsKey(serviceType) && !this.transients.ContainsKey(serviceType)) { throw new UnknownServiceException(serviceType.Name); } if (this.categories.TryGetValue(categoryType, out var serviceTypes)) { serviceTypes.Add(serviceType); } else { this.categories[categoryType] = new List<Type>() { serviceType }; } } public void AddServiceToCategory<TCategory, TService>() { AddServiceToCategory(typeof(TCategory), typeof(TService)); } public IEnumerable<object> GetServicesByCategory(Type category) { // // NB. Services registered for the same category in a lower layer // are not visible unless they are registered as "global". // // // Consider parent services. // IEnumerable<object> services; if (this.parent != null) { services = this.parent.GetServicesByCategory(category); } else { services = Enumerable.Empty<object>(); } // // Consider own services. // if (this.categories.TryGetValue(category, out var serviceTypes)) { services = services.Concat(serviceTypes.Select(t => GetService(t))); } return services; } public IEnumerable<TCategory> GetServicesByCategory<TCategory>() { return GetServicesByCategory(typeof(TCategory)).Cast<TCategory>(); } //--------------------------------------------------------------------- // Extension assembly handling. //--------------------------------------------------------------------- public void AddExtensionAssembly(Assembly assembly) { // // NB. By default, services are registered in this service registry, making // them visible to this and lower layers. // // Services can optionally register as "global" to be registered in the // root registry. This makes them visible across all layers. Their dependencies // are still resolved in this layer -- not in the root layer. // // // (1) First, register all transients. // foreach (var type in assembly.GetTypes()) { if (type.GetCustomAttribute<ServiceAttribute>() is ServiceAttribute attribute && attribute.Lifetime == ServiceLifetime.Transient) { AddTransient( attribute.ServiceInterface ?? type, type); } } // // (2) Register stubs, but do not create instances yet because they // might depend on another and we don't know what the right order // would have to be. // var singletonsToInstantiate = new LinkedList<SingletonStub>(); foreach (var type in assembly.GetTypes()) { if (type.GetCustomAttribute<ServiceAttribute>() is ServiceAttribute attribute && attribute.Lifetime == ServiceLifetime.Singleton) { var stub = new SingletonStub(() => CreateInstance(type)); if (!attribute.DelayCreation) { singletonsToInstantiate.AddLast(stub); } AddSingleton( attribute.ServiceInterface ?? type, stub); } } // // (3) Register categories. // foreach (var type in assembly.GetTypes()) { if (type.GetCustomAttribute<ServiceAttribute>() is ServiceAttribute attribute && type.GetCustomAttribute<ServiceCategoryAttribute>() is ServiceCategoryAttribute categoryAttribute) { AddServiceToCategory( categoryAttribute.Category, attribute.ServiceInterface ?? type); } } // // (4) Instantiate singletons that require eager creation. // // At this point, we've registered all services and categories, // so it's ok if one singleton depends on another singleton. // foreach (var singleton in singletonsToInstantiate) { var _ = singleton.Object; } } //--------------------------------------------------------------------- // Helper classes. //--------------------------------------------------------------------- private class SingletonStub { private readonly Lazy<object> factoryFunc; public SingletonStub(object value) : this(() => value) { } public SingletonStub(Func<object> factoryFunc) { this.factoryFunc = new Lazy<object>( factoryFunc, LazyThreadSafetyMode.ExecutionAndPublication); } public object Object => this.factoryFunc.Value; } } public static class ServiceProviderExtensions { public static TService GetService<TService>(this IServiceProvider provider) { return (TService)provider.GetService(typeof(TService)); } } [Serializable] public class UnknownServiceException : ApplicationException { protected UnknownServiceException(SerializationInfo info, StreamingContext context) : base(info, context) { } public UnknownServiceException(string service) : base(service) { } } }