diff --git a/pkgs/sdk/server/src/Components.cs b/pkgs/sdk/server/src/Components.cs
index d001fc8f..9ce264b2 100644
--- a/pkgs/sdk/server/src/Components.cs
+++ b/pkgs/sdk/server/src/Components.cs
@@ -6,6 +6,7 @@
using LaunchDarkly.Sdk.Server.Integrations;
using LaunchDarkly.Sdk.Server.Interfaces;
using LaunchDarkly.Sdk.Server.Internal;
+using LaunchDarkly.Sdk.Server.Plugins;
using LaunchDarkly.Sdk.Server.Subsystems;
namespace LaunchDarkly.Sdk.Server
@@ -218,6 +219,27 @@ public static LoggingConfigurationBuilder Logging(ILogAdapter adapter) =>
/// a configuration builder
public static HookConfigurationBuilder Hooks(IEnumerable hooks) => new HookConfigurationBuilder(hooks);
+ ///
+ /// Returns a configuration builder for the SDK's plugin configuration.
+ ///
+ ///
+ ///
+ /// var config = Configuration.Builder(sdkKey)
+ /// .Add(new MyPlugin(...))
+ /// .Add(new MyOtherPlugin(...))
+ /// ).Build();
+ ///
+ ///
+ /// a configuration builder
+ public static PluginConfigurationBuilder Plugins() => new PluginConfigurationBuilder();
+
+ ///
+ /// Returns a configuration builder for the SDK's plugin configuration, with an initial set of plugins.
+ ///
+ /// a collection of plugins
+ /// a configuration builder
+ public static PluginConfigurationBuilder Plugins(IEnumerable plugins) => new PluginConfigurationBuilder(plugins);
+
///
/// Returns a configuration object that disables analytics events.
///
diff --git a/pkgs/sdk/server/src/Configuration.cs b/pkgs/sdk/server/src/Configuration.cs
index d11dd4c7..092217ed 100644
--- a/pkgs/sdk/server/src/Configuration.cs
+++ b/pkgs/sdk/server/src/Configuration.cs
@@ -112,6 +112,11 @@ public class Configuration
///
public HookConfigurationBuilder Hooks { get; }
+ ///
+ /// Contains methods for configuring the SDK's 'plugins' to extend or customize SDK behavior.
+ ///
+ public PluginConfigurationBuilder Plugins { get; }
+
#endregion
#region Public methods
@@ -189,6 +194,7 @@ internal Configuration(ConfigurationBuilder builder)
ApplicationInfo = builder._applicationInfo;
WrapperInfo = builder._wrapperInfo;
Hooks = builder._hooks;
+ Plugins = builder._plugins;
}
#endregion
diff --git a/pkgs/sdk/server/src/ConfigurationBuilder.cs b/pkgs/sdk/server/src/ConfigurationBuilder.cs
index 82af4a2f..1e45d98a 100644
--- a/pkgs/sdk/server/src/ConfigurationBuilder.cs
+++ b/pkgs/sdk/server/src/ConfigurationBuilder.cs
@@ -39,6 +39,7 @@ public sealed class ConfigurationBuilder
internal bool _diagnosticOptOut = false;
internal IComponentConfigurer _events = null;
internal HookConfigurationBuilder _hooks = null;
+ internal PluginConfigurationBuilder _plugins = null;
internal IComponentConfigurer _http = null;
internal IComponentConfigurer _logging = null;
internal bool _offline = false;
@@ -65,6 +66,7 @@ internal ConfigurationBuilder(Configuration copyFrom)
_diagnosticOptOut = copyFrom.DiagnosticOptOut;
_events = copyFrom.Events;
_hooks = copyFrom.Hooks;
+ _plugins = copyFrom.Plugins;
_http = copyFrom.Http;
_logging = copyFrom.Logging;
_offline = copyFrom.Offline;
@@ -291,6 +293,17 @@ public ConfigurationBuilder Hooks(HookConfigurationBuilder hooksConfig)
return this;
}
+ ///
+ /// Configures the SDK's plugins.
+ ///
+ /// the plugin configuration
+ /// the same builder
+ public ConfigurationBuilder Plugins(PluginConfigurationBuilder pluginsConfig)
+ {
+ _plugins = pluginsConfig;
+ return this;
+ }
+
///
/// Sets whether or not this client is offline. If true, no calls to Launchdarkly will be made.
///
diff --git a/pkgs/sdk/server/src/Integrations/PluginConfigurationBuilder.cs b/pkgs/sdk/server/src/Integrations/PluginConfigurationBuilder.cs
new file mode 100644
index 00000000..de38acac
--- /dev/null
+++ b/pkgs/sdk/server/src/Integrations/PluginConfigurationBuilder.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Linq;
+using LaunchDarkly.Sdk.Server.Plugins;
+using LaunchDarkly.Sdk.Server.Subsystems;
+
+namespace LaunchDarkly.Sdk.Server.Integrations
+{
+ ///
+ /// PluginConfigurationBuilder is a builder for the SDK's plugin configuration.
+ ///
+ public sealed class PluginConfigurationBuilder
+ {
+ private readonly List _plugins;
+
+ ///
+ /// Constructs a configuration from an existing collection of plugins.
+ ///
+ public PluginConfigurationBuilder(IEnumerable plugins = null)
+ {
+ _plugins = plugins is null ? new List() : plugins.ToList();
+ }
+
+ ///
+ /// Adds a plugin to the configuration.
+ ///
+ /// the builder
+ public PluginConfigurationBuilder Add(Plugin plugin)
+ {
+ _plugins.Add(plugin);
+ return this;
+ }
+
+ ///
+ /// Builds the configuration.
+ ///
+ /// the built configuration
+ public PluginConfiguration Build()
+ {
+ return new PluginConfiguration(_plugins.ToList());
+ }
+ }
+}
diff --git a/pkgs/sdk/server/src/LaunchDarkly.ServerSdk.csproj b/pkgs/sdk/server/src/LaunchDarkly.ServerSdk.csproj
index e356cfd9..43d97988 100644
--- a/pkgs/sdk/server/src/LaunchDarkly.ServerSdk.csproj
+++ b/pkgs/sdk/server/src/LaunchDarkly.ServerSdk.csproj
@@ -39,9 +39,9 @@
-
+
-
+
@@ -51,7 +51,6 @@
-
../../../../LaunchDarkly.snk
true
diff --git a/pkgs/sdk/server/src/LdClient.cs b/pkgs/sdk/server/src/LdClient.cs
index 46f783df..49bd37dc 100644
--- a/pkgs/sdk/server/src/LdClient.cs
+++ b/pkgs/sdk/server/src/LdClient.cs
@@ -7,6 +7,7 @@
using LaunchDarkly.Sdk.Server.Hooks;
using LaunchDarkly.Sdk.Server.Interfaces;
using LaunchDarkly.Sdk.Server.Internal;
+using LaunchDarkly.Sdk.Integrations.Plugins;
using LaunchDarkly.Sdk.Server.Internal.BigSegments;
using LaunchDarkly.Sdk.Server.Internal.DataSources;
using LaunchDarkly.Sdk.Server.Internal.DataStores;
@@ -130,7 +131,7 @@ public LdClient(Configuration config)
{
_configuration = config;
- var logConfig = (config.Logging ?? Components.Logging()).Build(new LdClientContext(config.SdkKey));
+ var logConfig = (_configuration.Logging ?? Components.Logging()).Build(new LdClientContext(_configuration.SdkKey));
_log = logConfig.LogAdapter.Logger(logConfig.BaseLoggerName ?? LogNames.DefaultBase);
_log.Info("Starting LaunchDarkly client {0}",
@@ -140,24 +141,24 @@ public LdClient(Configuration config)
var taskExecutor = new TaskExecutor(this, _log);
var clientContext = new LdClientContext(
- config.SdkKey,
+ _configuration.SdkKey,
null,
null,
null,
_log,
- config.Offline,
- config.ServiceEndpoints,
+ _configuration.Offline,
+ _configuration.ServiceEndpoints,
null,
taskExecutor,
- config.ApplicationInfo?.Build() ?? new ApplicationInfo(),
- config.WrapperInfo?.Build()
+ _configuration.ApplicationInfo?.Build() ?? new ApplicationInfo(),
+ _configuration.WrapperInfo?.Build()
);
- var httpConfig = (config.Http ?? Components.HttpConfiguration()).Build(clientContext);
+ var httpConfig = (_configuration.Http ?? Components.HttpConfiguration()).Build(clientContext);
clientContext = clientContext.WithHttp(httpConfig);
var diagnosticStore = _configuration.DiagnosticOptOut ? null :
- new ServerDiagnosticStore(config, clientContext);
+ new ServerDiagnosticStore(_configuration, clientContext);
clientContext = clientContext.WithDiagnosticStore(diagnosticStore);
var dataStoreUpdates = new DataStoreUpdatesImpl(taskExecutor, _log.SubLogger(LogNames.DataStoreSubLog));
@@ -185,25 +186,33 @@ public LdClient(Configuration config)
);
var eventProcessorFactory =
- config.Offline ? Components.NoEvents :
- (_configuration.Events?? Components.SendEvents());
+ _configuration.Offline ? Components.NoEvents :
+ (_configuration.Events ?? Components.SendEvents());
_eventProcessor = eventProcessorFactory.Build(clientContext);
var dataSourceUpdates = new DataSourceUpdatesImpl(_dataStore, _dataStoreStatusProvider,
taskExecutor, _log, logConfig.LogDataSourceOutageAsErrorAfter);
IComponentConfigurer dataSourceFactory =
- config.Offline ? Components.ExternalUpdatesOnly :
+ _configuration.Offline ? Components.ExternalUpdatesOnly :
(_configuration.DataSource ?? Components.StreamingDataSource());
_dataSource = dataSourceFactory.Build(clientContext.WithDataSourceUpdates(dataSourceUpdates));
_dataSourceStatusProvider = new DataSourceStatusProviderImpl(dataSourceUpdates);
_flagTracker = new FlagTrackerImpl(dataSourceUpdates,
(string key, Context context) => JsonVariation(key, context, LdValue.Null));
- var hookConfig = (config.Hooks ?? Components.Hooks()).Build();
- _hookExecutor = hookConfig.Hooks.Any() ?
- (IHookExecutor) new Executor(_log.SubLogger(LogNames.HooksSubLog), hookConfig.Hooks)
+ var allHooks = new List();
+
+ var hookConfig = (_configuration.Hooks ?? Components.Hooks()).Build();
+ allHooks.AddRange(hookConfig.Hooks);
+
+ var pluginConfig = (_configuration.Plugins ?? Components.Plugins()).Build();
+ EnvironmentMetadata environmentMetadata = CreateEnvironmentMetadata(clientContext);
+ allHooks.AddRange(this.GetPluginHooks(pluginConfig.Plugins, environmentMetadata, _log));
+ _hookExecutor = allHooks.Any() ?
+ (IHookExecutor)new Executor(_log.SubLogger(LogNames.HooksSubLog), allHooks)
: new NoopExecutor();
+ this.RegisterPlugins(pluginConfig.Plugins, environmentMetadata, _log);
var initTask = _dataSource.Start();
@@ -323,7 +332,7 @@ public MigrationVariation MigrationVariation(string key, Context context, Migrat
var (detail, flag) = EvaluateWithHooks(Method.MigrationVariation, key, context, LdValue.Of(defaultStage.ToDataModelString()),
LdValue.Convert.String, true, EventFactory.Default);
- var nullableStage = MigrationStageExtensions.FromDataModelString(detail.Value);
+ var nullableStage = MigrationStageExtensions.FromDataModelString(detail.Value);
var stage = nullableStage ?? defaultStage;
if (nullableStage == null)
{
@@ -648,24 +657,34 @@ public Logger GetLogger()
#region Private methods
- private FeatureFlag GetFlag(string key)
+ ///
+ /// Creates metadata about the environment, including SDK and application information.
+ ///
+ /// The client context containing application and wrapper information.
+ /// An instance containing the environment metadata.
+ ///
+ /// This method constructs the environment metadata using the SDK key, application ID, and version,
+ /// along with any wrapper information if available. It is used to provide context for plugins and
+ /// hooks that may need to interact with the environment.
+ ///
+ private EnvironmentMetadata CreateEnvironmentMetadata(LdClientContext clientContext)
{
- var maybeItem = _dataStore.Get(DataModel.Features, key);
- if (maybeItem.HasValue && maybeItem.Value.Item != null && maybeItem.Value.Item is FeatureFlag f)
- {
- return f;
- }
- return null;
- }
-
- private Segment GetSegment(string key)
- {
- var maybeItem = _dataStore.Get(DataModel.Segments, key);
- if (maybeItem.HasValue && maybeItem.Value.Item != null && maybeItem.Value.Item is Segment s)
- {
- return s;
- }
- return null;
+ var applicationInfo = clientContext.ApplicationInfo;
+ var wrapperInfo = _configuration.WrapperInfo?.Build();
+
+ var sdkMetadata = new SdkMetadata(
+ "dotnet-server-sdk",
+ AssemblyVersions.GetAssemblyVersionStringForType(typeof(LdClient)),
+ wrapperInfo?.Name,
+ wrapperInfo?.Version
+ );
+
+ var applicationMetadata = new ApplicationMetadata(
+ applicationInfo.ApplicationId,
+ applicationInfo.ApplicationVersion
+ );
+
+ return new EnvironmentMetadata(sdkMetadata, _configuration.SdkKey, CredentialType.SdkKey, applicationMetadata);
}
private void Dispose(bool disposing)
@@ -694,6 +713,26 @@ private string GetEnvironmentId()
return null;
}
+ private FeatureFlag GetFlag(string key)
+ {
+ var maybeItem = _dataStore.Get(DataModel.Features, key);
+ if (maybeItem.HasValue && maybeItem.Value.Item != null && maybeItem.Value.Item is FeatureFlag f)
+ {
+ return f;
+ }
+ return null;
+ }
+
+ private Segment GetSegment(string key)
+ {
+ var maybeItem = _dataStore.Get(DataModel.Segments, key);
+ if (maybeItem.HasValue && maybeItem.Value.Item != null && maybeItem.Value.Item is Segment s)
+ {
+ return s;
+ }
+ return null;
+ }
+
#endregion
}
}
diff --git a/pkgs/sdk/server/src/Plugins/Plugin.cs b/pkgs/sdk/server/src/Plugins/Plugin.cs
new file mode 100644
index 00000000..5c8f2b50
--- /dev/null
+++ b/pkgs/sdk/server/src/Plugins/Plugin.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using LaunchDarkly.Sdk.Server.Hooks;
+using LaunchDarkly.Sdk.Server.Interfaces;
+using LaunchDarkly.Sdk.Integrations.Plugins;
+
+namespace LaunchDarkly.Sdk.Server.Plugins
+{
+ ///
+ /// Abstract base class for extending SDK functionality via plugins in the server-side SDK.
+ /// All provided server-side plugin implementations MUST inherit from this class.
+ ///
+ public abstract class Plugin : PluginBase
+ {
+ ///
+ /// Initializes a new instance of the class with the specified name.
+ ///
+ /// The name of the plugin.
+ protected Plugin(string name)
+ : base(name)
+ {
+ }
+ }
+}
diff --git a/pkgs/sdk/server/src/Subsystems/PluginConfiguration.cs b/pkgs/sdk/server/src/Subsystems/PluginConfiguration.cs
new file mode 100644
index 00000000..66855c5e
--- /dev/null
+++ b/pkgs/sdk/server/src/Subsystems/PluginConfiguration.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using LaunchDarkly.Sdk.Server.Plugins;
+
+namespace LaunchDarkly.Sdk.Server.Subsystems
+{
+ ///
+ /// Configuration containing plugins for the SDK.
+ ///
+ public sealed class PluginConfiguration
+ {
+ ///
+ /// The collection of plugins.
+ ///
+ public IEnumerable Plugins { get; }
+
+ ///
+ /// Initializes a new instance of the class with the specified plugins.
+ ///
+ /// The plugins to include in this configuration.
+ public PluginConfiguration(IEnumerable plugins)
+ {
+ Plugins = plugins;
+ }
+ }
+}
diff --git a/pkgs/sdk/server/test/Integrations/PluginConfigurationBuilderTest.cs b/pkgs/sdk/server/test/Integrations/PluginConfigurationBuilderTest.cs
new file mode 100644
index 00000000..77a33bc4
--- /dev/null
+++ b/pkgs/sdk/server/test/Integrations/PluginConfigurationBuilderTest.cs
@@ -0,0 +1,134 @@
+using System.Collections.Generic;
+using System.Linq;
+using LaunchDarkly.Sdk.Integrations.Plugins;
+using LaunchDarkly.Sdk.Server.Hooks;
+using LaunchDarkly.Sdk.Server.Interfaces;
+using LaunchDarkly.Sdk.Server.Plugins;
+using LaunchDarkly.Sdk.Server.Subsystems;
+using Xunit;
+
+namespace LaunchDarkly.Sdk.Server.Integrations
+{
+ // Mock plugin for testing
+ public class TestPlugin : Plugin
+ {
+ public TestPlugin(string name = "test-plugin")
+ :base(name) { }
+
+ public override void Register(ILdClient client, EnvironmentMetadata metadata)
+ {
+ // No-op for testing
+ }
+ }
+
+ public class PluginConfigurationBuilderTest
+ {
+ [Fact]
+ public void Constructor_WithNoParameters_CreatesEmptyConfiguration()
+ {
+ var builder = new PluginConfigurationBuilder();
+ var config = builder.Build();
+
+ Assert.NotNull(config.Plugins);
+ Assert.Empty(config.Plugins);
+ }
+
+ [Fact]
+ public void Constructor_WithPluginCollection_CreatesConfigurationWithPlugins()
+ {
+ var plugin1 = new TestPlugin("plugin1");
+ var plugin2 = new TestPlugin("plugin2");
+ var plugins = new List { plugin1, plugin2 };
+
+ var builder = new PluginConfigurationBuilder(plugins);
+ var config = builder.Build();
+
+ Assert.Equal(2, config.Plugins.Count());
+ Assert.Contains(plugin1, config.Plugins);
+ Assert.Contains(plugin2, config.Plugins);
+ }
+
+ [Fact]
+ public void Add_MultiplePlugins_AddsAllPluginsToConfiguration()
+ {
+ var builder = new PluginConfigurationBuilder();
+ var plugin1 = new TestPlugin("plugin1");
+ var plugin2 = new TestPlugin("plugin2");
+ var plugin3 = new TestPlugin("plugin3");
+
+ builder.Add(plugin1).Add(plugin2).Add(plugin3);
+ var config = builder.Build();
+
+ Assert.Equal(3, config.Plugins.Count());
+ Assert.Contains(plugin1, config.Plugins);
+ Assert.Contains(plugin2, config.Plugins);
+ Assert.Contains(plugin3, config.Plugins);
+ }
+
+ [Fact]
+ public void Add_ReturnsBuilderInstance_AllowsFluentInterface()
+ {
+ var builder = new PluginConfigurationBuilder();
+ var plugin = new TestPlugin("test-plugin");
+
+ var returnedBuilder = builder.Add(plugin);
+
+ Assert.Same(builder, returnedBuilder);
+ }
+
+ [Fact]
+ public void Build_CanBeCalledMultipleTimes()
+ {
+ var builder = new PluginConfigurationBuilder();
+ var plugin = new TestPlugin("test-plugin");
+ builder.Add(plugin);
+
+ var config1 = builder.Build();
+ var config2 = builder.Build();
+
+ Assert.Single(config1.Plugins);
+ Assert.Single(config2.Plugins);
+ Assert.Contains(plugin, config1.Plugins);
+ Assert.Contains(plugin, config2.Plugins);
+ }
+
+ [Fact]
+ public void Build_ModifyingBuilderAfterBuild_DoesNotAffectPreviousConfiguration()
+ {
+ var builder = new PluginConfigurationBuilder();
+ var plugin1 = new TestPlugin("plugin1");
+ var plugin2 = new TestPlugin("plugin2");
+
+ builder.Add(plugin1);
+ var config1 = builder.Build();
+
+ builder.Add(plugin2);
+ var config2 = builder.Build();
+
+ Assert.Single(config1.Plugins);
+ Assert.Equal(2, config2.Plugins.Count());
+ Assert.Contains(plugin1, config1.Plugins);
+ Assert.DoesNotContain(plugin2, config1.Plugins);
+ }
+
+ [Fact]
+ public void Constructor_WithEmptyPluginCollection_CreatesEmptyConfiguration()
+ {
+ var builder = new PluginConfigurationBuilder(new List());
+ var config = builder.Build();
+
+ Assert.NotNull(config.Plugins);
+ Assert.Empty(config.Plugins);
+ }
+
+ [Fact]
+ public void Constructor_WithNullPluginCollection_CreatesEmptyConfiguration()
+ {
+ var builder = new PluginConfigurationBuilder(null);
+ var config = builder.Build();
+
+ Assert.NotNull(config.Plugins);
+ Assert.Empty(config.Plugins);
+ }
+ }
+}