using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using Framework.Support;
using ConditionalAttribute = System.Diagnostics.ConditionalAttribute;
namespace Framework.Services {
/// Declares the scope in which a service will exist
public static class ServiceInjector {
/// Default scope for services missing the ServiceScope attribute
private const Scope DefaultScope = Scope.Session;
#region interface IBinding
/// Describes a service binding
public interface IBinding {
/// Component that is providing the service
Type ProviderType { get; }
/// Scope in which the service exists
Scope Scope { get; }
/// Returns the existing instance or creates a new one
/// An instance of the bound service provider
object GetOrCreate();
}
#endregion // interface IBinding
#region class ComponentBinding
/// Binding of a service to a Unity component (MonoBehaviour)
private class ComponentBinding : IBinding {
/// Provider that is implementing the service
public Type ProviderType;
/// Current instance of the sevice provider
///
/// This may cease to exist when the scene unloads or session ends.
/// In this case, Unity will will do some horribad trickery to make
/// the object compare as equal to null even though it still exists
/// (in an unusable state, not anchored in the hierarchy).
///
public object ProviderInstance;
/// Scope in which the provider exists
public Scope Scope;
/// Component that is providing the service
Type IBinding.ProviderType {
get { return this.ProviderType; }
}
/// Scope in which the service exists
Scope IBinding.Scope {
get { return this.Scope; }
}
/// Returns the existing instance or creates a new one
/// An instance of the bound service provider
object IBinding.GetOrCreate() {
if(this.ProviderInstance == null) {
GameObject container = ServiceContainer.GetOrCreate(this.Scope);
this.ProviderInstance = container.GetComponent(this.ProviderType);
if(this.ProviderInstance == null) {
this.ProviderInstance = container.AddComponent(this.ProviderType);
// Inject will be called by the component's Awake() method!
// Do we need circular dependency detection?
}
}
return this.ProviderInstance;
}
}
#endregion // class ComponentBinding
#region class MethodBinding
// TODO
#endregion // class MethodBinding
#if false
/// Manually looks up or creates a service provided by the injector
/// Type of sevice that will be looked up or created
/// An instance of the requested service
public static T Get() {
//return (T)getOrCreateComponent(typeof())
}
#endif
///
/// Calls the Inject() method on the specified object, creating all dependencies
///
/// Object on which the Inject() method will be called
///
/// This is incompatible with IL2CPP and probably the iOS platform, most likely
/// due to .
///
public static void InjectDependencies(object target) {
#if UNITY_EDITOR // No need for this check outside the editor
if(!Application.isPlaying) {
Debug.LogWarning(
"InjectDependencies() on " + target.GetType().ToString() + "' called in editor. " +
"This would instantiate runtime services as permanent scene hierarchy objects. " +
"Dependencies not injected."
);
return;
}
#endif
// See if this type has an Inject() method that we can call
MethodInfo[] injectMethods = target.GetType().GetMethods(
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
);
if((injectMethods == null) || (injectMethods.Length == 0)) {
return;
}
// Call all Inject() methods exposed by the type
for(int index = 0; index < injectMethods.Length; ++index) {
if(injectMethods[index].Name == "Inject") {
callInjectMethod(target, injectMethods[index]);
}
}
}
///
/// Retrieves the scope assigned to the specified type via the ServiceScope attribute
///
/// Type whose desired scope will be retrieved
/// The scope of the desired type, boxed in an object
public static Scope? GetScope(Type type) {
object[] attributes = type.GetCustomAttributes(typeof(ServiceScopeAttribute), true);
if(attributes != null) {
if(attributes.Length > 0) {
return ((ServiceScopeAttribute)attributes[0]).Scope;
}
}
return null;
}
/// Calls the Inject() method on the specified target
/// Target the Inject() method will be called on
/// Inject method that will be called
private static void callInjectMethod(object target, MethodInfo injectMethod) {
ParameterInfo[] parameterInfos = injectMethod.GetParameters();
verifyScopes(target.GetType(), parameterInfos);
// Fill the parameters of the Inject() method, also recursively injecting any
// dependencies that are newly constructed
object[] parameters = new object[parameterInfos.Length];
for(int index = 0; index < parameterInfos.Length; ++index) {
Type parameterType = parameterInfos[index].ParameterType;
// If no explicit binding has been made for this service,
// we need to figure it out by ourselves (default case!)
IBinding binding;
if(!bindings.TryGetValue(parameterType, out binding)) {
binding = deduceBinding(parameterInfos[index]);
bindings.Add(parameterType, binding);
}
// Create the provider (may result in further calls to this method!)
parameters[index] = binding.GetOrCreate();
}
injectMethod.Invoke(target, parameters);
}
/// Attempts to deduce the provider bound to a service
/// Service whose provider will be deduced
/// A binding for the specified service
private static IBinding deduceBinding(ParameterInfo parameterInfo) {
// The ServiceScope attribute should be on the service (interface).
// It would be invite inconsistency and chaos if we looked at the provider
// instead. We could check the provider to prevent user error, though.
Scope? parameterScope = GetScope(parameterInfo.ParameterType);
if(!parameterScope.HasValue) {
parameterScope = DefaultScope;
UnityEngine.Debug.LogWarning(
"Type '" + parameterInfo.ParameterType.Name + "' " +
"is missing the ServiceScope attribute"
);
}
Type binding = getDefaultBinding(parameterInfo);
if(binding == null) {
if(parameterInfo.ParameterType.IsAbstract) {
UnityEngine.Debug.LogError(
"Service '" + parameterInfo.ParameterType.Name + "' " +
"is abstract and no binding could be deduced for it."
);
throw new Exception(
"No provider for service '" + parameterInfo.ParameterType.Name + "' "
);
}
// No default binding specified but service is also the provider
// (other component specified instead of interface), so use it.
binding = parameterInfo.ParameterType;
}
return new ComponentBinding() {
Scope = parameterScope.Value,
ProviderType = binding
};
}
///
/// Verifies that no service depends on a shorter-lived service than itself
///
/// Type of service that will be verified
/// Dependencies of the service
[Conditional("DEBUG")]
private static void verifyScopes(Type serviceType, ParameterInfo[] parameterInfos) {
if(!Application.isEditor) {
return;
}
Scope? serviceScope = GetScope(serviceType);
if(serviceScope == null) {
return;
}
for(int index = 0; index < parameterInfos.Length; ++index) {
Scope dependencyScope = GetScope(parameterInfos[index].ParameterType) ?? DefaultScope;
if(dependencyScope < serviceScope) {
UnityEngine.Debug.LogError(
"Service '" + serviceType.Name + "' has a dependency on " +
"'" + parameterInfos[index].ParameterType.Name + "' which exists in " +
"a shorter lived scope. This WILL cause problems when scenes are loaded."
);
}
}
}
#if false
///
/// Looks up or creates the specified component, also injecting its dependencies
///
/// Type of component that will be created
/// Scope in which the component will be created
/// The existing or newly created component of the specified type
private static object getOrCreateComponent(Type componentType, Scope scope) {
GameObject container = ServiceContainer.GetOrCreate(scope);
Component component = container.GetComponent(componentType);
if(component == null) {
component = container.AddComponent(componentType);
// Inject will be called by the component's Awake() method!
}
return component;
}
#endif
/// Retrieves the default provider bound to the requested service
/// Injected parameter whose default service is needed
/// The default service for the injected parameter, if specified
private static Type getDefaultBinding(ParameterInfo parameterInfo) {
// Undecided:
// - All this is to support zero configuration DI
// - Putting DefaultBinding on service interface would promote consistency
// (no mismatching bindings depending on who first asks for a service)
// - But would be a problem if you wanted to put the interface separately
// from the implementation (is class library usage a common use case in Unity?)
// Priority 1 is the default provider directly specified for the parameter
object[] attributes = parameterInfo.GetCustomAttributes(
typeof(DefaultBindingAttribute), false
);
if(attributes != null) {
if(attributes.Length > 0) {
return ((DefaultBindingAttribute)attributes[0]).ServiceImplementationType;
}
}
// Priority 2 is the default provider specified by the service interface
attributes = parameterInfo.ParameterType.GetCustomAttributes(
typeof(DefaultBindingAttribute), false
);
if(attributes != null) {
if(attributes.Length > 0) {
return ((DefaultBindingAttribute)attributes[0]).ServiceImplementationType;
}
}
return null;
}
///
/// Reflection informations for the getOrCreateAndInjectComponent() method
///
//private static MethodInfo getOrCreateAndInjectComponentMethod;
/// Bindings that have been set up for services
///
/// Under the (safe) assumption that the bindings will not change
/// at runtime, we cache most information in which services have been bound.
/// This saves us a costly attribute query (calling
/// would construct
/// attribute instances and produce garbage for each Inject() use),
/// component lookup in the scene hierarchy and such.
///
private static IDictionary bindings = new Dictionary();
}
} // namespace Framework.Services