The CommandService provides a Discord.Net-style framework for creating and executing text-based commands with automatic argument parsing, preconditions, and dependency injection support.
using Fluxer.Net.Commands;
using Serilog;
var commands = new CommandService(
prefixChar: '/', // Commands start with /
logger: Log.Logger as Logger, // Optional: for command logging
services: null // Optional: for dependency injection
);
// Register all command modules from your assembly
await commands.AddModulesAsync(Assembly.GetExecutingAssembly());
using Fluxer.Net.Commands;
using Fluxer.Net.Commands.Attributes;
public class BasicCommands : ModuleBase
{
[Command("ping")]
[Summary("Check if the bot is responsive")]
public async Task PingCommand()
{
await ReplyAsync("Pong!");
}
[Command("hello")]
[Alias("hi", "hey")]
[Summary("Get a friendly greeting")]
public async Task HelloCommand()
{
await ReplyAsync($"Hello, <@{Context.User.Id}>!");
}
[Command("echo")]
[Summary("Echo back your message")]
public async Task EchoCommand([Remainder] string message)
{
await ReplyAsync(message);
}
}
gateway.MessageCreate += async messageData =>
{
// Ignore messages without an author (webhooks, system messages)
if (messageData.Author == null)
return;
// Check if message starts with the command prefix
int argPos = 0;
if (messageData.Content?.StartsWith('/') == true)
{
argPos = 1; // Skip the prefix character
// Create command context
var context = new CommandContext(api, gateway, messageData);
// Execute the command
var result = await commands.ExecuteAsync(context, argPos);
// Handle the result
if (!result.IsSuccess)
{
Log.Warning("Command failed: {Error}", result.Error);
}
}
};
Marks a method as a command and specifies its name:
[Command("commandname")]
public async Task MyCommand()
{
// Command implementation
}
Provides alternative names for a command:
[Command("information")]
[Alias("info", "i")]
public async Task InfoCommand()
{
await ReplyAsync("Bot information...");
}
Add documentation to your commands:
[Command("ban")]
[Summary("Bans a user from the guild")]
[Remarks("Requires administrator permissions")]
public async Task BanCommand(ulong userId, [Remainder] string reason = "No reason provided")
{
// Ban implementation
}
The CommandService automatically parses these types:
string - Text inputint, long, ulong, uint - Integer numbersshort, ushort, byte, sbyte - Small integersfloat, double, decimal - Decimal numbersbool - Boolean (true/false)DateTime - Date and timeTimeSpan - Time duration[Command("add")]
[Summary("Add two numbers together")]
public async Task AddCommand(int a, int b)
{
await ReplyAsync($"{a} + {b} = {a + b}");
}
[Command("remind")]
[Summary("Set a reminder")]
public async Task RemindCommand(TimeSpan delay, [Remainder] string message)
{
await ReplyAsync($"I'll remind you in {delay.TotalMinutes} minutes: {message}");
// Implementation to schedule reminder...
}
Parameters with default values are optional:
[Command("greet")]
[Summary("Greet someone (or yourself)")]
public async Task GreetCommand(string name = "stranger")
{
await ReplyAsync($"Hello, {name}!");
}
// Usage:
// /greet - "Hello, stranger!"
// /greet Alice - "Hello, Alice!"
Use the [Remainder] attribute to capture multi-word arguments:
[Command("announce")]
[Summary("Make an announcement")]
public async Task AnnounceCommand([Remainder] string announcement)
{
await ReplyAsync($"📢 **Announcement:** {announcement}");
}
// Usage:
// /announce This is a multi-word announcement
// announcement = "This is a multi-word announcement"
All command modules inherit from ModuleBase, which provides:
Context - Access to command context (user, channel, message, clients)ReplyAsync() - Convenience method to send messagesBeforeExecute() - Hook called before command executionAfterExecute() - Hook called after command executionpublic class LoggedCommands : ModuleBase
{
protected override void BeforeExecute(CommandInfo command)
{
Log.Information("Executing {Command} for user {UserId}",
command.Name, Context.User.Id);
}
protected override void AfterExecute(CommandInfo command)
{
Log.Information("Completed {Command}", command.Name);
}
[Command("test")]
public async Task TestCommand()
{
await ReplyAsync("Test command executed!");
}
}
// Modules/ModerationCommands.cs
public class ModerationCommands : ModuleBase
{
[Command("kick")]
public async Task KickCommand(ulong userId, [Remainder] string reason = "No reason")
{
// Kick implementation
}
[Command("ban")]
public async Task BanCommand(ulong userId, [Remainder] string reason = "No reason")
{
// Ban implementation
}
}
// Modules/FunCommands.cs
public class FunCommands : ModuleBase
{
[Command("joke")]
public async Task JokeCommand()
{
// Joke implementation
}
[Command("roll")]
public async Task RollCommand(int sides = 6)
{
var result = Random.Shared.Next(1, sides + 1);
await ReplyAsync($"You rolled a {result}!");
}
}
The CommandContext provides access to:
Context.Client - The ApiClient instanceContext.Gateway - The GatewayClient instanceContext.Message - The message that triggered the commandContext.User - The user who executed the commandContext.ChannelId - The channel where the command was executedContext.GuildId - The guild ID (null for DMs)[Command("userinfo")]
[Summary("Get information about yourself or a mentioned user")]
public async Task UserInfoCommand()
{
// Get the target user (mentioned user or command author)
var targetUser = Context.Message.Mentions?.FirstOrDefault() ?? Context.User;
var embed = new Fluxer.Net.EmbedBuilder.EmbedBuilder()
.WithTitle("User Information")
.AddField("Username", targetUser.Username, inline: true)
.AddField("ID", targetUser.Id.ToString(), inline: true)
.AddField("Premium", targetUser.Premium > 0 ? "Yes" : "No", inline: true)
.WithColor(0x5865F2)
.Build();
await Context.Client.SendMessage(Context.ChannelId, new()
{
Embeds = new List
The ExecuteAsync method returns an IResult that indicates success or failure:
var result = await commands.ExecuteAsync(context, argPos);
if (!result.IsSuccess)
{
switch (result.ErrorType)
{
case CommandError.UnknownCommand:
// Command not found - usually just ignore
break;
case CommandError.BadArgCount:
await api.SendMessage(messageData.ChannelId, new()
{
Content = $"❌ Error: {result.Error}"
});
break;
case CommandError.ParseFailed:
await api.SendMessage(messageData.ChannelId, new()
{
Content = $"❌ Invalid parameter: {result.Error}"
});
break;
case CommandError.UnmetPrecondition:
await api.SendMessage(messageData.ChannelId, new()
{
Content = $"⛔ {result.Error}"
});
break;
default:
await api.SendMessage(messageData.ChannelId, new()
{
Content = $"❌ An error occurred: {result.Error}"
});
break;
}
}
Commands can execute synchronously or asynchronously:
// Sync mode (default): Waits for command to complete
[Command("sync")]
public async Task SyncCommand()
{
await Task.Delay(1000);
await ReplyAsync("Completed!");
}
// Async mode: Fires and forgets (doesn't wait)
[Command("async", RunMode = RunMode.Async)]
public async Task AsyncCommand()
{
await Task.Delay(5000);
await ReplyAsync("This message appears after 5 seconds");
}
You can search for commands by name or alias:
var pingCommand = commands.Search("ping");
if (pingCommand != null)
{
Log.Information("Found command: {Name} with {ParamCount} parameters",
pingCommand.Name, pingCommand.Parameters.Count);
}
// Access all registered commands
foreach (var command in commands.Commands)
{
Log.Information("Command: {Name} ({Aliases})",
command.Name, string.Join(", ", command.Aliases));
}
[Command("help")]
[Summary("Shows all available commands")]
public async Task HelpCommand(string? commandName = null)
{
if (commandName != null)
{
// Show help for specific command
var command = Context.CommandService.Search(commandName);
if (command == null)
{
await ReplyAsync($"Command '{commandName}' not found.");
return;
}
var paramInfo = string.Join(" ", command.Parameters.Select(p =>
{
var name = p.Name;
if (p.IsOptional) name = $"[{name}]";
if (p.IsRemainder) name += "...";
return name;
}));
await ReplyAsync($"**{command.Name}** {paramInfo}\n{command.Summary}");
}
else
{
// Show all commands
var modules = Context.CommandService.Modules;
var helpText = "**Available Commands:**\n\n";
foreach (var module in modules)
{
helpText += $"**{module.Name}:**\n";
foreach (var cmd in module.Commands)
{
helpText += $"• `/{cmd.Name}` - {cmd.Summary}\n";
}
helpText += "\n";
}
await ReplyAsync(helpText);
}
}
// Good
[Command("userinfo")]
[Command("serverinfo")]
// Avoid
[Command("ui")]
[Command("si")]
[Command("kick")]
[Summary("Kicks a member from the guild")] // Always include
public async Task KickCommand(ulong userId) { }
// Good: Using ulong for IDs
[Command("getuser")]
public async Task GetUserCommand(ulong userId) { }
// Bad: Using string when a specific type is better
[Command("getuser")]
public async Task GetUserCommand(string userId) { }
[Command("divide")]
public async Task DivideCommand(double a, double b)
{
if (b == 0)
{
await ReplyAsync("Cannot divide by zero!");
return;
}
await ReplyAsync($"{a} / {b} = {a / b}");
}