Skip to content

Commit

Permalink
feat: Update YumeCore Dependency Injection
Browse files Browse the repository at this point in the history
- Updated the `Program.cs` file to use the new `AddYumeCoreServices` extension method for configuring services.
- Added a new file `YumeCoreDependencyInjectionExtensions.cs` which contains the implementation of the `AddYumeCoreServices` extension method.
- Added a new file `DiscordBotTokenProvider.cs` which provides the Discord bot token from either the `ICoreProperties` or environment variables.
- Updated the project file to enable nullable reference types.
- Updated the `YumeCore.cs` class to use dependency injection for configuring services and initializing core properties.
  • Loading branch information
SakuraIsayeki committed Jul 15, 2023
1 parent 8e24a23 commit e0e9edf
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 141 deletions.
21 changes: 10 additions & 11 deletions src/YumeChan.ConsoleRunner/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static async Task Main(string[] _)
_container.RegisterInstance(new ConsoleRunnerContext(RunnerType.Console, typeof(Program).Assembly.GetName().Name, informationalVersion));

Microsoft.Extensions.Logging.ILogger logger = _container.Resolve<Microsoft.Extensions.Logging.ILogger>();
logger.LogInformation("Yume-Chan ConsoleRunner v{Version}.", informationalVersion);
logger.LogInformation("Yume-Chan ConsoleRunner v{version}.", informationalVersion);

await Task.WhenAll(
host.StartAsync(),
Expand All @@ -50,16 +50,15 @@ public static async Task Main(string[] _)

public static IHostBuilder CreateHostBuilder(UnityContainer serviceRegistry = null) => new HostBuilder()
.UseUnityServiceProvider(serviceRegistry ?? new())
.ConfigureLogging(x => x.ClearProviders())
.ConfigureLogging(static x => x.ClearProviders())
.UseSerilog()
.ConfigureContainer<IUnityContainer>((_, container) =>
{
_container = container; // This assignment is necessary, as configuration only affects the child container.
.ConfigureContainer<IUnityContainer>(static (_, container) =>
{
_container = container; // This assignment is necessary, as configuration only affects the child container.
container.AddExtension(new LoggingExtension(new SerilogLoggerFactory()));
container.AddServices(new ServiceCollection().AddLogging(x => x.AddSerilog()));
YumeCore.Instance.ConfigureContainer(container);
}
);
container.AddExtension(new LoggingExtension(new SerilogLoggerFactory()));
container.AddServices(new ServiceCollection()
.AddYumeCoreServices()
.AddLogging(static x => x.AddSerilog()));
});
}
6 changes: 3 additions & 3 deletions src/YumeChan.Core/Config/ICoreProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,23 @@ public interface ICoreProperties
/// <summary>
/// The path to the bot's working directory.
/// </summary>
public string Path_Core { get; internal set; }
public string? Path_Core { get; internal set; }

/// <summary>
/// The path to the bot's configuration directory.
/// </summary>
/// <value>
/// Defaults to <c>{Path_Core}/config"</c>.
/// </value>
public string Path_Config { get; internal set; }
public string? Path_Config { get; internal set; }

/// <summary>
/// The path to the bot's plugin directory.
/// </summary>
/// <value>
/// Defaults to <c>{Path_Core}/plugins"</c>.
/// </value>
public string Path_Plugins { get; internal set; }
public string? Path_Plugins { get; internal set; }

/// <summary>
/// The prefix used to identify commands.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using DSharpPlus;
using Microsoft.Extensions.Logging;
using YumeChan.Core;
using YumeChan.Core.Config;
using YumeChan.Core.Services;
using YumeChan.Core.Services.Config;
using YumeChan.Core.Services.Plugins;
using YumeChan.PluginBase.Database.MongoDB;
using YumeChan.PluginBase.Database.Postgres;
using YumeChan.PluginBase.Tools;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extension methods for the YumeCore Dependency Injection.
/// </summary>
public static class YumeCoreDependencyInjectionExtensions
{
public static IServiceCollection AddYumeCoreServices(this IServiceCollection services)
{
services.AddSingleton<DiscordClient>(static services => new(new()
{
Intents = DiscordIntents.All,
TokenType = TokenType.Bot,
Token = services.GetRequiredService<DiscordBotTokenProvider>().GetBotToken(), // You should find a way to get this in DI context
LoggerFactory = services.GetService<ILoggerFactory>(),
MinimumLogLevel = LogLevel.Information
}));

services.AddSingleton<PluginsLoader>(serviceProvider =>
new(serviceProvider.GetRequiredService<ICoreProperties>().Path_Plugins));

services.AddSingleton(PluginLifetimeListener.Instance);
services.AddSingleton<CommandHandler>();
services.AddSingleton<LavalinkHandler>();
services.AddSingleton<NugetPluginsFetcher>();
services.AddSingleton<PluginsDependenciesManager>();

Check failure on line 38 in src/YumeChan.Core/Infrastructure/YumeCoreDependencyInjectionExtensions.cs

View workflow job for this annotation

GitHub Actions / build-debug

The type or namespace name 'PluginsDependenciesManager' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 38 in src/YumeChan.Core/Infrastructure/YumeCoreDependencyInjectionExtensions.cs

View workflow job for this annotation

GitHub Actions / build-debug

The type or namespace name 'PluginsDependenciesManager' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 38 in src/YumeChan.Core/Infrastructure/YumeCoreDependencyInjectionExtensions.cs

View workflow job for this annotation

GitHub Actions / build-release

The type or namespace name 'PluginsDependenciesManager' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 38 in src/YumeChan.Core/Infrastructure/YumeCoreDependencyInjectionExtensions.cs

View workflow job for this annotation

GitHub Actions / build-release

The type or namespace name 'PluginsDependenciesManager' could not be found (are you missing a using directive or an assembly reference?)
services.AddSingleton<DiscordBotTokenProvider>();

services.AddSingleton(typeof(InterfaceConfigProvider<>));

services.AddSingleton(typeof(IMongoDatabaseProvider<>), typeof(UnifiedDatabaseProvider<>));
services.AddSingleton(typeof(IPostgresDatabaseProvider<>), typeof(UnifiedDatabaseProvider<>));
services.AddSingleton(typeof(IInterfaceConfigProvider<>), typeof(InterfaceConfigProvider<>));
services.AddSingleton(typeof(IJsonConfigProvider<>), typeof(JsonConfigProvider<>));

services.AddSingleton<ICoreProperties>(sp =>
sp.GetRequiredService<InterfaceConfigProvider<ICoreProperties>>()
.InitConfig("coreconfig.json", true)
.InitDefaults());

services.AddSingleton<IPluginLoaderProperties>(sp =>
sp.GetRequiredService<InterfaceConfigProvider<IPluginLoaderProperties>>()
.InitConfig("plugins.json", true)
.InitDefaults());


services.AddHttpClient()
.AddNuGetPluginsFetcher(); // You should implement this in NuGetPluginsFetcher class as an extension method

return services;
}
}
77 changes: 77 additions & 0 deletions src/YumeChan.Core/Services/DiscordBotTokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using YumeChan.Core.Config;

namespace YumeChan.Core.Services;

/// <summary>
/// Provides the Discord bot token.
/// </summary>
internal sealed class DiscordBotTokenProvider
{
private readonly ICoreProperties _coreProperties;
private readonly ILogger<DiscordBotTokenProvider> _logger;

public DiscordBotTokenProvider(ICoreProperties coreProperties, ILogger<DiscordBotTokenProvider> logger)
{
_coreProperties = coreProperties;
_logger = logger;
}

/// <summary>
/// Gets the Discord bot token from either the <see cref="ICoreProperties"/>, or from the environment variables.
/// </summary>
/// <returns>The Discord bot token.</returns>
/// <exception cref="ApplicationException">Thrown if no bot token was supplied.</exception>
public string GetBotToken()
{
string? token = _coreProperties.BotToken;

if (!string.IsNullOrWhiteSpace(token))
{
return token;
}

string envVarName = $"{_coreProperties.AppInternalName}.Token";

if (TryBotTokenFromEnvironment(envVarName, out token, out EnvironmentVariableTarget target))
{
_logger.LogInformation("Bot Token was read from {target} Environment Variable \"{envVar}\", instead of \"coreproperties.json\" Config File", target, envVarName);
return token;
}

ApplicationException e = new("No Bot Token supplied.");
_logger.LogCritical(e, "No Bot Token was found in \"coreconfig.json\" Config File, and Environment Variables \"{envVar}\" from relevant targets are empty. Please set a Bot Token before launching the Bot", envVarName);

throw e;
}

private static bool TryBotTokenFromEnvironment(string envVarName, [NotNullWhen(true)] out string? token, out EnvironmentVariableTarget foundFromTarget)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
foreach (EnvironmentVariableTarget target in typeof(EnvironmentVariableTarget).GetEnumValues())
{
token = Environment.GetEnvironmentVariable(envVarName, target);

if (token is not null)
{
foundFromTarget = target;

return true;
}
}

token = null;
foundFromTarget = default;

return false;
}

token = Environment.GetEnvironmentVariable(envVarName);
foundFromTarget = EnvironmentVariableTarget.Process;

return token is not null;
}
}
1 change: 1 addition & 0 deletions src/YumeChan.Core/YumeChan.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<SignAssembly>false</SignAssembly>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
Expand Down
134 changes: 22 additions & 112 deletions src/YumeChan.Core/YumeCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public enum YumeCoreState

public sealed class YumeCore
{
public static YumeCore Instance => _instance ??= new();
public static YumeCore Instance => _instance!;
private static YumeCore? _instance;

public YumeCoreState CoreState { get; private set; }
Expand All @@ -42,59 +42,40 @@ public sealed class YumeCore
public ICoreProperties CoreProperties { get; private set; }


public YumeCore() { }
public YumeCore(IUnityContainer services)
{
Services = services;

Logger = Services.Resolve<ILogger<YumeCore>>();
ConfigProvider = Services.Resolve<InterfaceConfigProvider<ICoreProperties>>();

CoreProperties = ConfigProvider.InitConfig("coreconfig.json", true).InitDefaults();
CoreProperties.Path_Core ??= Directory.GetCurrentDirectory();
CoreProperties.Path_Plugins ??= Path.Combine(CoreProperties.Path_Core, "Plugins");
CoreProperties.Path_Config ??= Path.Combine(CoreProperties.Path_Core, "Config");

Client = Services.Resolve<DiscordClient>();
CommandHandler = Services.Resolve<CommandHandler>();
CommandHandler.Config = CoreProperties;

LavalinkHandler = Services.Resolve<LavalinkHandler>();
LavalinkHandler.Config = CoreProperties.LavalinkProperties;

_instance = this;
}

~YumeCore()
{
StopBotAsync().Wait();
}

public IUnityContainer ConfigureContainer(IUnityContainer container) => container
.RegisterFactory<DiscordClient>(uc => new DiscordClient(new()
{
Intents = DiscordIntents.All,
TokenType = TokenType.Bot,
Token = GetBotToken(),
LoggerFactory = uc.Resolve<ILoggerFactory>(),
MinimumLogLevel = LogLevel.Information
}),
FactoryLifetime.Singleton
)

.RegisterFactory<PluginsLoader>(static uc => new PluginsLoader(uc.Resolve<ICoreProperties>().Path_Plugins), FactoryLifetime.Singleton)

.RegisterInstance(PluginLifetimeListener.Instance)
.RegisterSingleton<CommandHandler>()
.RegisterSingleton<LavalinkHandler>()
.RegisterSingleton<NugetPluginsFetcher>()
.RegisterSingleton(typeof(IMongoDatabaseProvider<>), typeof(UnifiedDatabaseProvider<>))
.RegisterSingleton(typeof(IPostgresDatabaseProvider<>), typeof(UnifiedDatabaseProvider<>))
.RegisterSingleton(typeof(IInterfaceConfigProvider<>), typeof(InterfaceConfigProvider<>))
.RegisterSingleton(typeof(IJsonConfigProvider<>), typeof(JsonConfigProvider<>))

.RegisterFactory<ICoreProperties>(
static uc => uc.Resolve<InterfaceConfigProvider<ICoreProperties>>().InitConfig("coreconfig.json", true).InitDefaults(),
FactoryLifetime.Singleton)

.RegisterFactory<IPluginLoaderProperties>(
static uc => uc.Resolve<InterfaceConfigProvider<IPluginLoaderProperties>>().InitConfig("plugins.json", true).InitDefaults(),
FactoryLifetime.Singleton)

.AddServices(new ServiceCollection()
.AddHttpClient()
.AddNuGetPluginsFetcher()
);


public async Task StartBotAsync()
{
if (Services is null)
{
throw new InvalidOperationException("Service Provider has not been defined.", new ArgumentNullException(nameof(Services)));
}

ResolveCoreComponents();

Logger.LogInformation("YumeCore v{version}", CoreVersion);

CoreState = YumeCoreState.Starting;
Expand Down Expand Up @@ -136,75 +117,4 @@ public async Task ReloadCommandsAsync()
await CommandHandler.RegisterCommandsAsync();
CoreState = YumeCoreState.Online;
}


private string GetBotToken()
{
string? token = CoreProperties.BotToken;

if (!string.IsNullOrWhiteSpace(token))
{
return token;
}

string envVarName = $"{CoreProperties.AppInternalName}.Token";

if (TryBotTokenFromEnvironment(envVarName, out token, out EnvironmentVariableTarget target))
{
Logger.LogInformation("Bot Token was read from {target} Environment Variable \"{envVar}\", instead of \"coreproperties.json\" Config File", target, envVarName);
return token;
}

ApplicationException e = new("No Bot Token supplied.");
Logger.LogCritical(e, "No Bot Token was found in \"coreconfig.json\" Config File, and Environment Variables \"{envVar}\" from relevant targets are empty. Please set a Bot Token before launching the Bot", envVarName);

throw e;
}

private static bool TryBotTokenFromEnvironment(string envVarName, [NotNullWhen(true)] out string? token, out EnvironmentVariableTarget foundFromTarget)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
foreach (EnvironmentVariableTarget target in typeof(EnvironmentVariableTarget).GetEnumValues())
{
token = Environment.GetEnvironmentVariable(envVarName, target);

if (token is not null)
{
foundFromTarget = target;

return true;
}
}

token = null;
foundFromTarget = default;

return false;
}

token = Environment.GetEnvironmentVariable(envVarName);
foundFromTarget = EnvironmentVariableTarget.Process;

return token is not null;
}

[SuppressMessage("ReSharper", "NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract")]
private void ResolveCoreComponents()
{
Logger ??= Services.Resolve<ILogger<YumeCore>>();
ConfigProvider ??= Services.Resolve<InterfaceConfigProvider<ICoreProperties>>();

CoreProperties = ConfigProvider.InitConfig("coreconfig.json", true).InitDefaults();
CoreProperties.Path_Core ??= Directory.GetCurrentDirectory();
CoreProperties.Path_Plugins ??= Path.Combine(CoreProperties.Path_Core, "Plugins");
CoreProperties.Path_Config ??= Path.Combine(CoreProperties.Path_Core, "Config");

Client ??= Services.Resolve<DiscordClient>();
CommandHandler ??= Services.Resolve<CommandHandler>();
CommandHandler.Config ??= CoreProperties;

LavalinkHandler ??= Services.Resolve<LavalinkHandler>();
LavalinkHandler.Config ??= CoreProperties.LavalinkProperties;
}
}
Loading

0 comments on commit e0e9edf

Please sign in to comment.