Add Dependency Injection and Autoloading to the Commands servicetags/1.0-rc
| @@ -6,13 +6,16 @@ namespace Discord.Commands | |||||
| public class ModuleAttribute : Attribute | public class ModuleAttribute : Attribute | ||||
| { | { | ||||
| public string Prefix { get; } | public string Prefix { get; } | ||||
| public bool AutoLoad { get; set; } | |||||
| public ModuleAttribute() | public ModuleAttribute() | ||||
| { | { | ||||
| Prefix = null; | Prefix = null; | ||||
| AutoLoad = true; | |||||
| } | } | ||||
| public ModuleAttribute(string prefix) | public ModuleAttribute(string prefix) | ||||
| { | { | ||||
| Prefix = prefix; | Prefix = prefix; | ||||
| AutoLoad = true; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -164,7 +164,7 @@ namespace Discord.Commands | |||||
| return loadedModule; | return loadedModule; | ||||
| } | } | ||||
| public async Task<IEnumerable<Module>> LoadAssembly(Assembly assembly) | |||||
| public async Task<IEnumerable<Module>> LoadAssembly(Assembly assembly, IDependencyMap dependencyMap = null) | |||||
| { | { | ||||
| var modules = ImmutableArray.CreateBuilder<Module>(); | var modules = ImmutableArray.CreateBuilder<Module>(); | ||||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | await _moduleLock.WaitAsync().ConfigureAwait(false); | ||||
| @@ -174,9 +174,9 @@ namespace Discord.Commands | |||||
| { | { | ||||
| var typeInfo = type.GetTypeInfo(); | var typeInfo = type.GetTypeInfo(); | ||||
| var moduleAttr = typeInfo.GetCustomAttribute<ModuleAttribute>(); | var moduleAttr = typeInfo.GetCustomAttribute<ModuleAttribute>(); | ||||
| if (moduleAttr != null) | |||||
| if (moduleAttr != null && moduleAttr.AutoLoad) | |||||
| { | { | ||||
| var moduleInstance = ReflectionUtils.CreateObject(typeInfo); | |||||
| var moduleInstance = ReflectionUtils.CreateObject(typeInfo, this, dependencyMap); | |||||
| modules.Add(LoadInternal(moduleInstance, moduleAttr, typeInfo)); | modules.Add(LoadInternal(moduleInstance, moduleAttr, typeInfo)); | ||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,36 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Reflection; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public class DependencyMap : IDependencyMap | |||||
| { | |||||
| private Dictionary<Type, object> map; | |||||
| public DependencyMap() | |||||
| { | |||||
| map = new Dictionary<Type, object>(); | |||||
| } | |||||
| public object Get(Type t) | |||||
| { | |||||
| if (!map.ContainsKey(t)) | |||||
| throw new KeyNotFoundException($"The dependency map does not contain \"{t.FullName}\""); | |||||
| return map[t]; | |||||
| } | |||||
| public T Get<T>() where T : class | |||||
| { | |||||
| return Get(typeof(T)) as T; | |||||
| } | |||||
| public void Add<T>(T obj) | |||||
| { | |||||
| var t = typeof(T); | |||||
| if (map.ContainsKey(t)) | |||||
| throw new InvalidOperationException($"The dependency map already contains \"{t.FullName}\""); | |||||
| map.Add(t, obj); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public interface IDependencyMap | |||||
| { | |||||
| object Get(Type t); | |||||
| T Get<T>() where T : class; | |||||
| void Add<T>(T obj); | |||||
| } | |||||
| } | |||||
| @@ -43,7 +43,7 @@ namespace Discord.Commands | |||||
| nextGroupPrefix = groupPrefix + groupAttrib.Prefix ?? type.Name; | nextGroupPrefix = groupPrefix + groupAttrib.Prefix ?? type.Name; | ||||
| else | else | ||||
| nextGroupPrefix = groupPrefix; | nextGroupPrefix = groupPrefix; | ||||
| SearchClass(ReflectionUtils.CreateObject(type), commands, type, nextGroupPrefix); | |||||
| SearchClass(ReflectionUtils.CreateObject(type, Service), commands, type, nextGroupPrefix); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Reflection; | using System.Reflection; | ||||
| @@ -6,18 +7,64 @@ namespace Discord.Commands | |||||
| { | { | ||||
| internal class ReflectionUtils | internal class ReflectionUtils | ||||
| { | { | ||||
| internal static object CreateObject(TypeInfo typeInfo) | |||||
| internal static object CreateObject(TypeInfo typeInfo, CommandService service, IDependencyMap map = null) | |||||
| { | { | ||||
| var constructor = typeInfo.DeclaredConstructors.Where(x => x.GetParameters().Length == 0).FirstOrDefault(); | |||||
| if (typeInfo.DeclaredConstructors.Count() > 1) | |||||
| throw new InvalidOperationException($"Found too many constructors for \"{typeInfo.FullName}\""); | |||||
| var constructor = typeInfo.DeclaredConstructors.FirstOrDefault(); | |||||
| if (constructor == null) | if (constructor == null) | ||||
| throw new InvalidOperationException($"Failed to find a valid constructor for \"{typeInfo.FullName}\""); | |||||
| throw new InvalidOperationException($"Found no constructor for \"{typeInfo.FullName}\""); | |||||
| object[] arguments = null; | |||||
| ParameterInfo[] parameters = constructor.GetParameters(); | |||||
| // TODO: can this logic be made better/cleaner? | |||||
| if (parameters.Length == 1) | |||||
| { | |||||
| if (parameters[0].ParameterType == typeof(IDependencyMap)) | |||||
| { | |||||
| if (map != null) | |||||
| arguments = new object[] { map }; | |||||
| else | |||||
| throw new InvalidOperationException($"Could not find a valid constructor for \"{typeInfo.FullName}\" (an IDependencyMap is required)"); | |||||
| } | |||||
| } | |||||
| else if (parameters.Length == 2) | |||||
| { | |||||
| if (parameters[0].ParameterType == typeof(CommandService) && parameters[1].ParameterType == typeof(IDependencyMap)) | |||||
| if (map != null) | |||||
| arguments = new object[] { service, map }; | |||||
| else | |||||
| throw new InvalidOperationException($"Could not find a valid constructor for \"{typeInfo.FullName}\" (an IDependencyMap is required)"); | |||||
| } | |||||
| if (arguments == null) | |||||
| { | |||||
| try | |||||
| { | |||||
| // TODO: probably change this ternary into something sensible? | |||||
| arguments = parameters.Select(x => x.ParameterType == typeof(CommandService) ? service : map.Get(x.ParameterType)).ToArray(); | |||||
| } | |||||
| catch (KeyNotFoundException ex) // tried to inject an invalid dependency | |||||
| { | |||||
| throw new InvalidOperationException($"Could not find a valid constructor for \"{typeInfo.FullName}\" (could not provide parameter)", ex); | |||||
| } | |||||
| catch (NullReferenceException ex) // tried to find a dependency | |||||
| { | |||||
| throw new InvalidOperationException($"Could not find a valid constructor for \"{typeInfo.FullName}\" (an IDependencyMap is required)", ex); | |||||
| } | |||||
| } | |||||
| try | try | ||||
| { | { | ||||
| return constructor.Invoke(null); | |||||
| return constructor.Invoke(arguments); | |||||
| } | } | ||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | { | ||||
| throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\"", ex); | |||||
| throw new InvalidOperationException($"Could not create \"{typeInfo.FullName}\"", ex); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||