Skip to content

Commit d89d0db

Browse files
author
PawSharp
committed
fix(commands): cache precondition instances on Command at registration
GetCustomAttributes() constructs NEW attribute objects on every call. CooldownAttribute stores its per-bucket state (_buckets) on the instance, so re-creating it each invocation meant the dictionary was always empty and rate-limiting never fired. Collect and store IPrecondition instances once in Command.Preconditions during RegisterModule / RegisterModuleAsync, then use that list in OnMessageCreate instead of calling GetCustomAttributes at runtime.
1 parent c8aa215 commit d89d0db

1 file changed

Lines changed: 30 additions & 8 deletions

File tree

src/PawSharp.Commands/CommandsExtension.cs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,14 @@ public class Command
276276
/// </summary>
277277
public BaseCommandModule Module { get; }
278278

279+
/// <summary>
280+
/// Gets the precondition checks for this command, collected once at registration.
281+
/// Stored here so attribute instances (and their state, e.g. <see cref="CooldownAttribute._buckets"/>)
282+
/// are reused across invocations rather than being reconstructed by each
283+
/// <c>GetCustomAttributes</c> call.
284+
/// </summary>
285+
public IReadOnlyList<IPrecondition> Preconditions { get; }
286+
279287
/// <summary>
280288
/// Initializes a new instance of the <see cref="Command"/> class.
281289
/// </summary>
@@ -284,13 +292,15 @@ public class Command
284292
/// <param name="description">The command description.</param>
285293
/// <param name="method">The command method.</param>
286294
/// <param name="module">The command module.</param>
287-
public Command(string name, string[] aliases, string? description, MethodInfo method, BaseCommandModule module)
295+
/// <param name="preconditions">Pre-collected precondition instances for this command.</param>
296+
public Command(string name, string[] aliases, string? description, MethodInfo method, BaseCommandModule module, IReadOnlyList<IPrecondition> preconditions)
288297
{
289298
Name = name ?? throw new ArgumentNullException(nameof(name));
290299
Aliases = aliases ?? throw new ArgumentNullException(nameof(aliases));
291300
Description = description;
292301
Method = method ?? throw new ArgumentNullException(nameof(method));
293302
Module = module ?? throw new ArgumentNullException(nameof(module));
303+
Preconditions = preconditions ?? throw new ArgumentNullException(nameof(preconditions));
294304
}
295305
}
296306

@@ -390,7 +400,17 @@ public void RegisterModule(DiscordClient client, BaseCommandModule module)
390400
var aliases = aliasesAttr?.Aliases ?? Array.Empty<string>();
391401
var description = descriptionAttr?.Description;
392402

393-
var command = new Command(commandAttr.Name, aliases, description, method, module);
403+
// Collect preconditions once at registration so that stateful instances
404+
// (e.g. CooldownAttribute with its _buckets dictionary) are reused across
405+
// invocations. GetCustomAttributes creates NEW attribute objects on every
406+
// call, so we must NOT call it at execution time for stateful preconditions.
407+
var preconditions = method.GetCustomAttributes(typeof(IPrecondition), inherit: true)
408+
.Concat(type.GetCustomAttributes(typeof(IPrecondition), inherit: true))
409+
.Cast<IPrecondition>()
410+
.ToList()
411+
.AsReadOnly();
412+
413+
var command = new Command(commandAttr.Name, aliases, description, method, module, preconditions);
394414

395415
_commands[commandAttr.Name] = command;
396416
foreach (var alias in aliases)
@@ -453,7 +473,13 @@ public async Task RegisterModuleAsync(DiscordClient client, BaseCommandModule mo
453473
var aliases = aliasesAttr?.Aliases ?? Array.Empty<string>();
454474
var description = descriptionAttr?.Description;
455475

456-
var command = new Command(commandAttr.Name, aliases, description, method, module);
476+
var preconditions = method.GetCustomAttributes(typeof(IPrecondition), inherit: true)
477+
.Concat(type.GetCustomAttributes(typeof(IPrecondition), inherit: true))
478+
.Cast<IPrecondition>()
479+
.ToList()
480+
.AsReadOnly();
481+
482+
var command = new Command(commandAttr.Name, aliases, description, method, module, preconditions);
457483

458484
_commands[commandAttr.Name] = command;
459485
foreach (var alias in aliases)
@@ -504,11 +530,7 @@ private async Task OnMessageCreate(MessageCreateEvent evt)
504530
var ctx = new CommandContext(_client!, message, _prefix, commandName, args, rawArgs, evt.Member);
505531

506532
// ── Precondition checks ─────────────────────────────────────────────────
507-
var preconditions = command.Method.GetCustomAttributes(typeof(IPrecondition), inherit: true)
508-
.Concat(command.Module.GetType().GetCustomAttributes(typeof(IPrecondition), inherit: true))
509-
.Cast<IPrecondition>();
510-
511-
foreach (var check in preconditions)
533+
foreach (var check in command.Preconditions)
512534
{
513535
var result = await check.CheckAsync(ctx);
514536
if (!result.IsSuccess)

0 commit comments

Comments
 (0)