Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement transaction context #312

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions src/OpenFeature/Api.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public sealed class Api : IEventBus
private EventExecutor _eventExecutor = new EventExecutor();
private ProviderRepository _repository = new ProviderRepository();
private readonly ConcurrentStack<Hook> _hooks = new ConcurrentStack<Hook>();
private ITransactionContextPropagator _transactionContextPropagator = new NoOpTransactionContextPropagator();
private readonly object _transactionContextPropagatorLock = new();

/// The reader/writer locks are not disposed because the singleton instance should never be disposed.
private readonly ReaderWriterLockSlim _evaluationContextLock = new ReaderWriterLockSlim();
Expand All @@ -47,6 +49,7 @@ public async Task SetProviderAsync(FeatureProvider featureProvider)
{
this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider);
await this._repository.SetProviderAsync(featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false);

}

/// <summary>
Expand Down Expand Up @@ -85,7 +88,6 @@ public FeatureProvider GetProvider()
/// Gets the feature provider with given domain
/// </summary>
/// <param name="domain">An identifier which logically binds clients with providers</param>

/// <returns>A provider associated with the given domain, if domain is empty or doesn't
/// have a corresponding provider the default provider will be returned</returns>
public FeatureProvider GetProvider(string domain)
Expand All @@ -109,7 +111,6 @@ public FeatureProvider GetProvider(string domain)
/// assigned to it the default provider will be returned
/// </summary>
/// <param name="domain">An identifier which logically binds clients with providers</param>

/// <returns>Metadata assigned to provider</returns>
public Metadata? GetProviderMetadata(string domain) => this.GetProvider(domain).GetMetadata();

Expand Down Expand Up @@ -218,6 +219,59 @@ public EvaluationContext GetContext()
}
}

/// <summary>
/// Return the transaction context propagator.
/// </summary>
/// <returns><see cref="ITransactionContextPropagator"/>the registered transaction context propagator</returns>
public ITransactionContextPropagator GetTransactionContextPropagator()
{
return this._transactionContextPropagator;
}

/// <summary>
/// Sets the transaction context propagator.
/// </summary>
/// <param name="transactionContextPropagator">the transaction context propagator to be registered</param>
/// <exception cref="ArgumentNullException">Transaction context propagator cannot be null</exception>
public void SetTransactionContextPropagator(ITransactionContextPropagator transactionContextPropagator)
{
if (transactionContextPropagator == null)
{
throw new ArgumentNullException(nameof(transactionContextPropagator),
"Transaction context propagator cannot be null");
}

lock (this._transactionContextPropagatorLock)
{
this._transactionContextPropagator = transactionContextPropagator;
}
}

/// <summary>
/// Returns the currently defined transaction context using the registered transaction context propagator.
/// </summary>
/// <returns><see cref="EvaluationContext"/>The current transaction context</returns>
public EvaluationContext GetTransactionContext()
{
return this._transactionContextPropagator.GetTransactionContext();
}

/// <summary>
/// Sets the transaction context using the registered transaction context propagator.
/// </summary>
/// <param name="evaluationContext">The <see cref="EvaluationContext"/> to set</param>
/// <exception cref="InvalidOperationException">Transaction context propagator is not set.</exception>
/// <exception cref="ArgumentNullException">Evaluation context cannot be null</exception>
public void SetTransactionContext(EvaluationContext evaluationContext)
{
if (evaluationContext == null)
{
throw new ArgumentNullException(nameof(evaluationContext), "Evaluation context cannot be null");
}

this._transactionContextPropagator.SetTransactionContext(evaluationContext);
}

/// <summary>
/// <para>
/// Shut down and reset the current status of OpenFeature API.
Expand All @@ -234,6 +288,7 @@ public async Task ShutdownAsync()
{
this._evaluationContext = EvaluationContext.Empty;
this._hooks.Clear();
this._transactionContextPropagator = new NoOpTransactionContextPropagator();

// TODO: make these lazy to avoid extra allocations on the common cleanup path?
this._eventExecutor = new EventExecutor();
Expand Down
25 changes: 25 additions & 0 deletions src/OpenFeature/AsyncLocalTransactionContextPropagator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Threading;
using OpenFeature.Model;

namespace OpenFeature;

/// <summary>
/// It is a no-op implementation of <see cref="ITransactionContextPropagator"/>
/// It uses the <see cref="AsyncLocal{T}"/> to store the transaction context.
/// </summary>
public sealed class AsyncLocalTransactionContextPropagator : ITransactionContextPropagator
{
private readonly AsyncLocal<EvaluationContext> _transactionContext = new();

/// <inheritdoc />
public EvaluationContext GetTransactionContext()
{
return this._transactionContext.Value ?? EvaluationContext.Empty;
}

/// <inheritdoc />
public void SetTransactionContext(EvaluationContext evaluationContext)
{
this._transactionContext.Value = evaluationContext;
}
}
26 changes: 26 additions & 0 deletions src/OpenFeature/Model/ITransactionContextPropagator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace OpenFeature.Model;

/// <summary>
/// <see cref="ITransactionContextPropagator"/> is responsible for persisting a transactional context
/// for the duration of a single transaction.
/// Examples of potential transaction specific context include: a user id, user agent, IP.
/// Transaction context is merged with evaluation context prior to flag evaluation.
/// </summary>
/// <remarks>
/// The precedence of merging context can be seen in
/// <a href="https://openfeature.dev/specification/sections/evaluation-context#requirement-323">the specification</a>.
/// </remarks>
public interface ITransactionContextPropagator
{
/// <summary>
/// Returns the currently defined transaction context using the registered transaction context propagator.
/// </summary>
/// <returns><see cref="EvaluationContext"/>The current transaction context</returns>
EvaluationContext GetTransactionContext();

/// <summary>
/// Sets the transaction context.
/// </summary>
/// <param name="evaluationContext">The transaction context to be set</param>
void SetTransactionContext(EvaluationContext evaluationContext);
}
15 changes: 15 additions & 0 deletions src/OpenFeature/NoOpTransactionContextPropagator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using OpenFeature.Model;

namespace OpenFeature;

internal class NoOpTransactionContextPropagator : ITransactionContextPropagator
{
public EvaluationContext GetTransactionContext()
{
return EvaluationContext.Empty;
}

public void SetTransactionContext(EvaluationContext evaluationContext)
{
}
}
1 change: 1 addition & 0 deletions src/OpenFeature/OpenFeatureClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ private async Task<FlagEvaluationDetails<T>> EvaluateFlagAsync<T>(
evaluationContextBuilder.Merge(evaluationContext);
evaluationContextBuilder.Merge(this.GetContext());
evaluationContextBuilder.Merge(context);
evaluationContextBuilder.Merge(Api.Instance.GetTransactionContext());

var allHooks = new List<Hook>()
.Concat(Api.Instance.GetHooks())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using OpenFeature.Model;
using Xunit;

namespace OpenFeature.Tests;

public class AsyncLocalTransactionContextPropagatorTests
{
[Fact]
public void GetTransactionContext_ReturnsEmpty_WhenNoContextIsSet()
{
// Arrange
var propagator = new AsyncLocalTransactionContextPropagator();

// Act
var context = propagator.GetTransactionContext();

// Assert
Assert.Equal(EvaluationContext.Empty, context);
}

[Fact]
public void SetTransactionContext_SetsAndGetsContextCorrectly()
{
// Arrange
var propagator = new AsyncLocalTransactionContextPropagator();
var evaluationContext = EvaluationContext.Empty;

// Act
propagator.SetTransactionContext(evaluationContext);
var context = propagator.GetTransactionContext();

// Assert
Assert.Equal(evaluationContext, context);
}

[Fact]
public void SetTransactionContext_OverridesPreviousContext()
{
// Arrange
var propagator = new AsyncLocalTransactionContextPropagator();

var initialContext = EvaluationContext.Builder()
.Set("initial", "yes")
.Build();
var newContext = EvaluationContext.Empty;

// Act
propagator.SetTransactionContext(initialContext);
propagator.SetTransactionContext(newContext);
var context = propagator.GetTransactionContext();

// Assert
Assert.Equal(newContext, context);
}
}
70 changes: 70 additions & 0 deletions test/OpenFeature.Tests/OpenFeatureTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
Expand Down Expand Up @@ -244,5 +245,74 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping()
(await client1.GetBooleanValueAsync("test", false)).Should().BeTrue();
(await client2.GetBooleanValueAsync("test", false)).Should().BeFalse();
}

[Fact]
public void SetTransactionContextPropagator_ShouldThrowArgumentNullException_WhenNullPropagatorIsPassed()
{
// Arrange
var api = Api.Instance;

// Act & Assert
Assert.Throws<ArgumentNullException>(() => api.SetTransactionContextPropagator(null!));
}

[Fact]
public void SetTransactionContextPropagator_ShouldSetPropagator_WhenValidPropagatorIsPassed()
{
// Arrange
var api = Api.Instance;
var mockPropagator = Substitute.For<ITransactionContextPropagator>();

// Act
api.SetTransactionContextPropagator(mockPropagator);

// Assert
Assert.Equal(mockPropagator, api.GetTransactionContextPropagator());
}

[Fact]
public void SetTransactionContext_ShouldThrowArgumentNullException_WhenEvaluationContextIsNull()
{
// Arrange
var api = Api.Instance;

// Act & Assert
Assert.Throws<ArgumentNullException>(() => api.SetTransactionContext(null!));
}

[Fact]
public void SetTransactionContext_ShouldSetTransactionContext_WhenValidEvaluationContextIsProvided()
{
// Arrange
var api = Api.Instance;
var evaluationContext = EvaluationContext.Empty;
var mockPropagator = Substitute.For<ITransactionContextPropagator>();
mockPropagator.GetTransactionContext().Returns(evaluationContext);
api.SetTransactionContextPropagator(mockPropagator);
api.SetTransactionContext(evaluationContext);

// Act
api.SetTransactionContext(evaluationContext);
var result = api.GetTransactionContext();

// Assert
mockPropagator.Received().SetTransactionContext(evaluationContext);
Assert.Equal(evaluationContext, result);
}

[Fact]
public void GetTransactionContext_ShouldReturnEmptyEvaluationContext_WhenNoPropagatorIsSet()
{
// Arrange
var api = Api.Instance;
var context = EvaluationContext.Builder().Set("status", "not-ready").Build();
api.SetTransactionContext(context);

// Act
var result = api.GetTransactionContext();

// Assert
Assert.Equal(EvaluationContext.Empty, result);
}
}
}
Loading