ShortTree is a compact URL shortener service built with ASP.NET Core, Entity Framework Core, and SQLite. It focuses on fast redirects and reliable click tracking, using background processing and caching to keep latency low while still capturing analytics.
ShortTree lets you register custom, human-friendly slugs for long URLs and then redirect users to the original destination. Redirects are optimized by caching link lookups in memory, while click logs are queued in an unbounded channel and persisted by a background worker to avoid slowing the redirect path.
Key goals:
- Low-latency redirects with minimal database hits.
- Simple API-first design (JSON over HTTP).
- Durable analytics via click logs stored in SQLite.
- Clean separation of concerns using DI and hosted services.
- Client requests
GET /r/{username}/{slug}. - The controller checks
IMemoryCachefor{username}:{slug}. - On cache miss, the link is loaded from SQLite and cached with sliding expiration.
- The redirect is returned immediately.
- A click log entry is queued to a
Channeland written asynchronously by a background service.
This pattern ensures user-perceived latency is low while analytics remain reliable.
- User: Owner of links. Unique username and email.
- Link: Custom slug for a real URL. Tracks total clicks.
- ClickLog: Individual click events (timestamp, IP, user agent, referrer).
ClickLogBackgroundServicereads from a channel and writes click logs to the database.- The link's click counter is updated in the same transaction.
IMemoryCachestores{username}:{slug} -> {linkId, longUrl}.- Sliding expiration keeps frequently used links hot and evicts inactive ones.
POST /api/links- Create a linkGET /api/links/{username}/{slug}- Get a single linkGET /api/links/{username}- List all links for a user
GET /r/{username}/{slug}- Redirect to the long URL and record a click
POST /api/user- Create a userGET /api/user/{username}- Get a userGET /api/user- List users
GET /api/stats/users- Aggregate stats per userGET /api/stats/users/{username}- Stats for a specific userGET /api/stats/clicks?username={u}&slug={s}&take={n}- Click logs with filters
GET /api/link-stats/{username}- Click totals per link for a userGET /api/link-stats/{username}/{slug}- Click stats for a specific link
POST /api/user
Content-Type: application/json
{
"username": "ana",
"email": "ana@example.com"
}POST /api/links
Content-Type: application/json
{
"username": "ana",
"title": "My blog",
"longUrl": "https://example.com/blog",
"slug": "blog",
"visibleInProfile": true,
"email": "ana@example.com"
}GET /r/ana/blog
GET /api/stats/users/ana
GET /api/link-stats/ana
SQLite uses a local file named shorttree.db by default:
"ConnectionStrings": {
"DefaultConnection": "Data Source=shorttree.db"
}
- Sliding expiration is currently set to 10 minutes in
RedirectController. - You can adjust this by changing the
CacheSlidingExpirationvalue.
- Restore and run:
dotnet restoredotnet run
- The database is created automatically on startup (
EnsureCreated). - Use Postman or curl to test the endpoints.
MIT