Always load database config at start

Ensures that the database settings used by ef tools are those in config.
This commit is contained in:
Noi 2022-07-22 23:41:49 -07:00
parent c3ecf2a877
commit 584a55cd60
3 changed files with 62 additions and 35 deletions

View file

@ -1,22 +1,22 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Npgsql;
namespace RegexBot.Data; namespace RegexBot.Data;
/// <summary> /// <summary>
/// Represents a database connection using the settings defined in the bot's global configuration. /// Represents a database connection using the settings defined in the bot's global configuration.
/// </summary> /// </summary>
public class BotDatabaseContext : DbContext { public class BotDatabaseContext : DbContext {
private static string? _npgsqlConnectionString; private static readonly string _connectionString;
internal static string PostgresConnectionString {
#if DEBUG static BotDatabaseContext() {
get { // Get our own config loaded just for the SQL stuff
if (_npgsqlConnectionString != null) return _npgsqlConnectionString; var conf = new InstanceConfig();
Console.WriteLine($"{nameof(RegexBot)} - {nameof(BotDatabaseContext)} note: Using hardcoded connection string!"); _connectionString = new NpgsqlConnectionStringBuilder() {
return _npgsqlConnectionString ?? "Host=localhost;Username=regexbot;Password=rb"; Host = conf.SqlHost ?? "localhost", // default to localhost
} Database = conf.SqlDatabase,
#else Username = conf.SqlUsername,
get => _npgsqlConnectionString!; Password = conf.SqlPassword
#endif }.ToString();
set => _npgsqlConnectionString ??= value;
} }
/// <summary> /// <summary>
@ -37,7 +37,7 @@ public class BotDatabaseContext : DbContext {
/// <inheritdoc /> /// <inheritdoc />
protected sealed override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected sealed override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder => optionsBuilder
.UseNpgsql(PostgresConnectionString) .UseNpgsql(_connectionString)
.UseSnakeCaseNamingConvention(); .UseSnakeCaseNamingConvention();
/// <inheritdoc /> /// <inheritdoc />

View file

@ -1,5 +1,6 @@
using Newtonsoft.Json; using CommandLine;
using RegexBot.Data; using Newtonsoft.Json;
using System.Diagnostics.CodeAnalysis;
using System.Reflection; using System.Reflection;
namespace RegexBot; namespace RegexBot;
@ -18,20 +19,20 @@ class InstanceConfig {
/// List of assemblies to load, by file. Paths are always relative to the bot directory. /// List of assemblies to load, by file. Paths are always relative to the bot directory.
/// </summary> /// </summary>
internal IReadOnlyList<string> Assemblies { get; } internal IReadOnlyList<string> Assemblies { get; }
/// <summary>
/// Webhook URL for bot log reporting.
/// </summary>
internal string InstanceLogTarget { get; } internal string InstanceLogTarget { get; }
// TODO add fields for services to be configurable: DMRelay public string? SqlHost { get; }
public string? SqlDatabase { get; }
public string SqlUsername { get; }
public string SqlPassword { get; }
/// <summary> /// <summary>
/// Sets up instance configuration object from file and command line parameters. /// Sets up instance configuration object from file and command line parameters.
/// </summary> /// </summary>
internal InstanceConfig() { internal InstanceConfig() {
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) var args = CommandLineParameters.Parse(Environment.GetCommandLineArgs());
+ "." + Path.DirectorySeparatorChar + "instance.json"; var path = args?.ConfigFile ?? Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
+ Path.DirectorySeparatorChar + "." + Path.DirectorySeparatorChar + "instance.json";
JObject conf; JObject conf;
try { try {
@ -45,19 +46,8 @@ class InstanceConfig {
throw new Exception(pfx + ex.Message, ex); throw new Exception(pfx + ex.Message, ex);
} }
// Input validation - throw exception on errors. Exception messages printed as-is. BotToken = ReadConfKey<string>(conf, nameof(BotToken), true);
BotToken = conf[nameof(BotToken)]?.Value<string>()!; InstanceLogTarget = ReadConfKey<string>(conf, nameof(InstanceLogTarget), true);
if (string.IsNullOrEmpty(BotToken))
throw new Exception($"'{nameof(BotToken)}' is not properly specified in configuration.");
var pginput = conf[nameof(BotDatabaseContext.PostgresConnectionString)]?.Value<string>()!;
if (string.IsNullOrEmpty(pginput))
throw new Exception($"'{nameof(BotDatabaseContext.PostgresConnectionString)}' is not properly specified in configuration.");
BotDatabaseContext.PostgresConnectionString = pginput;
InstanceLogTarget = conf[nameof(InstanceLogTarget)]?.Value<string>()!;
if (string.IsNullOrEmpty(InstanceLogTarget))
throw new Exception($"'{nameof(InstanceLogTarget)}' is not properly specified in configuration.");
try { try {
Assemblies = Common.Utilities.LoadStringOrStringArray(conf[nameof(Assemblies)]).AsReadOnly(); Assemblies = Common.Utilities.LoadStringOrStringArray(conf[nameof(Assemblies)]).AsReadOnly();
@ -66,5 +56,41 @@ class InstanceConfig {
} catch (ArgumentException) { } catch (ArgumentException) {
throw new Exception($"'{nameof(Assemblies)}' is not properly specified in configuration."); throw new Exception($"'{nameof(Assemblies)}' is not properly specified in configuration.");
} }
SqlHost = ReadConfKey<string>(conf, nameof(SqlHost), false);
SqlDatabase = ReadConfKey<string?>(conf, nameof(SqlDatabase), false);
SqlUsername = ReadConfKey<string>(conf, nameof(SqlUsername), true);
SqlPassword = ReadConfKey<string>(conf, nameof(SqlPassword), true);
}
private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {
if (jc.ContainsKey(key)) return jc[key]!.Value<T>();
if (failOnEmpty) throw new Exception($"'{key}' must be specified in the instance configuration.");
return default;
}
/// <summary>
/// Command line options
/// </summary>
class CommandLineParameters {
[Option('c', "config", Default = null,
HelpText = "Custom path to instance configuration. Defaults to instance.json in bot directory.")]
public string ConfigFile { get; set; } = null!;
/// <summary>
/// Command line arguments parsed here. Depending on inputs, the program can exit here.
/// </summary>
public static CommandLineParameters? Parse(string[] args) {
CommandLineParameters? result = null;
new Parser(settings => {
settings.IgnoreUnknownArguments = true;
settings.AutoHelp = false;
settings.AutoVersion = false;
}).ParseArguments<CommandLineParameters>(args)
.WithParsed(p => result = p)
.WithNotParsed(e => { /* ignore */ });
return result;
}
} }
} }

View file

@ -20,6 +20,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net" Version="3.7.2" /> <PackageReference Include="Discord.Net" Version="3.7.2" />
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" /> <PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.7" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.7" />