From 5a5312cc082ccd880b65165135e05b4f3b035df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:32:24 +0100 Subject: [PATCH] chore!: Enable nullable reference types (#253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Enable nullable reference types - This PR enables the nullable validation and treats warnings as errors. ### Related Issues Fixes #208 ### Notes This PR turns on the nullable checks for the dotnet checks. This gives us a better and safer codebase since it checks for potential null exceptions. ### Breaking changes While this PR won't require changes to the exposed API, it might show some errors to the clients consuming it. --------- Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- build/Common.props | 2 + src/OpenFeature/Api.cs | 12 +- .../Error/FeatureProviderException.cs | 2 +- .../Error/FlagNotFoundException.cs | 2 +- src/OpenFeature/Error/GeneralException.cs | 2 +- .../Error/InvalidContextException.cs | 2 +- src/OpenFeature/Error/ParseErrorException.cs | 2 +- .../Error/ProviderNotReadyException.cs | 2 +- .../Error/TargetingKeyMissingException.cs | 2 +- .../Error/TypeMismatchException.cs | 2 +- src/OpenFeature/EventExecutor.cs | 36 ++--- src/OpenFeature/Extension/EnumExtensions.cs | 2 +- src/OpenFeature/FeatureProvider.cs | 10 +- src/OpenFeature/Hook.cs | 8 +- src/OpenFeature/IFeatureClient.cs | 20 +-- src/OpenFeature/Model/BaseMetadata.cs | 1 - src/OpenFeature/Model/ClientMetadata.cs | 4 +- src/OpenFeature/Model/EvaluationContext.cs | 6 +- .../Model/EvaluationContextBuilder.cs | 4 +- .../Model/FlagEvaluationDetails.cs | 12 +- .../Model/FlagEvaluationOptions.cs | 4 +- src/OpenFeature/Model/FlagMetadata.cs | 1 - src/OpenFeature/Model/HookContext.cs | 8 +- src/OpenFeature/Model/Metadata.cs | 4 +- src/OpenFeature/Model/ProviderEvents.cs | 10 +- src/OpenFeature/Model/ResolutionDetails.cs | 12 +- src/OpenFeature/Model/Structure.cs | 2 +- src/OpenFeature/Model/Value.cs | 10 +- src/OpenFeature/NoOpProvider.cs | 10 +- src/OpenFeature/OpenFeatureClient.cs | 56 +++---- src/OpenFeature/ProviderRepository.cs | 59 ++++--- src/OpenFeature/Providers/Memory/Flag.cs | 1 - .../Providers/Memory/InMemoryProvider.cs | 1 - .../Steps/EvaluationStepDefinitions.cs | 144 +++++++++--------- .../FeatureProviderExceptionTests.cs | 32 ++++ test/OpenFeature.Tests/FlagMetadataTest.cs | 1 - .../OpenFeatureClientTests.cs | 8 +- .../OpenFeatureEvaluationContextTests.cs | 20 ++- .../OpenFeatureEventTests.cs | 72 ++++++--- test/OpenFeature.Tests/OpenFeatureTests.cs | 14 +- .../ProviderRepositoryTests.cs | 16 +- .../Providers/Memory/InMemoryProviderTests.cs | 10 +- test/OpenFeature.Tests/StructureTests.cs | 4 +- test/OpenFeature.Tests/TestImplementations.cs | 18 +-- test/OpenFeature.Tests/ValueTests.cs | 108 ++++++++++++- 45 files changed, 486 insertions(+), 272 deletions(-) diff --git a/build/Common.props b/build/Common.props index b1857e0d..9f807b2c 100644 --- a/build/Common.props +++ b/build/Common.props @@ -6,6 +6,8 @@ true EnableGenerateDocumentationFile + enable + true diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index af302e7e..6dc0f863 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -54,7 +54,7 @@ public void SetProvider(FeatureProvider featureProvider) /// /// The provider cannot be set to null. Attempting to set the provider to null has no effect. /// Implementation of - public async Task SetProviderAsync(FeatureProvider featureProvider) + public async Task SetProviderAsync(FeatureProvider? featureProvider) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); await this._repository.SetProvider(featureProvider, this.GetContext()).ConfigureAwait(false); @@ -80,6 +80,10 @@ public void SetProvider(string clientName, FeatureProvider featureProvider) /// Implementation of public async Task SetProviderAsync(string clientName, FeatureProvider featureProvider) { + if (string.IsNullOrWhiteSpace(clientName)) + { + throw new ArgumentNullException(nameof(clientName)); + } this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); await this._repository.SetProvider(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); } @@ -138,8 +142,8 @@ public FeatureProvider GetProvider(string clientName) /// Logger instance used by client /// Context given to this client /// - public FeatureClient GetClient(string name = null, string version = null, ILogger logger = null, - EvaluationContext context = null) => + public FeatureClient GetClient(string? name = null, string? version = null, ILogger? logger = null, + EvaluationContext? context = null) => new FeatureClient(name, version, logger, context); /// @@ -200,7 +204,7 @@ public void AddHooks(IEnumerable hooks) /// Sets the global /// /// The to set - public void SetContext(EvaluationContext context) + public void SetContext(EvaluationContext? context) { this._evaluationContextLock.EnterWriteLock(); try diff --git a/src/OpenFeature/Error/FeatureProviderException.cs b/src/OpenFeature/Error/FeatureProviderException.cs index df74afa4..b2c43dc7 100644 --- a/src/OpenFeature/Error/FeatureProviderException.cs +++ b/src/OpenFeature/Error/FeatureProviderException.cs @@ -20,7 +20,7 @@ public class FeatureProviderException : Exception /// Common error types /// Exception message /// Optional inner exception - public FeatureProviderException(ErrorType errorType, string message = null, Exception innerException = null) + public FeatureProviderException(ErrorType errorType, string? message = null, Exception? innerException = null) : base(message, innerException) { this.ErrorType = errorType; diff --git a/src/OpenFeature/Error/FlagNotFoundException.cs b/src/OpenFeature/Error/FlagNotFoundException.cs index 6b8fb4bb..b1a5b64a 100644 --- a/src/OpenFeature/Error/FlagNotFoundException.cs +++ b/src/OpenFeature/Error/FlagNotFoundException.cs @@ -15,7 +15,7 @@ public class FlagNotFoundException : FeatureProviderException /// /// Exception message /// Optional inner exception - public FlagNotFoundException(string message = null, Exception innerException = null) + public FlagNotFoundException(string? message = null, Exception? innerException = null) : base(ErrorType.FlagNotFound, message, innerException) { } diff --git a/src/OpenFeature/Error/GeneralException.cs b/src/OpenFeature/Error/GeneralException.cs index 42e0fe73..4580ff31 100644 --- a/src/OpenFeature/Error/GeneralException.cs +++ b/src/OpenFeature/Error/GeneralException.cs @@ -15,7 +15,7 @@ public class GeneralException : FeatureProviderException /// /// Exception message /// Optional inner exception - public GeneralException(string message = null, Exception innerException = null) + public GeneralException(string? message = null, Exception? innerException = null) : base(ErrorType.General, message, innerException) { } diff --git a/src/OpenFeature/Error/InvalidContextException.cs b/src/OpenFeature/Error/InvalidContextException.cs index 6bcc8051..ffea8ab1 100644 --- a/src/OpenFeature/Error/InvalidContextException.cs +++ b/src/OpenFeature/Error/InvalidContextException.cs @@ -15,7 +15,7 @@ public class InvalidContextException : FeatureProviderException /// /// Exception message /// Optional inner exception - public InvalidContextException(string message = null, Exception innerException = null) + public InvalidContextException(string? message = null, Exception? innerException = null) : base(ErrorType.InvalidContext, message, innerException) { } diff --git a/src/OpenFeature/Error/ParseErrorException.cs b/src/OpenFeature/Error/ParseErrorException.cs index 7b3d21e9..81ded456 100644 --- a/src/OpenFeature/Error/ParseErrorException.cs +++ b/src/OpenFeature/Error/ParseErrorException.cs @@ -15,7 +15,7 @@ public class ParseErrorException : FeatureProviderException /// /// Exception message /// Optional inner exception - public ParseErrorException(string message = null, Exception innerException = null) + public ParseErrorException(string? message = null, Exception? innerException = null) : base(ErrorType.ParseError, message, innerException) { } diff --git a/src/OpenFeature/Error/ProviderNotReadyException.cs b/src/OpenFeature/Error/ProviderNotReadyException.cs index c3c8b5d0..ca509692 100644 --- a/src/OpenFeature/Error/ProviderNotReadyException.cs +++ b/src/OpenFeature/Error/ProviderNotReadyException.cs @@ -15,7 +15,7 @@ public class ProviderNotReadyException : FeatureProviderException /// /// Exception message /// Optional inner exception - public ProviderNotReadyException(string message = null, Exception innerException = null) + public ProviderNotReadyException(string? message = null, Exception? innerException = null) : base(ErrorType.ProviderNotReady, message, innerException) { } diff --git a/src/OpenFeature/Error/TargetingKeyMissingException.cs b/src/OpenFeature/Error/TargetingKeyMissingException.cs index 632cc791..71742413 100644 --- a/src/OpenFeature/Error/TargetingKeyMissingException.cs +++ b/src/OpenFeature/Error/TargetingKeyMissingException.cs @@ -15,7 +15,7 @@ public class TargetingKeyMissingException : FeatureProviderException /// /// Exception message /// Optional inner exception - public TargetingKeyMissingException(string message = null, Exception innerException = null) + public TargetingKeyMissingException(string? message = null, Exception? innerException = null) : base(ErrorType.TargetingKeyMissing, message, innerException) { } diff --git a/src/OpenFeature/Error/TypeMismatchException.cs b/src/OpenFeature/Error/TypeMismatchException.cs index 96c23872..83ff0cf3 100644 --- a/src/OpenFeature/Error/TypeMismatchException.cs +++ b/src/OpenFeature/Error/TypeMismatchException.cs @@ -15,7 +15,7 @@ public class TypeMismatchException : FeatureProviderException /// /// Exception message /// Optional inner exception - public TypeMismatchException(string message = null, Exception innerException = null) + public TypeMismatchException(string? message = null, Exception? innerException = null) : base(ErrorType.TypeMismatch, message, innerException) { } diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 7bdfeb6e..a80c92d4 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -14,7 +14,7 @@ internal class EventExecutor : IAsyncDisposable { private readonly object _lockObj = new object(); public readonly Channel EventChannel = Channel.CreateBounded(1); - private FeatureProvider _defaultProvider; + private FeatureProvider? _defaultProvider; private readonly Dictionary _namedProviderReferences = new Dictionary(); private readonly List _activeSubscriptions = new List(); @@ -99,7 +99,7 @@ internal void RemoveClientHandler(string client, ProviderEventTypes type, EventH } } - internal void RegisterDefaultFeatureProvider(FeatureProvider provider) + internal void RegisterDefaultFeatureProvider(FeatureProvider? provider) { if (provider == null) { @@ -115,7 +115,7 @@ internal void RegisterDefaultFeatureProvider(FeatureProvider provider) } } - internal void RegisterClientFeatureProvider(string client, FeatureProvider provider) + internal void RegisterClientFeatureProvider(string client, FeatureProvider? provider) { if (provider == null) { @@ -124,7 +124,7 @@ internal void RegisterClientFeatureProvider(string client, FeatureProvider provi lock (this._lockObj) { var newProvider = provider; - FeatureProvider oldProvider = null; + FeatureProvider? oldProvider = null; if (this._namedProviderReferences.TryGetValue(client, out var foundOldProvider)) { oldProvider = foundOldProvider; @@ -136,7 +136,7 @@ internal void RegisterClientFeatureProvider(string client, FeatureProvider provi } } - private void StartListeningAndShutdownOld(FeatureProvider newProvider, FeatureProvider oldProvider) + private void StartListeningAndShutdownOld(FeatureProvider newProvider, FeatureProvider? oldProvider) { // check if the provider is already active - if not, we need to start listening for its emitted events if (!this.IsProviderActive(newProvider)) @@ -174,7 +174,7 @@ private bool IsProviderActive(FeatureProvider providerRef) return this._activeSubscriptions.Contains(providerRef); } - private void EmitOnRegistration(FeatureProvider provider, ProviderEventTypes eventType, EventHandlerDelegate handler) + private void EmitOnRegistration(FeatureProvider? provider, ProviderEventTypes eventType, EventHandlerDelegate handler) { if (provider == null) { @@ -202,22 +202,22 @@ private void EmitOnRegistration(FeatureProvider provider, ProviderEventTypes eve { handler.Invoke(new ProviderEventPayload { - ProviderName = provider.GetMetadata()?.Name, + ProviderName = provider.GetMetadata().Name, Type = eventType, Message = message }); } catch (Exception exc) { - this.Logger?.LogError("Error running handler: " + exc); + this.Logger.LogError(exc, "Error running handler"); } } } - private async void ProcessFeatureProviderEventsAsync(object providerRef) + private async void ProcessFeatureProviderEventsAsync(object? providerRef) { - var typedProviderRef = (FeatureProvider)providerRef; - if (typedProviderRef.GetEventChannel() is not { Reader: { } reader }) + var typedProviderRef = (FeatureProvider?)providerRef; + if (typedProviderRef?.GetEventChannel() is not { Reader: { } reader }) { return; } @@ -249,7 +249,7 @@ private async void ProcessEventAsync() case Event e: lock (this._lockObj) { - if (this._apiHandlers.TryGetValue(e.EventPayload.Type, out var eventHandlers)) + if (e.EventPayload?.Type != null && this._apiHandlers.TryGetValue(e.EventPayload.Type, out var eventHandlers)) { foreach (var eventHandler in eventHandlers) { @@ -260,11 +260,11 @@ private async void ProcessEventAsync() // look for client handlers and call invoke method there foreach (var keyAndValue in this._namedProviderReferences) { - if (keyAndValue.Value == e.Provider) + if (keyAndValue.Value == e.Provider && keyAndValue.Key != null) { if (this._clientHandlers.TryGetValue(keyAndValue.Key, out var clientRegistry)) { - if (clientRegistry.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) + if (e.EventPayload?.Type != null && clientRegistry.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) { foreach (var eventHandler in clientEventHandlers) { @@ -288,7 +288,7 @@ private async void ProcessEventAsync() // if there is an association for the client to a specific feature provider, then continue continue; } - if (keyAndValues.Value.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) + if (e.EventPayload?.Type != null && keyAndValues.Value.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) { foreach (var eventHandler in clientEventHandlers) { @@ -311,7 +311,7 @@ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) } catch (Exception exc) { - this.Logger?.LogError("Error running handler: " + exc); + this.Logger.LogError(exc, "Error running handler"); } } @@ -325,7 +325,7 @@ public async Task Shutdown() internal class Event { - internal FeatureProvider Provider { get; set; } - internal ProviderEventPayload EventPayload { get; set; } + internal FeatureProvider? Provider { get; set; } + internal ProviderEventPayload? EventPayload { get; set; } } } diff --git a/src/OpenFeature/Extension/EnumExtensions.cs b/src/OpenFeature/Extension/EnumExtensions.cs index 155bfd3e..fe10afb5 100644 --- a/src/OpenFeature/Extension/EnumExtensions.cs +++ b/src/OpenFeature/Extension/EnumExtensions.cs @@ -9,7 +9,7 @@ internal static class EnumExtensions public static string GetDescription(this Enum value) { var field = value.GetType().GetField(value.ToString()); - var attribute = field.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute; + var attribute = field?.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute; return attribute?.Description ?? value.ToString(); } } diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index 000ab0fb..bcc66558 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -45,7 +45,7 @@ public abstract class FeatureProvider /// /// public abstract Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext context = null); + EvaluationContext? context = null); /// /// Resolves a string feature flag @@ -55,7 +55,7 @@ public abstract Task> ResolveBooleanValue(string flagKey /// /// public abstract Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext context = null); + EvaluationContext? context = null); /// /// Resolves a integer feature flag @@ -65,7 +65,7 @@ public abstract Task> ResolveStringValue(string flagKe /// /// public abstract Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext context = null); + EvaluationContext? context = null); /// /// Resolves a double feature flag @@ -75,7 +75,7 @@ public abstract Task> ResolveIntegerValue(string flagKey, /// /// public abstract Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext context = null); + EvaluationContext? context = null); /// /// Resolves a structured feature flag @@ -85,7 +85,7 @@ public abstract Task> ResolveDoubleValue(string flagKe /// /// public abstract Task> ResolveStructureValue(string flagKey, Value defaultValue, - EvaluationContext context = null); + EvaluationContext? context = null); /// /// Get the status of the provider. diff --git a/src/OpenFeature/Hook.cs b/src/OpenFeature/Hook.cs index c35c3cb4..50162729 100644 --- a/src/OpenFeature/Hook.cs +++ b/src/OpenFeature/Hook.cs @@ -29,7 +29,7 @@ public abstract class Hook /// Flag value type (bool|number|string|object) /// Modified EvaluationContext that is used for the flag evaluation public virtual Task Before(HookContext context, - IReadOnlyDictionary hints = null) + IReadOnlyDictionary? hints = null) { return Task.FromResult(EvaluationContext.Empty); } @@ -42,7 +42,7 @@ public virtual Task Before(HookContext context, /// Caller provided data /// Flag value type (bool|number|string|object) public virtual Task After(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary hints = null) + IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } @@ -55,7 +55,7 @@ public virtual Task After(HookContext context, FlagEvaluationDetails de /// Caller provided data /// Flag value type (bool|number|string|object) public virtual Task Error(HookContext context, Exception error, - IReadOnlyDictionary hints = null) + IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } @@ -66,7 +66,7 @@ public virtual Task Error(HookContext context, Exception error, /// Provides context of innovation /// Caller provided data /// Flag value type (bool|number|string|object) - public virtual Task Finally(HookContext context, IReadOnlyDictionary hints = null) + public virtual Task Finally(HookContext context, IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs index e1550ae4..b262f8f1 100644 --- a/src/OpenFeature/IFeatureClient.cs +++ b/src/OpenFeature/IFeatureClient.cs @@ -60,7 +60,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag value. - Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a boolean feature flag @@ -70,7 +70,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - Task> GetBooleanDetails(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetBooleanDetails(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a string feature flag @@ -80,7 +80,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag value. - Task GetStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetStringValue(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a string feature flag @@ -90,7 +90,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - Task> GetStringDetails(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetStringDetails(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a integer feature flag @@ -100,7 +100,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag value. - Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a integer feature flag @@ -110,7 +110,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a double feature flag @@ -120,7 +120,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag value. - Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a double feature flag @@ -130,7 +130,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a structure object feature flag @@ -140,7 +140,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag value. - Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a structure object feature flag @@ -150,6 +150,6 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); } } diff --git a/src/OpenFeature/Model/BaseMetadata.cs b/src/OpenFeature/Model/BaseMetadata.cs index 81f1bc50..876247df 100644 --- a/src/OpenFeature/Model/BaseMetadata.cs +++ b/src/OpenFeature/Model/BaseMetadata.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Collections.Immutable; -#nullable enable namespace OpenFeature.Model; /// diff --git a/src/OpenFeature/Model/ClientMetadata.cs b/src/OpenFeature/Model/ClientMetadata.cs index d006ff0d..b98e6e9d 100644 --- a/src/OpenFeature/Model/ClientMetadata.cs +++ b/src/OpenFeature/Model/ClientMetadata.cs @@ -8,14 +8,14 @@ public sealed class ClientMetadata : Metadata /// /// Version of the client /// - public string Version { get; } + public string? Version { get; } /// /// Initializes a new instance of the class /// /// Name of client /// Version of client - public ClientMetadata(string name, string version) : base(name) + public ClientMetadata(string? name, string? version) : base(name) { this.Version = version; } diff --git a/src/OpenFeature/Model/EvaluationContext.cs b/src/OpenFeature/Model/EvaluationContext.cs index 6db585a1..59b1fe20 100644 --- a/src/OpenFeature/Model/EvaluationContext.cs +++ b/src/OpenFeature/Model/EvaluationContext.cs @@ -18,7 +18,7 @@ public sealed class EvaluationContext /// /// The targeting key /// The content of the context. - internal EvaluationContext(string targetingKey, Structure content) + internal EvaluationContext(string? targetingKey, Structure content) { this.TargetingKey = targetingKey; this._structure = content; @@ -70,7 +70,7 @@ private EvaluationContext() /// /// Thrown when the key is /// - public bool TryGetValue(string key, out Value value) => this._structure.TryGetValue(key, out value); + public bool TryGetValue(string key, out Value? value) => this._structure.TryGetValue(key, out value); /// /// Gets all values as a Dictionary @@ -89,7 +89,7 @@ public IImmutableDictionary AsDictionary() /// /// Returns the targeting key for the context. /// - public string TargetingKey { get; } + public string? TargetingKey { get; } /// /// Return an enumerator for all values diff --git a/src/OpenFeature/Model/EvaluationContextBuilder.cs b/src/OpenFeature/Model/EvaluationContextBuilder.cs index 89174cf6..1afb02fc 100644 --- a/src/OpenFeature/Model/EvaluationContextBuilder.cs +++ b/src/OpenFeature/Model/EvaluationContextBuilder.cs @@ -14,7 +14,7 @@ public sealed class EvaluationContextBuilder { private readonly StructureBuilder _attributes = Structure.Builder(); - internal string TargetingKey { get; private set; } + internal string? TargetingKey { get; private set; } /// /// Internal to only allow direct creation by . @@ -138,7 +138,7 @@ public EvaluationContextBuilder Set(string key, DateTime value) /// This builder public EvaluationContextBuilder Merge(EvaluationContext context) { - string newTargetingKey = ""; + string? newTargetingKey = ""; if (!string.IsNullOrWhiteSpace(TargetingKey)) { diff --git a/src/OpenFeature/Model/FlagEvaluationDetails.cs b/src/OpenFeature/Model/FlagEvaluationDetails.cs index cff22a8b..9af2f4bf 100644 --- a/src/OpenFeature/Model/FlagEvaluationDetails.cs +++ b/src/OpenFeature/Model/FlagEvaluationDetails.cs @@ -31,24 +31,24 @@ public sealed class FlagEvaluationDetails /// details. /// /// - public string ErrorMessage { get; } + public string? ErrorMessage { get; } /// /// Describes the reason for the outcome of the evaluation process /// - public string Reason { get; } + public string? Reason { get; } /// /// A variant is a semantic identifier for a value. This allows for referral to particular values without /// necessarily including the value itself, which may be quite prohibitively large or otherwise unsuitable /// in some cases. /// - public string Variant { get; } + public string? Variant { get; } /// /// A structure which supports definition of arbitrary properties, with keys of type string, and values of type boolean, string, or number. /// - public FlagMetadata FlagMetadata { get; } + public FlagMetadata? FlagMetadata { get; } /// /// Initializes a new instance of the class. @@ -60,8 +60,8 @@ public sealed class FlagEvaluationDetails /// Variant /// Error message /// Flag metadata - public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, string reason, string variant, - string errorMessage = null, FlagMetadata flagMetadata = null) + public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, string? reason, string? variant, + string? errorMessage = null, FlagMetadata? flagMetadata = null) { this.Value = value; this.FlagKey = flagKey; diff --git a/src/OpenFeature/Model/FlagEvaluationOptions.cs b/src/OpenFeature/Model/FlagEvaluationOptions.cs index 6cf7478d..7bde600c 100644 --- a/src/OpenFeature/Model/FlagEvaluationOptions.cs +++ b/src/OpenFeature/Model/FlagEvaluationOptions.cs @@ -24,7 +24,7 @@ public sealed class FlagEvaluationOptions /// /// An immutable list of hooks to use during evaluation /// Optional - a list of hints that are passed through the hook lifecycle - public FlagEvaluationOptions(IImmutableList hooks, IImmutableDictionary hookHints = null) + public FlagEvaluationOptions(IImmutableList hooks, IImmutableDictionary? hookHints = null) { this.Hooks = hooks; this.HookHints = hookHints ?? ImmutableDictionary.Empty; @@ -35,7 +35,7 @@ public FlagEvaluationOptions(IImmutableList hooks, IImmutableDictionary /// A hook to use during the evaluation /// Optional - a list of hints that are passed through the hook lifecycle - public FlagEvaluationOptions(Hook hook, ImmutableDictionary hookHints = null) + public FlagEvaluationOptions(Hook hook, ImmutableDictionary? hookHints = null) { this.Hooks = ImmutableList.Create(hook); this.HookHints = hookHints ?? ImmutableDictionary.Empty; diff --git a/src/OpenFeature/Model/FlagMetadata.cs b/src/OpenFeature/Model/FlagMetadata.cs index 586a46bf..0fddbdd3 100644 --- a/src/OpenFeature/Model/FlagMetadata.cs +++ b/src/OpenFeature/Model/FlagMetadata.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; -#nullable enable namespace OpenFeature.Model; /// diff --git a/src/OpenFeature/Model/HookContext.cs b/src/OpenFeature/Model/HookContext.cs index 0b4a91f5..69f58bde 100644 --- a/src/OpenFeature/Model/HookContext.cs +++ b/src/OpenFeature/Model/HookContext.cs @@ -51,12 +51,12 @@ public sealed class HookContext /// Provider metadata /// Evaluation context /// When any of arguments are null - public HookContext(string flagKey, + public HookContext(string? flagKey, T defaultValue, FlagValueType flagValueType, - ClientMetadata clientMetadata, - Metadata providerMetadata, - EvaluationContext evaluationContext) + ClientMetadata? clientMetadata, + Metadata? providerMetadata, + EvaluationContext? evaluationContext) { this.FlagKey = flagKey ?? throw new ArgumentNullException(nameof(flagKey)); this.DefaultValue = defaultValue; diff --git a/src/OpenFeature/Model/Metadata.cs b/src/OpenFeature/Model/Metadata.cs index 0f8629e3..d7c972d7 100644 --- a/src/OpenFeature/Model/Metadata.cs +++ b/src/OpenFeature/Model/Metadata.cs @@ -8,13 +8,13 @@ public class Metadata /// /// Gets name of instance /// - public string Name { get; } + public string? Name { get; } /// /// Initializes a new instance of the class. /// /// Name of instance - public Metadata(string name) + public Metadata(string? name) { this.Name = name; } diff --git a/src/OpenFeature/Model/ProviderEvents.cs b/src/OpenFeature/Model/ProviderEvents.cs index ca7c7e1a..6feccfb0 100644 --- a/src/OpenFeature/Model/ProviderEvents.cs +++ b/src/OpenFeature/Model/ProviderEvents.cs @@ -6,7 +6,7 @@ namespace OpenFeature.Model /// /// The EventHandlerDelegate is an implementation of an Event Handler /// - public delegate void EventHandlerDelegate(ProviderEventPayload eventDetails); + public delegate void EventHandlerDelegate(ProviderEventPayload? eventDetails); /// /// Contains the payload of an OpenFeature Event. @@ -16,7 +16,7 @@ public class ProviderEventPayload /// /// Name of the provider. /// - public string ProviderName { get; set; } + public string? ProviderName { get; set; } /// /// Type of the event @@ -26,17 +26,17 @@ public class ProviderEventPayload /// /// A message providing more information about the event. /// - public string Message { get; set; } + public string? Message { get; set; } /// /// A List of flags that have been changed. /// - public List FlagsChanged { get; set; } + public List? FlagsChanged { get; set; } /// /// Metadata information for the event. /// // TODO: This needs to be changed to a EventMetadata object - public Dictionary EventMetadata { get; set; } + public Dictionary? EventMetadata { get; set; } } } diff --git a/src/OpenFeature/Model/ResolutionDetails.cs b/src/OpenFeature/Model/ResolutionDetails.cs index 9319096f..5f686d47 100644 --- a/src/OpenFeature/Model/ResolutionDetails.cs +++ b/src/OpenFeature/Model/ResolutionDetails.cs @@ -29,25 +29,25 @@ public sealed class ResolutionDetails /// /// Message containing additional details about an error. /// - public string ErrorMessage { get; } + public string? ErrorMessage { get; } /// /// Describes the reason for the outcome of the evaluation process /// /// - public string Reason { get; } + public string? Reason { get; } /// /// A variant is a semantic identifier for a value. This allows for referral to particular values without /// necessarily including the value itself, which may be quite prohibitively large or otherwise unsuitable /// in some cases. /// - public string Variant { get; } + public string? Variant { get; } /// /// A structure which supports definition of arbitrary properties, with keys of type string, and values of type boolean, string, or number. /// - public FlagMetadata FlagMetadata { get; } + public FlagMetadata? FlagMetadata { get; } /// /// Initializes a new instance of the class. @@ -59,8 +59,8 @@ public sealed class ResolutionDetails /// Variant /// Error message /// Flag metadata - public ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorType.None, string reason = null, - string variant = null, string errorMessage = null, FlagMetadata flagMetadata = null) + public ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorType.None, string? reason = null, + string? variant = null, string? errorMessage = null, FlagMetadata? flagMetadata = null) { this.Value = value; this.FlagKey = flagKey; diff --git a/src/OpenFeature/Model/Structure.cs b/src/OpenFeature/Model/Structure.cs index 8bf4b4c8..47c66923 100644 --- a/src/OpenFeature/Model/Structure.cs +++ b/src/OpenFeature/Model/Structure.cs @@ -62,7 +62,7 @@ public Structure(IDictionary attributes) /// The key of the value to be retrieved /// value to be mutated /// indicating the presence of the key. - public bool TryGetValue(string key, out Value value) => this._attributes.TryGetValue(key, out value); + public bool TryGetValue(string key, out Value? value) => this._attributes.TryGetValue(key, out value); /// /// Gets all values as a Dictionary diff --git a/src/OpenFeature/Model/Value.cs b/src/OpenFeature/Model/Value.cs index c7f60c44..5af3b8b3 100644 --- a/src/OpenFeature/Model/Value.cs +++ b/src/OpenFeature/Model/Value.cs @@ -10,7 +10,7 @@ namespace OpenFeature.Model /// public sealed class Value { - private readonly object _innerValue; + private readonly object? _innerValue; /// /// Creates a Value with the inner value set to null @@ -136,7 +136,7 @@ public Value(Object value) /// Returns the underlying inner value as an object. Returns null if the inner value is null. /// /// Value as object - public object AsObject => this._innerValue; + public object? AsObject => this._innerValue; /// /// Returns the underlying int value @@ -164,21 +164,21 @@ public Value(Object value) /// Value will be null if it isn't a string /// /// Value as string - public string AsString => this.IsString ? (string)this._innerValue : null; + public string? AsString => this.IsString ? (string?)this._innerValue : null; /// /// Returns the underlying Structure value /// Value will be null if it isn't a Structure /// /// Value as Structure - public Structure AsStructure => this.IsStructure ? (Structure)this._innerValue : null; + public Structure? AsStructure => this.IsStructure ? (Structure?)this._innerValue : null; /// /// Returns the underlying List value /// Value will be null if it isn't a List /// /// Value as List - public IImmutableList AsList => this.IsList ? (IImmutableList)this._innerValue : null; + public IImmutableList? AsList => this.IsList ? (IImmutableList?)this._innerValue : null; /// /// Returns the underlying DateTime value diff --git a/src/OpenFeature/NoOpProvider.cs b/src/OpenFeature/NoOpProvider.cs index 16ba38f4..693e504e 100644 --- a/src/OpenFeature/NoOpProvider.cs +++ b/src/OpenFeature/NoOpProvider.cs @@ -13,27 +13,27 @@ public override Metadata GetMetadata() return this._metadata; } - public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) + public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext? context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) + public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) + public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext? context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) + public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext? context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 56fc518f..c8513e8b 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -42,7 +42,7 @@ public sealed class FeatureClient : IFeatureClient { // Alias the provider reference so getting the method and returning the provider are // guaranteed to be the same object. - var provider = Api.Instance.GetProvider(this._metadata.Name); + var provider = Api.Instance.GetProvider(this._metadata.Name!); return (method(provider), provider); } @@ -57,7 +57,7 @@ public EvaluationContext GetContext() } /// - public void SetContext(EvaluationContext context) + public void SetContext(EvaluationContext? context) { lock (this._evaluationContextLock) { @@ -73,7 +73,7 @@ public void SetContext(EvaluationContext context) /// Logger used by client /// Context given to this client /// Throws if any of the required parameters are null - public FeatureClient(string name, string version, ILogger logger = null, EvaluationContext context = null) + public FeatureClient(string? name, string? version, ILogger? logger = null, EvaluationContext? context = null) { this._metadata = new ClientMetadata(name, version); this._logger = logger ?? new Logger(new NullLoggerFactory()); @@ -96,13 +96,13 @@ public FeatureClient(string name, string version, ILogger logger = null, Evaluat /// public void AddHandler(ProviderEventTypes eventType, EventHandlerDelegate handler) { - Api.Instance.AddClientHandler(this._metadata.Name, eventType, handler); + Api.Instance.AddClientHandler(this._metadata.Name!, eventType, handler); } /// public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) { - Api.Instance.RemoveClientHandler(this._metadata.Name, type, handler); + Api.Instance.RemoveClientHandler(this._metadata.Name!, type, handler); } /// @@ -136,70 +136,70 @@ public void AddHooks(IEnumerable hooks) public void ClearHooks() => this._hooks.Clear(); /// - public async Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, - FlagEvaluationOptions config = null) => + public async Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null) => (await this.GetBooleanDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetBooleanDetails(string flagKey, bool defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null) => + EvaluationContext? context = null, FlagEvaluationOptions? config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveBooleanValue), FlagValueType.Boolean, flagKey, defaultValue, context, config).ConfigureAwait(false); /// - public async Task GetStringValue(string flagKey, string defaultValue, EvaluationContext context = null, - FlagEvaluationOptions config = null) => + public async Task GetStringValue(string flagKey, string defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null) => (await this.GetStringDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetStringDetails(string flagKey, string defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null) => + EvaluationContext? context = null, FlagEvaluationOptions? config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStringValue), FlagValueType.String, flagKey, defaultValue, context, config).ConfigureAwait(false); /// - public async Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, - FlagEvaluationOptions config = null) => + public async Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null) => (await this.GetIntegerDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetIntegerDetails(string flagKey, int defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null) => + EvaluationContext? context = null, FlagEvaluationOptions? config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveIntegerValue), FlagValueType.Number, flagKey, defaultValue, context, config).ConfigureAwait(false); /// public async Task GetDoubleValue(string flagKey, double defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) => + EvaluationContext? context = null, + FlagEvaluationOptions? config = null) => (await this.GetDoubleDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetDoubleDetails(string flagKey, double defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null) => + EvaluationContext? context = null, FlagEvaluationOptions? config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveDoubleValue), FlagValueType.Number, flagKey, defaultValue, context, config).ConfigureAwait(false); /// - public async Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext context = null, - FlagEvaluationOptions config = null) => + public async Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null) => (await this.GetObjectDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetObjectDetails(string flagKey, Value defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null) => + EvaluationContext? context = null, FlagEvaluationOptions? config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStructureValue), FlagValueType.Object, flagKey, defaultValue, context, config).ConfigureAwait(false); private async Task> EvaluateFlag( (Func>>, FeatureProvider) providerInfo, - FlagValueType flagValueType, string flagKey, T defaultValue, EvaluationContext context = null, - FlagEvaluationOptions options = null) + FlagValueType flagValueType, string flagKey, T defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? options = null) { var resolveValueDelegate = providerInfo.Item1; var provider = providerInfo.Item2; @@ -274,7 +274,7 @@ private async Task> EvaluateFlag( } private async Task> TriggerBeforeHooks(IReadOnlyList hooks, HookContext context, - FlagEvaluationOptions options) + FlagEvaluationOptions? options) { var evalContextBuilder = EvaluationContext.Builder(); evalContextBuilder.Merge(context.EvaluationContext); @@ -298,7 +298,7 @@ private async Task> TriggerBeforeHooks(IReadOnlyList hoo } private async Task TriggerAfterHooks(IReadOnlyList hooks, HookContext context, - FlagEvaluationDetails evaluationDetails, FlagEvaluationOptions options) + FlagEvaluationDetails evaluationDetails, FlagEvaluationOptions? options) { foreach (var hook in hooks) { @@ -307,7 +307,7 @@ private async Task TriggerAfterHooks(IReadOnlyList hooks, HookContext(IReadOnlyList hooks, HookContext context, Exception exception, - FlagEvaluationOptions options) + FlagEvaluationOptions? options) { foreach (var hook in hooks) { @@ -317,13 +317,13 @@ private async Task TriggerErrorHooks(IReadOnlyList hooks, HookContext(IReadOnlyList hooks, HookContext context, - FlagEvaluationOptions options) + FlagEvaluationOptions? options) { foreach (var hook in hooks) { @@ -333,7 +333,7 @@ private async Task TriggerFinallyHooks(IReadOnlyList hooks, HookContext } catch (Exception e) { - this._logger.LogError(e, "Error while executing Finally hook {0}", hook.GetType().Name); + this._logger.LogError(e, "Error while executing Finally hook {HookName}", hook.GetType().Name); } } } diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index f95d805c..5b331d43 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -63,12 +63,12 @@ public async ValueTask DisposeAsync() /// /// called after a provider is shutdown, can be used to remove event handlers public async Task SetProvider( - FeatureProvider featureProvider, + FeatureProvider? featureProvider, EvaluationContext context, - Action afterSet = null, - Action afterInitialization = null, - Action afterError = null, - Action afterShutdown = null) + Action? afterSet = null, + Action? afterInitialization = null, + Action? afterError = null, + Action? afterShutdown = null) { // Cannot unset the feature provider. if (featureProvider == null) @@ -105,10 +105,10 @@ await InitProvider(this._defaultProvider, context, afterInitialization, afterErr } private static async Task InitProvider( - FeatureProvider newProvider, + FeatureProvider? newProvider, EvaluationContext context, - Action afterInitialization, - Action afterError) + Action? afterInitialization, + Action? afterError) { if (newProvider == null) { @@ -152,13 +152,13 @@ private static async Task InitProvider( /// initialization /// /// called after a provider is shutdown, can be used to remove event handlers - public async Task SetProvider(string clientName, - FeatureProvider featureProvider, + public async Task SetProvider(string? clientName, + FeatureProvider? featureProvider, EvaluationContext context, - Action afterSet = null, - Action afterInitialization = null, - Action afterError = null, - Action afterShutdown = null) + Action? afterSet = null, + Action? afterInitialization = null, + Action? afterError = null, + Action? afterShutdown = null) { // Cannot set a provider for a null clientName. if (clientName == null) @@ -202,16 +202,16 @@ public async Task SetProvider(string clientName, /// Shutdown the feature provider if it is unused. This must be called within a write lock of the _providersLock. /// private async Task ShutdownIfUnused( - FeatureProvider targetProvider, - Action afterShutdown, - Action afterError) + FeatureProvider? targetProvider, + Action? afterShutdown, + Action? afterError) { if (ReferenceEquals(this._defaultProvider, targetProvider)) { return; } - if (this._featureProviders.Values.Contains(targetProvider)) + if (targetProvider != null && this._featureProviders.Values.Contains(targetProvider)) { return; } @@ -228,10 +228,15 @@ private async Task ShutdownIfUnused( /// it would not be meaningful to emit an error. /// /// - private static async Task SafeShutdownProvider(FeatureProvider targetProvider, - Action afterShutdown, - Action afterError) + private static async Task SafeShutdownProvider(FeatureProvider? targetProvider, + Action? afterShutdown, + Action? afterError) { + if (targetProvider == null) + { + return; + } + try { await targetProvider.Shutdown().ConfigureAwait(false); @@ -256,19 +261,27 @@ public FeatureProvider GetProvider() } } - public FeatureProvider GetProvider(string clientName) + public FeatureProvider GetProvider(string? clientName) { +#if NET6_0_OR_GREATER if (string.IsNullOrEmpty(clientName)) { return this.GetProvider(); } +#else + // This is a workaround for the issue in .NET Framework where string.IsNullOrEmpty is not nullable compatible. + if (clientName == null || string.IsNullOrEmpty(clientName)) + { + return this.GetProvider(); + } +#endif return this._featureProviders.TryGetValue(clientName, out var featureProvider) ? featureProvider : this.GetProvider(); } - public async Task Shutdown(Action afterError = null) + public async Task Shutdown(Action? afterError = null) { var providers = new HashSet(); this._providersLock.EnterWriteLock(); diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index 99975de3..1a16bfe3 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -4,7 +4,6 @@ using OpenFeature.Error; using OpenFeature.Model; -#nullable enable namespace OpenFeature.Providers.Memory { /// diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index e463023a..766e4f3c 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -5,7 +5,6 @@ using OpenFeature.Error; using OpenFeature.Model; -#nullable enable namespace OpenFeature.Providers.Memory { /// diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index 6615fb0b..b7b1f9b5 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -12,32 +12,30 @@ namespace OpenFeature.E2ETests [Binding] public class EvaluationStepDefinitions { - private readonly ScenarioContext _scenarioContext; - private static FeatureClient client; - private Task booleanFlagValue; - private Task stringFlagValue; - private Task intFlagValue; - private Task doubleFlagValue; - private Task objectFlagValue; - private Task> booleanFlagDetails; - private Task> stringFlagDetails; - private Task> intFlagDetails; - private Task> doubleFlagDetails; - private Task> objectFlagDetails; - private string contextAwareFlagKey; - private string contextAwareDefaultValue; - private string contextAwareValue; - private EvaluationContext context; - private string notFoundFlagKey; - private string notFoundDefaultValue; - private FlagEvaluationDetails notFoundDetails; - private string typeErrorFlagKey; + private static FeatureClient? client; + private Task? booleanFlagValue; + private Task? stringFlagValue; + private Task? intFlagValue; + private Task? doubleFlagValue; + private Task? objectFlagValue; + private Task>? booleanFlagDetails; + private Task>? stringFlagDetails; + private Task>? intFlagDetails; + private Task>? doubleFlagDetails; + private Task>? objectFlagDetails; + private string? contextAwareFlagKey; + private string? contextAwareDefaultValue; + private string? contextAwareValue; + private EvaluationContext? context; + private string? notFoundFlagKey; + private string? notFoundDefaultValue; + private FlagEvaluationDetails? notFoundDetails; + private string? typeErrorFlagKey; private int typeErrorDefaultValue; - private FlagEvaluationDetails typeErrorDetails; + private FlagEvaluationDetails? typeErrorDetails; public EvaluationStepDefinitions(ScenarioContext scenarioContext) { - _scenarioContext = scenarioContext; } [Given(@"a provider is registered")] @@ -45,152 +43,152 @@ public void GivenAProviderIsRegistered() { var memProvider = new InMemoryProvider(e2eFlagConfig); Api.Instance.SetProviderAsync(memProvider).Wait(); - client = Api.Instance.GetClient(); + client = Api.Instance.GetClient("TestClient", "1.0.0"); } [When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Whenabooleanflagwithkeyisevaluatedwithdefaultvalue(string flagKey, bool defaultValue) { - this.booleanFlagValue = client.GetBooleanValue(flagKey, defaultValue); + this.booleanFlagValue = client?.GetBooleanValue(flagKey, defaultValue); } [Then(@"the resolved boolean value should be ""(.*)""")] public void Thentheresolvedbooleanvalueshouldbe(bool expectedValue) { - Assert.Equal(expectedValue, this.booleanFlagValue.Result); + Assert.Equal(expectedValue, this.booleanFlagValue?.Result); } [When(@"a string flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Whenastringflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) { - this.stringFlagValue = client.GetStringValue(flagKey, defaultValue); + this.stringFlagValue = client?.GetStringValue(flagKey, defaultValue); } [Then(@"the resolved string value should be ""(.*)""")] public void Thentheresolvedstringvalueshouldbe(string expected) { - Assert.Equal(expected, this.stringFlagValue.Result); + Assert.Equal(expected, this.stringFlagValue?.Result); } [When(@"an integer flag with key ""(.*)"" is evaluated with default value (.*)")] public void Whenanintegerflagwithkeyisevaluatedwithdefaultvalue(string flagKey, int defaultValue) { - this.intFlagValue = client.GetIntegerValue(flagKey, defaultValue); + this.intFlagValue = client?.GetIntegerValue(flagKey, defaultValue); } [Then(@"the resolved integer value should be (.*)")] public void Thentheresolvedintegervalueshouldbe(int expected) { - Assert.Equal(expected, this.intFlagValue.Result); + Assert.Equal(expected, this.intFlagValue?.Result); } [When(@"a float flag with key ""(.*)"" is evaluated with default value (.*)")] public void Whenafloatflagwithkeyisevaluatedwithdefaultvalue(string flagKey, double defaultValue) { - this.doubleFlagValue = client.GetDoubleValue(flagKey, defaultValue); + this.doubleFlagValue = client?.GetDoubleValue(flagKey, defaultValue); } [Then(@"the resolved float value should be (.*)")] public void Thentheresolvedfloatvalueshouldbe(double expected) { - Assert.Equal(expected, this.doubleFlagValue.Result); + Assert.Equal(expected, this.doubleFlagValue?.Result); } [When(@"an object flag with key ""(.*)"" is evaluated with a null default value")] public void Whenanobjectflagwithkeyisevaluatedwithanulldefaultvalue(string flagKey) { - this.objectFlagValue = client.GetObjectValue(flagKey, new Value()); + this.objectFlagValue = client?.GetObjectValue(flagKey, new Value()); } [Then(@"the resolved object value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] public void Thentheresolvedobjectvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) { - Value value = this.objectFlagValue.Result; - Assert.Equal(boolValue, value.AsStructure[boolField].AsBoolean); - Assert.Equal(stringValue, value.AsStructure[stringField].AsString); - Assert.Equal(numberValue, value.AsStructure[numberField].AsInteger); + Value? value = this.objectFlagValue?.Result; + Assert.Equal(boolValue, value?.AsStructure?[boolField].AsBoolean); + Assert.Equal(stringValue, value?.AsStructure?[stringField].AsString); + Assert.Equal(numberValue, value?.AsStructure?[numberField].AsInteger); } [When(@"a boolean flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] public void Whenabooleanflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, bool defaultValue) { - this.booleanFlagDetails = client.GetBooleanDetails(flagKey, defaultValue); + this.booleanFlagDetails = client?.GetBooleanDetails(flagKey, defaultValue); } [Then(@"the resolved boolean details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedbooleandetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(bool expectedValue, string expectedVariant, string expectedReason) { - var result = this.booleanFlagDetails.Result; - Assert.Equal(expectedValue, result.Value); - Assert.Equal(expectedVariant, result.Variant); - Assert.Equal(expectedReason, result.Reason); + var result = this.booleanFlagDetails?.Result; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); } [When(@"a string flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] public void Whenastringflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, string defaultValue) { - this.stringFlagDetails = client.GetStringDetails(flagKey, defaultValue); + this.stringFlagDetails = client?.GetStringDetails(flagKey, defaultValue); } [Then(@"the resolved string details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedstringdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(string expectedValue, string expectedVariant, string expectedReason) { - var result = this.stringFlagDetails.Result; - Assert.Equal(expectedValue, result.Value); - Assert.Equal(expectedVariant, result.Variant); - Assert.Equal(expectedReason, result.Reason); + var result = this.stringFlagDetails?.Result; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); } [When(@"an integer flag with key ""(.*)"" is evaluated with details and default value (.*)")] public void Whenanintegerflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, int defaultValue) { - this.intFlagDetails = client.GetIntegerDetails(flagKey, defaultValue); + this.intFlagDetails = client?.GetIntegerDetails(flagKey, defaultValue); } [Then(@"the resolved integer details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedintegerdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(int expectedValue, string expectedVariant, string expectedReason) { - var result = this.intFlagDetails.Result; - Assert.Equal(expectedValue, result.Value); - Assert.Equal(expectedVariant, result.Variant); - Assert.Equal(expectedReason, result.Reason); + var result = this.intFlagDetails?.Result; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); } [When(@"a float flag with key ""(.*)"" is evaluated with details and default value (.*)")] public void Whenafloatflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, double defaultValue) { - this.doubleFlagDetails = client.GetDoubleDetails(flagKey, defaultValue); + this.doubleFlagDetails = client?.GetDoubleDetails(flagKey, defaultValue); } [Then(@"the resolved float details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedfloatdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(double expectedValue, string expectedVariant, string expectedReason) { - var result = this.doubleFlagDetails.Result; - Assert.Equal(expectedValue, result.Value); - Assert.Equal(expectedVariant, result.Variant); - Assert.Equal(expectedReason, result.Reason); + var result = this.doubleFlagDetails?.Result; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); } [When(@"an object flag with key ""(.*)"" is evaluated with details and a null default value")] public void Whenanobjectflagwithkeyisevaluatedwithdetailsandanulldefaultvalue(string flagKey) { - this.objectFlagDetails = client.GetObjectDetails(flagKey, new Value()); + this.objectFlagDetails = client?.GetObjectDetails(flagKey, new Value()); } [Then(@"the resolved object details value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] public void Thentheresolvedobjectdetailsvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) { - Value value = this.objectFlagDetails.Result.Value; - Assert.Equal(boolValue, value.AsStructure[boolField].AsBoolean); - Assert.Equal(stringValue, value.AsStructure[stringField].AsString); - Assert.Equal(numberValue, value.AsStructure[numberField].AsInteger); + var value = this.objectFlagDetails?.Result.Value; + Assert.Equal(boolValue, value?.AsStructure?[boolField].AsBoolean); + Assert.Equal(stringValue, value?.AsStructure?[stringField].AsString); + Assert.Equal(numberValue, value?.AsStructure?[numberField].AsInteger); } [Then(@"the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Giventhevariantshouldbeandthereasonshouldbe(string expectedVariant, string expectedReason) { - Assert.Equal(expectedVariant, this.objectFlagDetails.Result.Variant); - Assert.Equal(expectedReason, this.objectFlagDetails.Result.Reason); + Assert.Equal(expectedVariant, this.objectFlagDetails?.Result.Variant); + Assert.Equal(expectedReason, this.objectFlagDetails?.Result.Reason); } [When(@"context contains keys ""(.*)"", ""(.*)"", ""(.*)"", ""(.*)"" with values ""(.*)"", ""(.*)"", (.*), ""(.*)""")] @@ -208,7 +206,7 @@ public void Givenaflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string { contextAwareFlagKey = flagKey; contextAwareDefaultValue = defaultValue; - contextAwareValue = client.GetStringValue(flagKey, contextAwareDefaultValue, context).Result; + contextAwareValue = client?.GetStringValue(flagKey, contextAwareDefaultValue, context)?.Result; } [Then(@"the resolved string response should be ""(.*)""")] @@ -220,7 +218,7 @@ public void Thentheresolvedstringresponseshouldbe(string expected) [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) { - string emptyContextValue = client.GetStringValue(contextAwareFlagKey, contextAwareDefaultValue, new EvaluationContextBuilder().Build()).Result; + string? emptyContextValue = client?.GetStringValue(contextAwareFlagKey!, contextAwareDefaultValue!, new EvaluationContextBuilder().Build()).Result; Assert.Equal(expected, emptyContextValue); } @@ -229,20 +227,20 @@ public void Whenanonexistentstringflagwithkeyisevaluatedwithdetailsandadefaultva { this.notFoundFlagKey = flagKey; this.notFoundDefaultValue = defaultValue; - this.notFoundDetails = client.GetStringDetails(this.notFoundFlagKey, this.notFoundDefaultValue).Result; + this.notFoundDetails = client?.GetStringDetails(this.notFoundFlagKey, this.notFoundDefaultValue).Result; } [Then(@"the default string value should be returned")] public void Thenthedefaultstringvalueshouldbereturned() { - Assert.Equal(this.notFoundDefaultValue, this.notFoundDetails.Value); + Assert.Equal(this.notFoundDefaultValue, this.notFoundDetails?.Value); } [Then(@"the reason should indicate an error and the error code should indicate a missing flag with ""(.*)""")] public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode) { - Assert.Equal(Reason.Error.ToString(), notFoundDetails.Reason); - Assert.Equal(errorCode, notFoundDetails.ErrorType.GetDescription()); + Assert.Equal(Reason.Error.ToString(), notFoundDetails?.Reason); + Assert.Equal(errorCode, notFoundDetails?.ErrorType.GetDescription()); } [When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")] @@ -250,20 +248,20 @@ public void Whenastringflagwithkeyisevaluatedasanintegerwithdetailsandadefaultva { this.typeErrorFlagKey = flagKey; this.typeErrorDefaultValue = defaultValue; - this.typeErrorDetails = client.GetIntegerDetails(this.typeErrorFlagKey, this.typeErrorDefaultValue).Result; + this.typeErrorDetails = client?.GetIntegerDetails(this.typeErrorFlagKey, this.typeErrorDefaultValue).Result; } [Then(@"the default integer value should be returned")] public void Thenthedefaultintegervalueshouldbereturned() { - Assert.Equal(this.typeErrorDefaultValue, this.typeErrorDetails.Value); + Assert.Equal(this.typeErrorDefaultValue, this.typeErrorDetails?.Value); } [Then(@"the reason should indicate an error and the error code should indicate a type mismatch with ""(.*)""")] public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode) { - Assert.Equal(Reason.Error.ToString(), typeErrorDetails.Reason); - Assert.Equal(errorCode, typeErrorDetails.ErrorType.GetDescription()); + Assert.Equal(Reason.Error.ToString(), typeErrorDetails?.Reason); + Assert.Equal(errorCode, typeErrorDetails?.ErrorType.GetDescription()); } private IDictionary e2eFlagConfig = new Dictionary(){ diff --git a/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs b/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs index 6a2f895c..fe011711 100644 --- a/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs @@ -31,5 +31,37 @@ public void FeatureProviderException_Should_Allow_Custom_ErrorCode_Messages(Erro ex.Message.Should().Be(message); ex.InnerException.Should().BeOfType(); } + + private enum TestEnum + { + TestValueWithoutDescription + } + + [Fact] + public void GetDescription_WhenCalledWithEnumWithoutDescription_ReturnsEnumName() + { + // Arrange + var testEnum = TestEnum.TestValueWithoutDescription; + var expectedDescription = "TestValueWithoutDescription"; + + // Act + var actualDescription = testEnum.GetDescription(); + + // Assert + Assert.Equal(expectedDescription, actualDescription); + } + + [Fact] + public void GetDescription_WhenFieldIsNull_ReturnsEnumValueAsString() + { + // Arrange + var testEnum = (TestEnum)999;// This value should not exist in the TestEnum + + // Act + var description = testEnum.GetDescription(); + + // Assert + Assert.Equal(testEnum.ToString(), description); + } } } diff --git a/test/OpenFeature.Tests/FlagMetadataTest.cs b/test/OpenFeature.Tests/FlagMetadataTest.cs index 2bb33b0d..d716d91e 100644 --- a/test/OpenFeature.Tests/FlagMetadataTest.cs +++ b/test/OpenFeature.Tests/FlagMetadataTest.cs @@ -3,7 +3,6 @@ using OpenFeature.Tests.Internal; using Xunit; -#nullable enable namespace OpenFeature.Tests; public class FlagMetadataTest diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 86c61f83..05965c51 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -26,11 +26,12 @@ public void OpenFeatureClient_Should_Allow_Hooks() { var fixture = new Fixture(); var clientName = fixture.Create(); + var clientVersion = fixture.Create(); var hook1 = Substitute.For(); var hook2 = Substitute.For(); var hook3 = Substitute.For(); - var client = Api.Instance.GetClient(clientName); + var client = Api.Instance.GetClient(clientName, clientVersion); client.AddHooks(new[] { hook1, hook2 }); @@ -359,9 +360,12 @@ public async Task Should_Use_No_Op_When_Provider_Is_Null() [Fact] public void Should_Get_And_Set_Context() { + var fixture = new Fixture(); + var clientName = fixture.Create(); + var clientVersion = fixture.Create(); var KEY = "key"; var VAL = 1; - FeatureClient client = Api.Instance.GetClient(); + FeatureClient client = Api.Instance.GetClient(clientName, clientVersion); client.SetContext(new EvaluationContextBuilder().Set(KEY, VAL).Build()); Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger); } diff --git a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs index 0b8ee097..5329620f 100644 --- a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using AutoFixture; using FluentAssertions; using OpenFeature.Model; @@ -91,7 +92,7 @@ public void EvaluationContext_Should_All_Types() var context = contextBuilder.Build(); context.TargetingKey.Should().Be("targeting_key"); - var targetingKeyValue = context.GetValue(context.TargetingKey); + var targetingKeyValue = context.GetValue(context.TargetingKey!); targetingKeyValue.IsString.Should().BeTrue(); targetingKeyValue.AsString.Should().Be("userId"); @@ -151,5 +152,22 @@ public void Should_Be_Able_To_Get_All_Values() context.Count.Should().Be(count); } + + [Fact] + public void TryGetValue_WhenCalledWithExistingKey_ReturnsTrueAndExpectedValue() + { + // Arrange + var key = "testKey"; + var expectedValue = new Value("testValue"); + var structure = new Structure(new Dictionary { { key, expectedValue } }); + var evaluationContext = new EvaluationContext("targetingKey", structure); + + // Act + var result = evaluationContext.TryGetValue(key, out var actualValue); + + // Assert + Assert.True(result); + Assert.Equal(expectedValue, actualValue); + } } } diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index 525241fc..599cea30 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -25,7 +25,12 @@ public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() eventExecutor.AddApiLevelHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); - var eventMetadata = new Dictionary { { "foo", "bar" } }; + var eventMetadata = new Dictionary + { + { + "foo", "bar" + } + }; var myEvent = new Event { EventPayload = new ProviderEventPayload @@ -33,7 +38,10 @@ public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() Type = ProviderEventTypes.ProviderConfigurationChanged, Message = "The provider is ready", EventMetadata = eventMetadata, - FlagsChanged = new List { "flag1", "flag2" } + FlagsChanged = new List + { + "flag1", "flag2" + } } }; eventExecutor.EventChannel.Writer.TryWrite(myEvent); @@ -46,7 +54,10 @@ public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() await eventExecutor.Shutdown(); // the next event should not be propagated to the event handler - var newEventPayload = new ProviderEventPayload { Type = ProviderEventTypes.ProviderStale }; + var newEventPayload = new ProviderEventPayload + { + Type = ProviderEventTypes.ProviderStale + }; eventExecutor.EventChannel.Writer.TryWrite(newEventPayload); @@ -141,9 +152,9 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_ var eventHandler = Substitute.For(); var testProvider = new TestProvider(); -#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS0618// Type or member is obsolete Api.Instance.SetProvider(testProvider); -#pragma warning restore CS0618 // Type or member is obsolete +#pragma warning restore CS0618// Type or member is obsolete Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -300,10 +311,12 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered() var fixture = new Fixture(); var eventHandler = Substitute.For(); - var myClient = Api.Instance.GetClient(fixture.Create()); + var clientName = fixture.Create(); + var clientVersion = fixture.Create(); + var myClient = Api.Instance.GetClient(clientName, clientVersion); var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -326,13 +339,15 @@ public async Task Client_Level_Event_Handlers_Should_Be_Executed_When_Other_Hand failingEventHandler.When(x => x.Invoke(Arg.Any())) .Do(x => throw new Exception()); - var myClient = Api.Instance.GetClient(fixture.Create()); + var clientName = fixture.Create(); + var clientVersion = fixture.Create(); + var myClient = Api.Instance.GetClient(clientName, clientVersion); myClient.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); await Utils.AssertUntilAsync( _ => failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) @@ -354,8 +369,8 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered_To_Default_Pr var eventHandler = Substitute.For(); var clientEventHandler = Substitute.For(); - var myClientWithNoBoundProvider = Api.Instance.GetClient(fixture.Create()); - var myClientWithBoundProvider = Api.Instance.GetClient(fixture.Create()); + var myClientWithNoBoundProvider = Api.Instance.GetClient(fixture.Create(), fixture.Create()); + var myClientWithBoundProvider = Api.Instance.GetClient(fixture.Create(), fixture.Create()); var apiProvider = new TestProvider(fixture.Create()); var clientProvider = new TestProvider(fixture.Create()); @@ -363,7 +378,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered_To_Default_Pr // set the default provider on API level, but not specifically to the client await Api.Instance.SetProviderAsync(apiProvider); // set the other provider specifically for the client - await Api.Instance.SetProviderAsync(myClientWithBoundProvider.GetMetadata().Name, clientProvider); + await Api.Instance.SetProviderAsync(myClientWithBoundProvider.GetMetadata().Name!, clientProvider); myClientWithNoBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); myClientWithBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, clientEventHandler); @@ -387,7 +402,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Name var fixture = new Fixture(); var clientEventHandler = Substitute.For(); - var client = Api.Instance.GetClient(fixture.Create()); + var client = Api.Instance.GetClient(fixture.Create(), fixture.Create()); var defaultProvider = new TestProvider(fixture.Create()); var clientProvider = new TestProvider(fixture.Create()); @@ -401,11 +416,12 @@ public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Name // verify that the client received the event from the default provider as there is no named provider registered yet await Utils.AssertUntilAsync( - _ => clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + _ => clientEventHandler.Received(1) + .Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) ); // set the other provider specifically for the client - await Api.Instance.SetProviderAsync(client.GetMetadata().Name, clientProvider); + await Api.Instance.SetProviderAsync(client.GetMetadata().Name!, clientProvider); // now, send another event for the default handler defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); @@ -417,7 +433,8 @@ await Utils.AssertUntilAsync( ); // for the default provider, the number of received events should stay unchanged await Utils.AssertUntilAsync( - _ => clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + _ => clientEventHandler.Received(1) + .Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) ); } @@ -433,10 +450,10 @@ public async Task Client_Level_Event_Handlers_Should_Be_Informed_About_Ready_Sta var fixture = new Fixture(); var eventHandler = Substitute.For(); - var myClient = Api.Instance.GetClient(fixture.Create()); + var myClient = Api.Instance.GetClient(fixture.Create(), fixture.Create()); var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); // add the event handler after the provider has already transitioned into the ready state myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -456,12 +473,12 @@ public async Task Client_Level_Event_Handlers_Should_Be_Removable() var eventHandler = Substitute.For(); - var myClient = Api.Instance.GetClient(fixture.Create()); + var myClient = Api.Instance.GetClient(fixture.Create(), fixture.Create()); myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); // wait for the first event to be received await Utils.AssertUntilAsync(_ => myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler)); @@ -474,5 +491,20 @@ await Utils.AssertUntilAsync( _ => eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) ); } + + [Fact] + public void RegisterClientFeatureProvider_WhenCalledWithNullProvider_DoesNotThrowException() + { + // Arrange + var eventExecutor = new EventExecutor(); + string client = "testClient"; + FeatureProvider? provider = null; + + // Act + var exception = Record.Exception(() => eventExecutor.RegisterClientFeatureProvider(client, provider)); + + // Assert + Assert.Null(exception); + } } } diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 02c41917..c34a013d 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -195,7 +196,7 @@ public async Task OpenFeature_Should_Get_Metadata() [InlineData("client2", null)] [InlineData(null, null)] [Specification("1.1.6", "The `API` MUST provide a function for creating a `client` which accepts the following options: - name (optional): A logical string identifier for the client.")] - public void OpenFeature_Should_Create_Client(string name = null, string version = null) + public void OpenFeature_Should_Create_Client(string? name = null, string? version = null) { var openFeature = Api.Instance; var client = openFeature.GetClient(name, version); @@ -244,5 +245,16 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() (await client1.GetBooleanValue("test", false)).Should().BeTrue(); (await client2.GetBooleanValue("test", false)).Should().BeFalse(); } + + [Fact] + public async Task SetProviderAsync_Should_Throw_When_Null_ClientName() + { + var openFeature = Api.Instance; + + var exception = await Assert.ThrowsAsync(() => openFeature.SetProviderAsync(null!, new TestProvider())); + + exception.Should().BeOfType(); + exception.ParamName.Should().Be("clientName"); + } } } diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index 62cbe9d6..0b25ebfa 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -78,14 +78,14 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Default_Provide var context = new EvaluationContextBuilder().Build(); providerMock.When(x => x.Initialize(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; - Exception receivedError = null; + Exception? receivedError = null; await repository.SetProvider(providerMock, context, afterError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; receivedError = error; }); - Assert.Equal("BAD THINGS", receivedError.Message); + Assert.Equal("BAD THINGS", receivedError?.Message); Assert.Equal(1, callCount); } @@ -170,7 +170,7 @@ public async Task AfterError_Is_Called_For_Shutdown_That_Throws() var context = new EvaluationContextBuilder().Build(); await repository.SetProvider(provider1, context); var callCount = 0; - Exception errorThrown = null; + Exception? errorThrown = null; await repository.SetProvider(provider2, context, afterError: (provider, ex) => { Assert.Equal(provider, provider1); @@ -178,7 +178,7 @@ await repository.SetProvider(provider2, context, afterError: (provider, ex) => callCount++; }); Assert.Equal(1, callCount); - Assert.Equal("SHUTDOWN ERROR", errorThrown.Message); + Assert.Equal("SHUTDOWN ERROR", errorThrown?.Message); } [Fact] @@ -244,14 +244,14 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Named_Provider( var context = new EvaluationContextBuilder().Build(); providerMock.When(x => x.Initialize(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; - Exception receivedError = null; + Exception? receivedError = null; await repository.SetProvider("the-provider", providerMock, context, afterError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; receivedError = error; }); - Assert.Equal("BAD THINGS", receivedError.Message); + Assert.Equal("BAD THINGS", receivedError?.Message); Assert.Equal(1, callCount); } @@ -337,7 +337,7 @@ public async Task AfterError_Is_Called_For_Shutdown_Named_Provider_That_Throws() var context = new EvaluationContextBuilder().Build(); await repository.SetProvider("the-name", provider1, context); var callCount = 0; - Exception errorThrown = null; + Exception? errorThrown = null; await repository.SetProvider("the-name", provider2, context, afterError: (provider, ex) => { Assert.Equal(provider, provider1); @@ -345,7 +345,7 @@ await repository.SetProvider("the-name", provider2, context, afterError: (provid callCount++; }); Assert.Equal(1, callCount); - Assert.Equal("SHUTDOWN ERROR", errorThrown.Message); + Assert.Equal("SHUTDOWN ERROR", errorThrown?.Message); } [Fact] diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 2e6cea12..64e1df46 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -60,7 +60,7 @@ public InMemoryProviderTests() }, defaultVariant: "external", (context) => { - if (context.GetValue("email").AsString.Contains("@faas.com")) + if (context.GetValue("email").AsString?.Contains("@faas.com") == true) { return "internal"; } @@ -148,9 +148,9 @@ public async void GetDouble_ShouldEvaluateWithReasonAndVariant() public async void GetStruct_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveStructureValue("object-flag", new Value(), EvaluationContext.Empty); - Assert.Equal(true, details.Value.AsStructure["showImages"].AsBoolean); - Assert.Equal("Check out these pics!", details.Value.AsStructure["title"].AsString); - Assert.Equal(100, details.Value.AsStructure["imagesPerPage"].AsInteger); + Assert.Equal(true, details.Value.AsStructure?["showImages"].AsBoolean); + Assert.Equal("Check out these pics!", details.Value.AsStructure?["title"].AsString); + Assert.Equal(100, details.Value.AsStructure?["imagesPerPage"].AsInteger); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("template", details.Variant); } @@ -227,7 +227,7 @@ await provider.UpdateFlags(new Dictionary(){ }}); var res = await provider.GetEventChannel().Reader.ReadAsync() as ProviderEventPayload; - Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res.Type); + Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res?.Type); await Assert.ThrowsAsync(() => provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty)); diff --git a/test/OpenFeature.Tests/StructureTests.cs b/test/OpenFeature.Tests/StructureTests.cs index 310303ed..2dd22ae7 100644 --- a/test/OpenFeature.Tests/StructureTests.cs +++ b/test/OpenFeature.Tests/StructureTests.cs @@ -76,9 +76,9 @@ public void TryGetValue_Should_Return_Value() var structure = Structure.Builder() .Set(KEY, VAL).Build(); - Value value; + Value? value; Assert.True(structure.TryGetValue(KEY, out value)); - Assert.Equal(VAL, value.AsString); + Assert.Equal(VAL, value?.AsString); } [Fact] diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index 9683e7ef..cdb59a0c 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -11,23 +11,23 @@ public class TestHookNoOverride : Hook { } public class TestHook : Hook { - public override Task Before(HookContext context, IReadOnlyDictionary hints = null) + public override Task Before(HookContext context, IReadOnlyDictionary? hints = null) { return Task.FromResult(EvaluationContext.Empty); } public override Task After(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary hints = null) + IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } - public override Task Error(HookContext context, Exception error, IReadOnlyDictionary hints = null) + public override Task Error(HookContext context, Exception error, IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } - public override Task Finally(HookContext context, IReadOnlyDictionary hints = null) + public override Task Finally(HookContext context, IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } @@ -65,31 +65,31 @@ public override Metadata GetMetadata() } public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(new ResolutionDetails(flagKey, !defaultValue)); } public override Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } public override Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } public override Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } public override Task> ResolveStructureValue(string flagKey, Value defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } diff --git a/test/OpenFeature.Tests/ValueTests.cs b/test/OpenFeature.Tests/ValueTests.cs index 031fea9a..ec623a68 100644 --- a/test/OpenFeature.Tests/ValueTests.cs +++ b/test/OpenFeature.Tests/ValueTests.cs @@ -117,7 +117,7 @@ public void Structure_Arg_Should_Contain_Structure() Structure innerValue = Structure.Builder().Set(INNER_KEY, INNER_VALUE).Build(); Value value = new Value(innerValue); Assert.True(value.IsStructure); - Assert.Equal(INNER_VALUE, value.AsStructure.GetValue(INNER_KEY).AsString); + Assert.Equal(INNER_VALUE, value.AsStructure?.GetValue(INNER_KEY).AsString); } [Fact] @@ -127,7 +127,111 @@ public void List_Arg_Should_Contain_List() IList innerValue = new List() { new Value(ITEM_VALUE) }; Value value = new Value(innerValue); Assert.True(value.IsList); - Assert.Equal(ITEM_VALUE, value.AsList[0].AsString); + Assert.Equal(ITEM_VALUE, value.AsList?[0].AsString); + } + + [Fact] + public void Constructor_WhenCalledWithAnotherValue_CopiesInnerValue() + { + // Arrange + var originalValue = new Value("testValue"); + + // Act + var copiedValue = new Value(originalValue); + + // Assert + Assert.Equal(originalValue.AsObject, copiedValue.AsObject); + } + + [Fact] + public void AsInteger_WhenCalledWithNonIntegerInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsInteger; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsBoolean_WhenCalledWithNonBooleanInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsBoolean; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsDouble_WhenCalledWithNonDoubleInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsDouble; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsString_WhenCalledWithNonStringInnerValue_ReturnsNull() + { + // Arrange + var value = new Value(123); + + // Act + var actualValue = value.AsString; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsStructure_WhenCalledWithNonStructureInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsStructure; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsList_WhenCalledWithNonListInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsList; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsDateTime_WhenCalledWithNonDateTimeInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsDateTime; + + // Assert + Assert.Null(actualValue); } } }