DEV Community

Dayvster 🌊
Dayvster 🌊

Posted on • Updated on • Originally published at dayvster.com

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.

Source Code

The entire source code for this project can be found 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.
setup image

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();

        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

header.cshtml

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

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

lisa birthday gif

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.
thatll do 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);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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 gif

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
            }
        }
}
Enter fullscreen mode Exit fullscreen mode

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

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! if you have any questions feel free to contact me @arctekdev on twitter.

Top comments (5)

Collapse
 
patzistar profile image
Patrick Schadler

Hey fellow Austrian, I like and appreciate the effort you put in there, but I would highly encourage you to look at the readability of the source code.

Some further notices:
Don't use Status Code 405 (Method not allowed) when the method is allowed but some validation failed.
Don't use Status Code 400 (Bad Request) when the request is fine but again some validation failed.
Use an auto formatter and try to be consistent with naming and lower/uppercase.
Catching generic "Exception" is considered an anti-pattern - rather catch specific exceptions. When rethrowing exceptions simply use throw - with throw new Exception(ex.Message); you loose the strack trace.
Separate Data and Functionality - Shortener.cs use both.
Usually in C# curly braces {} are not in the same line as the method name (in opposite to java).
When using POST request return a 201 Status Code (CREATED).

I have seen that you consider yourself an REST API Expert and a C# Genius on your homepage, so take that in mind when you publish code on GitHub, dev.to or any other platform. Potential customer will take some research and find this things quickly.

Again, don't take this personal, I just want to improve you and our community a bit. Feel free to ask if you have any questions.

Finally, use space instead of tabs :) (jk, just a personal preference).

Collapse
 
dayvster profile image
Dayvster 🌊 • Edited

Hey fellow Austrian

Servus!

Finally, use space instead of tabs :) (jk, just a personal preference).

I challenge you to a duel, fiend!

Again, don't take this personal, I just want to improve you and our community a bit. Feel free to ask if you have any questions.

No worries, I appreciate constructive criticism.

Don't use Status Code 405 (Method not allowed) when the method is allowed but some validation failed.

Noted, 401 would be the correct status code, I'll fix it later.

Don't use Status Code 400 (Bad Request) when the request is fine but again some validation failed.

Same as above.

Use an auto formatter and try to be consistent with naming and lower/uppercase.
Catching generic "Exception" is considered an anti-pattern - rather catch specific exceptions. When rethrowing exceptions simply use throw - with throw new Exception(ex.Message); you loose the strack trace.

Fully aware of this, however! Consider that this is intended for beginners as a fun little project.

Separate Data and Functionality - Shortener.cs use both.

See above

Usually in C# curly braces {} are not in the same line as the method name (in opposite to java).

Personal preference, actually.

When using POST request return a 201 Status Code (CREATED).

In this case since a resource was indeed created 201 would technically be more correct than 200, however 200 communicate a successfull response, so it does the job.

Collapse
 
vekzdran profile image
Vedran Mandić

Hey, its a really fun article and I guess very teaching to beginners (hence the fixes Patrick suggested are welcome). What I'd like to point out is that you could've went with LINQ's .Aggregate() fn, e.g. here you can see an exact 6 random chars variation to you code that does not reiterate with foreach but does everything in a single iteration:

  var r = new Random();

  string urlsafe =
     Enumerable.Range(48, 75)
              .Where(i => i < 58 || i > 64 && i < 91 || i > 96)
              .OrderBy(x => r.Next())
              .Aggregate("", (aggr, i) => aggr += Convert.ToChar(i))
              .Substring(0, 6);
Collapse
 
dayvster profile image
Dayvster 🌊

I am aware that I could have used LINQ, I thought about using LINQ. However it would not be very beginner friendly.

Additionally I might have a LINQ specific blog/tut planned ;)

Collapse
 
vekzdran profile image
Vedran Mandić

You already imported and used it with using System.Linq;. What matters is that you do not need an additional iteration with .ToList() just to call .ForEach(), that is what .Aggregate() does for you in a single loop. Good idea for the next article.