diff --git a/MicCheck.slnx b/MicCheck.slnx
index ca6f511..f519904 100755
--- a/MicCheck.slnx
+++ b/MicCheck.slnx
@@ -12,10 +12,12 @@
+
+
diff --git a/src/MicCheck.Client/Common/Guard.cs b/src/MicCheck.Client/Common/Guard.cs
new file mode 100644
index 0000000..fdb60ac
--- /dev/null
+++ b/src/MicCheck.Client/Common/Guard.cs
@@ -0,0 +1,40 @@
+namespace MicCheck.Common;
+
+internal static class Guard
+{
+ public static void Null(T t, string parameterName) where T : class
+ {
+ if (t is null)
+ throw new ArgumentNullException(parameterName, $"{nameof(parameterName)} can not be null");
+ }
+
+ public static void Empty(string value, string parameterName)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ throw new ArgumentException($"{parameterName} can not be empty", parameterName);
+ }
+
+ public static void Empty(IEnumerable collection, string parameterName)
+ {
+ if (collection == null || !collection.Any())
+ throw new ArgumentException($"{parameterName} can not be empty", parameterName);
+ }
+
+ public static void Negative(int value, string parameterName)
+ {
+ if (value < 0)
+ throw new ArgumentOutOfRangeException(parameterName, $"{parameterName} must be a positive number or zero");
+ }
+
+ public static void NegativeOrZero(int value, string parameterName)
+ {
+ if (value <= 0)
+ throw new ArgumentOutOfRangeException(parameterName, $"{nameof(parameterName)} must be a positive number greater then zero");
+ }
+
+ public static void Default(T value, string parameterName)
+ {
+ if (EqualityComparer.Default.Equals(value, default))
+ throw new ArgumentException($"{parameterName} can not be a default value", parameterName);
+ }
+}
diff --git a/src/MicCheck.Client/FeatureClient.cs b/src/MicCheck.Client/FeatureClient.cs
new file mode 100644
index 0000000..b1c0968
--- /dev/null
+++ b/src/MicCheck.Client/FeatureClient.cs
@@ -0,0 +1,22 @@
+using System.Net.Http.Json;
+using MicCheck.Common;
+
+namespace MicCheck;
+
+public class FeatureClient(HttpClient httpClient) : IFeatureClient
+{
+ public async Task IsEnabledAsync(string featureName, bool defaultValue = false, CancellationToken cancellationToken = default)
+ {
+ Guard.Empty(featureName, nameof(featureName));
+
+ var flags = await httpClient.GetFromJsonAsync>("api/v1/flags", cancellationToken);
+ var match = flags?.FirstOrDefault(flag => string.Equals(flag.Feature.Name, featureName, StringComparison.OrdinalIgnoreCase));
+
+ return match?.Enabled ?? defaultValue;
+ }
+}
+
+public interface IFeatureClient
+{
+ Task IsEnabledAsync(string featureName, bool defaultValue = false, CancellationToken cancellationToken = default);
+}
diff --git a/src/MicCheck.Client/FlagResult.cs b/src/MicCheck.Client/FlagResult.cs
new file mode 100644
index 0000000..75b6a85
--- /dev/null
+++ b/src/MicCheck.Client/FlagResult.cs
@@ -0,0 +1,5 @@
+namespace MicCheck;
+
+public record FlagResult(int Id, FeatureSummary Feature, bool Enabled, string? FeatureStateValue);
+
+public record FeatureSummary(int Id, string Name, string Type);
diff --git a/src/MicCheck.Client/MicCheck.Client.csproj b/src/MicCheck.Client/MicCheck.Client.csproj
new file mode 100644
index 0000000..62a79b9
--- /dev/null
+++ b/src/MicCheck.Client/MicCheck.Client.csproj
@@ -0,0 +1,12 @@
+
+
+
+ net10.0
+ enable
+ enable
+ latest
+ true
+ MicCheck
+
+
+
diff --git a/tests/client/MicCheck.Client.Tests.Unit/FeatureClientTests.cs b/tests/client/MicCheck.Client.Tests.Unit/FeatureClientTests.cs
new file mode 100644
index 0000000..70ef6aa
--- /dev/null
+++ b/tests/client/MicCheck.Client.Tests.Unit/FeatureClientTests.cs
@@ -0,0 +1,79 @@
+using System.Net;
+using System.Text;
+using Moq;
+using Moq.Protected;
+using NUnit.Framework;
+
+namespace MicCheck.Client.Tests.Unit;
+
+[TestFixture]
+public class FeatureClientTests
+{
+ [Test]
+ public async Task WhenFeatureIsEnabled_ThenIsEnabledAsyncReturnsTrue()
+ {
+ var client = CreateClient(HttpStatusCode.OK, """
+ [{"id":1,"feature":{"id":1,"name":"my-feature","type":"STANDARD"},"enabled":true,"featureStateValue":null}]
+ """);
+
+ var result = await client.IsEnabledAsync("my-feature");
+
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public async Task WhenFeatureIsDisabled_ThenIsEnabledAsyncReturnsFalse()
+ {
+ var client = CreateClient(HttpStatusCode.OK, """
+ [{"id":1,"feature":{"id":1,"name":"my-feature","type":"STANDARD"},"enabled":false,"featureStateValue":null}]
+ """);
+
+ var result = await client.IsEnabledAsync("my-feature");
+
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public async Task WhenFeatureNameDiffersOnlyByCase_ThenIsEnabledAsyncStillMatches()
+ {
+ var client = CreateClient(HttpStatusCode.OK, """
+ [{"id":1,"feature":{"id":1,"name":"My-Feature","type":"STANDARD"},"enabled":true,"featureStateValue":null}]
+ """);
+
+ var result = await client.IsEnabledAsync("my-feature");
+
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public async Task WhenFeatureIsNotFound_ThenIsEnabledAsyncReturnsProvidedDefaultValue()
+ {
+ var client = CreateClient(HttpStatusCode.OK, "[]");
+
+ var result = await client.IsEnabledAsync("missing-feature", defaultValue: true);
+
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void WhenFeatureNameIsEmpty_ThenIsEnabledAsyncThrowsArgumentException()
+ {
+ var client = CreateClient(HttpStatusCode.OK, "[]");
+
+ Assert.That(async () => await client.IsEnabledAsync(" "), Throws.ArgumentException);
+ }
+
+ private static FeatureClient CreateClient(HttpStatusCode responseStatus, string responseBody)
+ {
+ var handler = new Mock();
+ handler.Protected()
+ .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
+ .ReturnsAsync(new HttpResponseMessage(responseStatus)
+ {
+ Content = new StringContent(responseBody, Encoding.UTF8, "application/json")
+ });
+
+ var httpClient = new HttpClient(handler.Object) { BaseAddress = new Uri("https://example.com") };
+ return new FeatureClient(httpClient);
+ }
+}
diff --git a/tests/client/MicCheck.Client.Tests.Unit/MicCheck.Client.Tests.Unit.csproj b/tests/client/MicCheck.Client.Tests.Unit/MicCheck.Client.Tests.Unit.csproj
new file mode 100644
index 0000000..6679bf6
--- /dev/null
+++ b/tests/client/MicCheck.Client.Tests.Unit/MicCheck.Client.Tests.Unit.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net10.0
+ enable
+ enable
+ latest
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+