A sophisticated location-based activity recommendation system built with .NET 9, featuring AI-powered personalization, multi-provider orchestration, and production-ready observability.
Urban discovery systems usually provide generic results. WhatShouldIDo introduces a personalization-first architecture that:
WhatShouldIDo is an enterprise-grade API that answers the timeless question: "What should I do?"
The system intelligently recommends personalized places and activities by combining:
Users struggle to discover relevant activities that match their preferences, current context (weather, time, location), and intentions (quick bite, full-day plan, trying something new). This system solves that by:
QUICK_SUGGESTION β 3 rapid results
FOOD_ONLY β Restaurant/cafΓ© filtering (10 results)
ACTIVITY_ONLY β Entertainment/cultural venues (10 results)
ROUTE_PLANNING β Multi-stop optimized routes
TRY_SOMETHING_NEW β Novel recommendations
graph TB
subgraph "API Layer"
A[Controllers<br/>21 REST endpoints]
B[Middleware<br/>6 types]
C[DTOs<br/>Request/Response]
D[Validators<br/>FluentValidation]
end
subgraph "Application Layer"
E[Commands<br/>8 MediatR]
F[Queries<br/>7 MediatR]
G[Handlers<br/>CQRS]
H[Interfaces<br/>41+ contracts]
end
subgraph "Infrastructure Layer"
I[Services<br/>29 implementations]
J[Repositories<br/>Generic + Specialized]
K[External APIs<br/>Google, OpenAI, etc.]
L[Caching<br/>Redis + Fallback]
end
subgraph "Domain Layer"
M[Entities<br/>16 aggregates]
N[Value Objects<br/>Coordinates]
O[Domain Logic<br/>Invariants]
end
A --> E
A --> F
E --> G
F --> G
G --> H
H --> I
I --> J
I --> K
I --> L
G --> M
M --> O
style A fill:#e1f5ff
style E fill:#fff4e1
style I fill:#e8f5e9
style M fill:#fce4ec
| Layer | Responsibility | Examples |
|---|---|---|
| API | HTTP concerns, validation, auth | SuggestionsController, RoutesController |
| Application | Use cases, orchestration | CreateSuggestionsCommand, SearchPlacesQuery |
| Infrastructure | External integrations, data access | GooglePlacesProvider, RouteRepository |
| Domain | Business logic, invariants | UserSubscription, Route, UserTasteProfile |
graph LR
subgraph "Client Applications"
A[Mobile Apps<br/>iOS/Android]
B[Web App]
end
subgraph "API Gateway"
C[ASP.NET Core<br/>API]
end
subgraph "Data Stores"
D[(PostgreSQL<br/>Primary DB)]
E[(Redis Cluster<br/>Cache + Quota)]
end
subgraph "External Services"
F[Google Places<br/>API]
G[OpenTripMap<br/>API]
H[OpenAI<br/>GPT-4o-mini]
I[Google Directions<br/>API]
J[OpenWeather<br/>API]
end
subgraph "Observability"
K[Prometheus<br/>Metrics]
L[Grafana<br/>Dashboards]
M[Tempo<br/>Tracing]
N[Loki<br/>Logs]
end
A --> C
B --> C
C --> D
C --> E
C --> F
C --> G
C --> H
C --> I
C --> J
C --> K
K --> L
C --> M
C --> N
style C fill:#4CAF50,color:#fff
style D fill:#336791,color:#fff
style E fill:#DC382D,color:#fff
style H fill:#10A37F,color:#fff
sequenceDiagram
participant Client
participant API
participant Middleware
participant Handler
participant Context
participant Places
participant Personalization
participant RouteOpt
participant Cache
participant DB
Client->>API: POST /api/suggestions<br/>{intent, location}
API->>Middleware: EntitlementAndQuota
Middleware->>Cache: Check quota (Redis)
Cache-->>Middleware: Quota OK
Middleware->>Middleware: Consume quota (atomic)
API->>Handler: CreateSuggestionsCommand
Handler->>Context: Get contextual insights
Context-->>Handler: Weather, time, season
Handler->>Places: Search places (multi-provider)
Places->>Places: Google Places + OpenTripMap
Places->>Places: Deduplication (70m radius)
Places-->>Handler: Raw places (40-60 results)
Handler->>Handler: Apply intent filter<br/>(FOOD_ONLY β restaurants only)
alt User Authenticated
Handler->>DB: Load taste profile
DB-->>Handler: UserTasteProfile
Handler->>Personalization: HybridScorer
Personalization->>Personalization: Score places<br/>(5 dimensions)
Personalization-->>Handler: Scored suggestions
end
alt Intent == ROUTE_PLANNING
Handler->>RouteOpt: Optimize route (TSP)
RouteOpt-->>Handler: Optimized order + distance
end
Handler-->>API: SuggestionsResult
API-->>Client: JSON Response
erDiagram
User ||--o| UserProfile : has
User ||--o| UserTasteProfile : has
User ||--o| UserSubscription : has
User ||--o{ UserVisit : tracks
User ||--o{ UserFavorite : bookmarks
User ||--o{ UserExclusion : dislikes
User ||--o{ Route : creates
User ||--o{ UserSuggestionHistory : receives
Place ||--o{ UserFavorite : favorited_by
Place ||--o{ UserVisit : visited_by
Place ||--o{ RoutePoint : included_in
Route ||--|{ RoutePoint : contains
Route ||--o{ RouteRevision : versioned_by
Route ||--o| RouteShareToken : shared_via
User {
uuid id PK
string email UK
string username
string password_hash
subscription_tier tier
datetime subscription_expiry
int daily_api_usage
datetime last_api_reset
datetime created_at
datetime updated_at
}
UserTasteProfile {
uuid id PK
uuid user_id FK
double culture_weight
double food_weight
double nature_weight
double nightlife_weight
double shopping_weight
double art_weight
double wellness_weight
double sports_weight
double quality_weight
double atmosphere_weight
double design_weight
double calmness_weight
double spaciousness_weight
double novelty_tolerance
bytes row_version
datetime created_at_utc
datetime updated_at_utc
}
UserSubscription {
uuid id PK
uuid user_id FK
provider provider
plan plan
status status
datetime trial_ends_at_utc
datetime current_period_ends_at_utc
string external_subscription_id
datetime last_verified_at_utc
string notes
bytes row_version
}
Route {
uuid id PK
uuid user_id FK
string name
string description
bool is_public
double total_distance
int estimated_duration
datetime created_at
datetime updated_at
}
RoutePoint {
uuid id PK
uuid route_id FK
uuid place_id FK
int order_index
datetime estimated_arrival
int duration_minutes
}
Place {
uuid id PK
string name
float latitude
float longitude
string address
string rating
int review_count
string category
string google_place_id UK
string source
bool is_sponsored
datetime sponsored_until
datetime cached_at
}
stateDiagram-v2
[*] --> None: New User
None --> Trialing: Start Trial (IAP)
None --> Active: Direct Purchase (IAP)
None --> Active: Manual Grant (Admin)
Trialing --> Active: Trial β Paid
Trialing --> Expired: Trial Ends (No Purchase)
Active --> Canceled: User Cancels
Active --> PastDue: Payment Fails
Canceled --> Expired: Period Ends
PastDue --> Active: Payment Retry Success
PastDue --> Expired: Grace Period Ends
Expired --> None: Reset to Free
Expired --> Trialing: New Trial (Re-subscribe)
Active --> Active: Renewal Success
graph TB
A[HTTP Request] --> B[GlobalExceptionMiddleware]
B --> C[CorrelationIdMiddleware<br/>W3C Trace Context]
C --> D[MetricsMiddleware<br/>OpenTelemetry]
D --> E[AdvancedRateLimitMiddleware<br/>100 req/60s per user]
E --> F[CORS Middleware]
F --> G[Localization Middleware<br/>10 languages]
G --> H[Authentication<br/>JWT Bearer]
H --> I[Authorization<br/>Claims-based]
I --> J[EntitlementAndQuotaMiddleware<br/>Redis-backed]
J --> K[Controllers]
K --> L[HTTP Response]
style B fill:#f44336,color:#fff
style C fill:#2196F3,color:#fff
style D fill:#FF9800,color:#fff
style E fill:#9C27B0,color:#fff
style J fill:#4CAF50,color:#fff
graph LR
A[Place] --> B[Implicit Score<br/>25%]
A --> C[Explicit Score<br/>30%]
A --> D[Novelty Score<br/>20%]
A --> E[Context Score<br/>15%]
A --> F[Quality Score<br/>10%]
B --> G[Visit History<br/>Frequency + Recency]
C --> H[Taste Profile<br/>13 weights]
D --> I[Recent Suggestions<br/>1-week exclusion]
E --> J[Weather + Time<br/>+ Season]
F --> K[Rating + Reviews]
G --> L[Weighted Sum]
H --> L
I --> L
J --> L
K --> L
L --> M[Final Score<br/>0-1 range]
style L fill:#4CAF50,color:#fff
style M fill:#2196F3,color:#fff
graph TB
subgraph "Ingress Layer"
A[Ingress Controller<br/>NGINX/Traefik]
end
subgraph "Application Layer"
B[API Pod 1]
C[API Pod 2]
D[API Pod 3]
E[Horizontal Pod<br/>Autoscaler]
end
subgraph "Data Layer"
F[(PostgreSQL<br/>StatefulSet)]
G[(Redis Cluster<br/>6 nodes)]
end
subgraph "Observability Layer"
H[Prometheus]
I[Grafana]
J[Tempo]
K[Loki]
end
subgraph "Configuration"
L[ConfigMap]
M[Secrets]
end
A --> B
A --> C
A --> D
E --> B
E --> C
E --> D
B --> F
C --> F
D --> F
B --> G
C --> G
D --> G
B --> L
B --> M
B --> H
H --> I
B --> J
B --> K
style A fill:#326CE5,color:#fff
style E fill:#FF6F00,color:#fff
style F fill:#336791,color:#fff
style G fill:#DC382D,color:#fff
| Service | Purpose |
|---|---|
| Google Places API | Primary place search (40 places/request) |
| OpenTripMap API | Tourism POIs and cultural landmarks |
| Google Directions API | Route optimization and ETA calculation |
| Google Geocoding API | Location name β coordinates |
| OpenWeather API | Weather context for recommendations |
| OpenAI API | GPT-4o-mini for NLP and semantic ranking |
| HuggingFace API | Fallback AI provider |
| Ollama | Local AI models (self-hosted) |
Clone the repository
git clone https://github.com/yourusername/WhatShouldIDo.git
cd WhatShouldIDo/NeYapsamWeb/API
Configure environment variables
cp .env.example .env
# Edit .env with your API keys
Start infrastructure
docker-compose up -d postgres redis
Run database migrations
dotnet ef database update --project src/WhatShouldIDo.Infrastructure --startup-project src/WhatShouldIDo.API
Start the API
cd src/WhatShouldIDo.API
dotnet run
Access the API
# Start all services (API + DB + Redis + Observability)
docker-compose -f docker-compose.yml -f docker-compose.observability.yml up -d
# Access dashboards
# Grafana: http://localhost:3000 (admin/admin)
# Prometheus: http://localhost:9090
# Seq: http://localhost:5341
Install dependencies
dotnet restore
Update connection strings in appsettings.Development.json:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=Wisido;Username=postgres;Password=yourpassword"
},
"Redis": {
"Configuration": "localhost:6379"
}
}
Run migrations
dotnet ef database update --project src/WhatShouldIDo.Infrastructure --startup-project src/WhatShouldIDo.API
Run the API
cd src/WhatShouldIDo.API
dotnet watch run
Create a .env file or set environment variables:
# Database
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_NAME=Wisido
DATABASE_USER=postgres
DATABASE_PASSWORD=your_secure_password
# Redis
REDIS_CONFIGURATION=redis:6379
# External APIs
GOOGLE_PLACES_API_KEY=your_google_api_key
OPENTRIPMAP_API_KEY=your_opentripmap_key
OPENAI_API_KEY=your_openai_api_key
OPENWEATHER_API_KEY=your_openweather_key
# JWT Authentication
JWT_SECRET=your_256_bit_secret_key_here
JWT_ISSUER=WhatShouldIDo
JWT_AUDIENCE=WhatShouldIDoClients
JWT_EXPIRATION_MINUTES=60
# Observability
OTLP_ENDPOINT=http://tempo:4317
PROMETHEUS_ENABLED=true
# Feature Flags
QUOTA_ENABLED=true
QUOTA_DEFAULT_FREE_LIMIT=5
SUBSCRIPTION_VERIFICATION_ENABLED=false
{
"ConnectionStrings": {
"DefaultConnection": "Host=postgres;Database=Wisido;Username=postgres;Password=postgres"
},
"Redis": {
"Configuration": "redis:6379",
"UseCluster": true
},
"Feature": {
"Quota": {
"DefaultFreeQuota": 5,
"DailyResetEnabled": false,
"StorageBackend": "Redis"
},
"Subscription": {
"VerificationEnabled": false,
"AllowDevTestReceipts": false
},
"TasteQuiz": {
"Version": "v1",
"DraftTtlHours": 24
}
},
"Scoring": {
"ImplicitWeight": 0.25,
"ExplicitWeight": 0.30,
"NoveltyWeight": 0.20,
"ContextWeight": 0.15,
"QualityWeight": 0.10
},
"Observability": {
"Enabled": true,
"ServiceName": "whatshouldido-api",
"ServiceVersion": "2.0.0",
"TraceSamplingRatio": 0.05,
"PrometheusEnabled": true,
"OtlpTracesEnabled": true,
"OtlpTracesEndpoint": "http://tempo:4317"
}
}
http://localhost:5000/api
Most endpoints require JWT Bearer token:
Authorization: Bearer <your_jwt_token>
POST /api/suggestions
Get personalized suggestions based on intent.
{
"intent": "FOOD_ONLY",
"latitude": 41.0082,
"longitude": 28.9784,
"radiusMeters": 3000,
"walkingDistanceMeters": 1500,
"prompt": "romantic italian restaurant"
}
Response:
{
"intent": "FOOD_ONLY",
"isPersonalized": true,
"userId": "uuid",
"suggestions": [
{
"placeId": "uuid",
"name": "La Terrazza",
"latitude": 41.0085,
"longitude": 28.9795,
"category": "italian_restaurant",
"rating": "4.8",
"score": 0.92,
"reason": "Highly rated Italian restaurant with romantic atmosphere, matches your taste for quality dining.",
"photoUrl": "https://..."
}
],
"totalCount": 10,
"route": null,
"filters": {
"appliedCategories": ["restaurant", "cafe"],
"priceLevel": null,
"minRating": null
}
}
POST /api/routes
Create an optimized multi-stop route.
{
"name": "Istanbul Cultural Tour",
"description": "Museums and historic sites",
"placeIds": ["uuid1", "uuid2", "uuid3"],
"optimizeOrder": true,
"transportationMode": "walking"
}
GET /api/routes/{id}
Retrieve a specific route.
GET /api/routes/share/{token}
Access a shared route via token.
POST /api/dayplan
Generate a full-day itinerary.
{
"latitude": 41.0082,
"longitude": 28.9784,
"startTime": "09:00",
"endTime": "22:00",
"preferences": ["culture", "food", "nature"]
}
GET /api/tasteprofile
Get current user's taste profile.
PUT /api/tasteprofile
Update taste profile weights.
{
"cultureWeight": 0.8,
"foodWeight": 0.9,
"natureWeight": 0.6,
"nightlifeWeight": 0.3,
"noveltyTolerance": 0.7
}
POST /api/tasteprofile/quiz/submit
Submit quiz answers to create initial profile.
GET /api/subscriptions/me
Get current user's subscription status.
POST /api/subscriptions/verify
Verify Apple/Google IAP receipt.
{
"receiptData": "base64_receipt_data",
"provider": "AppleAppStore"
}
POST /api/auth/register
Register a new user.
{
"email": "user@example.com",
"username": "johndoe",
"password": "SecurePassword123!",
"firstName": "John",
"lastName": "Doe"
}
POST /api/auth/login
Login and receive JWT token.
{
"email": "user@example.com",
"password": "SecurePassword123!"
}
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiration": "2026-02-17T12:00:00Z",
"user": {
"id": "uuid",
"email": "user@example.com",
"username": "johndoe"
}
}
Visit /swagger when the API is running for interactive documentation.
Build the image:
docker build -f src/WhatShouldIDo.API/Dockerfile -t whatshouldido-api:latest .
Run the container:
docker run -d \
-p 8080:8080 \
-e DATABASE_HOST=postgres \
-e REDIS_CONFIGURATION=redis:6379 \
-e GOOGLE_PLACES_API_KEY=your_key \
--name whatshouldido-api \
whatshouldido-api:latest
Prerequisites:
Deploy:
# Create namespace
kubectl create namespace whatshouldido
# Apply manifests
kubectl apply -f k8s/
# Verify deployment
kubectl get pods -n whatshouldido
# Port forward for local access
kubectl port-forward -n whatshouldido svc/whatshouldido-api 8080:80
Scale deployment:
kubectl scale deployment whatshouldido-api --replicas=5 -n whatshouldido
Initialize Terraform:
cd infra/terraform
terraform init
Plan infrastructure:
terraform plan -var-file="production.tfvars"
Apply:
terraform apply -var-file="production.tfvars"
| Endpoint | Purpose | K8s Probe |
|---|---|---|
/health | Legacy health check | - |
/health/live | Liveness (is app running?) | Liveness |
/health/ready | Readiness (dependencies OK?) | Readiness |
/health/startup | Startup (initialized?) | Startup |
cd src/WhatShouldIDo.Tests
dotnet test --filter Category=Unit
# Start dependencies
docker-compose up -d postgres redis
# Run tests
dotnet test --filter Category=Integration
dotnet test --collect:"XPlat Code Coverage"
# Install k6: https://k6.io/docs/getting-started/installation/
# Run basic load test
k6 run k6-tests/load-test-basic.js
# Run stress test
k6 run k6-tests/load-test-stress.js
Example k6 test:
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 100 },
{ duration: '20s', target: 0 },
],
};
export default function () {
let res = http.post('http://localhost:5000/api/suggestions', JSON.stringify({
intent: "QUICK_SUGGESTION",
latitude: 41.0082,
longitude: 28.9784,
radiusMeters: 3000
}), {
headers: { 'Content-Type': 'application/json' },
});
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
Endpoint: http://localhost:5000/metrics
Key Metrics:
http_request_duration_seconds - Request latency histogramhttp_requests_total - Total request counterhttp_requests_in_progress - Active requests gaugequota_consumption_total - Quota usage countercache_hit_ratio - Cache effectivenessAccess: http://localhost:3000 (admin/admin)
Pre-configured Dashboards:
Traces are exported via OTLP to Tempo and visualized in Grafana.
Example trace:
POST /api/suggestions [500ms]
ββ ContextEngine.GetContextualInsights [50ms]
β ββ OpenWeatherService.GetWeather [45ms]
ββ GooglePlacesProvider.SearchPlaces [200ms]
ββ HybridScorer.ScorePlaces [150ms]
β ββ TasteProfileRepository.GetProfile [20ms]
β ββ UserHistoryRepository.GetRecentVisits [30ms]
ββ RouteOptimizationService.Optimize [100ms]
ββ GoogleDirectionsService.GetDistanceMatrix [95ms]
Structured logs are aggregated in Loki and queryable in Grafana.
Log Levels:
Trace: Very detailed diagnostic informationDebug: Internal system eventsInformation: General informational messagesWarning: Abnormal but expected conditionsError: Error events that don't stop executionCritical: Critical failures requiring immediate attentionQuery Example (LogQL):
{service="whatshouldido-api"}
|= "error"
| json
| line_format "{{.level}} {{.message}}"
WhatShouldIDo/
βββ src/
β βββ WhatShouldIDo.Domain/ # Business entities & logic
β β βββ Entities/ # 16 aggregate roots
β β βββ Enums/ # Domain enums
β β βββ ValueObjects/ # Value objects (Coordinates)
β β βββ Exception/ # Domain exceptions
β β
β βββ WhatShouldIDo.Application/ # Use cases & interfaces
β β βββ DTOs/ # Data transfer objects
β β βββ Interfaces/ # 41+ service contracts
β β βββ Configuration/ # Options classes
β β βββ Models/ # Application models
β β βββ UseCases/
β β β βββ Commands/ # 8 CQRS commands
β β β βββ Queries/ # 7 CQRS queries
β β β βββ Handlers/ # MediatR handlers
β β βββ Services/ # Service interfaces
β β
β βββ WhatShouldIDo.Infrastructure/ # External integrations
β β βββ Services/ # 29 service implementations
β β β βββ AI/ # OpenAI, HuggingFace, Ollama
β β β βββ Subscription/ # IAP verification
β β βββ Caching/ # Redis + In-Memory
β β βββ Quota/ # Redis + In-Memory quota stores
β β βββ Repositories/ # Data access layer
β β βββ Data/ # EF Core DbContext
β β βββ Migrations/ # Database migrations
β β βββ Observability/ # Metrics, tracing
β β βββ Health/ # Health checks
β β βββ BackgroundJobs/ # Scheduled tasks
β β βββ Options/ # Configuration options
β β
β βββ WhatShouldIDo.API/ # Web API layer
β β βββ Controllers/ # 21 REST controllers
β β βββ Middleware/ # 6 middleware components
β β βββ Attributes/ # Custom attributes
β β βββ DTOs/ # API-specific DTOs
β β βββ Validators/ # FluentValidation
β β βββ Resources/ # Localization .resx files
β β βββ Program.cs # DI + middleware setup
β β βββ appsettings.json # Configuration
β β βββ Dockerfile # Multi-stage build
β β
β βββ WhatShouldIDo.Tests/ # Test project
β βββ Unit/ # Unit tests
β βββ Integration/ # Integration tests
β βββ E2E/ # End-to-end tests
β
βββ docker-compose.yml # Local development stack
βββ docker-compose.observability.yml # Monitoring stack
βββ deploy/ # Deployment configs
β βββ prometheus/
β βββ grafana/
βββ k6-tests/ # Load tests
βββ infra/terraform/ # Infrastructure as Code
βββ k8s/ # Kubernetes manifests
βββ README.md # This file
We welcome contributions! Please see our Contributing Guidelines for details.
git checkout -b feature/amazing-feature
dotnet test
git commit -m "Add amazing feature"
git push origin feature/amazing-feature
This project is licensed under the MIT License - see the LICENSE file for details.
Built with β€οΈ using .NET 9
β Star us on GitHub | π Report Bug | π‘ Request Feature
C#
99.8%
A sophisticated location-based activity recommendation system built with .NET 9, featuring AI-powered personalization, multi-provider orchestration, and production-ready observability.
Urban discovery systems usually provide generic results. WhatShouldIDo introduces a personalization-first architecture that:
WhatShouldIDo is an enterprise-grade API that answers the timeless question: "What should I do?"
The system intelligently recommends personalized places and activities by combining:
Users struggle to discover relevant activities that match their preferences, current context (weather, time, location), and intentions (quick bite, full-day plan, trying something new). This system solves that by:
QUICK_SUGGESTION β 3 rapid results
FOOD_ONLY β Restaurant/cafΓ© filtering (10 results)
ACTIVITY_ONLY β Entertainment/cultural venues (10 results)
ROUTE_PLANNING β Multi-stop optimized routes
TRY_SOMETHING_NEW β Novel recommendations
graph TB
subgraph "API Layer"
A[Controllers<br/>21 REST endpoints]
B[Middleware<br/>6 types]
C[DTOs<br/>Request/Response]
D[Validators<br/>FluentValidation]
end
subgraph "Application Layer"
E[Commands<br/>8 MediatR]
F[Queries<br/>7 MediatR]
G[Handlers<br/>CQRS]
H[Interfaces<br/>41+ contracts]
end
subgraph "Infrastructure Layer"
I[Services<br/>29 implementations]
J[Repositories<br/>Generic + Specialized]
K[External APIs<br/>Google, OpenAI, etc.]
L[Caching<br/>Redis + Fallback]
end
subgraph "Domain Layer"
M[Entities<br/>16 aggregates]
N[Value Objects<br/>Coordinates]
O[Domain Logic<br/>Invariants]
end
A --> E
A --> F
E --> G
F --> G
G --> H
H --> I
I --> J
I --> K
I --> L
G --> M
M --> O
style A fill:#e1f5ff
style E fill:#fff4e1
style I fill:#e8f5e9
style M fill:#fce4ec
| Layer | Responsibility | Examples |
|---|---|---|
| API | HTTP concerns, validation, auth | SuggestionsController, RoutesController |
| Application | Use cases, orchestration | CreateSuggestionsCommand, SearchPlacesQuery |
| Infrastructure | External integrations, data access | GooglePlacesProvider, RouteRepository |
| Domain | Business logic, invariants | UserSubscription, Route, UserTasteProfile |
graph LR
subgraph "Client Applications"
A[Mobile Apps<br/>iOS/Android]
B[Web App]
end
subgraph "API Gateway"
C[ASP.NET Core<br/>API]
end
subgraph "Data Stores"
D[(PostgreSQL<br/>Primary DB)]
E[(Redis Cluster<br/>Cache + Quota)]
end
subgraph "External Services"
F[Google Places<br/>API]
G[OpenTripMap<br/>API]
H[OpenAI<br/>GPT-4o-mini]
I[Google Directions<br/>API]
J[OpenWeather<br/>API]
end
subgraph "Observability"
K[Prometheus<br/>Metrics]
L[Grafana<br/>Dashboards]
M[Tempo<br/>Tracing]
N[Loki<br/>Logs]
end
A --> C
B --> C
C --> D
C --> E
C --> F
C --> G
C --> H
C --> I
C --> J
C --> K
K --> L
C --> M
C --> N
style C fill:#4CAF50,color:#fff
style D fill:#336791,color:#fff
style E fill:#DC382D,color:#fff
style H fill:#10A37F,color:#fff
sequenceDiagram
participant Client
participant API
participant Middleware
participant Handler
participant Context
participant Places
participant Personalization
participant RouteOpt
participant Cache
participant DB
Client->>API: POST /api/suggestions<br/>{intent, location}
API->>Middleware: EntitlementAndQuota
Middleware->>Cache: Check quota (Redis)
Cache-->>Middleware: Quota OK
Middleware->>Middleware: Consume quota (atomic)
API->>Handler: CreateSuggestionsCommand
Handler->>Context: Get contextual insights
Context-->>Handler: Weather, time, season
Handler->>Places: Search places (multi-provider)
Places->>Places: Google Places + OpenTripMap
Places->>Places: Deduplication (70m radius)
Places-->>Handler: Raw places (40-60 results)
Handler->>Handler: Apply intent filter<br/>(FOOD_ONLY β restaurants only)
alt User Authenticated
Handler->>DB: Load taste profile
DB-->>Handler: UserTasteProfile
Handler->>Personalization: HybridScorer
Personalization->>Personalization: Score places<br/>(5 dimensions)
Personalization-->>Handler: Scored suggestions
end
alt Intent == ROUTE_PLANNING
Handler->>RouteOpt: Optimize route (TSP)
RouteOpt-->>Handler: Optimized order + distance
end
Handler-->>API: SuggestionsResult
API-->>Client: JSON Response
erDiagram
User ||--o| UserProfile : has
User ||--o| UserTasteProfile : has
User ||--o| UserSubscription : has
User ||--o{ UserVisit : tracks
User ||--o{ UserFavorite : bookmarks
User ||--o{ UserExclusion : dislikes
User ||--o{ Route : creates
User ||--o{ UserSuggestionHistory : receives
Place ||--o{ UserFavorite : favorited_by
Place ||--o{ UserVisit : visited_by
Place ||--o{ RoutePoint : included_in
Route ||--|{ RoutePoint : contains
Route ||--o{ RouteRevision : versioned_by
Route ||--o| RouteShareToken : shared_via
User {
uuid id PK
string email UK
string username
string password_hash
subscription_tier tier
datetime subscription_expiry
int daily_api_usage
datetime last_api_reset
datetime created_at
datetime updated_at
}
UserTasteProfile {
uuid id PK
uuid user_id FK
double culture_weight
double food_weight
double nature_weight
double nightlife_weight
double shopping_weight
double art_weight
double wellness_weight
double sports_weight
double quality_weight
double atmosphere_weight
double design_weight
double calmness_weight
double spaciousness_weight
double novelty_tolerance
bytes row_version
datetime created_at_utc
datetime updated_at_utc
}
UserSubscription {
uuid id PK
uuid user_id FK
provider provider
plan plan
status status
datetime trial_ends_at_utc
datetime current_period_ends_at_utc
string external_subscription_id
datetime last_verified_at_utc
string notes
bytes row_version
}
Route {
uuid id PK
uuid user_id FK
string name
string description
bool is_public
double total_distance
int estimated_duration
datetime created_at
datetime updated_at
}
RoutePoint {
uuid id PK
uuid route_id FK
uuid place_id FK
int order_index
datetime estimated_arrival
int duration_minutes
}
Place {
uuid id PK
string name
float latitude
float longitude
string address
string rating
int review_count
string category
string google_place_id UK
string source
bool is_sponsored
datetime sponsored_until
datetime cached_at
}
stateDiagram-v2
[*] --> None: New User
None --> Trialing: Start Trial (IAP)
None --> Active: Direct Purchase (IAP)
None --> Active: Manual Grant (Admin)
Trialing --> Active: Trial β Paid
Trialing --> Expired: Trial Ends (No Purchase)
Active --> Canceled: User Cancels
Active --> PastDue: Payment Fails
Canceled --> Expired: Period Ends
PastDue --> Active: Payment Retry Success
PastDue --> Expired: Grace Period Ends
Expired --> None: Reset to Free
Expired --> Trialing: New Trial (Re-subscribe)
Active --> Active: Renewal Success
graph TB
A[HTTP Request] --> B[GlobalExceptionMiddleware]
B --> C[CorrelationIdMiddleware<br/>W3C Trace Context]
C --> D[MetricsMiddleware<br/>OpenTelemetry]
D --> E[AdvancedRateLimitMiddleware<br/>100 req/60s per user]
E --> F[CORS Middleware]
F --> G[Localization Middleware<br/>10 languages]
G --> H[Authentication<br/>JWT Bearer]
H --> I[Authorization<br/>Claims-based]
I --> J[EntitlementAndQuotaMiddleware<br/>Redis-backed]
J --> K[Controllers]
K --> L[HTTP Response]
style B fill:#f44336,color:#fff
style C fill:#2196F3,color:#fff
style D fill:#FF9800,color:#fff
style E fill:#9C27B0,color:#fff
style J fill:#4CAF50,color:#fff
graph LR
A[Place] --> B[Implicit Score<br/>25%]
A --> C[Explicit Score<br/>30%]
A --> D[Novelty Score<br/>20%]
A --> E[Context Score<br/>15%]
A --> F[Quality Score<br/>10%]
B --> G[Visit History<br/>Frequency + Recency]
C --> H[Taste Profile<br/>13 weights]
D --> I[Recent Suggestions<br/>1-week exclusion]
E --> J[Weather + Time<br/>+ Season]
F --> K[Rating + Reviews]
G --> L[Weighted Sum]
H --> L
I --> L
J --> L
K --> L
L --> M[Final Score<br/>0-1 range]
style L fill:#4CAF50,color:#fff
style M fill:#2196F3,color:#fff
graph TB
subgraph "Ingress Layer"
A[Ingress Controller<br/>NGINX/Traefik]
end
subgraph "Application Layer"
B[API Pod 1]
C[API Pod 2]
D[API Pod 3]
E[Horizontal Pod<br/>Autoscaler]
end
subgraph "Data Layer"
F[(PostgreSQL<br/>StatefulSet)]
G[(Redis Cluster<br/>6 nodes)]
end
subgraph "Observability Layer"
H[Prometheus]
I[Grafana]
J[Tempo]
K[Loki]
end
subgraph "Configuration"
L[ConfigMap]
M[Secrets]
end
A --> B
A --> C
A --> D
E --> B
E --> C
E --> D
B --> F
C --> F
D --> F
B --> G
C --> G
D --> G
B --> L
B --> M
B --> H
H --> I
B --> J
B --> K
style A fill:#326CE5,color:#fff
style E fill:#FF6F00,color:#fff
style F fill:#336791,color:#fff
style G fill:#DC382D,color:#fff
| Service | Purpose |
|---|---|
| Google Places API | Primary place search (40 places/request) |
| OpenTripMap API | Tourism POIs and cultural landmarks |
| Google Directions API | Route optimization and ETA calculation |
| Google Geocoding API | Location name β coordinates |
| OpenWeather API | Weather context for recommendations |
| OpenAI API | GPT-4o-mini for NLP and semantic ranking |
| HuggingFace API | Fallback AI provider |
| Ollama | Local AI models (self-hosted) |
Clone the repository
git clone https://github.com/yourusername/WhatShouldIDo.git
cd WhatShouldIDo/NeYapsamWeb/API
Configure environment variables
cp .env.example .env
# Edit .env with your API keys
Start infrastructure
docker-compose up -d postgres redis
Run database migrations
dotnet ef database update --project src/WhatShouldIDo.Infrastructure --startup-project src/WhatShouldIDo.API
Start the API
cd src/WhatShouldIDo.API
dotnet run
Access the API
# Start all services (API + DB + Redis + Observability)
docker-compose -f docker-compose.yml -f docker-compose.observability.yml up -d
# Access dashboards
# Grafana: http://localhost:3000 (admin/admin)
# Prometheus: http://localhost:9090
# Seq: http://localhost:5341
Install dependencies
dotnet restore
Update connection strings in appsettings.Development.json:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=Wisido;Username=postgres;Password=yourpassword"
},
"Redis": {
"Configuration": "localhost:6379"
}
}
Run migrations
dotnet ef database update --project src/WhatShouldIDo.Infrastructure --startup-project src/WhatShouldIDo.API
Run the API
cd src/WhatShouldIDo.API
dotnet watch run
Create a .env file or set environment variables:
# Database
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_NAME=Wisido
DATABASE_USER=postgres
DATABASE_PASSWORD=your_secure_password
# Redis
REDIS_CONFIGURATION=redis:6379
# External APIs
GOOGLE_PLACES_API_KEY=your_google_api_key
OPENTRIPMAP_API_KEY=your_opentripmap_key
OPENAI_API_KEY=your_openai_api_key
OPENWEATHER_API_KEY=your_openweather_key
# JWT Authentication
JWT_SECRET=your_256_bit_secret_key_here
JWT_ISSUER=WhatShouldIDo
JWT_AUDIENCE=WhatShouldIDoClients
JWT_EXPIRATION_MINUTES=60
# Observability
OTLP_ENDPOINT=http://tempo:4317
PROMETHEUS_ENABLED=true
# Feature Flags
QUOTA_ENABLED=true
QUOTA_DEFAULT_FREE_LIMIT=5
SUBSCRIPTION_VERIFICATION_ENABLED=false
{
"ConnectionStrings": {
"DefaultConnection": "Host=postgres;Database=Wisido;Username=postgres;Password=postgres"
},
"Redis": {
"Configuration": "redis:6379",
"UseCluster": true
},
"Feature": {
"Quota": {
"DefaultFreeQuota": 5,
"DailyResetEnabled": false,
"StorageBackend": "Redis"
},
"Subscription": {
"VerificationEnabled": false,
"AllowDevTestReceipts": false
},
"TasteQuiz": {
"Version": "v1",
"DraftTtlHours": 24
}
},
"Scoring": {
"ImplicitWeight": 0.25,
"ExplicitWeight": 0.30,
"NoveltyWeight": 0.20,
"ContextWeight": 0.15,
"QualityWeight": 0.10
},
"Observability": {
"Enabled": true,
"ServiceName": "whatshouldido-api",
"ServiceVersion": "2.0.0",
"TraceSamplingRatio": 0.05,
"PrometheusEnabled": true,
"OtlpTracesEnabled": true,
"OtlpTracesEndpoint": "http://tempo:4317"
}
}
http://localhost:5000/api
Most endpoints require JWT Bearer token:
Authorization: Bearer <your_jwt_token>
POST /api/suggestions
Get personalized suggestions based on intent.
{
"intent": "FOOD_ONLY",
"latitude": 41.0082,
"longitude": 28.9784,
"radiusMeters": 3000,
"walkingDistanceMeters": 1500,
"prompt": "romantic italian restaurant"
}
Response:
{
"intent": "FOOD_ONLY",
"isPersonalized": true,
"userId": "uuid",
"suggestions": [
{
"placeId": "uuid",
"name": "La Terrazza",
"latitude": 41.0085,
"longitude": 28.9795,
"category": "italian_restaurant",
"rating": "4.8",
"score": 0.92,
"reason": "Highly rated Italian restaurant with romantic atmosphere, matches your taste for quality dining.",
"photoUrl": "https://..."
}
],
"totalCount": 10,
"route": null,
"filters": {
"appliedCategories": ["restaurant", "cafe"],
"priceLevel": null,
"minRating": null
}
}
POST /api/routes
Create an optimized multi-stop route.
{
"name": "Istanbul Cultural Tour",
"description": "Museums and historic sites",
"placeIds": ["uuid1", "uuid2", "uuid3"],
"optimizeOrder": true,
"transportationMode": "walking"
}
GET /api/routes/{id}
Retrieve a specific route.
GET /api/routes/share/{token}
Access a shared route via token.
POST /api/dayplan
Generate a full-day itinerary.
{
"latitude": 41.0082,
"longitude": 28.9784,
"startTime": "09:00",
"endTime": "22:00",
"preferences": ["culture", "food", "nature"]
}
GET /api/tasteprofile
Get current user's taste profile.
PUT /api/tasteprofile
Update taste profile weights.
{
"cultureWeight": 0.8,
"foodWeight": 0.9,
"natureWeight": 0.6,
"nightlifeWeight": 0.3,
"noveltyTolerance": 0.7
}
POST /api/tasteprofile/quiz/submit
Submit quiz answers to create initial profile.
GET /api/subscriptions/me
Get current user's subscription status.
POST /api/subscriptions/verify
Verify Apple/Google IAP receipt.
{
"receiptData": "base64_receipt_data",
"provider": "AppleAppStore"
}
POST /api/auth/register
Register a new user.
{
"email": "user@example.com",
"username": "johndoe",
"password": "SecurePassword123!",
"firstName": "John",
"lastName": "Doe"
}
POST /api/auth/login
Login and receive JWT token.
{
"email": "user@example.com",
"password": "SecurePassword123!"
}
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiration": "2026-02-17T12:00:00Z",
"user": {
"id": "uuid",
"email": "user@example.com",
"username": "johndoe"
}
}
Visit /swagger when the API is running for interactive documentation.
Build the image:
docker build -f src/WhatShouldIDo.API/Dockerfile -t whatshouldido-api:latest .
Run the container:
docker run -d \
-p 8080:8080 \
-e DATABASE_HOST=postgres \
-e REDIS_CONFIGURATION=redis:6379 \
-e GOOGLE_PLACES_API_KEY=your_key \
--name whatshouldido-api \
whatshouldido-api:latest
Prerequisites:
Deploy:
# Create namespace
kubectl create namespace whatshouldido
# Apply manifests
kubectl apply -f k8s/
# Verify deployment
kubectl get pods -n whatshouldido
# Port forward for local access
kubectl port-forward -n whatshouldido svc/whatshouldido-api 8080:80
Scale deployment:
kubectl scale deployment whatshouldido-api --replicas=5 -n whatshouldido
Initialize Terraform:
cd infra/terraform
terraform init
Plan infrastructure:
terraform plan -var-file="production.tfvars"
Apply:
terraform apply -var-file="production.tfvars"
| Endpoint | Purpose | K8s Probe |
|---|---|---|
/health | Legacy health check | - |
/health/live | Liveness (is app running?) | Liveness |
/health/ready | Readiness (dependencies OK?) | Readiness |
/health/startup | Startup (initialized?) | Startup |
cd src/WhatShouldIDo.Tests
dotnet test --filter Category=Unit
# Start dependencies
docker-compose up -d postgres redis
# Run tests
dotnet test --filter Category=Integration
dotnet test --collect:"XPlat Code Coverage"
# Install k6: https://k6.io/docs/getting-started/installation/
# Run basic load test
k6 run k6-tests/load-test-basic.js
# Run stress test
k6 run k6-tests/load-test-stress.js
Example k6 test:
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 100 },
{ duration: '20s', target: 0 },
],
};
export default function () {
let res = http.post('http://localhost:5000/api/suggestions', JSON.stringify({
intent: "QUICK_SUGGESTION",
latitude: 41.0082,
longitude: 28.9784,
radiusMeters: 3000
}), {
headers: { 'Content-Type': 'application/json' },
});
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
Endpoint: http://localhost:5000/metrics
Key Metrics:
http_request_duration_seconds - Request latency histogramhttp_requests_total - Total request counterhttp_requests_in_progress - Active requests gaugequota_consumption_total - Quota usage countercache_hit_ratio - Cache effectivenessAccess: http://localhost:3000 (admin/admin)
Pre-configured Dashboards:
Traces are exported via OTLP to Tempo and visualized in Grafana.
Example trace:
POST /api/suggestions [500ms]
ββ ContextEngine.GetContextualInsights [50ms]
β ββ OpenWeatherService.GetWeather [45ms]
ββ GooglePlacesProvider.SearchPlaces [200ms]
ββ HybridScorer.ScorePlaces [150ms]
β ββ TasteProfileRepository.GetProfile [20ms]
β ββ UserHistoryRepository.GetRecentVisits [30ms]
ββ RouteOptimizationService.Optimize [100ms]
ββ GoogleDirectionsService.GetDistanceMatrix [95ms]
Structured logs are aggregated in Loki and queryable in Grafana.
Log Levels:
Trace: Very detailed diagnostic informationDebug: Internal system eventsInformation: General informational messagesWarning: Abnormal but expected conditionsError: Error events that don't stop executionCritical: Critical failures requiring immediate attentionQuery Example (LogQL):
{service="whatshouldido-api"}
|= "error"
| json
| line_format "{{.level}} {{.message}}"
WhatShouldIDo/
βββ src/
β βββ WhatShouldIDo.Domain/ # Business entities & logic
β β βββ Entities/ # 16 aggregate roots
β β βββ Enums/ # Domain enums
β β βββ ValueObjects/ # Value objects (Coordinates)
β β βββ Exception/ # Domain exceptions
β β
β βββ WhatShouldIDo.Application/ # Use cases & interfaces
β β βββ DTOs/ # Data transfer objects
β β βββ Interfaces/ # 41+ service contracts
β β βββ Configuration/ # Options classes
β β βββ Models/ # Application models
β β βββ UseCases/
β β β βββ Commands/ # 8 CQRS commands
β β β βββ Queries/ # 7 CQRS queries
β β β βββ Handlers/ # MediatR handlers
β β βββ Services/ # Service interfaces
β β
β βββ WhatShouldIDo.Infrastructure/ # External integrations
β β βββ Services/ # 29 service implementations
β β β βββ AI/ # OpenAI, HuggingFace, Ollama
β β β βββ Subscription/ # IAP verification
β β βββ Caching/ # Redis + In-Memory
β β βββ Quota/ # Redis + In-Memory quota stores
β β βββ Repositories/ # Data access layer
β β βββ Data/ # EF Core DbContext
β β βββ Migrations/ # Database migrations
β β βββ Observability/ # Metrics, tracing
β β βββ Health/ # Health checks
β β βββ BackgroundJobs/ # Scheduled tasks
β β βββ Options/ # Configuration options
β β
β βββ WhatShouldIDo.API/ # Web API layer
β β βββ Controllers/ # 21 REST controllers
β β βββ Middleware/ # 6 middleware components
β β βββ Attributes/ # Custom attributes
β β βββ DTOs/ # API-specific DTOs
β β βββ Validators/ # FluentValidation
β β βββ Resources/ # Localization .resx files
β β βββ Program.cs # DI + middleware setup
β β βββ appsettings.json # Configuration
β β βββ Dockerfile # Multi-stage build
β β
β βββ WhatShouldIDo.Tests/ # Test project
β βββ Unit/ # Unit tests
β βββ Integration/ # Integration tests
β βββ E2E/ # End-to-end tests
β
βββ docker-compose.yml # Local development stack
βββ docker-compose.observability.yml # Monitoring stack
βββ deploy/ # Deployment configs
β βββ prometheus/
β βββ grafana/
βββ k6-tests/ # Load tests
βββ infra/terraform/ # Infrastructure as Code
βββ k8s/ # Kubernetes manifests
βββ README.md # This file
We welcome contributions! Please see our Contributing Guidelines for details.
git checkout -b feature/amazing-feature
dotnet test
git commit -m "Add amazing feature"
git push origin feature/amazing-feature
This project is licensed under the MIT License - see the LICENSE file for details.
Built with β€οΈ using .NET 9
β Star us on GitHub | π Report Bug | π‘ Request Feature
C#
99.8%