A .NET 8 Web API for serving personalized video content to mobile SDK clients with TikTok-style vertical video feeds.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client SDK │────▶│ Feed │────▶│ Ranker │ (simulated)
│ │ │ Service │ │ Service │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌───────────┐
│ Cache │ (in-memory)
│ (Redis) │
└───────────┘
FeedService/
├── src/
│ ├── FeedService.Api/ # API layer (controllers, Program.cs)
│ ├── FeedService.Application/ # Business logic (MediatR handlers, DTOs)
│ └── FeedService.Infrastructure/ # Data access (mock repositories)
└── tests/
└── FeedService.Tests/ # Test project
- Personalized Feeds: Content recommendations based on user category affinities
- Cold Start Support: Default feed for new users without profiles
- Prepared Feeds: Cache optimization for pagination
- Cursor-based Pagination: Efficient feed scrolling
- Event Ingestion: Batch processing of user interaction events
- Multi-tenant: Tenant isolation via headers
- .NET 8 SDK
-
Clone the repository
-
Restore dependencies:
cd FeedService dotnet restore -
Build the solution:
dotnet build
cd src/FeedService.Api
dotnet runThe API will be available at:
- HTTP:
http://localhost:5000 - Swagger UI:
http://localhost:5000/swagger
Get personalized video feed for a user.
Headers:
X-Tenant-Id(required): Tenant identifierX-User-Id(optional): Hashed user ID for personalizationIf-None-Match(optional): ETag for cache validation
Query Parameters:
limit(optional, default=20): Number of items to return (1-100)cursor(optional): Pagination cursor from previous response
Response:
{
"items": [
{
"id": "vid_001",
"title": "Amazing Football Goal #1",
"videoUrl": "https://cdn.example.com/v/vid_001.mp4",
"thumbnailUrl": "https://cdn.example.com/t/vid_001.jpg",
"durationSeconds": 45,
"categories": ["football"]
}
],
"cursor": "eyJvZmZzZXQiOjIwfQ==",
"hasMore": true,
"metadata": {
"isPersonalized": true,
"feedType": "personalized",
"source": "pool_based"
}
}Response Headers:
ETag: Cache validation tokenCache-Control: Caching directivesX-Feed-Type: Feed type (personalized/cold_start/pool_based)
Ingest batch of user interaction events.
Headers:
X-Tenant-Id(required): Tenant identifierX-User-Id(required): Hashed user ID
Request Body:
{
"events": [
{
"type": "video_watch",
"videoId": "vid_001",
"watchDurationSeconds": 38,
"watchPercentage": 0.84,
"timestamp": "2024-12-03T14:22:00Z"
},
{
"type": "video_like",
"videoId": "vid_001",
"timestamp": "2024-12-03T14:22:30Z"
}
]
}Response (202 Accepted):
{
"accepted": true,
"processedCount": 2
}The service includes three predefined test users:
1. Sports Fan (user_sports_fan)
curl http://localhost:5000/api/v1/feed?limit=20 \
-H "X-Tenant-Id: tenant-alpha" \
-H "X-User-Id: user_sports_fan"Expected: Feed with mainly football and sports content
2. Comedy Lover (user_comedy_lover)
curl http://localhost:5000/api/v1/feed?limit=20 \
-H "X-Tenant-Id: tenant-alpha" \
-H "X-User-Id: user_comedy_lover"Expected: Feed with comedy and entertainment content
3. Balanced User (user_balanced)
curl http://localhost:5000/api/v1/feed?limit=20 \
-H "X-Tenant-Id: tenant-alpha" \
-H "X-User-Id: user_balanced"Expected: Evenly distributed content across tech, news, cooking, and music
4. Cold Start (Anonymous)
curl http://localhost:5000/api/v1/feed?limit=20 \
-H "X-Tenant-Id: tenant-alpha"Expected: Default feed with football, sports, and cats content
# First request
curl http://localhost:5000/api/v1/feed?limit=20 \
-H "X-Tenant-Id: tenant-alpha" \
-H "X-User-Id: user_sports_fan"
# Copy cursor from response and use for next page
curl "http://localhost:5000/api/v1/feed?limit=20&cursor=eyJvZmZzZXQiOjIwfQ==" \
-H "X-Tenant-Id: tenant-alpha" \
-H "X-User-Id: user_sports_fan"curl -X POST http://localhost:5000/api/v1/events \
-H "Content-Type: application/json" \
-H "X-Tenant-Id: tenant-alpha" \
-H "X-User-Id: user_sports_fan" \
-d '{
"events": [
{
"type": "video_watch",
"videoId": "vid_football_001",
"watchPercentage": 0.84,
"watchDurationSeconds": 38,
"timestamp": "2024-12-08T14:22:00Z"
},
{
"type": "video_like",
"videoId": "vid_football_001",
"timestamp": "2024-12-08T14:22:30Z"
}
]
}'-
First Request (offset=0):
- Get user profile via MediatR query (checks cache → falls back to repository)
- Calculate category allocation based on affinities
- Pull videos from category pools
- Exclude already-seen videos (from cache)
- Shuffle and return first batch
- Fire async event to prepare personalized feed
- Mark returned videos as seen
-
Pagination (offset>0):
- Check for prepared feed in cache
- If exists: return from prepared feed
- If not: fall back to pool-based approach
-
Event Processing:
- Validate batch (max 100 events)
- Publish to message queue (simulated)
- Return 202 Accepted immediately
User profiles are cached with a two-tier approach:
- Cache Layer: 15-minute TTL for fast access
- Repository Layer: Mock data store simulating database
The GetUserProfileQuery handler checks cache first, then falls back to repository if needed.
Mock data includes 80 videos per category:
- football
- sports
- comedy
- entertainment
- gaming
- tech
- news
- cooking
- music
- fitness
- cats
Videos are sorted by trending score within each pool.
- .NET 8: Latest LTS version
- MediatR: CQRS pattern implementation
- FluentValidation: Request validation
- IMemoryCache: In-memory caching (simulates Redis)
- Swagger/OpenAPI: API documentation
- CQRS: Commands and Queries separated via MediatR
- Repository Pattern: Data access abstraction
- Dependency Injection: Constructor injection throughout
- Options Pattern: Configuration management
- Factory Pattern: Mock data generation
- Async/Await: All I/O operations are asynchronous
- Cursor Pagination: O(1) skip using prepared feeds
- Cache-First: User profiles and seen videos cached
- Fire-and-Forget: Event publishing doesn't block response
- Batch Processing: Events ingested in batches (max 100)
- Real Redis integration
- Database persistence (PostgreSQL/MongoDB)
- Message queue (RabbitMQ/Kafka)
- Real-time ranking service integration
- A/B testing framework
- Analytics and monitoring
- Rate limiting
- Authentication/Authorization
MIT