Skip to content

feat: Add plugin support to Client SDK#229

Open
abelonogov-ld wants to merge 8 commits intomainfrom
andrey/clientsdk-plugins-and-hooks
Open

feat: Add plugin support to Client SDK#229
abelonogov-ld wants to merge 8 commits intomainfrom
andrey/clientsdk-plugins-and-hooks

Conversation

@abelonogov-ld
Copy link

@abelonogov-ld abelonogov-ld commented Feb 25, 2026

Summary

This PR adds plugin architecture and hook-based flag evaluation lifecycle support to the LaunchDarkly Client SDK for .NET, ported from the Server SDK implementation.

Plugin System

  • Introduced a Plugin base class (Plugins/Plugin.cs) that provides a GetHooks() method, allowing plugins to register hooks with the SDK
  • Added PluginConfiguration and PluginConfigurationBuilder to configure plugins via ConfigurationBuilder.Plugins()
  • Updated LdClient to initialize plugins during startup, collect their hooks, and dispose of them properly during shutdown
  • Refactored LdClient cleanup logic to ensure proper disposal order of resources

Hook Framework

  • Hook base class with virtual BeforeEvaluation, AfterEvaluation, BeforeIdentify, and AfterIdentify stage methods, plus IDisposable support
  • EvaluationSeriesContext - captures flag key, context, default value, method, and environment ID for evaluation hooks
  • IdentifySeriesContext / IdentifySeriesResult - context and result types for identify hooks
  • Method constants mapping to SDK variation methods (e.g. LdClient.BoolVariation)
  • SeriesDataBuilder - fluent API for building immutable series data passed between hook stages

Hook Execution Engine

  • Executor / NoopExecutor implementing IHookExecutor - manages hook lifecycle with proper forward/reverse ordering
  • BeforeEvaluation / AfterEvaluation stage executors with error isolation - exceptions in one hook do not prevent other hooks from executing
  • Integrated hook execution into LdClient.EvaluateInternal(), wrapping flag evaluations with BeforeEvaluation and AfterEvaluation calls
  • Added GetMethodName<T>() to resolve the correct Method constant based on variation type and detail mode
  • Fixed prerequisite evaluation to use EvaluateInternal directly, avoiding recursive hook invocations
image

Test Plan

  • EvaluationSeriesTest - hooks execute in LIFO order (forward before, reverse after)
  • EvaluationSeriesTest - exceptions in hook stages don't prevent other hooks from running
  • EvaluationSeriesTest - stage failures log expected error messages with flag key and hook name
  • EvaluationSeriesTest - disposing executor disposes all registered hooks
  • PluginConfigurationBuilderTest - plugin configuration builder tests pass
  • LdClientPluginTests - end-to-end plugin/hook integration tests pass
  • All 7 evaluation series tests pass (dotnet test --filter EvaluationSeriesTest)

Note

Medium Risk
Touches core LdClient evaluation flow by wrapping all Variation* calls with hook execution and adds plugin registration at startup, so regressions could affect flag evaluation behavior/performance despite safeguards like noop execution when unconfigured.

Overview
Adds client-side plugin support by introducing PluginConfigurationBuilder/PluginConfiguration and wiring a new Plugins(...) option through Components, ConfigurationBuilder, and Configuration.

Integrates plugin lifecycle into LdClient initialization: builds plugin config, creates EnvironmentMetadata, collects plugin-provided Hooks, instantiates a hook Executor (or NoopExecutor when none), then registers plugins; hook execution is disposed with the client and logs under a new LogNames.HooksSubLog.

Introduces a hook API and execution pipeline for evaluations (Hook, EvaluationSeriesContext, stage executors) and wraps VariationInternal so hooks run before/after evaluations; prerequisite evaluations are refactored to call EvaluateInternal directly to avoid triggering hooks. Adds unit tests covering hook ordering/error isolation/disposal and plugin builder/registration behavior.

Written by Cursor Bugbot for commit e5d9dc0. This will update automatically on new commits. Configure here.

@abelonogov-ld abelonogov-ld requested a review from a team as a code owner February 25, 2026 19:38
@abelonogov-ld abelonogov-ld changed the title feat: Plugin support for CliendSDK feat: Add plugin support to Client SDK Feb 25, 2026
@tanderson-ld tanderson-ld self-requested a review February 26, 2026 17:10
@abelonogov-ld abelonogov-ld changed the title feat: Add plugin support to Client SDK WIP feat: Add plugin support to Client SDK Feb 26, 2026
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

@abelonogov-ld abelonogov-ld changed the title WIP feat: Add plugin support to Client SDK feat: Add plugin support to Client SDK Feb 26, 2026
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need onPluginsReady in the base plugin? I think this was added to solve the "two plugins need each other" interaction back when Agustine was working on Replay and Observability being separate plugins but needing to wait until both registered. Spec requirement

My impression is it makes sense to add while you're working on this story.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed that logic but I did workaround in Obs-SDK, I count number of getHooks calls and number of register calls. When they are equal it means everything is ready


var applicationMetadata = new ApplicationMetadata(
applicationInfo.ApplicationId,
applicationInfo.ApplicationVersion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also Name and VersionName params that should be carried over.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to add optional fields id and version in PluginMetadata? https://github.com/launchdarkly/sdk-specs/tree/main/specs/PLUGIN-sdk-plugin-support#requirement-113

Also related to the same sort of problem Agustine was solving.

}

_hookExecutor = _pluginHooks.Any()
? (IHookExecutor)new Executor(_log.SubLogger(LogNames.HooksSubLog), _pluginHooks)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_pluginHooks is only used here. Does it need to be a private member on LDClient? Could it just be local in this function that populates it.


PluginConfiguration pluginConfig = null;
EnvironmentMetadata environmentMetadata = null;
if (_config.Plugins != null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nullity, null checks, assignment, and usage of nullables in these next 20 lines of code seems fragile. Consider structuring differently so that their interaction is easier to discern.


PluginConfiguration pluginConfig = null;
EnvironmentMetadata environmentMetadata = null;
if (_config.Plugins != null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the server SDK uses Components.Plugins() if no config is provided. This may be an option to deal with the missing config in an elegant way.

/// <param name="context">parameters associated with this identify operation</param>
/// <param name="data">user-configurable data, currently empty</param>
/// <returns>user-configurable data, which will be forwarded to <see cref="AfterIdentify"/></returns>
public virtual SeriesData BeforeIdentify(IdentifySeriesContext context, SeriesData data) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see BeforeIdentify and AfterIdentify being invoked anywhere. Is that in scope of this PR? Seems like it must be if the Hook is now available. Also need tests for this case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be another PR

Copy link
Author

@abelonogov-ld abelonogov-ld Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ryan said that Evaluation flag is must to be PR mergable

bool checkType, EventFactory eventFactory)
{
var evalSeriesContext = new EvaluationSeriesContext(featureKey, Context, defaultJson,
GetMethodName<T>(eventFactory));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is environment id ever passed in to evaluation series context? I can't recall if Dotnet Client got support for this from the data source (ultimately from the headers on the responses from the server).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants