Build a Custom URL Shortener with .NET 8 and ASP.NET Core
November 15, 2024 • C# • .NET • ASP.NET Core • Software Development
Loading...
URL shorteners are everywhere—from social media to email campaigns. But building your own gives you control over analytics, branding, and custom features. In this guide, we'll build a production-ready URL shortener using .NET 8 and ASP.NET Core with a hash-based approach that generates compact, collision-resistant short codes.
Why Build Your Own?
- Custom analytics: Track clicks, referrers, and device types exactly how you need.
- Branding control: Use your own domain instead of generic short URLs.
- Privacy: Keep your data in-house without third-party tracking.
- Cost efficiency: Handle millions of redirects without per-click fees.
Prerequisites
- .NET 8 SDK installed
- Visual Studio 2022, VS Code, or Rider
- SQL Server, PostgreSQL, or SQLite for storage
- Basic understanding of ASP.NET Core and dependency injection
Project Setup
Create a new ASP.NET Core Web API project:
dotnet new webapi -n UrlShortener
cd UrlShortenerCore Implementation: Hash-Based Short Code Generation
Instead of using Base64 encoding, we'll use a hash-based approach that combines URL content with a timestamp to create unique, short codes. This method ensures better distribution and avoids collisions.
Step 1: Create the ShortLink Service
Add a new file Services/ShortLinkService.cs:
using System.Security.Cryptography;
using System.Text;
namespace UrlShortener.Services;
public interface IShortLinkService
{
string GenerateShortCode(string originalUrl);
bool IsValidShortCode(string code);
}
public class ShortLinkService : IShortLinkService
{
private const string Base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private const int ShortCodeLength = 7;
public string GenerateShortCode(string originalUrl)
{
// Combine URL with current timestamp for uniqueness
var input = $"{originalUrl}{DateTime.UtcNow.Ticks}";
// Compute SHA-256 hash
using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
// Convert first 4 bytes to a number
var number = BitConverter.ToUInt32(hashBytes, 0);
// Convert to Base62 string (URL-safe)
return ConvertToBase62(number);
}
private string ConvertToBase62(ulong number)
{
var result = new StringBuilder(ShortCodeLength);
for (int i = 0; i < ShortCodeLength; i++)
{
result.Append(Base62Chars[(int)(number % 62)]);
number /= 62;
}
return result.ToString();
}
public bool IsValidShortCode(string code)
{
return !string.IsNullOrWhiteSpace(code) &&
code.Length == ShortCodeLength &&
code.All(c => Base62Chars.Contains(c));
}
}Step 2: Create the Data Model
Add Models/ShortUrl.cs:
namespace UrlShortener.Models;
public class ShortUrl
{
public int Id { get; set; }
public string OriginalUrl { get; set; } = string.Empty;
public string ShortCode { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public int ClickCount { get; set; }
public DateTime? ExpiresAt { get; set; }
}Step 3: Database Context
If using Entity Framework Core, add Data/UrlShortenerContext.cs:
using Microsoft.EntityFrameworkCore;
using UrlShortener.Models;
namespace UrlShortener.Data;
public class UrlShortenerContext : DbContext
{
public UrlShortenerContext(DbContextOptions<UrlShortenerContext> options)
: base(options)
{
}
public DbSet<ShortUrl> ShortUrls { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ShortUrl>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.ShortCode).IsUnique();
entity.Property(e => e.OriginalUrl).IsRequired().HasMaxLength(2048);
entity.Property(e => e.ShortCode).IsRequired().HasMaxLength(10);
});
}
}Step 4: Controller Implementation
Create Controllers/UrlController.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using UrlShortener.Data;
using UrlShortener.Models;
using UrlShortener.Services;
namespace UrlShortener.Controllers;
[ApiController]
[Route("api/[controller]")]
public class UrlController : ControllerBase
{
private readonly UrlShortenerContext _context;
private readonly IShortLinkService _shortLinkService;
public UrlController(UrlShortenerContext context, IShortLinkService shortLinkService)
{
_context = context;
_shortLinkService = shortLinkService;
}
[HttpPost("shorten")]
public async Task<IActionResult> ShortenUrl([FromBody] ShortenRequest request)
{
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out var uri))
{
return BadRequest("Invalid URL format");
}
var shortCode = _shortLinkService.GenerateShortCode(request.Url);
// Ensure uniqueness (retry if collision)
while (await _context.ShortUrls.AnyAsync(s => s.ShortCode == shortCode))
{
shortCode = _shortLinkService.GenerateShortCode($"{request.Url}{Guid.NewGuid()}");
}
var shortUrl = new ShortUrl
{
OriginalUrl = request.Url,
ShortCode = shortCode,
CreatedAt = DateTime.UtcNow,
ExpiresAt = request.ExpiresInDays.HasValue
? DateTime.UtcNow.AddDays(request.ExpiresInDays.Value)
: null
};
_context.ShortUrls.Add(shortUrl);
await _context.SaveChangesAsync();
var baseUrl = $"{Request.Scheme}://{Request.Host}";
return Ok(new { shortUrl = $"{baseUrl}/{shortCode}" });
}
[HttpGet("{code}")]
public async Task<IActionResult> Redirect(string code)
{
if (!_shortLinkService.IsValidShortCode(code))
{
return NotFound();
}
var shortUrl = await _context.ShortUrls
.FirstOrDefaultAsync(s => s.ShortCode == code);
if (shortUrl == null || (shortUrl.ExpiresAt.HasValue && shortUrl.ExpiresAt.Value < DateTime.UtcNow))
{
return NotFound();
}
shortUrl.ClickCount++;
await _context.SaveChangesAsync();
return Redirect(shortUrl.OriginalUrl);
}
}
public record ShortenRequest(string Url, int? ExpiresInDays = null);Step 5: Register Services
Update Program.cs:
using Microsoft.EntityFrameworkCore;
using UrlShortener.Data;
using UrlShortener.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register our services
builder.Services.AddScoped<IShortLinkService, ShortLinkService>();
// Database configuration (example for SQLite)
builder.Services.AddDbContext<UrlShortenerContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();Advanced Features
1. Rate Limiting
Prevent abuse by adding rate limiting middleware:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("shorten", opt =>
{
opt.Window = TimeSpan.FromMinutes(1);
opt.PermitLimit = 10;
});
});2. Caching
Cache frequently accessed URLs for better performance:
builder.Services.AddMemoryCache();
// In controller, inject IMemoryCache and check cache before DB lookup3. Analytics Endpoint
Add an endpoint to track click statistics:
[HttpGet("analytics/{code}")]
public async Task<IActionResult> GetAnalytics(string code)
{
var shortUrl = await _context.ShortUrls
.FirstOrDefaultAsync(s => s.ShortCode == code);
if (shortUrl == null) return NotFound();
return Ok(new
{
shortCode = shortUrl.ShortCode,
originalUrl = shortUrl.OriginalUrl,
clickCount = shortUrl.ClickCount,
createdAt = shortUrl.CreatedAt
});
}Testing the Service
Use curl or Postman to test:
# Shorten a URL
POST https://localhost:5001/api/url/shorten
Content-Type: application/json
{
"url": "https://example.com/very/long/url/here",
"expiresInDays": 30
}
# Response
{
"shortUrl": "https://localhost:5001/aB3dF9k"
}
# Redirect (use in browser)
GET https://localhost:5001/aB3dF9kPerformance Considerations
- Database indexing: Ensure ShortCode column is indexed for fast lookups.
- Connection pooling: Configure appropriate pool sizes for your database.
- CDN integration: Use a CDN for static redirects to reduce server load.
- Cleanup job: Periodically delete expired URLs to keep the database lean.
Production Deployment Tips
- Use environment variables for connection strings and secrets.
- Enable HTTPS redirects for security.
- Implement proper logging and monitoring.
- Add health checks for database connectivity.
- Consider using Redis for distributed caching in multi-server setups.
Conclusion
You now have a production-ready URL shortener that you can customize for your specific needs. The hash-based approach ensures uniqueness while keeping codes short and memorable. Extend it with analytics, custom domains, or QR code generation as needed.
For more development workflows, check out Vibe Coding and MCP Server Use Cases.
Loading...
Related Articles
Vibe Coding: Ship Faster with Focused Flow
Build software in a state of focused flow—guided by rapid feedback and clear intent—without abandoning engineering discipline.
MCP Server Use Cases
Exploring Model Context Protocol servers and their practical applications in AI development.
RAG Explained Simply: Real-time Data & Why It Matters
Understanding Retrieval-Augmented Generation and why real-time data integration is crucial for AI applications.