Files
mic-check/src/api/MicCheck.Api/Features/FeatureEvaluationService.cs
James Wampler 27497e31a9 Move FeatureUsage code into MicCheck.Api.Features.Usage namespace
Groups usage-tracking (metrics, background flush, query service,
controller, entity) under its own namespace/folder, updates all
consumers and the EF migration snapshot's CLR type strings to match.
2026-07-05 22:00:13 -07:00

149 lines
5.2 KiB
C#
Executable File

using MicCheck.Api.Data;
using MicCheck.Api.Features.Usage;
using MicCheck.Api.Identities;
using MicCheck.Api.Segments;
using Microsoft.EntityFrameworkCore;
namespace MicCheck.Api.Features;
public class FeatureEvaluationService(
IMicCheckDbContext db,
SegmentEvaluator segmentEvaluator,
FlagCache flagCache,
FeatureUsageMetrics usageMetrics)
{
public async Task<IReadOnlyList<FeatureStateResult>> EvaluateForEnvironmentAsync(
int environmentId, CancellationToken ct = default)
{
var cached = flagCache.Get(environmentId);
if (cached is not null)
return cached;
var results = await (
from fs in db.FeatureStates
join f in db.Features on fs.FeatureId equals f.Id
where fs.EnvironmentId == environmentId
&& fs.IdentityId == null
&& fs.FeatureSegmentId == null
select new FeatureStateResult
{
Feature = f,
Enabled = fs.Enabled,
Value = fs.Value
}
).ToListAsync(ct);
flagCache.Set(environmentId, results);
foreach (var r in results)
usageMetrics.RecordEvaluation(environmentId, r.Feature.Id, r.Feature.Name);
return results;
}
public async Task<IReadOnlyList<FeatureStateResult>> EvaluateForIdentityAsync(
int environmentId,
string identifier,
IReadOnlyList<TraitInput>? traits = null,
CancellationToken ct = default)
{
var environment = await db.Environments.FindAsync([environmentId], ct);
if (environment is null)
return [];
var identity = await new IdentityResolutionService(db)
.ResolveAsync(environmentId, identifier, traits, ct);
var features = await db.Features
.Where(f => f.ProjectId == environment.ProjectId)
.ToListAsync(ct);
var featureIds = features.Select(f => f.Id).ToList();
var allFeatureStates = await db.FeatureStates
.Where(fs => fs.EnvironmentId == environmentId && featureIds.Contains(fs.FeatureId))
.ToListAsync(ct);
var featureSegments = await db.FeatureSegments
.Where(fsg => fsg.EnvironmentId == environmentId && featureIds.Contains(fsg.FeatureId))
.OrderBy(fsg => fsg.Priority)
.ToListAsync(ct);
var segmentIds = featureSegments.Select(fsg => fsg.SegmentId).Distinct().ToList();
var segments = await db.Segments
.Include(s => s.Rules)
.ThenInclude(r => r.Conditions)
.Include(s => s.Rules)
.ThenInclude(r => r.ChildRules)
.ThenInclude(cr => cr.Conditions)
.Where(s => segmentIds.Contains(s.Id))
.ToDictionaryAsync(s => s.Id, ct);
var identityTraits = identity.Traits.ToList();
var results = new List<FeatureStateResult>();
foreach (var feature in features)
{
// 1. Identity-level override
var identityOverride = allFeatureStates.FirstOrDefault(
fs => fs.FeatureId == feature.Id && fs.IdentityId == identity.Id);
if (identityOverride is not null)
{
results.Add(new FeatureStateResult
{
Feature = feature,
Enabled = identityOverride.Enabled,
Value = identityOverride.Value
});
continue;
}
// 2. Segment overrides (highest priority segment that matches)
var segmentOverrideFound = false;
foreach (var fsg in featureSegments.Where(fsg => fsg.FeatureId == feature.Id))
{
if (!segments.TryGetValue(fsg.SegmentId, out var segment))
continue;
if (!segmentEvaluator.Evaluate(segment, identityTraits, identity.Identifier))
continue;
var segmentState = allFeatureStates.FirstOrDefault(
fs => fs.FeatureId == feature.Id && fs.FeatureSegmentId == fsg.Id);
if (segmentState is null)
continue;
results.Add(new FeatureStateResult
{
Feature = feature,
Enabled = segmentState.Enabled,
Value = segmentState.Value
});
segmentOverrideFound = true;
break;
}
if (segmentOverrideFound)
continue;
// 3. Environment default
var envDefault = allFeatureStates.FirstOrDefault(
fs => fs.FeatureId == feature.Id
&& fs.IdentityId == null
&& fs.FeatureSegmentId == null);
if (envDefault is not null)
{
results.Add(new FeatureStateResult
{
Feature = feature,
Enabled = envDefault.Enabled,
Value = envDefault.Value
});
}
}
foreach (var r in results)
usageMetrics.RecordEvaluation(environmentId, r.Feature.Id, r.Feature.Name);
return results;
}
}