diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs index c209b62df..e61016d33 100644 --- a/docs/guides/getting_started/samples/intro/structure.cs +++ b/docs/guides/getting_started/samples/intro/structure.cs @@ -19,9 +19,8 @@ class Program private readonly DiscordSocketClient _client; - // Keep the CommandService and IServiceProvider around for use with commands. - // These two types require you install the Discord.Net.Commands package. - private readonly IServiceProvider _services; + // Keep the CommandService around for use with commands. + // This type requires you install the Discord.Net.Commands package. private readonly CommandService _commands; private Program() @@ -47,6 +46,9 @@ class Program // Again, log level: LogLevel = LogSeverity.Info, + // Setup your DI container. + ServiceProvider = ConfigureServices(), + // There's a few more properties you can set, // for example, case-insensitive commands. CaseSensitiveCommands = false, @@ -56,8 +58,6 @@ class Program _client.Log += Logger; _commands.Log += Logger; - // Setup your DI container. - _services = ConfigureServices(); } // If any services require the client, or the CommandService, or something else you keep on hand, @@ -161,7 +161,7 @@ class Program // Execute the command. (result does not indicate a return value, // rather an object stating if the command executed successfully). - var result = await _commands.ExecuteAsync(context, pos, _services); + var result = await _commands.ExecuteAsync(context, pos); // Uncomment the following lines if you want the bot // to send a message if it failed (not advised for most situations). diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index 67fb5904d..5a3f3a8ca 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -26,7 +26,7 @@ namespace Discord.Commands.Builders public IReadOnlyList Attributes => _attributes; public IReadOnlyList Aliases => _aliases; - internal Optional TypeInfo { get; set; } + internal TypeInfo TypeInfo { get; set; } //Automatic internal ModuleBuilder(CommandService service, ModuleBuilder parent) @@ -120,6 +120,22 @@ namespace Discord.Commands.Builders if (Name == null) Name = _aliases[0]; + if (TypeInfo != null) + { + //keep this for safety? + //services = services ?? EmptyServiceProvider.Instance; + try + { + var moduleInstance = ReflectionUtils.CreateObject(TypeInfo, service, service._serviceProvider); + moduleInstance.OnModuleBuilding(service); + } + catch (Exception) + { + //unsure of what to do here + throw; + } + } + return new ModuleInfo(this, service, parent); } diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index cf7b43555..e56495632 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -297,7 +297,7 @@ namespace Discord.Commands } //We dont have a cached type reader, create one - reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, EmptyServiceProvider.Instance); + reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, service._serviceProvider); service.AddTypeReader(paramType, reader); return reader; diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 6c9ceeacb..b213971cd 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -28,6 +28,8 @@ namespace Discord.Commands private readonly HashSet _moduleDefs; private readonly CommandMap _map; + internal readonly IServiceProvider _serviceProvider; + internal readonly bool _caseSensitive, _throwOnError, _ignoreExtraArgs; internal readonly char _separatorChar; internal readonly RunMode _defaultRunMode; @@ -41,6 +43,7 @@ namespace Discord.Commands public CommandService() : this(new CommandServiceConfig()) { } public CommandService(CommandServiceConfig config) { + _serviceProvider = config.ServiceProvider ?? EmptyServiceProvider.Instance; _caseSensitive = config.CaseSensitiveCommands; _throwOnError = config.ThrowOnError; _ignoreExtraArgs = config.IgnoreExtraArgs; @@ -86,18 +89,18 @@ namespace Discord.Commands var builder = new ModuleBuilder(this, null, primaryAlias); buildFunc(builder); - var module = builder.Build(this); + var module = builder.Build(this, null); //should be fine to pass null here since it'll never get checked from this path anyway - return LoadModuleInternal(module, null); + return LoadModuleInternal(module); } finally { _moduleLock.Release(); } } - public Task AddModuleAsync(IServiceProvider services) => AddModuleAsync(typeof(T), services); - public async Task AddModuleAsync(Type type, IServiceProvider services) + public Task AddModuleAsync() => AddModuleAsync(typeof(T)); + public async Task AddModuleAsync(Type type) { await _moduleLock.WaitAsync().ConfigureAwait(false); try @@ -114,14 +117,14 @@ namespace Discord.Commands _typedModuleDefs[module.Key] = module.Value; - return LoadModuleInternal(module.Value, services); + return LoadModuleInternal(module.Value); } finally { _moduleLock.Release(); } } - public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) + public async Task> AddModulesAsync(Assembly assembly) { await _moduleLock.WaitAsync().ConfigureAwait(false); try @@ -132,7 +135,7 @@ namespace Discord.Commands foreach (var info in moduleDefs) { _typedModuleDefs[info.Key] = info.Value; - LoadModuleInternal(info.Value, services); + LoadModuleInternal(info.Value); } return moduleDefs.Select(x => x.Value).ToImmutableArray(); @@ -142,31 +145,31 @@ namespace Discord.Commands _moduleLock.Release(); } } - private ModuleInfo LoadModuleInternal(ModuleInfo module, IServiceProvider services) + private ModuleInfo LoadModuleInternal(ModuleInfo module) { _moduleDefs.Add(module); - if (module.TypeInfo.IsSpecified) - { - //keep this for safety? - services = services ?? EmptyServiceProvider.Instance; - try - { - var moduleInstance = ReflectionUtils.CreateObject(module.TypeInfo.Value, this, services); - moduleInstance.OnModuleBuilding(this); - } - catch(Exception) - { - //unsure of what to do here - throw; - } - } + //if (module.TypeInfo.IsSpecified) + //{ + // //keep this for safety? + // services = services ?? EmptyServiceProvider.Instance; + // try + // { + // var moduleInstance = ReflectionUtils.CreateObject(module.TypeInfo.Value, this, services); + // moduleInstance.OnModuleBuilding(this); + // } + // catch(Exception) + // { + // //unsure of what to do here + // throw; + // } + //} foreach (var command in module.Commands) _map.AddCommand(command); foreach (var submodule in module.Submodules) - LoadModuleInternal(submodule, services); + LoadModuleInternal(submodule); return module; } @@ -243,7 +246,7 @@ namespace Discord.Commands var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary()); var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); readers[nullableReader.GetType()] = nullableReader; - } + } internal IDictionary GetTypeReaders(Type type) { if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) @@ -291,97 +294,99 @@ namespace Discord.Commands return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } - public Task ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) - => ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); - public async Task ExecuteAsync(ICommandContext context, string input, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + public Task ExecuteAsync(ICommandContext context, int argPos, /*IServiceProvider services = null,*/ MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + => ExecuteAsync(context, context.Message.Content.Substring(argPos), /*services,*/ multiMatchHandling); + public async Task ExecuteAsync(ICommandContext context, string input, /*IServiceProvider services = null,*/ MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { - services = services ?? EmptyServiceProvider.Instance; - - var searchResult = Search(context, input); - if (!searchResult.IsSuccess) - return searchResult; - - var commands = searchResult.Commands; - var preconditionResults = new Dictionary(); - - foreach (var match in commands) + //services = services ?? EmptyServiceProvider.Instance; + using (var scope = _serviceProvider.CreateScope()) { - preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); - } + var searchResult = Search(context, input); + if (!searchResult.IsSuccess) + return searchResult; - var successfulPreconditions = preconditionResults - .Where(x => x.Value.IsSuccess) - .ToArray(); + var commands = searchResult.Commands; + var preconditionResults = new Dictionary(); - if (successfulPreconditions.Length == 0) - { - //All preconditions failed, return the one from the highest priority command - var bestCandidate = preconditionResults - .OrderByDescending(x => x.Key.Command.Priority) - .FirstOrDefault(x => !x.Value.IsSuccess); - return bestCandidate.Value; - } + foreach (var match in commands) + { + preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, scope.ServiceProvider).ConfigureAwait(false); + } - //If we get this far, at least one precondition was successful. + var successfulPreconditions = preconditionResults + .Where(x => x.Value.IsSuccess) + .ToArray(); - var parseResultsDict = new Dictionary(); - foreach (var pair in successfulPreconditions) - { - var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); + if (successfulPreconditions.Length == 0) + { + //All preconditions failed, return the one from the highest priority command + var bestCandidate = preconditionResults + .OrderByDescending(x => x.Key.Command.Priority) + .FirstOrDefault(x => !x.Value.IsSuccess); + return bestCandidate.Value; + } + + //If we get this far, at least one precondition was successful. - if (parseResult.Error == CommandError.MultipleMatches) + var parseResultsDict = new Dictionary(); + foreach (var pair in successfulPreconditions) { - IReadOnlyList argList, paramList; - switch (multiMatchHandling) + var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, scope.ServiceProvider).ConfigureAwait(false); + + if (parseResult.Error == CommandError.MultipleMatches) { - case MultiMatchHandling.Best: - argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); - paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); - parseResult = ParseResult.FromSuccess(argList, paramList); - break; + IReadOnlyList argList, paramList; + switch (multiMatchHandling) + { + case MultiMatchHandling.Best: + argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + parseResult = ParseResult.FromSuccess(argList, paramList); + break; + } } - } - parseResultsDict[pair.Key] = parseResult; - } + parseResultsDict[pair.Key] = parseResult; + } - // Calculates the 'score' of a command given a parse result - float CalculateScore(CommandMatch match, ParseResult parseResult) - { - float argValuesScore = 0, paramValuesScore = 0; - - if (match.Command.Parameters.Count > 0) + // Calculates the 'score' of a command given a parse result + float CalculateScore(CommandMatch match, ParseResult parseResult) { - var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; - var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + float argValuesScore = 0, paramValuesScore = 0; - argValuesScore = argValuesSum / match.Command.Parameters.Count; - paramValuesScore = paramValuesSum / match.Command.Parameters.Count; + if (match.Command.Parameters.Count > 0) + { + var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + + argValuesScore = argValuesSum / match.Command.Parameters.Count; + paramValuesScore = paramValuesSum / match.Command.Parameters.Count; + } + + var totalArgsScore = (argValuesScore + paramValuesScore) / 2; + return match.Command.Priority + totalArgsScore * 0.99f; } - var totalArgsScore = (argValuesScore + paramValuesScore) / 2; - return match.Command.Priority + totalArgsScore * 0.99f; - } + //Order the parse results by their score so that we choose the most likely result to execute + var parseResults = parseResultsDict + .OrderByDescending(x => CalculateScore(x.Key, x.Value)); - //Order the parse results by their score so that we choose the most likely result to execute - var parseResults = parseResultsDict - .OrderByDescending(x => CalculateScore(x.Key, x.Value)); + var successfulParses = parseResults + .Where(x => x.Value.IsSuccess) + .ToArray(); - var successfulParses = parseResults - .Where(x => x.Value.IsSuccess) - .ToArray(); + if (successfulParses.Length == 0) + { + //All parses failed, return the one from the highest priority command, using score as a tie breaker + var bestMatch = parseResults + .FirstOrDefault(x => !x.Value.IsSuccess); + return bestMatch.Value; + } - if (successfulParses.Length == 0) - { - //All parses failed, return the one from the highest priority command, using score as a tie breaker - var bestMatch = parseResults - .FirstOrDefault(x => !x.Value.IsSuccess); - return bestMatch.Value; + //If we get this far, at least one parse was successful. Execute the most likely overload. + var chosenOverload = successfulParses[0]; + return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, scope.ServiceProvider).ConfigureAwait(false); } - - //If we get this far, at least one parse was successful. Execute the most likely overload. - var chosenOverload = successfulParses[0]; - return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 7fdbe368b..c7157cf51 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -1,4 +1,6 @@ -namespace Discord.Commands +using System; + +namespace Discord.Commands { public class CommandServiceConfig { @@ -18,5 +20,7 @@ /// Determines whether extra parameters should be ignored. public bool IgnoreExtraArgs { get; set; } = false; + + public IServiceProvider ServiceProvider { get; set; } = EmptyServiceProvider.Instance; } } diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index e554391ea..6ebd901c7 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -22,7 +22,7 @@ namespace Discord.Commands public ModuleInfo Parent { get; } public bool IsSubmodule => Parent != null; - internal Optional TypeInfo { get; } + //public TypeInfo TypeInfo { get; } internal ModuleInfo(ModuleBuilder builder, CommandService service, ModuleInfo parent = null) { @@ -33,7 +33,7 @@ namespace Discord.Commands Remarks = builder.Remarks; Parent = parent; - TypeInfo = builder.TypeInfo; + //TypeInfo = builder.TypeInfo; Aliases = BuildAliases(builder, service).ToImmutableArray(); Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray();