Skip to content
Merged
26 changes: 25 additions & 1 deletion pkgs/sdk/client/src/Components.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using LaunchDarkly.Logging;
using System.Collections.Generic;
using LaunchDarkly.Logging;
using LaunchDarkly.Sdk.Client.Hooks;
using LaunchDarkly.Sdk.Client.Integrations;
using LaunchDarkly.Sdk.Client.Internal;
using LaunchDarkly.Sdk.Client.Internal.DataStores;
using LaunchDarkly.Sdk.Client.Plugins;
using LaunchDarkly.Sdk.Client.Subsystems;

namespace LaunchDarkly.Sdk.Client
Expand Down Expand Up @@ -336,5 +339,26 @@ public static PollingDataSourceBuilder PollingDataSource() =>
/// <see cref="ConfigurationBuilder.DataSource(IComponentConfigurer{IDataSource})"/>
public static StreamingDataSourceBuilder StreamingDataSource() =>
new StreamingDataSourceBuilder();

/// <summary>
/// Returns a configuration builder for the SDK's plugin configuration.
/// </summary>
/// <example>
/// <code>
/// var config = Configuration.Builder(mobileKey, AutoEnvAttributes.Enabled)
/// .Plugins(Components.Plugins()
/// .Add(new MyPlugin(...))
/// ).Build();
/// </code>
/// </example>
/// <returns>a configuration builder</returns>
public static PluginConfigurationBuilder Plugins() => new PluginConfigurationBuilder();

/// <summary>
/// Returns a configuration builder for the SDK's plugin configuration, with an initial set of plugins.
/// </summary>
/// <param name="plugins">a collection of plugins</param>
/// <returns>a configuration builder</returns>
public static PluginConfigurationBuilder Plugins(IEnumerable<Plugin> plugins) => new PluginConfigurationBuilder(plugins);
}
}
9 changes: 8 additions & 1 deletion pkgs/sdk/client/src/Configuration.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System;
using System;
using System.Collections.Immutable;
using LaunchDarkly.Sdk.Client.Integrations;
using LaunchDarkly.Sdk.Client.Interfaces;
using LaunchDarkly.Sdk.Client.Internal.Interfaces;
using LaunchDarkly.Sdk.Client.PlatformSpecific;
using LaunchDarkly.Sdk.Client.Plugins;
using LaunchDarkly.Sdk.Client.Subsystems;

namespace LaunchDarkly.Sdk.Client
Expand Down Expand Up @@ -125,6 +126,11 @@ public sealed class Configuration
/// <seealso cref="ConfigurationBuilder.Persistence(Integrations.PersistenceConfigurationBuilder)"/>
public PersistenceConfigurationBuilder PersistenceConfigurationBuilder { get; }

/// <summary>
/// Contains methods for configuring the SDK's 'plugins' to extend or customize SDK behavior.
/// </summary>
public PluginConfigurationBuilder Plugins { get; }

/// <summary>
/// Defines the base service URIs used by SDK components.
/// </summary>
Expand Down Expand Up @@ -210,6 +216,7 @@ internal Configuration(ConfigurationBuilder builder)
MobileKey = builder._mobileKey;
Offline = builder._offline;
PersistenceConfigurationBuilder = builder._persistenceConfigurationBuilder;
Plugins = builder._plugins;
ServiceEndpoints = (builder._serviceEndpointsBuilder ?? Components.ServiceEndpoints()).Build();
BackgroundModeManager = builder._backgroundModeManager;
ConnectivityStateManager = builder._connectivityStateManager;
Expand Down
16 changes: 15 additions & 1 deletion pkgs/sdk/client/src/ConfigurationBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using System.Net.Http;
using System.Net.Http;
using LaunchDarkly.Logging;
using LaunchDarkly.Sdk.Client.Integrations;
using LaunchDarkly.Sdk.Client.Internal.Interfaces;
using LaunchDarkly.Sdk.Client.Plugins;
using LaunchDarkly.Sdk.Client.Subsystems;
using LaunchDarkly.Sdk.Helpers;

Expand Down Expand Up @@ -70,6 +71,7 @@ public enum AutoEnvAttributes
internal string _mobileKey;
internal bool _offline = false;
internal PersistenceConfigurationBuilder _persistenceConfigurationBuilder = null;
internal PluginConfigurationBuilder _plugins = null;
internal ServiceEndpointsBuilder _serviceEndpointsBuilder = null;

// Internal properties only settable for testing
Expand Down Expand Up @@ -108,6 +110,7 @@ internal ConfigurationBuilder(Configuration copyFrom)
SetMobileKeyIfValid(copyFrom.MobileKey);
_offline = copyFrom.Offline;
_persistenceConfigurationBuilder = copyFrom.PersistenceConfigurationBuilder;
_plugins = copyFrom.Plugins;
_serviceEndpointsBuilder = new ServiceEndpointsBuilder(copyFrom.ServiceEndpoints);
}

Expand Down Expand Up @@ -435,6 +438,17 @@ public ConfigurationBuilder Persistence(PersistenceConfigurationBuilder persiste
return this;
}

/// <summary>
/// Sets the SDK's plugin configuration.
/// </summary>
/// <param name="pluginsConfig">the plugin configuration</param>
/// <returns>the same builder</returns>
public ConfigurationBuilder Plugins(PluginConfigurationBuilder pluginsConfig)
{
_plugins = pluginsConfig;
return this;
}

/// <summary>
/// Sets the SDK's service URIs, using a configuration builder obtained from
/// <see cref="Components.ServiceEndpoints"/>.
Expand Down
42 changes: 42 additions & 0 deletions pkgs/sdk/client/src/Hooks/EvaluationSeriesContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace LaunchDarkly.Sdk.Client.Hooks
{
/// <summary>
/// EvaluationSeriesContext represents parameters associated with a feature flag evaluation. It is
/// made available in <see cref="Hook"/> stage callbacks.
/// </summary>
public sealed class EvaluationSeriesContext {
/// <summary>
/// The key of the feature flag.
/// </summary>
public string FlagKey { get; }

/// <summary>
/// The Context used for evaluation.
/// </summary>
public Context Context { get; }

/// <summary>
/// The user-provided default value for the evaluation.
/// </summary>
public LdValue DefaultValue { get; }

/// <summary>
/// The variation method that triggered the evaluation.
/// </summary>
public string Method { get; }

/// <summary>
/// Constructs a new EvaluationSeriesContext.
/// </summary>
/// <param name="flagKey">the flag key</param>
/// <param name="context">the context</param>
/// <param name="defaultValue">the default value</param>
/// <param name="method">the variation method</param>
public EvaluationSeriesContext(string flagKey, Context context, LdValue defaultValue, string method) {
FlagKey = flagKey;
Context = context;
DefaultValue = defaultValue;
Method = method;
}
}
}
124 changes: 124 additions & 0 deletions pkgs/sdk/client/src/Hooks/Hook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System;
using System.Collections.Immutable;

namespace LaunchDarkly.Sdk.Client.Hooks
{

using SeriesData = ImmutableDictionary<string, object>;

/// <summary>
/// A Hook is a set of user-defined callbacks that are executed by the SDK at various points
/// of interest. To create your own hook with customized logic, derive from Hook and override its methods.
///
/// Hook currently defines an "evaluation" series, which is composed of two stages:
/// "beforeEvaluation" and "afterEvaluation".
///
/// These are executed by the SDK before and after the evaluation of a
/// feature flag.
///
/// Multiple hooks may be configured in the SDK. By default, the SDK will execute each hook's beforeEvaluation
/// stage in the order they were configured, and afterEvaluation in reverse order.
///
/// This means the last hook defined will tightly wrap the evaluation process, while hooks defined earlier in the
/// sequence are nested outside of it.
/// </summary>
public class Hook : IDisposable
{
/// <summary>
/// Access this hook's <see cref="HookMetadata"/>.
/// </summary>
public HookMetadata Metadata { get; private set; }


/// <summary>
/// BeforeEvaluation is executed by the SDK before the evaluation of a feature flag. It does not apply to
/// evaluations performed during a call to AllFlagsState.
///
/// To pass user-configured data to <see cref="AfterEvaluation"/>, return a modification of the given
/// <see cref="SeriesData"/>. You may use existing ImmutableDictionary methods, for example:
///
/// <code>
/// var builder = data.ToBuilder();
/// builder["foo"] = "bar";
/// return builder.ToImmutable();
/// </code>
///
/// Or, you may use the <see cref="SeriesDataBuilder"/> for a fluent API:
/// <code>
/// return new SeriesDataBuilder(data).Set("foo", "bar").Build();
/// </code>
///
/// The modified data is not shared with any other hook. It will be passed to subsequent stages in the evaluation
/// series, including <see cref="AfterEvaluation"/>.
///
/// </summary>
/// <param name="context">parameters associated with this evaluation</param>
/// <param name="data">user-configurable data, currently empty</param>
/// <returns>user-configurable data, which will be forwarded to <see cref="AfterEvaluation"/></returns>
public virtual SeriesData BeforeEvaluation(EvaluationSeriesContext context, SeriesData data) =>
data;


/// <summary>
/// AfterEvaluation is executed by the SDK after the evaluation of a feature flag. It does not apply to
/// evaluations performed during a call to AllFlagsState.
///
/// The function should return the given <see cref="SeriesData"/> unmodified, for forward compatibility with subsequent
/// stages that may be added.
///
/// </summary>
/// <param name="context">parameters associated with this evaluation</param>
/// <param name="data">user-configurable data from the <see cref="BeforeEvaluation"/> stage</param>
/// <param name="detail">flag evaluation result</param>
/// <returns>user-configurable data, which is currently unused but may be forwarded to subsequent stages in future versions of the SDK</returns>
public virtual SeriesData AfterEvaluation(EvaluationSeriesContext context, SeriesData data,
EvaluationDetail<LdValue> detail) => data;

/// <summary>
/// Constructs a new Hook with the given name. The name may be used in log messages.
/// </summary>
/// <param name="name">the name of the hook</param>
public Hook(string name)
{
Metadata = new HookMetadata(name);
}

/// <summary>
/// Disposes the hook. This method will be called when the SDK is disposed.
/// </summary>
/// <param name="disposing">true if the caller is Dispose, false if the caller is a finalizer</param>
protected virtual void Dispose(bool disposing)
{
}

/// <summary>
/// Disposes the hook. This method will be called when the SDK is disposed.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

/// <summary>
/// HookMetadata contains information related to a Hook which can be inspected by the SDK, or within
/// a hook stage.
/// </summary>
public sealed class HookMetadata
{
/// <summary>
/// Constructs a new HookMetadata with the given hook name.
/// </summary>
/// <param name="name">name of the hook. May be used in logs by the SDK</param>
public HookMetadata(string name)
{
Name = name;
}

/// <summary>
/// Returns the name of the hook.
/// </summary>
public string Name { get; }
}
}
23 changes: 23 additions & 0 deletions pkgs/sdk/client/src/Hooks/Method.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace LaunchDarkly.Sdk.Client.Hooks
{
/// <summary>
/// Method represents the SDK client method that triggered a hook invocation.
/// </summary>
#pragma warning disable 1591
public static class Method
{
public const string BoolVariation = "LdClient.BoolVariation";
public const string BoolVariationDetail = "LdClient.BoolVariationDetail";
public const string IntVariation = "LdClient.IntVariation";
public const string IntVariationDetail = "LdClient.IntVariationDetail";
public const string FloatVariation = "LdClient.FloatVariation";
public const string FloatVariationDetail = "LdClient.FloatVariationDetail";
public const string DoubleVariation = "LdClient.DoubleVariation";
public const string DoubleVariationDetail = "LdClient.DoubleVariationDetail";
public const string StringVariation = "LdClient.StringVariation";
public const string StringVariationDetail = "LdClient.StringVariationDetail";
public const string JsonVariation = "LdClient.JsonVariation";
public const string JsonVariationDetail = "LdClient.JsonVariationDetail";
}
#pragma warning restore 1591
}
59 changes: 59 additions & 0 deletions pkgs/sdk/client/src/Hooks/SeriesDataBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Collections.Immutable;

namespace LaunchDarkly.Sdk.Client.Hooks
{
/// <summary>
/// Builder for constructing series data, which is passed to between <see cref="Hook"/> methods.
///
/// Use of this builder is optional; it is provided for convenience.
///
/// <example>
/// <code>
/// // ImmutableDictionary passed into Hook method:
/// var data = ...
/// // Add a new key and return an updated dictionary:
/// return new SeriesDataBuilder(data).Set("key", "value").Build();
/// </code>
/// </example>
/// </summary>
public sealed class SeriesDataBuilder
{
private readonly ImmutableDictionary<string, object>.Builder _builder;

/// <summary>
/// Constructs a new builder from pre-existing series data.
/// </summary>
/// <param name="dictionary">pre-existing series data</param>
public SeriesDataBuilder(ImmutableDictionary<string, object> dictionary)
{
_builder = dictionary.ToBuilder();
}

/// <summary>
/// Constructs a new builder with empty series data.
/// </summary>
public SeriesDataBuilder(): this(ImmutableDictionary<string, object>.Empty) {}


/// <summary>
/// Sets a key-value pair.
/// </summary>
/// <param name="key">key of value</param>
/// <param name="value">the value to set</param>
/// <returns>this builder</returns>
public SeriesDataBuilder Set(string key, object value)
{
_builder[key] = value;
return this;
}

/// <summary>
/// Returns a SeriesData based on the current state of the builder.
/// </summary>
/// <returns>new series data</returns>
public ImmutableDictionary<string, object> Build()
{
return _builder.Count == 0 ? ImmutableDictionary<string, object>.Empty : _builder.ToImmutable();
}
}
}
Loading
Loading