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