Hey you! Thanks for reading.

Before starting I want to shoutout the Ginganinja for his awesome setup tutorial for a C# discord bot. I referenced this during my initial setup.

If you’d like to checkout my full project so far you can view it on my Github.

Ok lets build a c# discord bot.

  • The structure of this project will look something like this:
  • It contains 4 separate sub-projects. The main will be the actual .net console app which runs the bot and communicates with discord
  • The next is a class library. This contains all the shared models used throughout the project along with the DBContext to allow access to the database.
  • Next is an api project. This is not used as much since I refactored the project but it was used to pull data from the bot to the website.
  • And finally is the blazor server app project. This is a site where users can view their bots, get statistics on it’s usage and enable / disable features of their bot.
  • Creating the initial console app that runs the bot is super easy and there are tons of guids on how to do it. The more difficult part is adding a website to interact with it.

  • The Discord.Net nuget package is really nice to use and the setup is easy enough.

    [cc lang=”c#”]
    static void Main(string[] args)
    {
    new Program().MainAsync().GetAwaiter().GetResult();
    }

    public Program()
    {
    // Replace connection string with below for testing
    // Server=(localdb)\mssqllocaldb;Database=aspnet-53bc9b9d-9d6a-45d4-8429-2a2761773502;Tru
    // Test DB name: 20210910022534_testDb
    var _builder = new ConfigurationBuilder()
    .SetBasePath(AppContext.BaseDirectory) // <---- UNCOMMENT FOR MIGRATIONS // .SetBasePath(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)) .AddJsonFile(path: "appsettings.json") .AddUserSecrets();

    // build the configuration and assign to _config
    _config = _builder.Build();
    }
    [/cc]

  • This is the main method and program class in the discord bot console app. You can see some evidence of testing / debugging with the backup database.
  • One reason I keep coming back to C# is Entity Framework which just makes it so easy to integrate with a database. Using a code-first approach is the way to go.
  • Next we have the MainAsync method which is where we will log in with our discord bot credentials and connect with the server.

    [cc lang=”c#”]
    public async Task MainAsync()
    {
    // call ConfigureServices to create the ServiceCollection/Provider for passing around the services
    using (var services = ConfigureServices())
    {
    // get the client and assign to client
    // you get the services via GetRequiredService
    var client = services.GetRequiredService();
    _client = client;

    //_context = new ApplicationDbContext(services);
    // setup logging and the ready event
    client.Log += LogAsync;
    client.Ready += ReadyAsync;
    services.GetRequiredService().Log += LogAsync;

    // Get the token from the configuration file, and start the bot
    var tok = _config[“Token”];
    await client.LoginAsync(TokenType.Bot, tok);
    await client.StartAsync();

    // we get the CommandHandler class here and call the InitializeAsync to start
    await services.GetRequiredService().InitializeAsync();
    //await _client.DownloadUsersAsync(_client.Guilds);
    await Task.Delay(-1);
    }
    }
    [/cc]

  • Next up we have the class library. This project has grown significantly since it was created as more and more features have been added.
  • Something I made sure I implemented with this project was using DTO’s (Data Transfer Objects) instead of using the direct model for everything.
  • The benefit of DTO’s is that they further separate the end user from the data models. Instead using an intermediary class to handle all transactions allowing more finely tuned control over access.

  • The first item to talk about in the class library would have to be the ApplicationDBContext.
  • AppDataContextFactory.cs
    [cc lang=”c#”]
    public class AppDataContextFactory : IDesignTimeDbContextFactory
    {

    ApplicationDbContext IDesignTimeDbContextFactory.CreateDbContext(string[] args)
    {
    var optionsBuilder = new DbContextOptionsBuilder();
    var config = new ConfigurationBuilder()
    .SetBasePath(AppContext.BaseDirectory) // <---- UNCOMMENT FOR MIGRATIONS // .SetBasePath(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)) // <-- COMMENT THIS LINE FOR MIGRATIONS .AddJsonFile(path: "appsettings.json") .Build(); optionsBuilder.UseSqlServer(config.GetConnectionString("DefaultDb")); return new ApplicationDbContext(optionsBuilder.Options); } } [/cc] ApplicationDbContext.cs
    [cc lang=”c#”]
    public ApplicationDbContext(DbContextOptions options) //, IConfiguration config)
    : base(options)
    {
    //_config = config;
    _options = options;
    //_options.UseSqlite($”Data Source={_config.GetConnectionString(“DefaultDb”)}”);
    IConfigurationBuilder _tmpConfig = new ConfigurationBuilder();
    _tmpConfig.AddJsonFile(“appsettings.json”);
    _config = _tmpConfig.Build();

    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
    if (!optionsBuilder.IsConfigured)
    {
    //optionsBuilder.UseSqlite(_config.GetConnectionString(“DefaultDb”));
    optionsBuilder.UseSqlServer(_config.GetConnectionString(“DefaultDb”));

    }
    base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    modelBuilder.Entity()
    .HasKey(s => s.userId);
    modelBuilder.Entity().HasKey(s => s.id);
    modelBuilder.Entity()
    .HasKey(p => p.id);
    modelBuilder.Entity().HasKey(s => s.id);
    modelBuilder.Entity().HasKey(s => s.id);
    modelBuilder.Entity().HasKey(s => s.id);
    modelBuilder.Entity().HasKey(s => s.id);
    modelBuilder.Entity().HasKey(s => s.id);
    modelBuilder.Entity().HasKey(s => s.id);
    modelBuilder.Entity().HasKey(s => s.commandId);
    modelBuilder.Entity().HasKey(s => s.serverCommandId);
    base.OnModelCreating(modelBuilder);
    }

    public DbSet UserModels { get; set; }
    public DbSet ServerModels { get; set; }
    public DbSet UserExperiences { get; set; }
    public DbSet CryptoModels { get; set; }
    public DbSet UserDashes { get; set; }
    public DbSet DashItems { get; set; }
    public DbSet StatModels { get; set; }
    public DbSet UserStatModels { get; set; }
    public DbSet ReminderModels { get; set; }
    public DbSet CommandModels { get; set; }
    public DbSet ServerCommandModels { get; set; }

    }
    [/cc]

    IdentityUserContext
    [cc lang=”c#”]
    public class IdentityUserContext
    : IdentityUserContext
    where UserModel : IdentityUser
    {
    }
    [/cc]

  • The IdentityUserContext model was added after the addition of the user dashboard. It’s used to override the default IdentityUser model in order to add additional fields to a user when they register / login
  • One of the nice things about the DTO classes is that they can implement IDisposable which allows you to exploit the using statement and gives you an easy way to clean up managed objects.
  • This is a portion of the UserModelDTO class.

    [cc lang=”c#”]
    public UserModelDTO(ApplicationDbContext dbContext)
    {
    _context = dbContext;
    }

    public async Task AddUser(IUser user)
    {
    UserModel newUser = new UserModel()
    {
    userId = user.Id,
    userNameEntry = user.Username,
    isBotAdmin = false,
    hasLinkedAccount = false,
    slowModeEnabled = false,
    slowModeTime = 0
    };

    await _context.UserModels.AddAsync(newUser);
    await _context.SaveChangesAsync();
    }

    public async Task> GetAllUsers()
    {
    List list;
    list = await _context.UserModels
    .ToListAsync();
    return list;
    }

    public async Task GetUser(string? userName, ulong? userId)
    {

    var user = userName != null ? await _context.UserModels
    .FirstOrDefaultAsync(x => x.UserName == userName) :
    userId != null ? await _context.UserModels
    .FirstOrDefaultAsync(x => x.userId == userId) : null;
    if (null == user)
    {
    if (userId != null) return await AddUser((ulong)userId);
    }
    return user;
    }
    [/cc]

  • Moving on to the Blazor Server Dashboard project
  • The main feature of the dashboard is the control panel. This is where users can go to configure the commands avaliable in their servers.
  • Example of the control panel razor page

    [cc lang=”c#”]
    @if (!_isLoggedIn)
    {

    }
    else
    {

    @if (_servers != null)
    {















    }

    }
    [/cc]

  • When running the dashboard currently looks like this but it’s still being worked on:
  • The last thing I want to cover here is how I set this bot up to run in a docker container on a Linode server.
  • When using Visual Studio you have the option to add Docker to any new project. This is awesome because it creates a docker file for you
  • FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
    WORKDIR /app

    FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
    WORKDIR /src
    COPY ["DiscBotConsole/DiscBotConsole.csproj", "DiscBotConsole/"]
    RUN dotnet restore "DiscBotConsole/DiscBotConsole.csproj"
    COPY . .
    WORKDIR "/src/DiscBotConsole"
    RUN dotnet build "DiscBotConsole.csproj" -c Release -o /app/build

    FROM build AS publish
    RUN dotnet publish "DiscBotConsole.csproj" -c Release -o /app/publish
    ENV ASPNETCORE_ENVIRONMENT Development

    FROM base AS final
    WORKDIR /app
    COPY --from=publish /app/publish .

    EXPOSE 80

    ENTRYPOINT ["dotnet", "DiscBotConsole.dll"]

  • Building the docker image is super simple once you have the docker file setup. You can just run something like this in the root of your solution: docker build -t botname:tag .
  • Then you can push the image up to docker hub and ssh into the server where you want to host the bot. Once there you can pull the image with: docker pull botname:tag and run it with docker run botname:tag
  • Starting the bot on my server looks something like this