@@ -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