Squash and merge of version-0.1 Co-authored-by: James Wampler <james@wamp.dev> Co-committed-by: James Wampler <james@wamp.dev>
265 lines
7.6 KiB
Markdown
Executable File
265 lines
7.6 KiB
Markdown
Executable File
# Step 5: Flags API (Public SDK Endpoint)
|
|
|
|
## Goal
|
|
Implement the public Flags API — the endpoints consumed by SDK clients to retrieve feature flags and identify users. This is the high-traffic, read-heavy API authenticated via `X-Environment-Key`.
|
|
|
|
---
|
|
|
|
## Endpoints
|
|
|
|
All routes prefixed with `/api/v1/` and authenticated with the `EnvironmentKey` scheme.
|
|
|
|
### GET /api/v1/flags/
|
|
Returns all enabled feature flags for the resolved environment.
|
|
|
|
**Response:**
|
|
```json
|
|
[
|
|
{
|
|
"id": 1,
|
|
"feature": {
|
|
"id": 1,
|
|
"name": "dark_mode",
|
|
"type": "STANDARD"
|
|
},
|
|
"enabled": true,
|
|
"feature_state_value": null
|
|
}
|
|
]
|
|
```
|
|
|
|
### GET /api/v1/identities/?identifier=user123
|
|
Returns flags for a specific user identity, including segment overrides and identity-level overrides.
|
|
|
|
### POST /api/v1/identities/
|
|
Identifies a user, optionally sets traits, and returns flags.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"identifier": "user123",
|
|
"traits": [
|
|
{ "trait_key": "plan", "trait_value": "premium" }
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"traits": [...],
|
|
"flags": [...]
|
|
}
|
|
```
|
|
|
|
### GET /api/v1/environment-document/
|
|
Returns the full environment configuration for **local evaluation mode**.
|
|
Used by server-side SDKs to pull all flags, segments, and targeting rules locally.
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"id": 1,
|
|
"api_key": "env-key-xxx",
|
|
"feature_states": [...],
|
|
"project": {
|
|
"id": 1,
|
|
"name": "My Project",
|
|
"segments": [...]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Feature Evaluation Service
|
|
|
|
Lives in `MicCheck.Api/Features/`. Injects `MicCheckDbContext` and returns domain objects — no DTOs.
|
|
|
|
```csharp
|
|
// MicCheck.Api/Features/FeatureEvaluationService.cs
|
|
public class FeatureEvaluationService
|
|
{
|
|
private readonly MicCheckDbContext _db;
|
|
private readonly SegmentEvaluator _segmentEvaluator;
|
|
private readonly FlagCache _flagCache;
|
|
|
|
// Resolves the effective FeatureState for a given environment
|
|
// Priority order (highest wins):
|
|
// 1. Identity-level override
|
|
// 2. Segment override (highest priority segment that matches)
|
|
// 3. Environment default
|
|
|
|
public Task<IReadOnlyList<FeatureStateResult>> EvaluateForEnvironmentAsync(
|
|
int environmentId, CancellationToken ct = default);
|
|
|
|
public Task<IReadOnlyList<FeatureStateResult>> EvaluateForIdentityAsync(
|
|
int environmentId,
|
|
string identifier,
|
|
IReadOnlyList<TraitInput>? traits = null,
|
|
CancellationToken ct = default);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Identity Resolution Service
|
|
|
|
Lives in `MicCheck.Api/Identities/`. Injects `MicCheckDbContext` and returns domain objects.
|
|
|
|
```csharp
|
|
// MicCheck.Api/Identities/IdentityResolutionService.cs
|
|
public class IdentityResolutionService
|
|
{
|
|
private readonly MicCheckDbContext _db;
|
|
|
|
// 1. Look up Identity by (EnvironmentId, Identifier) — create if not found
|
|
// 2. Upsert provided traits by key
|
|
// 3. Return the resolved Identity with current traits loaded
|
|
public Task<Identity> ResolveAsync(
|
|
int environmentId,
|
|
string identifier,
|
|
IReadOnlyList<TraitInput>? traits,
|
|
CancellationToken ct = default);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Segment Evaluator
|
|
|
|
Lives in `MicCheck.Api/Segments/`. Pure logic — no DB access (data is preloaded by callers).
|
|
|
|
```csharp
|
|
// MicCheck.Api/Segments/SegmentEvaluator.cs
|
|
public class SegmentEvaluator
|
|
{
|
|
public bool Evaluate(Segment segment, IReadOnlyList<IdentityTrait> traits);
|
|
|
|
private bool EvaluateRule(SegmentRule rule, IReadOnlyList<IdentityTrait> traits);
|
|
private bool EvaluateCondition(SegmentCondition condition, IReadOnlyList<IdentityTrait> traits);
|
|
}
|
|
```
|
|
|
|
**Operator implementations** (pure functions):
|
|
- String: Equal, NotEqual, Contains, NotContains, Regex
|
|
- Numeric: GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual
|
|
- Boolean: IsTrue, IsFalse
|
|
- Membership: In, NotIn
|
|
- Existence: IsSet, IsNotSet
|
|
- Sampling: PercentageSplit (deterministic by identity+segment hash)
|
|
- Modulo: ModuloValueDivisorRemainder
|
|
|
|
---
|
|
|
|
## Request/Response DTOs
|
|
|
|
Live alongside their controllers in the relevant feature folder. DTOs are only constructed in controller action methods — services never return or accept DTOs.
|
|
|
|
```csharp
|
|
// Features/FlagResponse.cs
|
|
public record FlagResponse(
|
|
int Id,
|
|
FeatureSummaryResponse Feature,
|
|
bool Enabled,
|
|
string? FeatureStateValue
|
|
)
|
|
{
|
|
public static FlagResponse From(FeatureStateResult result) => new(
|
|
result.Feature.Id, // mapping happens here, in the response type, called from the controller
|
|
FeatureSummaryResponse.From(result.Feature),
|
|
result.Enabled,
|
|
result.Value);
|
|
}
|
|
|
|
// Features/FeatureSummaryResponse.cs
|
|
public record FeatureSummaryResponse(int Id, string Name, string Type)
|
|
{
|
|
public static FeatureSummaryResponse From(Feature feature) =>
|
|
new(feature.Id, feature.Name, feature.Type.ToString().ToUpperInvariant());
|
|
}
|
|
|
|
// Identities/IdentityRequest.cs
|
|
public record IdentityRequest(
|
|
string Identifier,
|
|
IReadOnlyList<TraitInput>? Traits
|
|
);
|
|
|
|
// Identities/TraitInput.cs
|
|
public record TraitInput(
|
|
string TraitKey,
|
|
object? TraitValue // string | int | float | bool
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Controllers
|
|
|
|
Controllers receive a request DTO, call a service with domain-level parameters, then map the domain result to a response DTO before returning.
|
|
|
|
```csharp
|
|
// MicCheck.Api/Features/FlagsController.cs
|
|
[ApiController]
|
|
[Route("api/v1/flags")]
|
|
[Authorize(Policy = AuthorizationPolicies.FlagsApiAccess)]
|
|
public class FlagsController : ControllerBase
|
|
{
|
|
private readonly FeatureEvaluationService _featureEvaluationService;
|
|
|
|
[HttpGet]
|
|
public async Task<ActionResult<IReadOnlyList<FlagResponse>>> GetAll(CancellationToken ct)
|
|
{
|
|
var environmentId = int.Parse(User.FindFirstValue("EnvironmentId")!);
|
|
var results = await _featureEvaluationService.EvaluateForEnvironmentAsync(environmentId, ct);
|
|
return Ok(results.Select(FlagResponse.From).ToList());
|
|
}
|
|
}
|
|
|
|
// MicCheck.Api/Identities/IdentitiesController.cs
|
|
[ApiController]
|
|
[Route("api/v1/identities")]
|
|
[Authorize(Policy = AuthorizationPolicies.FlagsApiAccess)]
|
|
public class IdentitiesController : ControllerBase { ... }
|
|
|
|
// MicCheck.Api/Environments/EnvironmentDocumentController.cs
|
|
[ApiController]
|
|
[Route("api/v1/environment-document")]
|
|
[Authorize(Policy = AuthorizationPolicies.FlagsApiAccess)]
|
|
public class EnvironmentDocumentController : ControllerBase { ... }
|
|
```
|
|
|
|
---
|
|
|
|
## Caching
|
|
|
|
Lives in `MicCheck.Api/Features/`. Cache stores domain results, not serialized responses.
|
|
|
|
```csharp
|
|
// MicCheck.Api/Features/FlagCache.cs
|
|
public class FlagCache
|
|
{
|
|
private readonly IMemoryCache _cache;
|
|
|
|
public IReadOnlyList<FeatureStateResult>? Get(int environmentId);
|
|
public void Set(int environmentId, IReadOnlyList<FeatureStateResult> flags);
|
|
public void Invalidate(int environmentId);
|
|
}
|
|
```
|
|
|
|
Cache is invalidated inside `FeatureEvaluationService` whenever a `FeatureState` is modified. Designed to swap the backing store for Redis without changing the `FlagCache` interface.
|
|
|
|
---
|
|
|
|
## Acceptance Criteria
|
|
- [ ] `GET /api/v1/flags/` returns correct flags for a valid environment key
|
|
- [ ] `GET /api/v1/flags/` returns 401 for missing or invalid environment key
|
|
- [ ] `POST /api/v1/identities/` creates identity + upserts traits + returns correct flags
|
|
- [ ] Segment override takes priority over environment default
|
|
- [ ] Identity override takes priority over segment override
|
|
- [ ] `GET /api/v1/environment-document/` returns full environment config
|
|
- [ ] Controllers never pass DTOs into services — only primitive/domain values
|
|
- [ ] `SegmentEvaluator` unit tests cover all operator types
|
|
- [ ] `FeatureEvaluationService` unit tests cover all three priority tiers
|
|
- [ ] `PercentageSplit` distributes identities deterministically and uniformly
|