diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 736499ad24..9c4fa7a64e 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -19,6 +19,7 @@
+
diff --git a/src/ServiceControl.Configuration/AppConfigConfigurationProvider.cs b/src/ServiceControl.Configuration/AppConfigConfigurationProvider.cs
new file mode 100644
index 0000000000..03e11611a6
--- /dev/null
+++ b/src/ServiceControl.Configuration/AppConfigConfigurationProvider.cs
@@ -0,0 +1,25 @@
+#nullable enable
+
+namespace ServiceControl.Configuration;
+
+using System.Collections.Generic;
+using Microsoft.Extensions.Configuration;
+
+public class AppConfigConfigurationProvider : ConfigurationProvider
+{
+ public AppConfigConfigurationProvider(Dictionary mappings)
+ {
+ foreach (var (msConfigurationExtensionKey, appConfigKeys) in mappings)
+ {
+ foreach (var appConfigKey in appConfigKeys)
+ {
+ var appConfigValue = System.Configuration.ConfigurationManager.AppSettings[appConfigKey];
+
+ if (appConfigValue is not null)
+ {
+ Data[msConfigurationExtensionKey] = appConfigValue;
+ }
+ }
+ }
+ }
+}
diff --git a/src/ServiceControl.Configuration/AppConfigConfigurationSource.cs b/src/ServiceControl.Configuration/AppConfigConfigurationSource.cs
new file mode 100644
index 0000000000..8321b521a0
--- /dev/null
+++ b/src/ServiceControl.Configuration/AppConfigConfigurationSource.cs
@@ -0,0 +1,32 @@
+#nullable enable
+
+namespace ServiceControl.Configuration;
+
+using Microsoft.Extensions.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+public class AppConfigConfigurationSource : IConfigurationSource
+{
+ public IConfigurationProvider Build(IConfigurationBuilder builder)
+ {
+ var propertiesWithAttribute = from a in AppDomain.CurrentDomain.GetAssemblies()
+ from t in a.GetTypes()
+ from p in t.GetProperties()
+ let attributes = p.GetCustomAttributes(typeof(AppConfigSettingAttribute), true)
+ where attributes != null && attributes.Length > 0
+ select new { Type = p, Attribute = attributes.Cast().Single() };
+
+ var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var property in propertiesWithAttribute)
+ {
+ var section = property.Type.DeclaringType!.Name.Replace("Options", "");
+ var name = property.Type.Name;
+ mappings[$"{section}:{name}"] = property.Attribute.Keys;
+ }
+
+ return new AppConfigConfigurationProvider(mappings);
+ }
+}
diff --git a/src/ServiceControl.Configuration/AppConfigSettingAttribute.cs b/src/ServiceControl.Configuration/AppConfigSettingAttribute.cs
new file mode 100644
index 0000000000..794da79c7f
--- /dev/null
+++ b/src/ServiceControl.Configuration/AppConfigSettingAttribute.cs
@@ -0,0 +1,11 @@
+#nullable enable
+
+namespace ServiceControl.Configuration;
+
+using System;
+
+[AttributeUsage(AttributeTargets.All)]
+public class AppConfigSettingAttribute(params string[] keys) : Attribute
+{
+ public string[] Keys { get; } = keys;
+}
\ No newline at end of file
diff --git a/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj b/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj
index 302ef7cbea..08dd5000ad 100644
--- a/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj
+++ b/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj
@@ -7,6 +7,7 @@
+
diff --git a/src/ServiceControl.UnitTests/API/APIApprovals.cs b/src/ServiceControl.UnitTests/API/APIApprovals.cs
index 1a75ce026f..706bec93b9 100644
--- a/src/ServiceControl.UnitTests/API/APIApprovals.cs
+++ b/src/ServiceControl.UnitTests/API/APIApprovals.cs
@@ -13,12 +13,14 @@
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging.Abstractions;
+ using Microsoft.Extensions.Options;
using NServiceBus.CustomChecks;
using NUnit.Framework;
using Particular.Approvals;
using Particular.ServiceControl.Licensing;
using ServiceBus.Management.Infrastructure.Settings;
using ServiceControl.Infrastructure.Api;
+ using ServiceControl.Infrastructure.Settings;
using ServiceControl.Infrastructure.WebApi;
using ServiceControl.Monitoring.HeartbeatMonitoring;
@@ -31,9 +33,11 @@ public async Task RootPathValue()
var httpContext = new DefaultHttpContext { Request = { Scheme = "http", Host = new HostString("localhost") } };
var actionContext = new ActionContext { HttpContext = httpContext, RouteData = new RouteData(), ActionDescriptor = new ControllerActionDescriptor() };
var controllerContext = new ControllerContext(actionContext);
+
var configurationApi = new ConfigurationApi(
new ActiveLicense(null, NullLogger.Instance) { IsValid = true },
new Settings(),
+ Options.Create(new ServiceControlOptions()),
null,
new MassTransitConnectorHeartbeatStatus());
diff --git a/src/ServiceControl/HostApplicationBuilderExtensions.cs b/src/ServiceControl/HostApplicationBuilderExtensions.cs
index fbe99dada5..d34eebde08 100644
--- a/src/ServiceControl/HostApplicationBuilderExtensions.cs
+++ b/src/ServiceControl/HostApplicationBuilderExtensions.cs
@@ -10,6 +10,7 @@ namespace Particular.ServiceControl
using global::ServiceControl.Infrastructure.BackgroundTasks;
using global::ServiceControl.Infrastructure.DomainEvents;
using global::ServiceControl.Infrastructure.Metrics;
+ using global::ServiceControl.Infrastructure.Settings;
using global::ServiceControl.Infrastructure.SignalR;
using global::ServiceControl.Infrastructure.WebApi;
using global::ServiceControl.Notifications.Email;
@@ -17,6 +18,7 @@ namespace Particular.ServiceControl
using global::ServiceControl.Transports;
using Licensing;
using Microsoft.AspNetCore.HttpLogging;
+ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.WindowsServices;
@@ -76,7 +78,8 @@ public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, S
services.AddPersistence(settings);
services.AddMetrics(settings.PrintMetrics);
- NServiceBusFactory.Configure(settings, transportCustomization, transportSettings, configuration);
+ var scOptions = hostBuilder.Configuration.GetSection("ServiceControl").Get();
+ NServiceBusFactory.Configure(scOptions, transportCustomization, transportSettings, configuration);
hostBuilder.UseNServiceBus(configuration);
if (!settings.DisableExternalIntegrationsPublishing)
diff --git a/src/ServiceControl/Hosting/Commands/RunCommand.cs b/src/ServiceControl/Hosting/Commands/RunCommand.cs
index db658857de..19a6912c36 100644
--- a/src/ServiceControl/Hosting/Commands/RunCommand.cs
+++ b/src/ServiceControl/Hosting/Commands/RunCommand.cs
@@ -3,11 +3,15 @@
using System.Threading.Tasks;
using Infrastructure.WebApi;
using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.Configuration;
+ using Microsoft.Extensions.DependencyInjection;
using NServiceBus;
using Particular.ServiceControl;
using Particular.ServiceControl.Hosting;
using ServiceBus.Management.Infrastructure.Settings;
using ServiceControl;
+ using ServiceControl.Configuration;
+ using ServiceControl.Infrastructure.Settings;
class RunCommand : AbstractCommand
{
@@ -20,6 +24,10 @@ public override async Task Execute(HostArguments args, Settings settings)
settings.RunCleanupBundle = true;
var hostBuilder = WebApplication.CreateBuilder();
+
+ hostBuilder.Configuration.Add(source => { });
+ hostBuilder.Services.Configure(hostBuilder.Configuration.GetSection("ServiceControl"));
+
hostBuilder.AddServiceControl(settings, endpointConfiguration);
hostBuilder.AddServiceControlApi();
diff --git a/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs b/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs
index f8af272eb4..82c8b6056f 100644
--- a/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs
+++ b/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs
@@ -8,16 +8,23 @@
using System.Threading;
using System.Threading.Tasks;
using Configuration;
+using Microsoft.Extensions.Options;
using Monitoring.HeartbeatMonitoring;
using Particular.ServiceControl.Licensing;
using ServiceBus.Management.Infrastructure.Settings;
using ServiceControl.Api;
using ServiceControl.Api.Contracts;
+using ServiceControl.Infrastructure.Settings;
-class ConfigurationApi(ActiveLicense license,
+class ConfigurationApi(
+ ActiveLicense license,
Settings settings,
- IHttpClientFactory httpClientFactory, MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus) : IConfigurationApi
+ IOptions scOptions,
+ IHttpClientFactory httpClientFactory,
+ MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus) : IConfigurationApi
{
+ readonly ServiceControlOptions scOptions = scOptions.Value;
+
public Task GetUrls(string baseUrl, CancellationToken cancellationToken)
{
var model = new RootUrls
@@ -56,33 +63,33 @@ public Task