Published on

How to design a URL Shortener in dotnet ?

Authors

Target Audience

The focus of this article is to provide insights for those seeking to acquire knowledge on designing a .NET-based URL shortening service.

Learning Objectives

After completing this article, you will know how to do the following:

  • Design a URL shortener
  • Create an API service for a URL shortener

What is URL Shortening?

Wikipedia Definition

URL shortening is a technique on the World Wide Web in which a Uniform Resource Locator (URL) may be made substantially shorter while still directing users to the required page. This is achieved by using a redirect that links to the web page associated with a long URL.

What are the Benefits of Using a URL Shortener?

  1. Saves Characters: URL shorteners help save characters in tweets, texts, and other social media posts where character count is limited. This can make your message more concise and easier to share.
  2. Track Clicks: URL shorteners often provide analytics that allow you to track clicks on your links. This can be useful for measuring the success of your marketing campaigns and for determining which types of content are most engaging to your audience.
  3. Makes Links More Manageable: Long and complex URLs can be difficult to remember or type out. URL shorteners can make links more manageable and easier to share.
  4. Mask the Original URL: Some URL shorteners allow you to mask the original URL, which can be useful for hiding affiliate links or links to sites that you don't want to reveal.
  5. Customize Links: Many URL shorteners allow you to customize the shortened link to include a keyword or brand name. This can help increase brand awareness and make links more memorable.

Design of the Service

Here is the general structure of the URL shortener system.

design

This design can be modified according to requirements. For example, if we want to handle high volume, we can use a load balancer to manage that traffic. Additionally, if we want to provide high availability, we can use clustered databases.

Example Implementation in .NET

Here are the endpoints:

app.MapGet("{code}", (string code) => $"You are redirected to. Code: {code}");
app.MapGet("/get/{code}", (string code) => $"Here is the code: {code}");
app.MapGet("/get", () => $"Here is the list of URLs. List: {GetAll()}");
app.MapPost("/create", ([FromBody] CreateShortUrlRequest request) =>
{
    var url = request.Url;
    return $"Here is your URL: {url}";
});
app.MapDelete("/delete/{code}", (string code) => $"URL in {code} deleted successfully");

In this demo, we will use the create endpoint to generate a code that represents URLs.

Here is the complete implementation:

using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddCors();
builder.Services.AddSingleton(_ => new UrlShortenerService(new Database()));
var app = builder.Build();

app.UseCors(x =>
{
    x.AllowAnyHeader();
    x.AllowAnyOrigin();
    x.AllowAnyMethod();
});

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// Middleware for redirection

app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();
    if (currentEndpoint is { DisplayName: "HTTP: GET {code}" } and RouteEndpoint routeEndpoint && routeEndpoint.RoutePattern.RawText == "{code}" && routeEndpoint.RoutePattern.Parameters.Count == 1)
    {
        var urlShortenerService = context.RequestServices.GetService<UrlShortenerService>();
        var reqUrl = context.Request.Path.Value[1..];
        var url = urlShortenerService.Get(reqUrl);
        if (string.IsNullOrWhiteSpace(url))
            await next();
        context.Response.Redirect(url);
    }

    await next();
});

app.MapGet("{code}", (UrlShortenerService service, string code) => service.RedirectTo(code));
app.MapGet("/get/{code}", (UrlShortenerService service, string code) => service.Get(code));
app.MapGet("/get", (UrlShortenerService service) => $"Here is the list of URLs. List: {service.GetAll()}");
app.MapPost("/create", (UrlShortenerService service, [FromBody] CreateShortUrlRequest request) => service.Create(request.Url));
app.MapDelete("/delete/{code}", (UrlShortenerService service, string code) => service.Delete(code));

app.Run();

public class Database : Dictionary<string, string>
{
}

public class UrlShortenerService
{
    private readonly Database _database;

    public UrlShortenerService(Database database)
    {
        _database = database;
    }

    public string RedirectTo(string code)
    {
        if (!_database.ContainsKey(code))
        {
            return "Not Found";
        }
        var url = _database[code];
        return $"You are redirected to: {url}";
    }

    public string GetAll()
    {
        if (_database.Count < 0)
        {
            return string.Empty;
        }

        var list = JsonConvert.SerializeObject(_database.Keys);

        return list;
    }

    public string Get(string code)
    {
        if (_database.Count <= 0 || !_database.ContainsKey(code))
        {
            return string.Empty;
        }

        return _database[code];
    }

    public string Create(string url)
    {
        var rnd = Guid.NewGuid().ToString("n").Substring(0, 8);
        _database.Add(rnd, url);
        return $"Here is your new code for the URL: {rnd}";
    }

    public string Delete(string code)
    {
        _database.Remove(code);
        return $"Successfully removed. Code: {code}";
    }
}

public class CreateShortUrlRequest
{
    [JsonConstructor]
    public CreateShortUrlRequest(string url)
    {
        Url = url;
    }

    [JsonRequired]
    [JsonProperty("url")]
    public string Url { get; set; }
}

In this implementation, we created CRUD endpoints for a URL shortener and used a Dictionary to represent a key-value pair database. In the middleware, we check the current request method to ensure it matches the intended method, and if it does, we use the unique code to search the database for the corresponding long URL and redirect the user to it.

Overall, this implementation demonstrates how we can use basic CRUD operations and a key-value pair database to create a functional URL shortener. Additionally, it showcases how middleware can be utilized to handle the redirection of shortened URLs to their corresponding long URLs.

Here is a snapshot from the network tab:

redirect