arctek.dev logo

Make a quick URL shortener in C#

blog post cover image
to blog

Make a quick URL shortener in C#

What we are going to be building?

In this short tutorial I will guide you through building our very own URL shortener similar to bit.ly and other such services. The language we'll be using will be C# with .NET core 2.2, so make sure you have at the very least intermediate knowledge of C#. I'll make sure to cover the theory and some quick and simple solutions in the tutorial below, so non C# developers are welcome to join us as well.

The result

The source code for this project is readily available over at Github

The Setup

First thing is, we'll want to set up our simple ASP.NET API project. We can do this either directly through Visual Studio, in this case Visual Studio 2017. Simply head over to File > New > Project make sure to select ASP.NET Web Application from the selection menu, name your project whatever name you want down in the name field and then click Ok. After that you'll be presented with a variety of templates. What you're gonna wanna do is select Web Application(Model-View-Controller), then uncheck Enable Docker Support and uncheck Configure for HTTPS. Make sure that Authentication is set to No Authentication and whenever you're ready click OK.

the result

Your settings should look a little bit like this.

You may also set everything up using the dotnet new command. In which case simply open up the command window or terminal in your desired project directory and run dotnet new mvc, this should set everything up for you.

the next thing you wanna do is install LiteDB

Let's get started

The very first thing I always like to do is clean up some of the useless default code that Visual Studio sets up for me. Then I head over to create a new controller and call it HomeController.cs. Once the controller is good and ready I like to add the endpoints that I expect to use. So basically for a URL Shortener we'll need:

  • a default "index" endpoint that delivers our webpage to the user http method: GET

  • an endpoint that received the URL the user wishes to shorten and returns the shortened version http method POST

  • a redirect endpoint that gets redirects the user from the shortened URL to the original URL http method GET

Pretty simple stuff right there. Our controller should now look something like this:

using System.Net.Http;
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using nixfox.App;


namespace nixfox.Controllers
{
	// This will be needed later in the PostURL() method
	public class URLResponse{
		public string url { get; set; }
		public string status { get; set; }
		public string token { get; set; }
	}
	public class HomeController : Controller{
    	// Our index Route
		[HttpGet, Route("/")]
		public IActionResult Index() {
			return View();
		}

		// Our URL shorten route
		[HttpPost, Route("/")]
		public IActionResult PostURL([FromBody] string url) {
			throw new NotImplementedException();
		}
		
        // Our Redirect route
		[HttpGet, Route("/{token}")]
		public IActionResult NixRedirect([FromRoute] string token) {
			throw new NotImplementedException();

		}
	}
}

That will serve as the basic skeleton of our HomeController.

Create a razor view

Go down to your Views folder and create a new directory named Home in that directory create a new file named index.cshtml. Now you could simply stuff all your HTML + Razor code in that one .cshtml file and then serve it to the user. but I like to stay a bit more organized so what I like to do is:

Create a new partial for the header of the page by creating a new .cshtml file named header.cshtml in the partials directory. Then in shared you'll want to create a _layout.cshtml file which will serve as the main layout of your view.

_layout.cshtml

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>nixfox | shorten your URLs and links in style</title>

    <link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet"> 
    <link rel="stylesheet" href="css/site.css">
</head>
<body>
    @await Html.PartialAsync("~/Views/Partials/Header.cshtml")
    <section id="main-wrapper">
        @RenderBody()
    </section>

    <script src="~/js/site.js"></script>
</body>
</html>

header.cshtml

<header id="main-header">
    <section>
        <h1>nixfox.de</h1>
        <h2>a super light and friendly url shortener</h2>
    </section>
</header>

Home/index.cshtml

<div class="shortener-form-wrap">    
<section class="shortener-form">
    <h2>Enter your link below and get it shortened, nice and easy.</h2>
    <div class="input-wrap">
        <input type="text" name="url" id="urlshort" placeholder="Shorten your URL" /><button id="submit">Shorten</button>
    </div>
    <div class="resp-area inactive" id="resp-area">

    </div>
</section>
</div>

Now you might be wondering, ain't that a lot of work for something that simple? Yes, absolutely, but trust me it's worth it when you decide to eventually extend your web application to have everything split up like that. Imagine if you'll later decide ''oh wait I should probably add a terms of service page, or an about page, or a documentation page etc.''. It will be a lot easier to not have to create a whole new HTML boilerplate page every time you wish to create a subpage like that. Plus there's a fair bit of magic .NET does when serving reusable files, that shows a slight improvement on performance. Granted it won't be noticeable on a small scale application like this but it's definitely good practice.

Onward!

With that out of the way feel free to style your page as you see fit. The important part is that it contains this input field


<input  type="text"  name="url"  id="urlshort"  placeholder="Shorten your URL"  /><button  id="submit">Shorten</button>

The meat of the project

The main part the actual URL shortener is fairly easy and short(badum tss 🥁 💥). Your first instinct might be to simply hash the original URL and use that as a token, that would of course provide a lot of uniqueness, however short hashing functions are often unreliable and sooner or later you'll run into the birthday paradox problem, which is not half as fun as it sounds.

terrible birthday

Right so how to guarantee a unique token, really simple, we resort back to good old randomization. The plan is to generate a string of random characters between 2 and 6 characters in length. Using the full English alphabet plus all numerals from 0-9 that gives us 62 available characters, meaning we have:

(62^2) + (62^3) + (62^4) + (62^5) + (62^6) possible unique tokens which equals: `57 billion 731 million 386 thousand 924´

That'll do pig... that'll do.

babe the pig

Basically every single person in the world could without a problem shorten 8 URLs with us, every... single... one of them. That of course is an impossible scenario.

The Code

using LiteDB;
using System;
using System.Linq;

namespace nixfox.App{
	public class NixURL{
		public Guid ID { get; set; }
		public string URL { get; set; }
		public string ShortenedURL { get; set; }
		public string Token { get; set; }
		public int Clicked { get; set; } = 0;
		public DateTime Created { get; set; } = DateTime.Now;
	}

	public class Shortener{
		public string Token { get; set; } 
		private NixURL biturl;
		// The method with which we generate the token
		private Shortener GenerateToken() {
			string urlsafe = string.Empty;
			Enumerable.Range(48, 75)
              .Where(i => i < 58 || i > 64 && i < 91 || i > 96)
              .OrderBy(o => new Random().Next())
              .ToList()
              .ForEach(i => urlsafe += Convert.ToChar(i)); // Store each char into urlsafe
			Token = urlsafe.Substring(new Random().Next(0, urlsafe.Length), new Random().Next(2, 6));
			return this;
		}
		public Shortener(string url) {
			var db = new LiteDatabase("Data/Urls.db");
			var urls = db.GetCollection<NixURL>();
            // While the token exists in our LiteDB we generate a new one
            // It basically means that if a token already exists we simply generate a new one
			while (urls.Exists(u => u.Token == GenerateToken().Token)) ;
            // Store the values in the NixURL model
			biturl = new NixURL() { 
                Token = Token, 
                URL = url, 
                ShortenedURL = new NixConf().Config.BASE_URL + Token 
            };
			if (urls.Exists(u => u.URL == url))
				throw new Exception("URL already exists");
            // Save the NixURL model to  the DB
			urls.Insert(biturl);
		}
	}
}

Confused? Don't be, I'll explain everything.

The database model

	public class NixURL{
		public Guid ID { get; set; }
		public string URL { get; set; }
		public string ShortenedURL { get; set; }
		public string Token { get; set; }
		public int Clicked { get; set; } = 0;
		public DateTime Created { get; set; } = DateTime.Now;
	}

This simply represents a single entry in our database. Each column in our DB will have the following fields:

  • string URL

  • string ShortenedURL

  • string Token

  • int Clicked

  • DateTime Created which will default to the DateTime.Now

All fairly simple and self explanatory.

Token generator

		private Shortener GenerateToken() {
			string urlsafe = string.Empty;
			Enumerable.Range(48, 75)
              .Where(i => i < 58 || i > 64 && i < 91 || i > 96)
              .OrderBy(o => new Random().Next())
              .ToList()
              .ForEach(i => urlsafe += Convert.ToChar(i)); // Store each char into urlsafe
			Token = urlsafe.Substring(new Random().Next(0, urlsafe.Length), new Random().Next(2, 6));
			return this;
		}

Now this is where I like to complicate things a tiny bit and probably owe you an explanation or two. So first I create a string which will carry all of our URL safe characters and assign it a value of string.Empty.

After that comes the fun part, what we know about characters is that they can be represented using numerical values, so I decided to simply loop through the ranges of numerical values where the URL safe characters are located and add that to a string of URL safe characters programmatically. There is nothing stopping you from storing them as:


string  urlsafe = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789"

But that's boring isn't it... you could also simply use a for loop instead of LINQ. Personally I like using LINQ and it does seem like the perfect job for link and opportunity for you to practice your LINQ skills.

After I generated my string of URL safe chars I simply call


urlsafe.Substring(new  Random().Next(0, urlsafe.Length), new  Random().Next(2, 6));

Store it to the class member named Token and return this, which allows us to chain methods (more on that later).

To generate a random string between 2 and 6 (at random) characters long. double randomization WOO

WOO

And just like that we're generating our random token.

Shortener

		public Shortener(string url) {
			var db = new LiteDatabase("Data/Urls.db");
			var urls = db.GetCollection<NixURL>();
            // While the token exists in our LiteDB we generate a new one
            // It basically means that if a token already exists we simply generate a new one
			while (urls.Exists(u => u.Token == GenerateToken().Token)) ;
            // Store the values in the NixURL model
			biturl = new NixURL() { 
                Token = Token, 
                URL = url, 
                ShortenedURL = new NixConf().Config.BASE_URL + Token 
            };
			if (urls.Exists(u => u.URL == url))
				throw new Exception("URL already exists");
            // Save the NixURL model to  the DB
			urls.Insert(biturl);
		}

Now all there's left for us to do is "connect" to the database and store our token with the URL it points to and the rest of the relevant data. Make sure to follow the code above to ensure you don't store duplicate tokens.

The Return Of the HomeController.cs

Now we come back to our HomeController.cs and extend our endpoints

[HttpPost, Route("/")]
public IActionResult PostURL([FromBody] string url) {
	// Connect to the database
    var DB = LiteDB.LiteDatabase("Data/Urls.db").GetCollection < NixURL > ();
    try {
    	// If the url does not contain HTTP prefix it with it
        if (!url.Contains("http")) {
            url = "http://" + url;
        }
        // check if the shortened URL already exists within our database
        if (new DB.Exists(u => u.ShortenedURL == url)) {
            Response.StatusCode = 405;
            return Json(new URLResponse() {
                url = url, status = "already shortened", token = null
            });
        }
        // Shorten the URL and return the token as a json string
        Shortener shortURL = new Shortener(url);
        return Json(shortURL.Token);
    // Catch and react to exceptions
    } catch (Exception ex) {
        if (ex.Message == "URL already exists") {
            Response.StatusCode = 400;
            return Json(new URLResponse() {
                url = url,
                    status = "URL already exists",
                    token = DB.Find(u => u.URL == url).FirstOrDefault().Token
            });
        }
        throw new Exception(ex.Message);
    }
}

First let's start with our PostURL endpoint. As you can clearly see much has changed and all is explained in the comments of the actual code. It's very important for redirection purposes to ensure every URL stored in your database is prefixed with HTTP:// at least, otherwise when ASP.NET tried to redirect the user without there being a HTTP:// it will attempt to redirect the user to another endpoint on the server. So in this case, say you store www.google.com without HTTP, without this check my nixfox.de shortener would attempt to redirect the user to https://nixfox.de/www.google.com which would naturally result in an error.

You also have to make damn sure that the URL the user wishes to shorten does not already exist in the database, otherwise a potential troublemaker might shortned a URL take the shortened URL, shorten it again and again and so on building a chain of redirects which would naturally slow down our server substantially.

After you preformed all your important checks which you can freely add to your project yourself. Shorten the URL by calling new Shortener(url) and return the token as a JSON string.

The redirect

[HttpGet, Route("/{token}")]
public IActionResult NixRedirect([FromRoute] string token) {
    return Redirect(
    	new LiteDB.LiteDatabase("Data/Urls.db")
        .GetCollection < NixURL > ()
        .FindOne(u => u.Token == token).URL
    );
}

private string FindRedirect(string url) {
    string result = string.Empty;
    using(var client = new HttpClient()) {
        var response = client.GetAsync(url).Result;
        if (response.IsSuccessStatusCode) {
            result = response.Headers.Location.ToString();
        }
    }
    return result;
}

This is possibly the simplest endpoint we've got as it simply takes the token finds it in the database and finally redirects the user to the URL associated with the token, and that's all there is to it.

The final part the Javascript

All you have to do now is get the onclick event of your shorten button, post the URL from the input field to the correct end point and you'll get your redirect token back in the form of a JSON string.

You can easily do this by:

var submitBtn = document.querySelector("#submit");
var urlInput = document.querySelector("#urlshort");
submitBtn.onclick = function (ev) {
    let url = urlInput.value;
    fetch("/", {
            method: "POST",
            body: JSON.stringify(url),
            headers: {
                'Content-Type': 'application/json'
            }
        }).then(res => res.json())
        .then(response => {
                console.log(response);
            }
        }
}

Sending a simple fetch POST request to the "/" endpoint with the desired URL. Which will return the redirect token in the console.log, feel free to change that.

Improvement potential

Obviously this application is not perfect and has a lot of improvement potential such as:

  • Precalculating tokens to prevent time outing an unlucky user that gets a non-unique token too many times

  • Freeing up tokens based of off last activity

  • A click counter for the tokens in question

  • more validation both client and serverside

  • and much much more.

Hope you had fun!

Saturday 29 June 2019