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)
{
}
}
}