Azure-native. .NET-first. AI-assisted feature flag management.
A production-quality feature flag evaluation service built for .NET teams on Azure — designed from the ground up as an open-source alternative to LaunchDarkly and Unleash, with AI-assisted flag analysis and a first-class .NET SDK as core product features.
- Why This Exists
- What Makes This Different
- Architecture
- Key Design Decisions
- Features
- Getting Started
- API Overview
- Error Handling
- Testing
- AI-Assisted Development Workflow
- AI Features
- Tech Stack
- Roadmap
- Contributing
Mid-market engineering teams running .NET on Azure are underserved by the feature flag market:
| Tool | The Problem |
|---|---|
| LaunchDarkly | Powerful, but pricing scales aggressively. SDK is language-agnostic — not .NET-first. |
| Unleash | Open source, but limited .NET SDK support and no Azure-native integration story. |
| Azure App Configuration | Feature flags are a secondary concern — no rollout strategies, targeting rules, or evaluation analytics. |
Banderas is built to fill that gap. Self-hostable, open-core in direction, and designed specifically for .NET teams on Azure — with AI-assisted flag analysis built in from the start, not bolted on later.
The target demo: clone the repo, run the local quickstart, have a working flag service with a .NET SDK, and ask "which of my flags need attention?" — all in under 15 minutes.
🏗️ Azure-native by design Key Vault, Application Insights, Container Apps, and Azure OpenAI are designed in from Phase 1.5 onward — not integrations added as an afterthought.
🎯 .NET-first A production-quality NuGet SDK ships alongside the service. ASP.NET Core teams get middleware extensions, action filter attributes, and service registration helpers — all idiomatic .NET.
🤖 AI-assisted flag management Natural language flag health analysis, stale flag detection, rollout risk reasoning, and evaluation debugging are core product features — powered by Azure OpenAI and Semantic Kernel.
🔓 Open core direction Self-hostable by design. Managed hosting and enterprise features are the intended business model — not infrastructure lock-in.
🧪 Production-quality engineering 278 tests cover strategies, the registry dispatch engine, validators, service behavior, prompt sanitization, AI analysis orchestration, domain invariants, metadata normalization, and HTTP integration paths. CI runs format gating, zero-warnings builds, unit tests, integration tests, and an optional AI reviewer for Clean Architecture compliance.
Banderas follows Clean Architecture with strict unidirectional dependencies. Every layer has one job and knows nothing about the layers above it.
| System Landscape | Container View |
|---|---|
![]() |
![]() |
| Application Layer Components | Infrastructure Layer Components |
![]() |
![]() |
| Evaluation Request Flow | |
![]() |
|
Diagrams are generated from
Docs/c4/banderas.c4using LikeC4. To explore interactively:npx likec4 start Docs/c4
Banderas/
├── Banderas.Domain/ # Entities, enums, value objects, interfaces
│ └── Exceptions/ # Domain exception hierarchy (FlagNotFoundException, DuplicateFlagNameException, etc.)
├── Banderas.Application/ # Use cases, strategies, evaluator, DTOs, validators
│ ├── AI/ # IAiFlagAnalyzer, IPromptSanitizer, health constants
│ ├── Evaluation/ # FeatureEvaluator + IRolloutStrategy implementations
│ ├── Validators/ # FluentValidation v12 request validators
│ └── Services/ # IBanderasService implementation
├── Banderas.Infrastructure/ # EF Core, Postgres, telemetry, Azure OpenAI implementation
├── Banderas.Api/ # Controllers, middleware, DI composition root
│ └── Middleware/ # GlobalExceptionMiddleware, RouteParameterGuard
├── Banderas.Tests/ # Unit tests — xUnit + FluentAssertions
└── Banderas.Tests.Integration/ # HTTP integration tests with Testcontainers Postgres
The Flag domain entity never crosses the service layer boundary. CRUD and AI flows use DTOs (FlagResponse, CreateFlagRequest, FlagHealthRequest, etc.). Evaluation intentionally passes FeatureEvaluationContext, an immutable value object, into IBanderasService because it is the natural input to the pure evaluation core.
Rollout strategies (NoneStrategy, PercentageStrategy, RoleStrategy) are registered in a dictionary keyed by RolloutStrategy enum. FeatureEvaluator dispatches to the correct strategy at runtime — adding a new strategy requires zero changes to existing code.
FluentValidation.AspNetCore is deprecated. All validators call ValidateAsync() explicitly in controllers before any service code runs. A shared InputSanitizer handles HTTP boundary sanitization. A shared StrategyConfigRules class keeps config validation logic DRY across create and update validators.
Every error returns an application/problem+json response conforming to RFC 9457. A domain exception hierarchy (BanderasException → FlagNotFoundException, DuplicateFlagNameException, BanderasValidationException) maps cleanly to HTTP status codes via GlobalExceptionMiddleware. AI availability failures use AiAnalysisUnavailableException and return 503.
RouteParameterGuard enforces an allowlist on flag-name route parameters. Requests with characters outside the allowlist are rejected before service logic runs.
ExistsAsync() checks before insert catch the common case. For concurrent requests that slip through, SaveChangesAsync intercepts Postgres error code 23505 (unique constraint violation) and converts it to DuplicateFlagNameException. The DB catch lives in Infrastructure to avoid leaking EF Core dependencies upward.
Flags are never hard-deleted. IsArchived = true removes them from active queries. A partial unique index on (Name, Environment) filtered to IsArchived = false enforces name uniqueness without blocking recreation of previously archived flags.
PercentageStrategy uses SHA-256 to hash userId + flagName into a 0–99 bucket. The same user always gets the same result across servers and restarts — no sticky sessions or shared state required.
Azure OpenAI integration is behind IAiFlagAnalyzer. Missing AzureOpenAI:Endpoint registers UnavailableAiFlagAnalyzer, so non-AI endpoints still start and only POST /api/flags/health returns the documented 503.
| Feature | Details |
|---|---|
| Percentage rollouts | Deterministic SHA-256 bucketing — same user always gets the same result across servers and restarts |
| Role-based targeting | Enable features for specific user roles with case-insensitive matching |
| Environment isolation | Flags scoped independently to Development, Staging, and Production |
| Input validation | FluentValidation v12 with two-point sanitization (InputSanitizer + validators) on all write paths |
| Route parameter hardening | RouteParameterGuard enforces character allowlists on flag-name route parameters |
| Name uniqueness | TOCTOU-safe via ExistsAsync check + Postgres constraint intercept in Infrastructure |
| Standardized error responses | RFC 9457 ProblemDetails shape on every error (application/problem+json) |
| Domain exception hierarchy | FlagNotFoundException (404), DuplicateFlagNameException (409), BanderasValidationException (400) |
| Self-documenting API | Enriched OpenAPI spec with Scalar UI at /scalar/v1 |
| Seed data | Six local-development flags are available after development startup |
| Evaluation telemetry | Structured logs and Application Insights custom events for evaluation decisions |
| Azure Key Vault integration | Runtime secret loading via Azure:KeyVaultUri and DefaultAzureCredential |
| Application Insights integration | Azure-native telemetry sink with evaluation custom events |
| AI flag health analysis | POST /api/flags/health uses Azure OpenAI + Semantic Kernel behind IAiFlagAnalyzer |
| Endpoint-scoped AI failure | Missing Azure OpenAI endpoint leaves non-AI endpoints available and returns 503 only for AI analysis |
| AI PR Reviewer | Claude-powered code review on every PR — checks Clean Architecture, FluentValidation v12 patterns, and project conventions |
| CI pipeline | GitHub Actions — format gate (CSharpier), zero-warnings build, unit tests, integration tests, optional AI review |
| Flag description + tags | Operator-authored metadata on every flag — description (varchar 500) and tags (jsonb array); included in AI health analysis prompts |
| AI response contract validation | AiFlagAnalyzer validates model output before returning — full flag coverage, allowed status values, non-empty summary |
| Archived state terminal enforcement | All Flag mutation methods guard against operating on archived flags; FlagDomainException → 409 |
Typed StrategyConfig value object |
Strategy configuration validated at write time via IStrategyConfigValidator registry; type/config consistency enforced by Flag |
| Feature | Details |
|---|---|
| AI response semantic validation | AiFlagAnalyzer validates full flag coverage, allowed status values, and non-empty summary before returning 200 |
| Archived state as terminal | All Flag mutation methods guard against IsArchived = true — FlagDomainException on violation |
Typed StrategyConfig value object |
StrategyConfig is a sealed record; Flag enforces config.ValidatedFor == StrategyType at construction and mutation |
| Concern-named mutation methods | Reconfigure, UpdateName, UpdateMetadata, Archive replace the former field-shaped SetEnabled / UpdateStrategy / Update surface |
| Flag description + tags | Flag.Description (nullable varchar 500) and Flag.Tags (jsonb) — operator-authored metadata; AI health prompts include sanitized values |
IsSeeded removed from domain |
Provenance tracking moved to EF Core shadow property — domain entity no longer carries infrastructure concerns |
| API response contract tests | ContractTests.cs pins JSON wire shape for all 4 success response types and all error response shapes via ReadRawJsonAsync + field-name assertions |
| Phase | Feature |
|---|---|
| 2 | Multivariate flag support (Variation value object) or Flag → FlagDefinition / FlagEnvironmentConfig aggregate split |
| 3 | JWT authentication and RBAC |
| 5 | User targeting, time-based activation, gradual rollout |
| 6 | Redis caching layer |
| 7 | .NET NuGet SDK — UseBanderas(), [RequireFlag], services.AddBanderasClient() |
- Docker Desktop
- .NET 10 SDK
- VS Code + Dev Containers extension (recommended)
git clone https://github.com/amodelandme/Banderas.git
cd Banderas
docker compose up -d
Azure__KeyVaultUri="" dotnet run --project Banderas.Api --launch-profile httpThe API starts at http://localhost:5227.
Interactive docs are available at http://localhost:5227/scalar/v1.
docker compose up -dstarts PostgreSQL.dotnet runstarts the API. TheAzure__KeyVaultUri=""override disables the development Key Vault setting for local runs that are not authenticated to Azure.
The repo ships with a fully configured devcontainer including .NET 10, Claude Code, GitHub CLI, and Docker-outside-of-Docker:
- Open the repo in VS Code
- Click Reopen in Container when prompted
- Run
docker compose up -dfrom the integrated terminal - Run
Azure__KeyVaultUri="" dotnet run --project Banderas.Api --launch-profile http - The devcontainer auto-joins the Postgres Docker network on start —
Host=postgresresolves without any manual configuration
Note: Start
docker compose up -dbefore or immediately after opening the devcontainer.
All responses use application/json. All errors use application/problem+json (RFC 9457).
| Method | Route | Description |
|---|---|---|
GET |
/api/flags?environment=Development |
List all active flags in an environment |
GET |
/api/flags/{name}?environment=Development |
Get a specific flag by name |
POST |
/api/flags |
Create a new feature flag |
PUT |
/api/flags/{name}?environment=Development |
Update an existing flag |
DELETE |
/api/flags/{name}?environment=Development |
Archive a flag (soft delete) |
| Method | Route | Description |
|---|---|---|
POST |
/api/evaluate |
Evaluate a flag for a specific user and context |
POST /api/evaluate
{
"flagName": "dark-mode",
"environment": "Production",
"userId": "user-123",
"userRoles": ["beta-tester", "admin"]
}{
"isEnabled": true
}| Method | Route | Description |
|---|---|---|
POST |
/api/flags/health |
Analyze all active flags across environments |
POST /api/flags/health
{
"stalenessThresholdDays": 7
}Percentage Rollout (30% of users)
{
"name": "checkout-v2",
"environment": "Production",
"isEnabled": true,
"strategyType": "Percentage",
"strategyConfig": "{\"percentage\": 30}"
}Role-Based Targeting
{
"name": "admin-dashboard",
"environment": "Production",
"isEnabled": true,
"strategyType": "RoleBased",
"strategyConfig": "{\"roles\": [\"admin\", \"superuser\"]}"
}All errors return RFC 9457 ProblemDetails with Content-Type: application/problem+json.
| Scenario | Status | Type |
|---|---|---|
| Flag not found | 404 |
FlagNotFoundException |
| Duplicate flag name | 409 |
DuplicateFlagNameException |
| Validation failure | 400 |
BanderasValidationException |
| Invalid route parameter | 400 |
RouteParameterGuard rejection |
| AI analysis unavailable | 503 |
AiAnalysisUnavailableException |
| Unexpected server error | 500 |
Generic ProblemDetails |
Example error response:
{
"type": "https://tools.ietf.org/html/rfc9457",
"title": "Flag Not Found",
"status": 404,
"detail": "Flag 'dark-mode' not found in Production.",
"instance": "/api/flags/dark-mode?environment=Production"
}The exception hierarchy follows the Open/Closed Principle — new exception types extend BanderasException without modifying GlobalExceptionMiddleware.
Unit tests live in Banderas.Tests/ and cover pure logic: strategies, evaluator behavior, validators, domain value objects, service orchestration, logging, prompt sanitization, AI analysis orchestration, Flag archived-terminal invariants, StrategyConfig value object, config validators, metadata normalization, and StrategyConfigFactory.
Integration tests live in Banderas.Tests.Integration/ and run the HTTP stack against Testcontainers PostgreSQL. They cover CRUD, evaluation, archived-flag mutation paths, FlagDomainException → 409 middleware contract, optimistic concurrency, seed-data startup, AI health analysis, AI-unavailable 503 behavior, semantic AI response validation, metadata round-trips, and the missing-Azure-OpenAI startup resilience path, and API response contract shapes.
| Suite | Count | Coverage |
|---|---|---|
| Unit | 203 | Domain, strategies, evaluator, validators, services, logging, prompt sanitization, AI analysis, metadata, StrategyConfig VO |
| Integration | 75 | API endpoints, ProblemDetails responses, archived-flag paths, concurrency, seed data, AI health, startup resilience, contract tests |
| Total | 278 |
During the test session, two silent production bugs were discovered and fixed:
-
Missing
JsonExceptioncatch —PercentageStrategyandRoleStrategywould throw an unhandled exception on malformedStrategyConfigJSON instead of failing closed. -
System.Text.Jsoncase sensitivity —System.Text.Jsonis case-sensitive by default.StrategyConfigJSON stored in Postgres usesPascalCaseproperty names (Percentage,Roles). WithoutPropertyNameCaseInsensitive = true, every Percentage and RoleBased evaluation silently returnedfalse— the flag appeared to be working but was always disabled for real users. Fixed with a staticJsonSerializerOptionsinstance.
These bugs had no visible errors. They would have been invisible in production without tests.
dotnet test Banderas.slnThis project uses a two-agent AI development workflow as a deliberate engineering practice — not just as a productivity hack:
┌─────────────────────────────────────────────────────────────────┐
│ HUMAN ORCHESTRATOR │
│ (Jose — Product Owner) │
└──────────────┬──────────────────────────────┬───────────────────┘
│ │
┌──────────────▼──────────┐ ┌──────────────▼──────────────────┐
│ ARCHITECT AGENT │ │ ENGINEERING AGENT │
│ Claude.ai (Project) │ │ Claude Code (VS Code) │
│ │ │ │
│ Reads living docs → │ │ Reads living docs → │
│ Reasons through design │ │ Reads spec → │
│ Writes spec.md │ │ Implements feature │
│ Flags interview moments │ │ Writes implementation notes │
└─────────────────────────┘ └─────────────────────────────────┘
│
┌─────────────▼───────────────────┐
│ AI PR REVIEWER (GitHub CI) │
│ Claude API — PR #35 │
│ │
│ Reviews every labeled PR for: │
│ • Clean Architecture │
│ • FluentValidation v12 rules │
│ • Project conventions │
│ Posts structured comments │
└─────────────────────────────────┘
Three documents serve as the persistent memory across sessions:
| Document | Purpose |
|---|---|
Docs/architecture.md |
Structural source of truth — layer boundaries, design decisions |
Docs/current-state.md |
Where things stand right now — updated after every PR |
Docs/roadmap.md |
Phase-gated plan — where things are going |
Specs are written before implementation and committed to Docs/Decisions/ as historical artifacts. They are never updated post-implementation.
AI is split between capabilities available now and features planned for later phases:
| Feature | Phase | Description |
|---|---|---|
| Flag health analysis | 1.5 ✅ | Natural language analysis of flag state, age, and strategy configuration |
| Stale flag detection | 1.5 ✅ | Uses UpdatedAt and caller-supplied staleness threshold to flag stale candidates |
| AI unavailable fallback | 1.5 ✅ | Missing Azure OpenAI endpoint returns 503 only on the AI health endpoint |
| AI response contract validation | 2 ✅ | Full flag coverage, allowed status values, and non-empty summary enforced before returning 200 |
| Rollout risk reasoning | Future | "Is it safe to roll this flag out to 100%?" — answered in plain English |
| Natural language flag creation | Future | Describe a flag in English; get a fully configured flag back |
| Anomaly detection | Future | Alert when evaluation patterns change unexpectedly |
| Evaluation debugging | Future | "Why was this flag OFF for user X?" answered in plain English |
All AI features will use Azure OpenAI and Semantic Kernel — consistent with the Azure-native design principle.
| Category | Technology |
|---|---|
| Runtime | .NET 10 / ASP.NET Core |
| ORM | EF Core 10 + Npgsql |
| Database | PostgreSQL 16 (Docker locally; Azure Database for PostgreSQL Flexible Server in production) |
| Validation | FluentValidation v12 |
| Testing | xUnit + FluentAssertions v8 |
| API Docs | Scalar UI (replaces Swagger) |
| Code Style | CSharpier 1.x + .editorconfig |
| CI/CD | GitHub Actions |
| AI (Dev Workflow) | Claude API (Anthropic) — PR reviewer |
| AI (Product) | Azure OpenAI + Semantic Kernel |
| Containerization | Docker + Docker Compose |
| Dev Environment | VS Code Dev Containers |
Phase 0 ✅ Foundation — domain, strategies, persistence, API
Phase 1 ✅ MVP Completion — validation, CI, error handling, tests, telemetry
Phase 1.5 ✅ Azure Foundation + AI — Key Vault, App Insights, AI analysis endpoint
Phase 2 🔄 Testing & Reliability — domain invariants, StrategyConfig VO, metadata, contract tests remaining
Phase 3 Auth & Security — JWT, RBAC, rate limiting, audit trail
Phase 4 Observability — evaluation telemetry, debugging endpoint, dashboards
Phase 5 Advanced Strategies — user targeting, time-based, gradual rollout
Phase 6 Performance — caching, Redis, horizontal scaling
Phase 7 ⭐ .NET SDK — first-class NuGet SDK, middleware extensions, action filters
Phase 8 Production Readiness — CD to Azure Container Apps, SLA baseline
Phase 9 Open Core Launch — public Docker image, hosted offering
- FluentValidation on all request DTOs
- Global exception middleware — RFC 9457 ProblemDetails
- Input sanitization + route parameter hardening
- Name uniqueness with TOCTOU protection
- Unit tests for strategies, evaluator, validators, services, and AI helpers
- CI pipeline — format gate + zero-warnings build
- AI PR reviewer in CI
- Integration tests for all current endpoints
-
.httpsmoke test file - Seed data for local development
- Evaluation decision logging
- Azure Key Vault integration
- Application Insights integration
- AI flag health analysis endpoint
- Prompt sanitization before AI calls
- AI unavailability maps to
503 ProblemDetails - Missing Azure OpenAI endpoint does not block non-AI app startup
- Architecture review completed — gate: GO WITH CONDITIONS
- AI response semantic validation — full flag coverage, allowed statuses, non-empty summary
- Archived state as terminal — guard clause on all
Flagmutation methods -
IsSeededremoved from domain entity — moved to EF Core shadow property - Archived-flag integration test coverage — PUT/DELETE/evaluate → 404,
FlagDomainException→ 409 - Typed
StrategyConfigvalue object — config/strategy consistency enforced byFlag - Mutation methods consolidated by concern —
Reconfigure,UpdateName,UpdateMetadata,Archive -
Flag.Description+Flag.Tags— operator metadata, zero-downtime migration, AI prompt enrichment - Contract tests for API responses — JSON wire shape pinned for all 4 success types and all error shapes
- Multivariate flag support /
Flag→FlagDefinitionaggregate split
This project is in active development. Contributions, feedback, and questions are welcome.
Contribution guidelines are planned for Phase 9.
For questions or architectural discussions, open an issue.




