diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterConfigureOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterConfigureOptions.cs
new file mode 100644
index 00000000000000..106c3c8263e64e
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterConfigureOptions.cs
@@ -0,0 +1,49 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Versioning;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging.Configuration;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging
+{
+ ///
+ /// Configures a ConsoleFormatterOptions object from an IConfiguration.
+ ///
+ ///
+ /// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies,
+ /// to be trimmed. This improves app size and startup.
+ ///
+ [UnsupportedOSPlatform("browser")]
+ internal sealed class ConsoleFormatterConfigureOptions : IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+
+ public ConsoleFormatterConfigureOptions(ILoggerProviderConfiguration providerConfiguration)
+ {
+ _configuration = providerConfiguration.GetFormatterOptionsSection();
+ }
+
+ public void Configure(ConsoleFormatterOptions options) => Bind(_configuration, options);
+
+ public static void Bind(IConfiguration configuration, ConsoleFormatterOptions options)
+ {
+ if (ConsoleLoggerConfigureOptions.ParseBool(configuration, "IncludeScopes", out bool includeScopes))
+ {
+ options.IncludeScopes = includeScopes;
+ }
+
+ if (configuration["TimestampFormat"] is string timestampFormat)
+ {
+ options.TimestampFormat = timestampFormat;
+ }
+
+ if (ConsoleLoggerConfigureOptions.ParseBool(configuration, "UseUtcTimestamp", out bool useUtcTimestamp))
+ {
+ options.UseUtcTimestamp = useUtcTimestamp;
+ }
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerConfigureOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerConfigureOptions.cs
new file mode 100644
index 00000000000000..c0bd42be6f97be
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerConfigureOptions.cs
@@ -0,0 +1,172 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Globalization;
+using System.Runtime.Versioning;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging.Configuration;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging
+{
+ ///
+ /// Configures a ConsoleLoggerOptions object from an IConfiguration.
+ ///
+ ///
+ /// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies,
+ /// to be trimmed. This improves app size and startup.
+ ///
+ [UnsupportedOSPlatform("browser")]
+ internal sealed class ConsoleLoggerConfigureOptions : IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+
+ public ConsoleLoggerConfigureOptions(ILoggerProviderConfiguration providerConfiguration)
+ {
+ _configuration = providerConfiguration.Configuration;
+ }
+
+ public void Configure(ConsoleLoggerOptions options)
+ {
+ if (ParseBool(_configuration, "DisableColors", out bool disableColors))
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ options.DisableColors = disableColors;
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ if (ParseEnum(_configuration, "Format", out ConsoleLoggerFormat format))
+ {
+ options.Format = format;
+ }
+#pragma warning restore CS0618 // Type or member is obsolete
+
+ if (_configuration["FormatterName"] is string formatterName)
+ {
+ options.FormatterName = formatterName;
+ }
+
+ if (ParseBool(_configuration, "IncludeScopes", out bool includeScopes))
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ options.IncludeScopes = includeScopes;
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ if (ParseEnum(_configuration, "LogToStandardErrorThreshold", out LogLevel logToStandardErrorThreshold))
+ {
+ options.LogToStandardErrorThreshold = logToStandardErrorThreshold;
+ }
+
+ if (ParseInt(_configuration, "MaxQueueLength", out int maxQueueLength))
+ {
+ options.MaxQueueLength = maxQueueLength;
+ }
+
+ if (ParseEnum(_configuration, "QueueFullMode", out ConsoleLoggerQueueFullMode queueFullMode))
+ {
+ options.QueueFullMode = queueFullMode;
+ }
+
+ if (_configuration["TimestampFormat"] is string timestampFormat)
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ options.TimestampFormat = timestampFormat;
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ if (ParseBool(_configuration, "UseUtcTimestamp", out bool useUtcTimestamp))
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ options.UseUtcTimestamp = useUtcTimestamp;
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+ }
+
+ ///
+ /// Parses the configuration value at the specified key into a bool.
+ ///
+ /// true if the value was successfully found and parsed. false if the key wasn't found.
+ /// Thrown when invalid data was found at the specified configuration key.
+ public static bool ParseBool(IConfiguration configuration, string key, out bool value)
+ {
+ if (configuration[key] is string valueString)
+ {
+ try
+ {
+ value = bool.Parse(valueString);
+ return true;
+ }
+ catch (Exception e)
+ {
+ ThrowInvalidConfigurationException(configuration, key, typeof(bool), e);
+ }
+ }
+
+ value = default;
+ return false;
+ }
+
+ ///
+ /// Parses the configuration value at the specified key into an enum.
+ ///
+ /// true if the value was successfully found and parsed. false if the key wasn't found.
+ /// Thrown when invalid data was found at the specified configuration key.
+ public static bool ParseEnum(IConfiguration configuration, string key, out T value) where T : struct
+ {
+ if (configuration[key] is string valueString)
+ {
+ try
+ {
+ value =
+#if NETFRAMEWORK || NETSTANDARD2_0
+ (T)Enum.Parse(typeof(T), valueString, ignoreCase: true);
+#else
+ Enum.Parse(valueString, ignoreCase: true);
+#endif
+ return true;
+ }
+ catch (Exception e)
+ {
+ ThrowInvalidConfigurationException(configuration, key, typeof(T), e);
+ }
+ }
+
+ value = default;
+ return false;
+ }
+
+ ///
+ /// Parses the configuration value at the specified key into an int.
+ ///
+ /// true if the value was successfully found and parsed. false if the key wasn't found.
+ /// Thrown when invalid data was found at the specified configuration key.
+ public static bool ParseInt(IConfiguration configuration, string key, out int value)
+ {
+ if (configuration[key] is string valueString)
+ {
+ try
+ {
+ value = int.Parse(valueString, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
+ return true;
+ }
+ catch (Exception e)
+ {
+ ThrowInvalidConfigurationException(configuration, key, typeof(int), e);
+ }
+ }
+
+ value = default;
+ return false;
+ }
+
+ private static void ThrowInvalidConfigurationException(IConfiguration configuration, string key, Type valueType, Exception innerException)
+ {
+ IConfigurationSection section = configuration.GetSection(key);
+ throw new InvalidOperationException(SR.Format(SR.InvalidConfigurationData, section.Path, valueType), innerException);
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerExtensions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerExtensions.cs
index 160e099097f8c0..da4ff6bf0c991e 100644
--- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerExtensions.cs
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerExtensions.cs
@@ -25,22 +25,18 @@ public static class ConsoleLoggerExtensions
/// Adds a console logger named 'Console' to the factory.
///
/// The to use.
- [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode",
- Justification = "AddConsoleFormatter and RegisterProviderOptions are only called with Options types which only have simple properties.")]
- [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
- Justification = "AddConsoleFormatter and RegisterProviderOptions are only dangerous when the Options type cannot be statically analyzed, but that is not the case here. " +
- "The DynamicallyAccessedMembers annotations on them will make sure to preserve the right members from the different options objects.")]
- [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(JsonWriterOptions))]
public static ILoggingBuilder AddConsole(this ILoggingBuilder builder)
{
builder.AddConfiguration();
- builder.AddConsoleFormatter();
- builder.AddConsoleFormatter();
- builder.AddConsoleFormatter();
+ builder.AddConsoleFormatter();
+ builder.AddConsoleFormatter();
+ builder.AddConsoleFormatter();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
- LoggerProviderOptions.RegisterProviderOptions(builder.Services);
+
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConsoleLoggerConfigureOptions>());
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, LoggerProviderOptionsChangeTokenSource>());
return builder;
}
@@ -135,13 +131,7 @@ private static ILoggingBuilder AddFormatterWithName(this ILoggingBuilder builder
where TOptions : ConsoleFormatterOptions
where TFormatter : ConsoleFormatter
{
- builder.AddConfiguration();
-
- builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
- builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConsoleLoggerFormatterConfigureOptions>());
- builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConsoleLoggerFormatterOptionsChangeTokenSource>());
-
- return builder;
+ return AddConsoleFormatter>(builder);
}
///
@@ -161,6 +151,25 @@ private static ILoggingBuilder AddFormatterWithName(this ILoggingBuilder builder
builder.Services.Configure(configure);
return builder;
}
+
+ private static ILoggingBuilder AddConsoleFormatter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFormatter, TOptions, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TConfigureOptions>(this ILoggingBuilder builder)
+ where TOptions : ConsoleFormatterOptions
+ where TFormatter : ConsoleFormatter
+ where TConfigureOptions : class, IConfigureOptions
+ {
+ builder.AddConfiguration();
+
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, TConfigureOptions>());
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConsoleLoggerFormatterOptionsChangeTokenSource>());
+
+ return builder;
+ }
+
+ internal static IConfiguration GetFormatterOptionsSection(this ILoggerProviderConfiguration providerConfiguration)
+ {
+ return providerConfiguration.Configuration.GetSection("FormatterOptions");
+ }
}
[UnsupportedOSPlatform("browser")]
@@ -171,7 +180,7 @@ internal sealed class ConsoleLoggerFormatterConfigureOptions providerConfiguration) :
- base(providerConfiguration.Configuration.GetSection("FormatterOptions"))
+ base(providerConfiguration.GetFormatterOptionsSection())
{
}
}
@@ -182,7 +191,7 @@ internal sealed class ConsoleLoggerFormatterOptionsChangeTokenSource providerConfiguration)
- : base(providerConfiguration.Configuration.GetSection("FormatterOptions"))
+ : base(providerConfiguration.GetFormatterOptionsSection())
{
}
}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatterConfigureOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatterConfigureOptions.cs
new file mode 100644
index 00000000000000..acfb948f56a95b
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatterConfigureOptions.cs
@@ -0,0 +1,57 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Versioning;
+using System.Text.Json;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging.Configuration;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging
+{
+ ///
+ /// Configures a JsonConsoleFormatterOptions object from an IConfiguration.
+ ///
+ ///
+ /// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies,
+ /// to be trimmed. This improves app size and startup.
+ ///
+ [UnsupportedOSPlatform("browser")]
+ internal sealed class JsonConsoleFormatterConfigureOptions : IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+
+ public JsonConsoleFormatterConfigureOptions(ILoggerProviderConfiguration providerConfiguration)
+ {
+ _configuration = providerConfiguration.GetFormatterOptionsSection();
+ }
+
+ public void Configure(JsonConsoleFormatterOptions options)
+ {
+ ConsoleFormatterConfigureOptions.Bind(_configuration, options);
+
+ if (_configuration.GetSection("JsonWriterOptions") is IConfigurationSection jsonWriterOptionsConfig)
+ {
+ JsonWriterOptions jsonWriterOptions = options.JsonWriterOptions;
+
+ if (ConsoleLoggerConfigureOptions.ParseBool(jsonWriterOptionsConfig, "Indented", out bool indented))
+ {
+ jsonWriterOptions.Indented = indented;
+ }
+
+ if (ConsoleLoggerConfigureOptions.ParseInt(jsonWriterOptionsConfig, "MaxDepth", out int maxDepth))
+ {
+ jsonWriterOptions.MaxDepth = maxDepth;
+ }
+
+ if (ConsoleLoggerConfigureOptions.ParseBool(jsonWriterOptionsConfig, "SkipValidation", out bool skipValidation))
+ {
+ jsonWriterOptions.SkipValidation = skipValidation;
+ }
+
+ options.JsonWriterOptions = jsonWriterOptions;
+ }
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Logging.Console/src/Resources/Strings.resx
index c3d2473f20cefa..8d925d89baf50f 100644
--- a/src/libraries/Microsoft.Extensions.Logging.Console/src/Resources/Strings.resx
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/Resources/Strings.resx
@@ -129,4 +129,7 @@
{0} message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this. You may change `ConsoleLoggerQueueFullMode` to stop dropping messages.
+
+ Failed to convert configuration value at '{0}' to type '{1}'.
+
\ No newline at end of file
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatterConfigureOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatterConfigureOptions.cs
new file mode 100644
index 00000000000000..a8f9a6cf88f212
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatterConfigureOptions.cs
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Versioning;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging.Configuration;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging
+{
+ ///
+ /// Configures a SimpleConsoleFormatterOptions object from an IConfiguration.
+ ///
+ ///
+ /// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies,
+ /// to be trimmed. This improves app size and startup.
+ ///
+ [UnsupportedOSPlatform("browser")]
+ internal sealed class SimpleConsoleFormatterConfigureOptions : IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+
+ public SimpleConsoleFormatterConfigureOptions(ILoggerProviderConfiguration providerConfiguration)
+ {
+ _configuration = providerConfiguration.GetFormatterOptionsSection();
+ }
+
+ public void Configure(SimpleConsoleFormatterOptions options)
+ {
+ ConsoleFormatterConfigureOptions.Bind(_configuration, options);
+
+ if (ConsoleLoggerConfigureOptions.ParseEnum(_configuration, "ColorBehavior", out LoggerColorBehavior colorBehavior))
+ {
+ options.ColorBehavior = colorBehavior;
+ }
+
+ if (ConsoleLoggerConfigureOptions.ParseBool(_configuration, "SingleLine", out bool singleLine))
+ {
+ options.SingleLine = singleLine;
+ }
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerConfigureOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerConfigureOptions.cs
new file mode 100644
index 00000000000000..c30e842d3fc3ee
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerConfigureOptions.cs
@@ -0,0 +1,60 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text.Json;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.Console.Test
+{
+ public class ConsoleLoggerConfigureOptions
+ {
+ [Fact]
+ public void EnsureConsoleLoggerOptions_ConfigureOptions_SupportsAllProperties()
+ {
+ // NOTE: if this test fails, it is because a property was added to one of the following types.
+ // When adding a new property to one of these types, ensure the corresponding
+ // IConfigureOptions class is updated for the new property.
+
+ BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
+ Assert.Equal(9, typeof(ConsoleLoggerOptions).GetProperties(flags).Length);
+ Assert.Equal(3, typeof(ConsoleFormatterOptions).GetProperties(flags).Length);
+ Assert.Equal(5, typeof(SimpleConsoleFormatterOptions).GetProperties(flags).Length);
+ Assert.Equal(4, typeof(JsonConsoleFormatterOptions).GetProperties(flags).Length);
+ Assert.Equal(4, typeof(JsonWriterOptions).GetProperties(flags).Length);
+ }
+
+ [Theory]
+ [InlineData("Console:LogToStandardErrorThreshold", "invalid")]
+ [InlineData("Console:MaxQueueLength", "notANumber")]
+ [InlineData("Console:QueueFullMode", "invalid")]
+ [InlineData("Console:FormatterOptions:IncludeScopes", "not a bool")]
+ [InlineData("Console:FormatterOptions:UseUtcTimestamp", "not a bool")]
+ [InlineData("Console:FormatterOptions:ColorBehavior", "not a behavior")]
+ [InlineData("Console:FormatterOptions:SingleLine", "not a bool")]
+ [InlineData("Console:FormatterOptions:JsonWriterOptions:Indented", "not a bool")]
+ [InlineData("Console:FormatterOptions:JsonWriterOptions:MaxDepth", "not an int")]
+ [InlineData("Console:FormatterOptions:JsonWriterOptions:SkipValidation", "not a bool")]
+ public void ConsoleLoggerConfigureOptions_InvalidConfigurationData(string key, string value)
+ {
+ var configuration = new ConfigurationManager();
+ configuration.AddInMemoryCollection(new[] { new KeyValuePair(key, value) });
+
+ IServiceProvider serviceProvider = new ServiceCollection()
+ .AddLogging(builder => builder
+ .AddConfiguration(configuration)
+ .AddConsole())
+ .BuildServiceProvider();
+
+ InvalidOperationException e = Assert.Throws(() => serviceProvider.GetRequiredService());
+
+ // "Console:" is stripped off from the config path since that config section is read by the ConsoleLogger, and not part of the Options path.
+ string configPath = key.Substring("Console:".Length);
+ Assert.Contains(configPath, e.Message);
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerExtensionsTests.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerExtensionsTests.cs
index 38771eb2584993..99b49170ed8e1d 100644
--- a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerExtensionsTests.cs
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerExtensionsTests.cs
@@ -374,8 +374,7 @@ public void AddConsole_MaxQueueLengthSetToNegativeOrZero_Throws(int invalidMaxQu
)
.BuildServiceProvider();
- // the configuration binder throws TargetInvocationException when setting options property MaxQueueLength throws exception
- Assert.Throws(() => serviceProvider.GetRequiredService());
+ Assert.Throws(() => serviceProvider.GetRequiredService());
}
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs
index 0994980590122b..8202204fcf4bb8 100644
--- a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs
@@ -6,6 +6,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Reflection;
+using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.DotNet.RemoteExecutor;
using Microsoft.Extensions.Configuration;