Welcome
A database can be used to store and retrieve data that can make your bot even more robust. In this post, I will be going over how to create a database, and use it to expand our 8Ball command.
Prerequisites
.NET Core 3.x
A fundamental understanding of the Discord.Net library, or following along step-by-step with (if you’re working on a local bot, just omit the Raspberry Pi steps!):
Navigation
Setting things up
This post will be building off of the code found in the logging post, found here: https://github.com/gngrninja/csharpi/tree/04-efdb
If you‘d like to go through the logging post first, check that out as well, here:
Let’s add the package we’ll need to get started using EF Core w/sqlite.
Drop down to a console, navigate to your project folder, and run:
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet restore
Creating our database model
Now we’ll want to create a model for Entity Framework Core to use when generating the database. The model will be converted into a table.
To do this, we’ll:
Create a folder in our project root named Database
In the Database folder, create two files:
CsharpiContext.cs
EightBallAnswer.cs
Let’s start defining our EightBallAnswer, by adding the following to the EightBallAnswer.cs file:
https://github.com/gngrninja/csharpi/blob/04-efdb/Database/EightBallAnswer.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 | using System; using System.ComponentModel.DataAnnotations; namespace csharpi.Database { public partial class EightBallAnswer { [Key] public long AnswerId { get; set; } public string AnswerText { get; set; } public string AnswerColor { get; set; } } } |
Now we can create our DbSet in CsharpEntities.cs, which will tell EF Core what we want our database to look like:
https://github.com/gngrninja/csharpi/blob/04-efdb/Database/CsharpiEntities.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace csharpi.Database { public partial class CsharpiEntities : DbContext { public virtual DbSet<EightBallAnswer> EightBallAnswer { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = "csharpi.db" }; var connectionString = connectionStringBuilder.ToString(); var connection = new SqliteConnection(connectionString); optionsBuilder.UseSqlite(connection); } } } |
Now let’s add some pieces to our Program.cs file:
At the top:
using csharpi.Database;
And to inject the DB Context via Dependency Injection:
.AddDbContext<CsharpiEntities>()
That will make Program.cs look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | using System; using Discord; using Discord.Net; using Discord.Commands; using Discord.WebSocket; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; using csharpi.Services; using System.Linq; using Serilog; using csharpi.Database; namespace csharpi { class Program { // setup our fields we assign later private readonly IConfiguration _config; private DiscordSocketClient _client; private static string _logLevel; static void Main(string[] args = null) { if (args.Count() != 0) { _logLevel = args[0]; } Log.Logger = new LoggerConfiguration() .WriteTo.File("logs/csharpi.log", rollingInterval: RollingInterval.Day) .WriteTo.Console() .CreateLogger(); new Program().MainAsync().GetAwaiter().GetResult(); } public Program() { // create the configuration var _builder = new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddJsonFile(path: "config.json"); // build the configuration and assign to _config _config = _builder.Build(); } 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<T> var client = services.GetRequiredService<DiscordSocketClient>(); _client = client; // setup logging and the ready event services.GetRequiredService<LoggingService>(); // this is where we get the Token value from the configuration file, and start the bot await client.LoginAsync(TokenType.Bot, _config["Token"]); await client.StartAsync(); // we get the CommandHandler class here and call the InitializeAsync method to start things up for the CommandHandler service await services.GetRequiredService<CommandHandler>().InitializeAsync(); await Task.Delay(-1); } } private Task LogAsync(LogMessage log) { Console.WriteLine(log.ToString()); return Task.CompletedTask; } private Task ReadyAsync() { Console.WriteLine($"Connected as -> [{_client.CurrentUser}] :)"); return Task.CompletedTask; } // this method handles the ServiceCollection creation/configuration, and builds out the service provider we can call on later private ServiceProvider ConfigureServices() { // this returns a ServiceProvider that is used later to call for those services // we can add types we have access to here, hence adding the new using statement: // using csharpi.Services; // the config we build is also added, which comes in handy for setting the command prefix! var services = new ServiceCollection() .AddSingleton(_config) .AddSingleton<DiscordSocketClient>() .AddSingleton<CommandService>() .AddSingleton<CommandHandler>() .AddSingleton<LoggingService>() .AddDbContext<CsharpiEntities>() .AddLogging(configure => configure.AddSerilog()); if (!string.IsNullOrEmpty(_logLevel)) { switch (_logLevel.ToLower()) { case "info": { services.Configure<LoggerFilterOptions>(options => options.MinLevel = LogLevel.Information); break; } case "error": { services.Configure<LoggerFilterOptions>(options => options.MinLevel = LogLevel.Error); break; } case "debug": { services.Configure<LoggerFilterOptions>(options => options.MinLevel = LogLevel.Debug); break; } default: { services.Configure<LoggerFilterOptions>(options => options.MinLevel = LogLevel.Error); break; } } } else { services.Configure<LoggerFilterOptions>(options => options.MinLevel = LogLevel.Information); } var serviceProvider = services.BuildServiceProvider(); return serviceProvider; } } } |
Create the Database File
Now that we’ve defined what we want our database to look like, we can run some commands to get it created. Run these commands while in the root of your project folder:
Install tooling for EF in .NET Core
dotnet tool install --global dotnet-ef dotnet add package Microsoft.EntityFrameworkCore.Design
Create the database using our model
dotnet ef migrations add InitialCreate dotnet ef database update
If all went well, you should see a new file named csharpi.db:
Taking a peek at the DB
To take a look and verify the database was created as per our defined model, you can use this tool: https://sqlitebrowser.org/
Here is what the file I created looks like:
Add Eight Ball Commands
Now we will want to add some commands to the bot that will allow us to:
Add an answer w/associated color to the database
List out answers currently in the database
Remove an answer from the database
Ask the 8Ball a question
The first thing we will want to do is remove the 8Ball command from the Modules/ExampleCommands.cs file. After removing it, ExampleCommands.cs should look like this:
https://github.com/gngrninja/csharpi/blob/04-efdb/Modules/ExampleCommands.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | using Discord; using Discord.Net; using Discord.WebSocket; using Discord.Commands; using System; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Collections.Generic; using Microsoft.Extensions.Configuration; namespace csharpi.Modules { // for commands to be available, and have the Context passed to them, we must inherit ModuleBase public class ExampleCommands : ModuleBase { [Command("hello")] public async Task HelloCommand() { // initialize empty string builder for reply var sb = new StringBuilder(); // get user info from the Context var user = Context.User; // build out the reply sb.AppendLine($"You are -> [{user.Username}]"); sb.AppendLine("I must now say, World!"); // send simple string reply await ReplyAsync(sb.ToString()); } } } |
The next thing we will do is add all of our eight ball handling commands to a new file in the Modules folder named EightBallCommands.cs
The contents for EightBallCommands.cs should be as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | using Discord; using Discord.Net; using Discord.WebSocket; using Discord.Commands; using System; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Collections.Generic; using Microsoft.Extensions.Configuration; using csharpi.Database; using Microsoft.Extensions.DependencyInjection; namespace csharpi.Modules { // for commands to be available, and have the Context passed to them, we must inherit ModuleBase public class EightBallCommands : ModuleBase { private readonly CsharpiEntities _db; private List<String> _validColors = new List<String>(); private readonly IConfiguration _config; public EightBallCommands(IServiceProvider services) { // we can pass in the db context via depedency injection _db = services.GetRequiredService<CsharpiEntities>(); _config = services.GetRequiredService<IConfiguration>(); _validColors.Add("green"); _validColors.Add("red"); _validColors.Add("blue"); } [Command("add")] public async Task AddResponse(string answer, string color) { var sb = new StringBuilder(); var embed = new EmbedBuilder(); // get user info from the Context var user = Context.User; // check to see if the color is valid if (!_validColors.Contains(color.ToLower())) { sb.AppendLine($"**Sorry, [{user.Username}], you must specify a valid color.**"); sb.AppendLine("Valid colors are:"); sb.AppendLine(); foreach (var validColor in _validColors) { sb.AppendLine($"{validColor}"); } embed.Color = new Color(255, 0, 0); } else { // add answer/color to table await _db.AddAsync(new EightBallAnswer { AnswerText = answer, AnswerColor = color.ToLower() } ); // save changes to database await _db.SaveChangesAsync(); sb.AppendLine(); sb.AppendLine("**Added answer:**"); sb.AppendLine(answer); sb.AppendLine(); sb.AppendLine("**With color:**"); sb.AppendLine(color); embed.Color = new Color(0, 255, 0); } // set embed embed.Title = "Eight Ball Answer Addition"; embed.Description = sb.ToString(); // send embed reply await ReplyAsync(null, false, embed.Build()); } [Command("list")] public async Task ListAnswers() { var sb = new StringBuilder(); var embed = new EmbedBuilder(); // get user info from the Context var user = Context.User; var answers = await _db.EightBallAnswer.ToListAsync(); if (answers.Count > 0) { foreach (var answer in answers) { sb.AppendLine($":small_blue_diamond: [{answer.AnswerId}] **{answer.AnswerText}**"); } } else { sb.AppendLine("No answers found!"); } // set embed embed.Title = "Eight Ball Answer List"; embed.Description = sb.ToString(); // send embed reply await ReplyAsync(null, false, embed.Build()); } [Command("remove")] public async Task RemoveAnswer(int id) { var sb = new StringBuilder(); var embed = new EmbedBuilder(); // get user info from the Context var user = Context.User; var answers = await _db.EightBallAnswer.ToListAsync(); var answerToRemove = answers.Where(a => a.AnswerId == id).FirstOrDefault(); if (answerToRemove != null) { _db.Remove(answerToRemove); await _db.SaveChangesAsync(); sb.AppendLine($"Removed answer -> [{answerToRemove.AnswerText}]"); } else { sb.AppendLine($"Did not find answer with id [**{id}**] in the database"); sb.AppendLine($"Perhaps use the {_config["prefix"]}list command to list out answers"); } // set embed embed.Title = "Eight Ball Answer List"; embed.Description = sb.ToString(); // send embed reply await ReplyAsync(null, false, embed.Build()); } [Command("8ball")] [Alias("ask")] public async Task AskEightBall([Remainder]string args = null) { // I like using StringBuilder to build out the reply var sb = new StringBuilder(); // let's use an embed for this one! var embed = new EmbedBuilder(); // add our possible replies from the database var replies = await _db.EightBallAnswer.ToListAsync(); // add a title embed.Title = "Welcome to the 8-ball!"; // we can get lots of information from the Context that is passed into the commands // here I'm setting up the preface with the user's name and a comma sb.AppendLine($"{Context.User.Username},"); sb.AppendLine(); // let's make sure the supplied question isn't null if (args == null) { // if no question is asked (args are null), reply with the below text sb.AppendLine("Sorry, can't answer a question you didn't ask!"); } else { // if we have a question, let's give an answer! // get a random number to index our list with var answer = replies[new Random().Next(replies.Count)]; // build out our reply with the handy StringBuilder sb.AppendLine($"You asked: [**{args}**]..."); sb.AppendLine(); sb.AppendLine($"...your answer is [**{answer.AnswerText}**]"); switch (answer.AnswerColor) { case "red": { embed.WithColor(255, 0, 0); break; } case "blue": { embed.WithColor(0, 0, 255); break; } case "green": { embed.WithColor(0, 255, 0); break; } } } // now we can assign the description of the embed to the contents of the StringBuilder we created embed.Description = sb.ToString(); // this will reply with the embed await ReplyAsync(null, false, embed.Build()); } } } |
Testing it out
Let’s test it out!
Adding answers
+add "yes!" "green"
+add "that's a no, bob!" "red"
+add "I dono" "blue"
Listing answers
+list
Removing answers
+remove 3
Asking a question
+8ball is the sky blue?
I asked it a couple times so I could see the different responses and color changes.
Conclusion
This is a very basic example of what you can do with a simple database, created via code, for your Discord Bot to use. If you have any questions or feedback, please leave a comment below!