diff --git a/DisCatSharp.Configuration/ConfigurationExtensions.cs b/DisCatSharp.Configuration/ConfigurationExtensions.cs
index 0f7729d29..bd7d8f40f 100644
--- a/DisCatSharp.Configuration/ConfigurationExtensions.cs
+++ b/DisCatSharp.Configuration/ConfigurationExtensions.cs
@@ -1,313 +1,315 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections;
using System.Linq;
using DisCatSharp.Configuration.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace DisCatSharp.Configuration
{
///
/// The configuration extensions.
///
internal static class ConfigurationExtensions
{
///
/// The factory error message.
///
private const string FACTORY_ERROR_MESSAGE = "Require a function which provides a default entity to work with";
///
/// The default root lib.
///
public const string DEFAULT_ROOT_LIB = "DisCatSharp";
///
/// The config suffix.
///
private const string CONFIG_SUFFIX = "Configuration";
///
/// Easily piece together paths that will work within
///
/// (not used - only for adding context based functionality)
/// The strings to piece together
/// Strings joined together via ':'
public static string ConfigPath(this IConfiguration config, params string[] values) => string.Join(":", values);
///
/// Skims over the configuration section and only overrides values that are explicitly defined within the config
///
/// Instance of config
/// Section which contains values for
private static void HydrateInstance(ref object config, ConfigSection section)
{
var props = config.GetType().GetProperties();
foreach (var prop in props)
{
// Must have a set method for this to work, otherwise continue on
if (prop.SetMethod == null)
continue;
var entry = section.GetValue(prop.Name);
object? value = null;
if (typeof(string) == prop.PropertyType)
{
// We do NOT want to override value if nothing was provided
if (!string.IsNullOrEmpty(entry))
prop.SetValue(config, entry);
continue;
}
// We need to address collections a bit differently
// They can come in the form of "root:section:name" with a string representation OR
// "root:section:name:0" <--- this is not detectable when checking the above path
if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType))
{
value = string.IsNullOrEmpty(section.GetValue(prop.Name))
? section.Config
.GetSection(section.GetPath(prop.Name)).Get(prop.PropertyType)
: Newtonsoft.Json.JsonConvert.DeserializeObject(entry, prop.PropertyType);
if (value == null)
continue;
prop.SetValue(config, value);
}
// From this point onward we require the 'entry' value to have something useful
if (string.IsNullOrEmpty(entry))
continue;
try
{
// Primitive types are simple to convert
if (prop.PropertyType.IsPrimitive)
value = Convert.ChangeType(entry, prop.PropertyType);
else
{
// The following types require a different approach
if (prop.PropertyType.IsEnum)
value = Enum.Parse(prop.PropertyType, entry);
else if (typeof(TimeSpan) == prop.PropertyType)
value = TimeSpan.Parse(entry);
else if (typeof(DateTime) == prop.PropertyType)
value = DateTime.Parse(entry);
else if (typeof(DateTimeOffset) == prop.PropertyType)
value = DateTimeOffset.Parse(entry);
}
// Update value within our config instance
prop.SetValue(config, value);
}
catch (Exception ex)
{
Console.Error.WriteLine(
$"Unable to convert value of '{entry}' to type '{prop.PropertyType.Name}' for prop '{prop.Name}' in config '{config.GetType().Name}'\n\t\t{ex.Message}");
}
}
}
///
/// Instantiate an entity using then walk through the specified
/// and translate user-defined config values to the instantiated instance from
///
/// Section containing values for targeted config
/// Function which generates a default entity
/// Hydrated instance of an entity which contains user-defined values (if any)
/// When is null
public static object ExtractConfig(this ConfigSection section, Func factory)
{
if (factory == null)
throw new ArgumentNullException(nameof(factory), FACTORY_ERROR_MESSAGE);
// Create default instance
var config = factory();
HydrateInstance(ref config, section);
return config;
}
///
/// Instantiate an entity using then walk through the specified
/// in . Translate user-defined config values to the instantiated instance from
///
/// Loaded App Configuration
/// Name of section to load
/// Function which creates a default entity to work with
/// (Optional) Used when section is nested within another. Default value is
/// Hydrated instance of an entity which contains user-defined values (if any)
/// When is null
public static object ExtractConfig(this IConfiguration config, string sectionName, Func factory,
string? rootSectionName = DEFAULT_ROOT_LIB)
{
if (factory == null)
throw new ArgumentNullException(nameof(factory), FACTORY_ERROR_MESSAGE);
// create default instance
var instance = factory();
HydrateInstance(ref instance, new ConfigSection(ref config, sectionName, rootSectionName));
return instance;
}
///
/// Instantiate a new instance of , then walk through the specified
/// in . Translate user-defined config values to the instance.
///
/// Loaded App Configuration
///
/// Name of section to load
/// (Optional) Used when section is nested with another. Default value is
/// Type of instance that represents
/// Hydrated instance of which contains the user-defined values (if any).
public static TConfig ExtractConfig(this IConfiguration config, IServiceProvider serviceProvider, string sectionName, string? rootSectionName = DEFAULT_ROOT_LIB)
where TConfig : new()
{
// Default values should hopefully be provided from the constructor
var configInstance = ActivatorUtilities.CreateInstance(serviceProvider, typeof(TConfig));
HydrateInstance(ref configInstance, new ConfigSection(ref config, sectionName, rootSectionName));
return (TConfig)configInstance;
}
///
/// Instantiate a new instance of , then walk through the specified
/// in . Translate user-defined config values to the instance.
///
/// Loaded App Configuration
/// Name of section to load
/// (Optional) Used when section is nested with another. Default value is
/// Type of instance that represents
/// Hydrated instance of which contains the user-defined values (if any).
public static TConfig ExtractConfig(this IConfiguration config, string sectionName, string? rootSectionName = DEFAULT_ROOT_LIB)
where TConfig : new()
{
// Default values should hopefully be provided from the constructor
object configInstance = new TConfig();
HydrateInstance(ref configInstance, new ConfigSection(ref config, sectionName, rootSectionName));
return (TConfig)configInstance;
}
///
/// Determines if contains a particular section/object (not value)
///
///
///
/// {
/// "Discord": { // this is a section/object
///
/// },
/// "Value": "something" // this is not a section/object
/// }
///
///
///
///
/// True if section exists, otherwise false
public static bool HasSection(this IConfiguration config, params string[] values)
{
if (!values.Any())
return false;
if (values.Length == 1)
return config.GetChildren().Any(x => x.Key == values[0]);
if (config.GetChildren().All(x => x.Key != values[0]))
return false;
var current = config.GetSection(values[0]);
for (var i = 1; i < values.Length - 1; i++)
{
if (current.GetChildren().All(x => x.Key != values[i]))
return false;
current = current.GetSection(values[i]);
}
return current.GetChildren().Any(x => x.Key == values[^1]);
}
///
/// Instantiates an instance of , then consumes any custom
/// configuration from user/developer from .
/// View remarks for more info
///
///
/// This is an example of how your JSON structure should look if you wish
/// to override one or more of the default values from
///
/// {
/// "DisCatSharp": {
/// "Discord": { }
/// }
/// }
///
///
/// Alternatively, you can use the type name itself
///
/// {
/// "DisCatSharp": {
/// "DiscordConfiguration": { }
/// }
/// }
///
///
/// {
/// "botSectionName": {
/// "DiscordConfiguration": { }
/// }
/// }
///
///
///
///
///
+ ///
/// Instance of
public static DiscordClient BuildClient(this IConfiguration config, IServiceProvider serviceProvider,
- string botSectionName = DEFAULT_ROOT_LIB)
+ string botSectionName = DEFAULT_ROOT_LIB, ILoggerFactory? logger = null)
{
var section = config.HasSection(botSectionName, "Discord")
? "Discord"
: config.HasSection(botSectionName, $"Discord{CONFIG_SUFFIX}")
? $"Discord:{CONFIG_SUFFIX}"
: null;
- return string.IsNullOrEmpty(section)
- ? new DiscordClient(new DiscordConfiguration(serviceProvider))
- : new DiscordClient(config.ExtractConfig(serviceProvider, section, botSectionName));
+ var cfg = string.IsNullOrEmpty(section) ? new DiscordConfiguration(serviceProvider) : config.ExtractConfig(serviceProvider, section, botSectionName);
+ cfg.LoggerFactory = logger;
+ return new DiscordClient(cfg);
}
}
}
diff --git a/DisCatSharp.Hosting/BaseHostedService.cs b/DisCatSharp.Hosting/BaseHostedService.cs
index a5f0720ca..73fbe2044 100644
--- a/DisCatSharp.Hosting/BaseHostedService.cs
+++ b/DisCatSharp.Hosting/BaseHostedService.cs
@@ -1,206 +1,210 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DisCatSharp.Hosting
{
///
/// Contains the common logic between having a or
/// as a Hosted Service
///
public abstract class BaseHostedService : BackgroundService
{
protected readonly ILogger Logger;
protected readonly IHostApplicationLifetime ApplicationLifetime;
protected readonly IConfiguration Configuration;
protected readonly IServiceProvider ServiceProvider;
protected readonly string BotSection;
+ protected readonly ILoggerFactory? LoggerFactory;
///
/// Initializes a new instance of the class.
///
/// The config.
/// The logger.
/// The service provider.
/// The application lifetime.
/// The config bot section.
+ /// The logger factory.
internal BaseHostedService(IConfiguration config,
ILogger logger,
IServiceProvider serviceProvider,
IHostApplicationLifetime applicationLifetime,
- string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB)
+ string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB,
+ ILoggerFactory? loggerFactory = null)
{
this.Configuration = config;
this.Logger = logger;
this.ApplicationLifetime = applicationLifetime;
this.ServiceProvider = serviceProvider;
this.BotSection = configBotSection;
+ this.LoggerFactory = loggerFactory;
}
///
/// When the bot(s) fail to start, this method will be invoked. (Default behavior is to shutdown)
///
/// The exception/reason for not starting
protected virtual void OnInitializationError(Exception ex) => this.ApplicationLifetime.StopApplication();
///
/// Connect your client(s) to Discord
///
/// Task
protected abstract Task ConnectAsync();
///
/// Dynamically load extensions by using and
///
///
/// Client to add extension method(s) to
/// Task
protected Task InitializeExtensions(DiscordClient client)
{
var typeMap = this.Configuration.FindImplementedExtensions(this.BotSection);
this.Logger.LogDebug($"Found the following config types: {string.Join("\n\t", typeMap.Keys)}");
foreach (var typePair in typeMap)
try
{
/*
If section is null --> utilize the default constructor
This means the extension was explicitly added in the 'Using' array,
but user did not wish to override any value(s) in the extension's config
*/
var configInstance = typePair.Value.Section.HasValue
? typePair.Value.Section.Value.ExtractConfig(() =>
ActivatorUtilities.CreateInstance(this.ServiceProvider, typePair.Value.ConfigType))
: ActivatorUtilities.CreateInstance(this.ServiceProvider, typePair.Value.ConfigType);
/*
Explanation for bindings
Internal Constructors --> NonPublic
Public Constructors --> Public
Constructors --> Instance
*/
var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
var ctors = typePair.Value.ImplementationType.GetConstructors(flags);
var instance = ctors.Any(x => x.GetParameters().Length == 1 && x.GetParameters().First().ParameterType == typePair.Value.ConfigType)
? Activator.CreateInstance(typePair.Value.ImplementationType, flags, null,
new[] { configInstance }, null)
: Activator.CreateInstance(typePair.Value.ImplementationType, true);
/*
Certain extensions do not require a configuration argument
Those who do -- pass config instance in,
Those who don't -- simply instantiate
ActivatorUtilities requires a public constructor, anything with internal breaks
*/
if (instance == null)
{
this.Logger.LogError($"Unable to instantiate '{typePair.Value.ImplementationType.Name}'");
continue;
}
// Add an easy reference to our extensions for later use
client.AddExtension((BaseExtension)instance);
}
catch (Exception ex)
{
this.Logger.LogError($"Unable to register '{typePair.Value.ImplementationType.Name}': \n\t{ex.Message}");
this.OnInitializationError(ex);
}
return Task.CompletedTask;
}
///
/// Configure / Initialize the or
///
///
///
protected abstract Task ConfigureAsync();
///
/// Configure the extensions for your or
///
///
///
protected abstract Task ConfigureExtensionsAsync();
///
/// Runs just prior to .
///
///
protected virtual Task PreConnectAsync() => Task.CompletedTask;
///
/// Runs immediately after .
///
/// Task
protected virtual Task PostConnectAsync() => Task.CompletedTask;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
await this.ConfigureAsync();
await this.PreConnectAsync();
await this.ConnectAsync();
await this.ConfigureExtensionsAsync();
await this.PostConnectAsync();
}
catch (Exception ex)
{
/*
* Anything before DOTNET 6 will
* fail silently despite throwing an exception in this method
* So to overcome this obstacle we need to log what happens and
* manually exit
*/
this.Logger.LogError($"Was unable to start {this.GetType().Name} Bot as a Hosted Service");
// Power given to developer for handling exception
this.OnInitializationError(ex);
}
// Wait indefinitely -- but use stopping token so we can properly cancel if needed
await Task.Delay(-1, stoppingToken);
}
}
}
diff --git a/DisCatSharp.Hosting/DiscordHostedService.cs b/DisCatSharp.Hosting/DiscordHostedService.cs
index c55b53a8e..932e50562 100644
--- a/DisCatSharp.Hosting/DiscordHostedService.cs
+++ b/DisCatSharp.Hosting/DiscordHostedService.cs
@@ -1,84 +1,86 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Threading.Tasks;
using DisCatSharp.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DisCatSharp.Hosting
{
///
/// Simple implementation for to work as a
///
public abstract class DiscordHostedService : BaseHostedService, IDiscordHostedService
{
///
public DiscordClient Client { get; protected set; }
#pragma warning disable 8618
///
/// Initializes a new instance of the class.
///
/// IConfiguration provided via Dependency Injection. Aggregate method to access configuration files
/// An ILogger to work with, provided via Dependency Injection
/// ServiceProvider reference which contains all items currently registered for Dependency Injection
/// Contains the appropriate methods for disposing / stopping BackgroundServices during runtime
/// The name of the JSON/Config Key which contains the configuration for this Discord Service
+ /// The logger factory to use for the bots logging.
protected DiscordHostedService(IConfiguration config,
ILogger logger,
IServiceProvider serviceProvider,
IHostApplicationLifetime applicationLifetime,
- string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB)
- : base(config, logger, serviceProvider, applicationLifetime, configBotSection)
+ string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB,
+ ILoggerFactory? loggerFactory = null)
+ : base(config, logger, serviceProvider, applicationLifetime, configBotSection, loggerFactory)
{
}
#pragma warning restore 8618
protected override Task ConfigureAsync()
{
try
{
- this.Client = this.Configuration.BuildClient(this.ServiceProvider, this.BotSection);
+ this.Client = this.Configuration.BuildClient(this.ServiceProvider, this.BotSection, this.LoggerFactory);
}
catch (Exception ex)
{
this.Logger.LogError($"Was unable to build {nameof(DiscordClient)} for {this.GetType().Name}");
this.OnInitializationError(ex);
}
return Task.CompletedTask;
}
protected sealed override async Task ConnectAsync() => await this.Client.ConnectAsync();
protected override Task ConfigureExtensionsAsync()
{
this.InitializeExtensions(this.Client);
return Task.CompletedTask;
}
}
}
diff --git a/DisCatSharp.Hosting/DiscordSharedHostedService.cs b/DisCatSharp.Hosting/DiscordSharedHostedService.cs
index d4a15b019..0653f6285 100644
--- a/DisCatSharp.Hosting/DiscordSharedHostedService.cs
+++ b/DisCatSharp.Hosting/DiscordSharedHostedService.cs
@@ -1,89 +1,92 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Threading.Tasks;
using DisCatSharp.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DisCatSharp.Hosting
{
///
/// Simple Implementation for to work as a
///
public abstract class DiscordShardedHostedService : BaseHostedService, IDiscordHostedShardService
{
public DiscordShardedClient ShardedClient { get; protected set; }
#pragma warning disable 8618
///
/// Initializes a new instance of the class.
///
/// The config.
/// The logger.
/// The service provider.
/// The application lifetime.
/// The config bot section.
+ ///
protected DiscordShardedHostedService(IConfiguration config,
ILogger logger,
IServiceProvider serviceProvider,
IHostApplicationLifetime applicationLifetime,
- string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB)
- : base(config, logger, serviceProvider, applicationLifetime, configBotSection)
+ string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB,
+ ILoggerFactory? loggerFactory = null)
+ : base(config, logger, serviceProvider, applicationLifetime, configBotSection, loggerFactory)
{
}
#pragma warning restore 8618
protected override Task ConfigureAsync()
{
try
{
var config = this.Configuration.ExtractConfig(this.ServiceProvider, "Discord", this.BotSection);
+ config.LoggerFactory = this.LoggerFactory;
this.ShardedClient = new DiscordShardedClient(config);
}
catch (Exception ex)
{
this.Logger.LogError($"Was unable to build {nameof(DiscordShardedClient)} for {this.GetType().Name}");
this.OnInitializationError(ex);
}
return Task.CompletedTask;
}
protected sealed override async Task ConnectAsync() => await this.ShardedClient.StartAsync();
protected override Task ConfigureExtensionsAsync()
{
foreach (var client in this.ShardedClient.ShardClients.Values)
{
this.InitializeExtensions(client);
}
return Task.CompletedTask;
}
}
}