2025-08-09 19:29:43 +02:00
using System.Collections.Concurrent ;
using System.Linq.Expressions ;
using AIStudio.Settings.DataModel ;
using AIStudio.Tools.PluginSystem ;
namespace AIStudio.Settings ;
2025-10-19 11:51:28 +02:00
public static partial class ManagedConfiguration
2025-08-09 19:29:43 +02:00
{
private static readonly ConcurrentDictionary < string , IConfig > METADATA = new ();
2026-08-06 20:59:53 +02:00
2026-06-10 21:01:27 +02:00
private static SettingsManager SettingsManagerAccess => Program . SERVICE_PROVIDER . GetRequiredService < SettingsManager >();
2026-08-06 20:59:53 +02:00
private static ILogger Log => Program . LOGGER_FACTORY . CreateLogger ( nameof ( ManagedConfiguration ));
2025-10-19 11:51:28 +02:00
2025-08-09 19:29:43 +02:00
/// <summary>
2025-10-19 11:51:28 +02:00
/// Attempts to retrieve the configuration metadata for a given configuration selection and
/// property expression (enum-based).
2025-08-09 19:29:43 +02:00
/// </summary>
/// <remarks>
2025-10-19 11:51:28 +02: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 19:29:43 +02:00
/// </remarks>
/// <param name="configSelection">The expression to select the configuration class.</param>
2025-10-19 11:51:28 +02: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 19:29:43 +02: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 11:51:28 +02:00
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
2026-08-06 20:59:53 +02: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 11:51:28 +02:00
where TValue : Enum
2025-08-09 19:29:43 +02:00
{
var configPath = Path ( configSelection , propertyExpression );
2025-10-19 11:51:28 +02:00
if ( METADATA . TryGetValue ( configPath , out var value ) && value is ConfigMeta < TClass , TValue > meta )
{
2026-08-06 20:59:53 +02:00
meta . RestoreLockedConfiguration ();
2025-10-19 11:51:28 +02:00
configMeta = meta ;
return true ;
}
2025-08-09 19:29:43 +02:00
2025-10-19 11:51:28 +02:00
configMeta = new NoConfig < TClass , TValue >( configSelection , propertyExpression )
2025-08-09 19:29:43 +02:00
{
2025-10-19 11:51:28 +02:00
Default = default !,
2025-08-09 19:29:43 +02:00
};
2025-10-19 11:51:28 +02: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 20:59:53 +02:00
public static bool TryGet < TClass >( Expression < Func < Data , TClass >> configSelection , Expression < Func < TClass , string >> propertyExpression , out ConfigMeta < TClass , string > configMeta )
2025-10-19 11:51:28 +02:00
{
var configPath = Path ( configSelection , propertyExpression );
if ( METADATA . TryGetValue ( configPath , out var value ) && value is ConfigMeta < TClass , string > meta )
{
2026-08-06 20:59:53 +02:00
meta . RestoreLockedConfiguration ();
2025-10-19 11:51:28 +02:00
configMeta = meta ;
return true ;
}
2025-08-09 19:29:43 +02:00
2025-10-19 11:51:28 +02:00
configMeta = new NoConfig < TClass , string >( configSelection , propertyExpression )
{
Default = string . Empty ,
};
return false ;
2025-08-09 19:29:43 +02:00
}
/// <summary>
2025-10-19 11:51:28 +02:00
/// Attempts to retrieve the configuration metadata for a given configuration selection and
/// property expression (ISpanParsable-based).
2025-08-09 19:29:43 +02:00
/// </summary>
/// <remarks>
2025-10-19 11:51:28 +02: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 19:29:43 +02:00
/// </remarks>
/// <param name="configSelection">The expression to select the configuration class.</param>
2025-10-19 11:51:28 +02: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 19:29:43 +02: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 11:51:28 +02:00
// ReSharper disable MethodOverloadWithOptionalParameter
2026-08-06 20:59:53 +02: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 11:51:28 +02:00
where TValue : struct , ISpanParsable < TValue >
2025-08-09 19:29:43 +02:00
{
var configPath = Path ( configSelection , propertyExpression );
if ( METADATA . TryGetValue ( configPath , out var value ) && value is ConfigMeta < TClass , TValue > meta )
{
2026-08-06 20:59:53 +02:00
meta . RestoreLockedConfiguration ();
2025-08-09 19:29:43 +02:00
configMeta = meta ;
return true ;
}
2025-10-19 11:51:28 +02:00
configMeta = new NoConfig < TClass , TValue >( configSelection , propertyExpression )
2025-08-09 19:29:43 +02:00
{
Default = default !,
};
return false ;
}
2025-10-19 11:51:28 +02:00
// ReSharper restore MethodOverloadWithOptionalParameter
2025-08-09 19:29:43 +02:00
/// <summary>
2025-10-19 11:51:28 +02:00
/// Attempts to retrieve the configuration metadata for a list-based setting.
2025-08-09 19:29:43 +02:00
/// </summary>
/// <remarks>
2025-10-19 11:51:28 +02: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 19:29:43 +02:00
/// </remarks>
/// <param name="configSelection">The expression to select the configuration class.</param>
2025-10-19 11:51:28 +02: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 19:29:43 +02: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 11:51:28 +02:00
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
2026-08-06 20:59:53 +02: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 19:29:43 +02:00
{
2025-10-19 11:51:28 +02:00
var configPath = Path ( configSelection , propertyExpression );
if ( METADATA . TryGetValue ( configPath , out var value ) && value is ConfigMeta < TClass , IList < TValue >> meta )
2025-08-09 19:29:43 +02:00
{
2026-08-06 20:59:53 +02:00
meta . RestoreLockedConfiguration ();
2025-10-19 11:51:28 +02:00
configMeta = meta ;
return true ;
}
2025-08-09 19:29:43 +02:00
2025-10-19 11:51:28 +02:00
configMeta = new NoConfig < TClass , IList < TValue >>( configSelection , propertyExpression )
{
Default = [],
};
return false ;
}
2025-08-09 19:29:43 +02:00
2025-10-19 11:51:28 +02: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 20:59:53 +02: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 11:51:28 +02:00
{
var configPath = Path ( configSelection , propertyExpression );
if ( METADATA . TryGetValue ( configPath , out var value ) && value is ConfigMeta < TClass , ISet < TValue >> meta )
{
2026-08-06 20:59:53 +02:00
meta . RestoreLockedConfiguration ();
2025-10-19 11:51:28 +02:00
configMeta = meta ;
return true ;
}
2025-08-09 19:29:43 +02:00
2025-10-19 11:51:28 +02:00
configMeta = new NoConfig < TClass , ISet < TValue >>( configSelection , propertyExpression )
{
Default = new HashSet < TValue >(),
2025-08-09 19:29:43 +02:00
};
2025-10-19 11:51:28 +02: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 20:59:53 +02: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 11:51:28 +02:00
{
var configPath = Path ( configSelection , propertyExpression );
if ( METADATA . TryGetValue ( configPath , out var value ) && value is ConfigMeta < TClass , IDictionary < string , string >> meta )
{
2026-08-06 20:59:53 +02:00
meta . RestoreLockedConfiguration ();
2025-10-19 11:51:28 +02:00
configMeta = meta ;
return true ;
2025-08-09 19:29:43 +02:00
}
2025-10-19 11:51:28 +02:00
configMeta = new NoConfig < TClass , IDictionary < string , string >>( configSelection , propertyExpression )
{
Default = new Dictionary < string , string >(),
};
return false ;
2025-08-09 19:29:43 +02:00
}
2026-06-21 11:52:02 +02: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 20:59:53 +02: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 11:52:02 +02: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 20:59:53 +02:00
meta . RestoreLockedConfiguration ();
2026-06-21 11:52:02 +02:00
configMeta = meta ;
return true ;
}
configMeta = new NoConfig < TClass , Dictionary < TKey , TValue >>( configSelection , propertyExpression )
{
Default = new Dictionary < TKey , TValue >(),
};
return false ;
}
2026-08-08 18:35:46 +02: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 19:01:58 +02:00
if (! PluginFactory . IsOrganizationConfigurationPlugin ( owningConfigPluginId ))
2026-08-08 18:35:46 +02:00
return true ;
2026-08-09 19:01:58 +02:00
if ( PluginFactory . IsOrganizationConfigurationPlugin ( configPluginId ))
2026-08-08 18:35:46 +02: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 19:29:43 +02:00
/// <summary>
2026-08-06 20:59:53 +02:00
/// Removes all managed states whose configuration plugin is not available anymore.
2025-08-09 19:29:43 +02:00
/// </summary>
2026-08-06 20:59:53 +02: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 19:29:43 +02:00
/// <param name="availablePlugins">The collection of available plugins to check against.</param>
2026-08-08 18:35:46 +02: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 20:59:53 +02:00
/// </param>
/// <returns>True when at least one setting was changed, otherwise false.</returns>
2026-08-08 18:35:46 +02:00
public static bool CleanupLeftOverManagedConfigurations ( IReadOnlyCollection < IAvailablePlugin > availablePlugins , IReadOnlySet < Guid > deployedEnterpriseConfigPluginIds )
2025-08-09 19:29:43 +02:00
{
2026-08-06 20:59:53 +02:00
var wasChanged = false ;
var registeredSettingNames = new HashSet < string >( StringComparer . Ordinal );
2025-08-09 19:29:43 +02:00
2026-08-06 20:59:53 +02:00
foreach ( var config in METADATA . Values )
2025-08-09 19:29:43 +02:00
{
2026-08-06 20:59:53 +02:00
if ( config is not ConfigMetaBase configMeta )
continue ;
2025-10-19 11:51:28 +02:00
2026-08-06 20:59:53 +02:00
registeredSettingNames . Add ( configMeta . SettingName );
2025-10-19 11:51:28 +02:00
2026-08-06 20:59:53 +02: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 11:51:28 +02:00
2026-08-06 20:59:53 +02:00
// Check the locked state:
2026-08-08 18:35:46 +02:00
if ( configMeta . IsLocked && configMeta . LockedByConfigPluginId != Guid . Empty && ! IsPluginPresent ( configMeta . LockedByConfigPluginId , availablePlugins , deployedEnterpriseConfigPluginIds ))
2026-03-21 18:05:06 +01:00
{
2026-08-06 20:59:53 +02: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 18:05:06 +01:00
configMeta . ResetLockedConfiguration ();
2026-08-06 20:59:53 +02:00
wasChanged = true ;
2026-03-21 18:05:06 +01:00
}
2025-10-19 11:51:28 +02:00
2026-08-06 20:59:53 +02:00
// Check the editable default state:
2026-08-08 18:35:46 +02:00
if ( CleanupEditableDefaultState ( configMeta , availablePlugins , deployedEnterpriseConfigPluginIds ))
2026-08-06 20:59:53 +02:00
wasChanged = true ;
2025-10-19 11:51:28 +02:00
2026-08-08 18:35:46 +02: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 18:05:06 +01:00
{
2026-08-08 18:35:46 +02: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 20:59:53 +02:00
wasChanged = true ;
2026-03-21 18:05:06 +01:00
}
2026-08-09 20:47:30 +02:00
//
// Finally, drop any snapshot of the user's value which nobody claims anymore. Without
// this, a setting which stopped being managed outside of the paths above would keep its
// snapshot in the settings file forever. The persisted editable default counts as a
// claim as well: it survives a configuration plugin which is deployed but could not be
// loaded, and that plugin is still in charge:
//
if ( configMeta . ManagedMode is null && ! TryGetEditableDefaultState ( configMeta . SettingName , out _ ) && configMeta . ClearUserValueSnapshot ())
{
Log . LogInformation ( $"Dropping the snapshot of the user's value for the setting '{configMeta.SettingName}': no configuration plugin manages it anymore." );
wasChanged = true ;
}
2025-10-19 11:51:28 +02:00
}
2026-08-06 20:59:53 +02:00
// Remove persisted states which belong to settings that do not exist anymore:
if ( RemoveUnknownManagedStates ( registeredSettingNames ))
wasChanged = true ;
2025-10-19 11:51:28 +02:00
2026-08-06 20:59:53 +02:00
return wasChanged ;
2025-10-19 11:51:28 +02:00
}
2026-08-06 20:59:53 +02: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 18:35:46 +02: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 12:40:38 +01:00
/// <summary>
2026-08-06 20:59:53 +02:00
/// Removes persisted managed states which belong to settings that are not registered anymore.
2026-02-20 12:40:38 +01:00
/// </summary>
2026-08-06 20:59:53 +02: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 12:40:38 +01:00
{
2026-08-06 20:59:53 +02:00
var wasChanged = false ;
var configurationData = SettingsManagerAccess . ConfigurationData ;
2026-02-20 12:40:38 +01:00
2026-08-06 20:59:53 +02:00
foreach ( var settingName in configurationData . ManagedLockedConfigurations . Keys . Where ( x => ! registeredSettingNames . Contains ( x )). ToList ())
2026-02-20 12:40:38 +01:00
{
2026-08-06 20:59:53 +02: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 11:51:28 +02:00
}
2026-08-06 20:59:53 +02:00
foreach ( var settingName in configurationData . ManagedEditableDefaults . Keys . Where ( x => ! registeredSettingNames . Contains ( x )). ToList ())
2025-10-19 11:51:28 +02:00
{
2026-08-06 20:59:53 +02: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 11:51:28 +02:00
}
2026-08-09 20:47:30 +02:00
foreach ( var settingName in configurationData . ManagedUserValueSnapshots . Keys . Where ( x => ! registeredSettingNames . Contains ( x )). ToList ())
{
Log . LogInformation ( $"Removing the snapshot of the user's value for the setting '{settingName}': this setting does not exist anymore." );
configurationData . ManagedUserValueSnapshots . Remove ( settingName );
wasChanged = true ;
}
2026-08-06 20:59:53 +02:00
return wasChanged ;
2025-08-09 19:29:43 +02:00
}
2026-06-21 11:52:02 +02:00
2025-08-09 19:29:43 +02: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 18:05:06 +01: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 21:01:27 +02:00
return SettingsManagerAccess . ConfigurationData . ManagedEditableDefaults . TryGetValue ( settingName , out editableDefaultState !);
2026-03-21 18:05:06 +01:00
}
private static void SetEditableDefaultState ( string settingName , Guid pluginId , string lastAppliedValue )
{
2026-06-10 21:01:27 +02:00
SettingsManagerAccess . ConfigurationData . ManagedEditableDefaults [ settingName ] = new ()
2026-03-21 18:05:06 +01:00
{
ConfigPluginId = pluginId ,
LastAppliedValue = lastAppliedValue ,
};
}
2026-06-10 21:01:27 +02:00
private static bool ClearEditableDefaultState ( string settingName ) => SettingsManagerAccess . ConfigurationData . ManagedEditableDefaults . Remove ( settingName );
2026-03-21 18:05:06 +01:00
2026-08-08 18:35:46 +02:00
private static bool CleanupEditableDefaultState ( ConfigMetaBase configMeta , IReadOnlyCollection < IAvailablePlugin > availablePlugins , IReadOnlySet < Guid > deployedEnterpriseConfigPluginIds )
2026-03-21 18:05:06 +01:00
{
2026-08-06 20:59:53 +02:00
if (! TryGetEditableDefaultState ( configMeta . SettingName , out var editableDefaultState ))
2026-03-21 18:05:06 +01:00
{
if ( configMeta . ManagedMode is not ManagedConfigurationMode . EDITABLE_DEFAULT )
return false ;
2026-08-09 20:47:30 +02:00
configMeta . ResetEditableDefaultConfiguration ( keepCurrentValue : false );
2026-03-21 18:05:06 +01:00
return true ;
}
2026-08-08 18:35:46 +02:00
if ( IsPluginPresent ( editableDefaultState . ConfigPluginId , availablePlugins , deployedEnterpriseConfigPluginIds ))
2026-03-21 18:05:06 +01:00
return false ;
2026-08-06 20:59:53 +02:00
Log . LogInformation ( $"Clearing the editable default of the setting '{configMeta.SettingName}': the configuration plugin '{editableDefaultState.ConfigPluginId}' is not available anymore." );
2026-08-09 20:47:30 +02:00
configMeta . ResetEditableDefaultConfiguration ( HasUserChangedEditableDefault ( configMeta , editableDefaultState ));
2026-08-06 20:59:53 +02:00
return ClearEditableDefaultState ( configMeta . SettingName );
2026-03-21 18:05:06 +01:00
}
2026-08-09 20:47:30 +02:00
/// <summary>
/// Checks whether the user has changed an editable default themselves.
/// </summary>
/// <remarks>
/// The user may change an editable default at any time. When the current value is not the one
/// the configuration plugin applied last, the user decided against that value, and their
/// decision outlives the plugin.
/// </remarks>
private static bool HasUserChangedEditableDefault ( ConfigMetaBase configMeta , ManagedEditableDefaultState editableDefaultState ) => ! string . Equals ( configMeta . SerializeCurrentValue (), editableDefaultState . LastAppliedValue , StringComparison . Ordinal );
2025-08-09 19:29:43 +02:00
}