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