2025-08-09 17:29:43 +00:00
using System.Collections.Concurrent ;
using System.Linq.Expressions ;
using AIStudio.Settings.DataModel ;
using AIStudio.Tools.PluginSystem ;
namespace AIStudio.Settings ;
2025-10-19 09:51:28 +00:00
public static partial class ManagedConfiguration
2025-08-09 17:29:43 +00:00
{
private static readonly ConcurrentDictionary < string , IConfig > METADATA = new ( ) ;
2026-08-06 18:59:53 +00:00
2026-06-10 19:01:27 +00:00
private static SettingsManager SettingsManagerAccess = > Program . SERVICE_PROVIDER . GetRequiredService < SettingsManager > ( ) ;
2026-08-06 18:59:53 +00:00
private static ILogger Log = > Program . LOGGER_FACTORY . CreateLogger ( nameof ( ManagedConfiguration ) ) ;
2025-10-19 09:51:28 +00:00
2025-08-09 17:29:43 +00:00
/// <summary>
2025-10-19 09:51:28 +00:00
/// Attempts to retrieve the configuration metadata for a given configuration selection and
/// property expression (enum-based).
2025-08-09 17:29:43 +00:00
/// </summary>
/// <remarks>
2025-10-19 09:51:28 +00:00
/// When no configuration metadata is found, it returns a NoConfig instance with the default
/// value set to default(TValue). This allows the caller to handle the absence of configuration
/// gracefully. In such cases, the return value of the method will be false.
2025-08-09 17:29:43 +00:00
/// </remarks>
/// <param name="configSelection">The expression to select the configuration class.</param>
2025-10-19 09:51:28 +00:00
/// <param name="propertyExpression">The expression to select the property within the
/// configuration class.</param>
/// <param name="configMeta">The output parameter that will hold the configuration metadata
/// if found.</param>
2025-08-09 17:29:43 +00:00
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
2025-10-19 09:51:28 +00:00
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
2026-08-06 18:59:53 +00:00
public static bool TryGet < TClass , TValue > ( Expression < Func < Data , TClass > > configSelection , Expression < Func < TClass , TValue > > propertyExpression , out ConfigMeta < TClass , TValue > configMeta )
2025-10-19 09:51:28 +00:00
where TValue : Enum
2025-08-09 17:29:43 +00:00
{
var configPath = Path ( configSelection , propertyExpression ) ;
2025-10-19 09:51:28 +00:00
if ( METADATA . TryGetValue ( configPath , out var value ) & & value is ConfigMeta < TClass , TValue > meta )
{
2026-08-06 18:59:53 +00:00
meta . RestoreLockedConfiguration ( ) ;
2025-10-19 09:51:28 +00:00
configMeta = meta ;
return true ;
}
2025-08-09 17:29:43 +00:00
2025-10-19 09:51:28 +00:00
configMeta = new NoConfig < TClass , TValue > ( configSelection , propertyExpression )
2025-08-09 17:29:43 +00:00
{
2025-10-19 09:51:28 +00:00
Default = default ! ,
2025-08-09 17:29:43 +00:00
} ;
2025-10-19 09:51:28 +00:00
return false ;
}
/// <summary>
/// Attempts to retrieve the configuration metadata for a given configuration selection and
/// property expression (string-based).
/// </summary>
/// <remarks>
/// When no configuration metadata is found, it returns a NoConfig instance with the default
/// value set to default(TValue). This allows the caller to handle the absence of configuration
/// gracefully. In such cases, the return value of the method will be false.
/// </remarks>
/// <param name="configSelection">The expression to select the configuration class.</param>
/// <param name="propertyExpression">The expression to select the property within the
/// configuration class.</param>
/// <param name="configMeta">The output parameter that will hold the configuration metadata
/// if found.</param>
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
2026-08-06 18:59:53 +00:00
public static bool TryGet < TClass > ( Expression < Func < Data , TClass > > configSelection , Expression < Func < TClass , string > > propertyExpression , out ConfigMeta < TClass , string > configMeta )
2025-10-19 09:51:28 +00:00
{
var configPath = Path ( configSelection , propertyExpression ) ;
if ( METADATA . TryGetValue ( configPath , out var value ) & & value is ConfigMeta < TClass , string > meta )
{
2026-08-06 18:59:53 +00:00
meta . RestoreLockedConfiguration ( ) ;
2025-10-19 09:51:28 +00:00
configMeta = meta ;
return true ;
}
2025-08-09 17:29:43 +00:00
2025-10-19 09:51:28 +00:00
configMeta = new NoConfig < TClass , string > ( configSelection , propertyExpression )
{
Default = string . Empty ,
} ;
return false ;
2025-08-09 17:29:43 +00:00
}
/// <summary>
2025-10-19 09:51:28 +00:00
/// Attempts to retrieve the configuration metadata for a given configuration selection and
/// property expression (ISpanParsable-based).
2025-08-09 17:29:43 +00:00
/// </summary>
/// <remarks>
2025-10-19 09:51:28 +00:00
/// When no configuration metadata is found, it returns a NoConfig instance with the default
/// value set to default(TValue). This allows the caller to handle the absence of configuration
/// gracefully. In such cases, the return value of the method will be false.
2025-08-09 17:29:43 +00:00
/// </remarks>
/// <param name="configSelection">The expression to select the configuration class.</param>
2025-10-19 09:51:28 +00:00
/// <param name="propertyExpression">The expression to select the property within the
/// configuration class.</param>
/// <param name="configMeta">The output parameter that will hold the configuration metadata
/// if found.</param>
/// <param name="_">An optional parameter to help with method overload resolution.</param>
2025-08-09 17:29:43 +00:00
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
2025-10-19 09:51:28 +00:00
// ReSharper disable MethodOverloadWithOptionalParameter
2026-08-06 18:59:53 +00:00
public static bool TryGet < TClass , TValue > ( Expression < Func < Data , TClass > > configSelection , Expression < Func < TClass , TValue > > propertyExpression , out ConfigMeta < TClass , TValue > configMeta , ISpanParsable < TValue > ? _ = null )
2025-10-19 09:51:28 +00:00
where TValue : struct , ISpanParsable < TValue >
2025-08-09 17:29:43 +00:00
{
var configPath = Path ( configSelection , propertyExpression ) ;
if ( METADATA . TryGetValue ( configPath , out var value ) & & value is ConfigMeta < TClass , TValue > meta )
{
2026-08-06 18:59:53 +00:00
meta . RestoreLockedConfiguration ( ) ;
2025-08-09 17:29:43 +00:00
configMeta = meta ;
return true ;
}
2025-10-19 09:51:28 +00:00
configMeta = new NoConfig < TClass , TValue > ( configSelection , propertyExpression )
2025-08-09 17:29:43 +00:00
{
Default = default ! ,
} ;
return false ;
}
2025-10-19 09:51:28 +00:00
// ReSharper restore MethodOverloadWithOptionalParameter
2025-08-09 17:29:43 +00:00
/// <summary>
2025-10-19 09:51:28 +00:00
/// Attempts to retrieve the configuration metadata for a list-based setting.
2025-08-09 17:29:43 +00:00
/// </summary>
/// <remarks>
2025-10-19 09:51:28 +00:00
/// When no configuration metadata is found, it returns a NoConfig instance with the default
/// value set to an empty list. This allows the caller to handle the absence of configuration
/// gracefully. In such cases, the return value of the method will be false.
2025-08-09 17:29:43 +00:00
/// </remarks>
/// <param name="configSelection">The expression to select the configuration class.</param>
2025-10-19 09:51:28 +00:00
/// <param name="propertyExpression">The expression to select the property within the
/// configuration class.</param>
/// <param name="configMeta">The output parameter that will hold the configuration metadata
/// if found.</param>
2025-08-09 17:29:43 +00:00
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
2025-10-19 09:51:28 +00:00
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
2026-08-06 18:59:53 +00:00
public static bool TryGet < TClass , TValue > ( Expression < Func < Data , TClass > > configSelection , Expression < Func < TClass , IList < TValue > > > propertyExpression , out ConfigMeta < TClass , IList < TValue > > configMeta )
2025-08-09 17:29:43 +00:00
{
2025-10-19 09:51:28 +00:00
var configPath = Path ( configSelection , propertyExpression ) ;
if ( METADATA . TryGetValue ( configPath , out var value ) & & value is ConfigMeta < TClass , IList < TValue > > meta )
2025-08-09 17:29:43 +00:00
{
2026-08-06 18:59:53 +00:00
meta . RestoreLockedConfiguration ( ) ;
2025-10-19 09:51:28 +00:00
configMeta = meta ;
return true ;
}
2025-08-09 17:29:43 +00:00
2025-10-19 09:51:28 +00:00
configMeta = new NoConfig < TClass , IList < TValue > > ( configSelection , propertyExpression )
{
Default = [ ] ,
} ;
return false ;
}
2025-08-09 17:29:43 +00:00
2025-10-19 09:51:28 +00:00
/// <summary>
/// Attempts to retrieve the configuration metadata for a set-based setting.
/// </summary>
/// <remarks>
/// When no configuration metadata is found, it returns a NoConfig instance with the default
/// value set to an empty set. This allows the caller to handle the absence of configuration
/// gracefully. In such cases, the return value of the method will be false.
/// </remarks>
/// <param name="configSelection">The expression to select the configuration class.</param>
/// <param name="propertyExpression">The expression to select the property within the
/// configuration class.</param>
/// <param name="configMeta">The output parameter that will hold the configuration metadata
/// if found.</param>
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
2026-08-06 18:59:53 +00:00
public static bool TryGet < TClass , TValue > ( Expression < Func < Data , TClass > > configSelection , Expression < Func < TClass , ISet < TValue > > > propertyExpression , out ConfigMeta < TClass , ISet < TValue > > configMeta )
2025-10-19 09:51:28 +00:00
{
var configPath = Path ( configSelection , propertyExpression ) ;
if ( METADATA . TryGetValue ( configPath , out var value ) & & value is ConfigMeta < TClass , ISet < TValue > > meta )
{
2026-08-06 18:59:53 +00:00
meta . RestoreLockedConfiguration ( ) ;
2025-10-19 09:51:28 +00:00
configMeta = meta ;
return true ;
}
2025-08-09 17:29:43 +00:00
2025-10-19 09:51:28 +00:00
configMeta = new NoConfig < TClass , ISet < TValue > > ( configSelection , propertyExpression )
{
Default = new HashSet < TValue > ( ) ,
2025-08-09 17:29:43 +00:00
} ;
2025-10-19 09:51:28 +00:00
return false ;
}
/// <summary>
/// Attempts to retrieve the configuration metadata for a string dictionary-based setting.
/// </summary>
/// <remarks>
/// When no configuration metadata is found, it returns a NoConfig instance with the default
/// value set to an empty dictionary. This allows the caller to handle the absence of configuration
/// gracefully. In such cases, the return value of the method will be false.
/// </remarks>
/// <param name="configSelection">The expression to select the configuration class.</param>
/// <param name="propertyExpression">The expression to select the property within the
/// configuration class.</param>
/// <param name="configMeta">The output parameter that will hold the configuration metadata
/// if found.</param>
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
2026-08-06 18:59:53 +00:00
public static bool TryGet < TClass > ( Expression < Func < Data , TClass > > configSelection , Expression < Func < TClass , IDictionary < string , string > > > propertyExpression , out ConfigMeta < TClass , IDictionary < string , string > > configMeta )
2025-10-19 09:51:28 +00:00
{
var configPath = Path ( configSelection , propertyExpression ) ;
if ( METADATA . TryGetValue ( configPath , out var value ) & & value is ConfigMeta < TClass , IDictionary < string , string > > meta )
{
2026-08-06 18:59:53 +00:00
meta . RestoreLockedConfiguration ( ) ;
2025-10-19 09:51:28 +00:00
configMeta = meta ;
return true ;
2025-08-09 17:29:43 +00:00
}
2025-10-19 09:51:28 +00:00
configMeta = new NoConfig < TClass , IDictionary < string , string > > ( configSelection , propertyExpression )
{
Default = new Dictionary < string , string > ( ) ,
} ;
return false ;
2025-08-09 17:29:43 +00:00
}
2026-06-21 09:52:02 +00:00
/// <summary>
/// Attempts to retrieve the configuration metadata for an enum dictionary-based setting.
/// </summary>
/// <remarks>
/// When no configuration metadata is found, it returns a NoConfig instance with the default
/// value set to an empty dictionary. This allows the caller to handle the absence of configuration
/// gracefully. In such cases, the return value of the method will be false.
/// </remarks>
/// <param name="configSelection">The expression to select the configuration class.</param>
/// <param name="propertyExpression">The expression to select the property within the
/// configuration class.</param>
/// <param name="configMeta">The output parameter that will hold the configuration metadata
/// if found.</param>
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <typeparam name="TKey">The enum type of the dictionary keys.</typeparam>
/// <typeparam name="TValue">The enum type of the dictionary values.</typeparam>
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
2026-08-06 18:59:53 +00:00
public static bool TryGet < TClass , TKey , TValue > ( Expression < Func < Data , TClass > > configSelection , Expression < Func < TClass , Dictionary < TKey , TValue > > > propertyExpression , out ConfigMeta < TClass , Dictionary < TKey , TValue > > configMeta )
2026-06-21 09:52:02 +00:00
where TKey : struct , Enum
where TValue : struct , Enum
{
var configPath = Path ( configSelection , propertyExpression ) ;
if ( METADATA . TryGetValue ( configPath , out var value ) & & value is ConfigMeta < TClass , Dictionary < TKey , TValue > > meta )
{
2026-08-06 18:59:53 +00:00
meta . RestoreLockedConfiguration ( ) ;
2026-06-21 09:52:02 +00:00
configMeta = meta ;
return true ;
}
configMeta = new NoConfig < TClass , Dictionary < TKey , TValue > > ( configSelection , propertyExpression )
{
Default = new Dictionary < TKey , TValue > ( ) ,
} ;
return false ;
}
2026-08-08 16:35:46 +00:00
/// <summary>
/// Checks whether a configuration plugin may manage a setting, or whether that setting belongs
/// to the IT department of an organization.
/// </summary>
/// <remarks>
/// A local configuration plugin must not take over a setting an organization manages. Otherwise,
/// anyone could hand out a configuration plugin that quietly replaces parts of the organization
/// configuration, e.g. the address of a self-hosted provider.<br/><br/>
/// Between two configuration plugins of the same organization, we do not interfere: both belong
/// to the IT department, so the one processed later wins, as before.
/// </remarks>
/// <param name="configPluginId">The configuration plugin which wants to manage the setting.</param>
/// <param name="configMeta">The configuration metadata of the setting.</param>
/// <returns>True when the plugin may manage this setting, otherwise false.</returns>
private static bool MayManageSetting ( Guid configPluginId , ConfigMetaBase configMeta )
{
var owningConfigPluginId = GetSettingOwner ( configMeta ) ;
if ( owningConfigPluginId = = Guid . Empty | | owningConfigPluginId = = configPluginId )
return true ;
2026-08-09 16:36:55 +00:00
if ( ! PluginFactory . IsOrganizationConfigurationPlugin ( owningConfigPluginId ) )
2026-08-08 16:35:46 +00:00
return true ;
2026-08-09 16:36:55 +00:00
if ( PluginFactory . IsOrganizationConfigurationPlugin ( configPluginId ) )
2026-08-08 16:35:46 +00:00
return true ;
Log . LogWarning ( $"The configuration plugin '{configPluginId}' tried to manage the setting '{configMeta.SettingName}', which is managed by the configuration plugin '{owningConfigPluginId}' of your organization. Ignoring the attempt: configurations deployed by your organization's IT take precedence." ) ;
return false ;
}
/// <summary>
/// Determines the configuration plugin which currently manages a setting, if any.
/// </summary>
private static Guid GetSettingOwner ( ConfigMetaBase configMeta )
{
if ( configMeta . IsLocked & & configMeta . LockedByConfigPluginId ! = Guid . Empty )
return configMeta . LockedByConfigPluginId ;
// The editable default is persisted as well, so we prefer it over the in-memory state:
if ( TryGetEditableDefaultState ( configMeta . SettingName , out var editableDefaultState ) & & editableDefaultState . ConfigPluginId ! = Guid . Empty )
return editableDefaultState . ConfigPluginId ;
return configMeta . EditableDefaultByConfigPluginId ;
}
2025-08-09 17:29:43 +00:00
/// <summary>
2026-08-06 18:59:53 +00:00
/// Removes all managed states whose configuration plugin is not available anymore.
2025-08-09 17:29:43 +00:00
/// </summary>
2026-08-06 18:59:53 +00:00
/// <remarks>
/// This covers every registered setting, regardless of its type: locked settings, editable
/// defaults, and additive plugin contributions. Settings do not need to be listed anywhere for
/// this cleanup to work, so adding a new managed setting cannot be forgotten here.<br/><br/>
/// A locked setting whose plugin is gone is reset to its default value. That is intended: the
/// value belonged to the organization, not to the user, and the user might not be able to
/// change it at all.
/// </remarks>
2025-08-09 17:29:43 +00:00
/// <param name="availablePlugins">The collection of available plugins to check against.</param>
2026-08-08 16:35:46 +00:00
/// <param name="deployedEnterpriseConfigPluginIds">
/// The IDs of the configuration plugins which an organization deployed on this machine, including
/// those which could not be loaded. A deployed plugin was not removed, so its settings must stay
/// untouched.
2026-08-06 18:59:53 +00:00
/// </param>
/// <returns>True when at least one setting was changed, otherwise false.</returns>
2026-08-08 16:35:46 +00:00
public static bool CleanupLeftOverManagedConfigurations ( IReadOnlyCollection < IAvailablePlugin > availablePlugins , IReadOnlySet < Guid > deployedEnterpriseConfigPluginIds )
2025-08-09 17:29:43 +00:00
{
2026-08-06 18:59:53 +00:00
var wasChanged = false ;
var registeredSettingNames = new HashSet < string > ( StringComparer . Ordinal ) ;
2025-08-09 17:29:43 +00:00
2026-08-06 18:59:53 +00:00
foreach ( var config in METADATA . Values )
2025-08-09 17:29:43 +00:00
{
2026-08-06 18:59:53 +00:00
if ( config is not ConfigMetaBase configMeta )
continue ;
2025-10-19 09:51:28 +00:00
2026-08-06 18:59:53 +00:00
registeredSettingNames . Add ( configMeta . SettingName ) ;
2025-10-19 09:51:28 +00:00
2026-08-06 18:59:53 +00:00
//
// Restore the persisted ownership first. Otherwise, we would not recognize a left-over
// lock when nobody has read this setting since the settings were loaded:
//
configMeta . RestoreLockedConfiguration ( ) ;
2025-10-19 09:51:28 +00:00
2026-08-06 18:59:53 +00:00
// Check the locked state:
2026-08-08 16:35:46 +00:00
if ( configMeta . IsLocked & & configMeta . LockedByConfigPluginId ! = Guid . Empty & & ! IsPluginPresent ( configMeta . LockedByConfigPluginId , availablePlugins , deployedEnterpriseConfigPluginIds ) )
2026-03-21 17:05:06 +00:00
{
2026-08-06 18:59:53 +00:00
Log . LogInformation ( $"Resetting the setting '{configMeta.SettingName}': it was locked by the configuration plugin '{configMeta.LockedByConfigPluginId}', which is not available anymore." ) ;
2026-03-21 17:05:06 +00:00
configMeta . ResetLockedConfiguration ( ) ;
2026-08-06 18:59:53 +00:00
wasChanged = true ;
2026-03-21 17:05:06 +00:00
}
2025-10-19 09:51:28 +00:00
2026-08-06 18:59:53 +00:00
// Check the editable default state:
2026-08-08 16:35:46 +00:00
if ( CleanupEditableDefaultState ( configMeta , availablePlugins , deployedEnterpriseConfigPluginIds ) )
2026-08-06 18:59:53 +00:00
wasChanged = true ;
2025-10-19 09:51:28 +00:00
2026-08-08 16:35:46 +00:00
// Check the additive plugin contributions. Every contributing plugin is checked on its
// own, so one removed plugin does not take the contributions of the others with it:
foreach ( var contributingConfigPluginId in configMeta . ContributingConfigPluginIds . ToList ( ) )
2026-03-21 17:05:06 +00:00
{
2026-08-08 16:35:46 +00:00
if ( contributingConfigPluginId ! = Guid . Empty & & IsPluginPresent ( contributingConfigPluginId , availablePlugins , deployedEnterpriseConfigPluginIds ) )
continue ;
Log . LogInformation ( $"Clearing the contribution of the configuration plugin '{contributingConfigPluginId}' to the setting '{configMeta.SettingName}': the plugin is not available anymore." ) ;
configMeta . RemovePluginContribution ( contributingConfigPluginId ) ;
2026-08-06 18:59:53 +00:00
wasChanged = true ;
2026-03-21 17:05:06 +00:00
}
2025-10-19 09:51:28 +00:00
}
2026-08-06 18:59:53 +00:00
// Remove persisted states which belong to settings that do not exist anymore:
if ( RemoveUnknownManagedStates ( registeredSettingNames ) )
wasChanged = true ;
2025-10-19 09:51:28 +00:00
2026-08-06 18:59:53 +00:00
return wasChanged ;
2025-10-19 09:51:28 +00:00
}
2026-08-06 18:59:53 +00:00
/// <summary>
/// Checks whether a configuration plugin is still present on this machine.
/// </summary>
/// <remarks>
/// A plugin counts as present when it was loaded, or when it is deployed but could not be loaded.
/// The latter matters for organizations: a broken configuration plugin is still in charge, so we
/// must not treat its settings as left over.
/// </remarks>
2026-08-08 16:35:46 +00:00
private static bool IsPluginPresent ( Guid configPluginId , IReadOnlyCollection < IAvailablePlugin > availablePlugins , IReadOnlySet < Guid > deployedEnterpriseConfigPluginIds ) = > deployedEnterpriseConfigPluginIds . Contains ( configPluginId ) | | availablePlugins . Any ( x = > x . Id = = configPluginId ) ;
2026-02-20 11:40:38 +00:00
/// <summary>
2026-08-06 18:59:53 +00:00
/// Removes persisted managed states which belong to settings that are not registered anymore.
2026-02-20 11:40:38 +00:00
/// </summary>
2026-08-06 18:59:53 +00:00
/// <remarks>
/// Without this, states of removed or renamed settings would stay in the settings file forever.
/// </remarks>
private static bool RemoveUnknownManagedStates ( IReadOnlySet < string > registeredSettingNames )
2026-02-20 11:40:38 +00:00
{
2026-08-06 18:59:53 +00:00
var wasChanged = false ;
var configurationData = SettingsManagerAccess . ConfigurationData ;
2026-02-20 11:40:38 +00:00
2026-08-06 18:59:53 +00:00
foreach ( var settingName in configurationData . ManagedLockedConfigurations . Keys . Where ( x = > ! registeredSettingNames . Contains ( x ) ) . ToList ( ) )
2026-02-20 11:40:38 +00:00
{
2026-08-06 18:59:53 +00:00
Log . LogInformation ( $"Removing the persisted lock of the setting '{settingName}': this setting does not exist anymore." ) ;
configurationData . ManagedLockedConfigurations . Remove ( settingName ) ;
wasChanged = true ;
2025-10-19 09:51:28 +00:00
}
2026-08-06 18:59:53 +00:00
foreach ( var settingName in configurationData . ManagedEditableDefaults . Keys . Where ( x = > ! registeredSettingNames . Contains ( x ) ) . ToList ( ) )
2025-10-19 09:51:28 +00:00
{
2026-08-06 18:59:53 +00:00
Log . LogInformation ( $"Removing the persisted editable default of the setting '{settingName}': this setting does not exist anymore." ) ;
configurationData . ManagedEditableDefaults . Remove ( settingName ) ;
wasChanged = true ;
2025-10-19 09:51:28 +00:00
}
2026-08-06 18:59:53 +00:00
return wasChanged ;
2025-08-09 17:29:43 +00:00
}
2026-06-21 09:52:02 +00:00
2025-08-09 17:29:43 +00:00
private static string Path < TClass , TValue > ( Expression < Func < Data , TClass > > configSelection , Expression < Func < TClass , TValue > > propertyExpression )
{
var className = typeof ( TClass ) . Name ;
var memberExpressionConfig = configSelection . GetMemberExpression ( ) ;
var configName = memberExpressionConfig . Member . Name ;
var memberExpressionProperty = propertyExpression . GetMemberExpression ( ) ;
var propertyName = memberExpressionProperty . Member . Name ;
var configPath = $"{configName}.{className}.{propertyName}" ;
return configPath ;
}
2026-03-21 17:05:06 +00:00
private static string SettingName < TClass , TValue > ( Expression < Func < TClass , TValue > > propertyExpression ) = > SettingsManager . ToSettingName ( propertyExpression ) ;
private static bool TryGetEditableDefaultState ( string settingName , out ManagedEditableDefaultState editableDefaultState )
{
2026-06-10 19:01:27 +00:00
return SettingsManagerAccess . ConfigurationData . ManagedEditableDefaults . TryGetValue ( settingName , out editableDefaultState ! ) ;
2026-03-21 17:05:06 +00:00
}
private static void SetEditableDefaultState ( string settingName , Guid pluginId , string lastAppliedValue )
{
2026-06-10 19:01:27 +00:00
SettingsManagerAccess . ConfigurationData . ManagedEditableDefaults [ settingName ] = new ( )
2026-03-21 17:05:06 +00:00
{
ConfigPluginId = pluginId ,
LastAppliedValue = lastAppliedValue ,
} ;
}
2026-06-10 19:01:27 +00:00
private static bool ClearEditableDefaultState ( string settingName ) = > SettingsManagerAccess . ConfigurationData . ManagedEditableDefaults . Remove ( settingName ) ;
2026-03-21 17:05:06 +00:00
2026-08-08 16:35:46 +00:00
private static bool CleanupEditableDefaultState ( ConfigMetaBase configMeta , IReadOnlyCollection < IAvailablePlugin > availablePlugins , IReadOnlySet < Guid > deployedEnterpriseConfigPluginIds )
2026-03-21 17:05:06 +00:00
{
2026-08-06 18:59:53 +00:00
if ( ! TryGetEditableDefaultState ( configMeta . SettingName , out var editableDefaultState ) )
2026-03-21 17:05:06 +00:00
{
if ( configMeta . ManagedMode is not ManagedConfigurationMode . EDITABLE_DEFAULT )
return false ;
configMeta . ClearEditableDefaultConfiguration ( ) ;
return true ;
}
2026-08-08 16:35:46 +00:00
if ( IsPluginPresent ( editableDefaultState . ConfigPluginId , availablePlugins , deployedEnterpriseConfigPluginIds ) )
2026-03-21 17:05:06 +00:00
return false ;
2026-08-06 18:59:53 +00:00
Log . LogInformation ( $"Clearing the editable default of the setting '{configMeta.SettingName}': the configuration plugin '{editableDefaultState.ConfigPluginId}' is not available anymore." ) ;
2026-03-21 17:05:06 +00:00
configMeta . ClearEditableDefaultConfiguration ( ) ;
2026-08-06 18:59:53 +00:00
return ClearEditableDefaultState ( configMeta . SettingName ) ;
2026-03-21 17:05:06 +00:00
}
2025-08-09 17:29:43 +00:00
}