ASP.NET Core 10 Minimal API serving a nationwide U.S. church directory, backed by SQL Server through BCL ADO.NET (no EF Core, no Dapper). Public search and church lookup are anonymous; correction submissions require a directory-scoped JWT, and moderation/crawl operations require the churches.mod claim. Observable via OpenTelemetry (Grafana Alloy) and documented via OpenAPI.
Directory is a standalone resource server. It was extracted from the Churches repo: the Church / Search / Crawling / Moderation / User feature slices and the SQL schema now live here, while Churches is an Angular SSR app + Node (Express) BFF that proxies to this API.
The end-to-end platform architecture — how this API, the Churches UI/BFF, and the Functions data pipeline fit together (queue cascade, single-writer invariant, corrections lifecycle, Azure hosting/RBAC) — is documented in Churches/ARCHITECTURE.md. This README is the API-level reference.
| Repo | Role | How Directory interacts |
|---|---|---|
| Identity | OIDC Identity Provider | Issues the access tokens Directory validates (scope directory); the churches.mod claim authorizes moderators |
| Churches | Angular 21 SSR + Node (Express) BFF | Sole interactive client — the BFF proxies /directory/api/** to this API, attaching the user access token when present |
| Functions | Azure Functions isolated worker | The crawl/extract/enrich/dedup processing pipeline writes to the same Directory SQL database |
| Infrastructure | Health monitoring dashboard | Not yet — DirectoryHealthCheck is planned; currently covered by Uptime Kuma |
- .NET 10 / ASP.NET Core (Minimal API, feature-folder vertical slices)
- SQL Server via BCL ADO.NET —
DbConnectionscoped fromSqlClientFactory.Instance; schema owned byDirectory.Data(Microsoft.Build.SqlSDK,Sql150), deployed as a.dacpac - JWT Bearer / OIDC — authorization policies
Directory(scope: directory) andChurchesMod(claimchurches.mod: true) - Azure Service Bus — correction submissions are enqueued for the processing pipeline
- OpenAPI (
Microsoft.AspNetCore.OpenApi) — contract at/openapi/v1.json - Azure — Key Vault (secrets), Blob Storage (data protection keys)
- OpenTelemetry → Grafana Alloy (OTLP traces & metrics)
- Serilog → Elasticsearch (
logs-app-directorydata stream)
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/search |
Anonymous | Search churches by free text, location (lat/lng/radius), state, denomination, worship style, accessibility |
GET |
/churches |
Anonymous | Paged church list |
GET |
/churches/{slug} |
Anonymous | Single church by slug |
POST |
/churches |
churches.mod |
Create a church |
PUT |
/churches/{id:guid} |
churches.mod |
Replace a church |
PATCH |
/churches/{id:guid} |
churches.mod |
Partially update a church |
DELETE |
/churches/{id:guid} |
churches.mod |
Soft-delete a church |
POST |
/churches/{survivingId:guid}/merge/{absorbedId:guid} |
churches.mod |
Merge two church records (transactional) |
GET |
/crawl-sources |
churches.mod |
List crawl sources |
POST |
/crawl-sources |
churches.mod |
Register a crawl source |
DELETE |
/crawl-sources/{id:guid} |
churches.mod |
Remove a crawl source |
POST |
/crawl-sources/{id:guid}/trigger |
churches.mod |
Trigger a crawl run |
GET |
/corrections |
churches.mod |
List submitted corrections |
GET |
/corrections/{id:guid} |
churches.mod |
Get a single correction |
POST |
/corrections |
directory scope |
Submit a user correction (enqueued to Service Bus) |
PATCH |
/corrections/{id:guid}/approve |
churches.mod |
Approve a correction |
PATCH |
/corrections/{id:guid}/reject |
churches.mod |
Reject a correction |
GET |
/me |
Anonymous | Current identity (IsAuthenticated, Sub, Email, Name, HasModerationScope) |
OIDC tokens are issued by Identity; the Churches BFF forwards the user token (type UserOrNone) when proxying /directory/api/**.
The schema is owned by Directory.Data (SQL Database Project). Core tables:
| Table | Purpose |
|---|---|
Directory |
Church records (canonical name, slug, address, contact, confidence score, soft-delete) |
Denominations |
Denomination lookup |
Campuses |
Multi-campus church locations |
Ministries |
Per-church ministries |
ServiceSchedules |
Service times |
ChurchAttributes |
Key/value enrichment attributes (feed the confidence score) |
CrawlSources |
Registered crawl sources and run state |
UserCorrections |
Submitted corrections awaiting moderation |
MergeAuditLog |
Audit trail for church merges |
Functions/fn_HaversineDistance powers radius search. Each church's ConfidenceScore is computed by the processing pipeline: ConfidenceScoreCalculator lives in the Functions repo and runs when ConfidenceWorker consumes a confidence-requests message after every pipeline write.
In production these are sourced from Azure Key Vault and App Service configuration; locally, use User Secrets (ID 61549613-3239-4c31-8300-39334a7c2657).
| Key | Source | Description |
|---|---|---|
OidcAuthority |
Config | OIDC authority URL for JWT validation |
SqlConnectionStringBuilder:DataSource |
Config | SQL Server host (catalog defaults to Directory) |
SqlConnectionStringBuilder:UserID |
Key Vault secret | SQL Server login user |
SqlConnectionStringBuilder:Password |
Key Vault secret | SQL Server login password |
ServiceBusNamespace |
Config | Service Bus fully-qualified namespace (production) |
ServiceBusConnectionString |
Config | Service Bus connection string (non-production) |
ElasticsearchNode |
Config | Elasticsearch node URL |
ElasticsearchUsername |
Key Vault secret | Elasticsearch username |
ElasticsearchPassword |
Key Vault secret | Elasticsearch password |
BlobUri |
Config | Azure Blob Storage URL for data protection keys (production) |
DataProtectionKeyIdentifier |
Config | Azure Key Vault key URI for data protection (production) |
# Prerequisites: User Secrets configured, ASPNETCORE_ENVIRONMENT=Development
# Build
dotnet build Directory/
# Run (https://localhost:7002)
dotnet run --project Directory/
# View OpenAPI doc
curl https://localhost:7002/openapi/v1.json
# Run unit tests (no Azure creds, no live SQL — fully mocked)
dotnet build Directory.Tests.Unit --configuration Debug
.\Directory.Tests.Unit\bin\Debug\net10.0\Directory.Tests.Unit.exe -trait "Category=Unit" -showLiveOutput# Install sqlpackage once (if not already installed)
dotnet tool install --global microsoft.sqlpackage
dotnet build Directory.Data/Directory.Data.sqlproj --configuration Release
sqlpackage /Action:Publish /SourceFile:Directory.Data/bin/Release/Directory.Data.dacpac /TargetConnectionString:"<connection-string>"See TESTING.md for the full testing guide and CI pipeline details.
GET /health
Returns Healthy when the application is running. No authentication required.
The GitHub Actions workflow (.github/workflows/main_crgolden-directory.yml) runs on every push and PR:
- Build solution (
dotnet build --no-incremental --configuration Release), which compilesDirectory.Data.sqlprojto a.dacpac - Unit tests with coverage (
dotnet coverlet … --filter-trait Category=Unit, OpenCover →coverage.opencover.xml),ASPNETCORE_ENVIRONMENT=CI - SonarCloud analysis
- Publish the web app and upload both the app and dacpac artifacts
The deploy job (after a successful build) deploys the .dacpac to the production SQL Server via SqlPackage, then deploys the web app to Azure App Service crgolden-directory (Production slot) via Azure OIDC. The database schema is always deployed before the app.