Skip to content
Photo by @brookelark / Unsplash.com

Building a custom URL shortener service using Azure and a Serverless approach

Thanks for reading my blog! If you have any questions or need a second opinion with anything Microsoft Azure, security or Power Platform related, don't hesitate to contact me.

Summer in Finland was two weeks ago. Now it’s dull, gray, cold, and windy. So the perfect time to build yet another solution using Azure!

This time, I wanted to build a custom URL shortener service. Microsoft has one, and it’s called aka.ms. Another one is Twitter’s t.co, and one that I see many people using is bit.ly. The idea is that you can alias a website – say, https://jussiroine.com would be accessible via https://bit.ly/jussi, for example. You can of course add complex URL parameters, to avoid some headaches. Sometimes the shortened URLs are called vanity URLs or vanity domains.

I wrote about harvesting aka.ms vanity URLs previously here.

The components of a URL shortener service are:

  • A tool to add and store mappings – URL and vanity URL (the shortened address)
  • A web page to accept calls to the vanity URL, look up the mappings and redirecting to the real destination

Pretty simple! But turns out – again – that building simple things often leads to discovering different challenges, and managing through those.

Designing the solution

As is customary, I want to avoid using virtual machines. They add unnecessary complexity. When ideal, I also like to avoid writing tons of code. With custom code, you can admittedly tackle any challenge, but you introduce so many moving parts, at times it’s not reasonable.

I wanted to use .NET Core to future-proof my solution. I set out to design the solution as follows:

  • Storing my vanity URLs and mappings I chose to use an Azure SQL database – it’s super simple, affordable, and widely supported.
  • Adding new URLs to my vanity URL database I chose to create a simple command-line tool using .NET Core. This tool calls into a Logic App that takes care of parsing the input.
  • For redirecting the vanity URL requests to real URLs I chose to create a simple ASP.NET Core Web App with some redirection magic.

I thus needed to create the command-line tool for adding new mappings, a Logic App to process them, and a Web App to act as the landing site for my URL shortening service.

Next, let’s build each of these in order.

Creating the Azure SQL database

This was easy. I provisioned a new Azure SQL Server in Azure and created a single database. The pricing tier is Basic, which translates to ‘cheap’, yet performant enough for my needs.

I created the following SQL table structure:

Creating the command-line tool to add new vanity URL mappings

Next, it’s time to create a command-line tool. This is useful for quickly testing the service later on, and can be easily ported to other platforms when needed.

I chose to use .NET Core, as it is so versatile.

First, I defined a class object to hold my vanity data:

public class vanityObject
{
	public string url { get; set; }
	public string vanity { get; set; }
	public string submittedBy { get; set; }
}

url is the true URL, vanity is the nice-looking alias for it, and submittedBy is to keep track who submitted the new entry.

And now, the main program – it’s relatively easy so I’ll just lay it out first:

static void Main(string[] args)
{
	vanityObject vanityObj = new vanityObject();

	vanityObj.url = args[0];
	vanityObj.vanity = args[1];
	vanityObj.submittedBy = "shortURLCLI"; 

	string jsonString;
	jsonString = JsonSerializer.Serialize(vanityObj);

	string uri = "URL-TO-LOGIC-APPS";

	HttpClient client = new HttpClient();

	StringContent content = new StringContent(jsonString, System.Text.Encoding.UTF8, "application/json");

	client.DefaultRequestHeaders.Accept.Clear();
	client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
	var response = client.PostAsync(uri, content);

	string returnContent = response.Result.Content.ReadAsStringAsync().Result;

	Console.WriteLine($"{returnContent}");

}

I create a new object with my data (passed on as parameters from the command line) and serialize that with help from System.Text.Json. See my previous article on using it.

Then, I’ll perform a HTTP POST to my API, which I’ll create shortly using Azure Logic Apps. And assuming all goes well, I’ll print out the HTTP/200 OK from the API.

Here is an example of running the tool, to add Google with a vanity URL of ‘g’:

In essence, the command-line utility simply calls an API and passes on the data in a JSON payload. The payload the API is expecting is:

{
    "properties": {
        "submittedby": {
            "type": "string"
        },
        "url": {
            "type": "string"
        },
        "vanity": {
            "type": "string"
        }
    },
    "type": "object"
}

Building the Logic App

Moving on to the Logic App. This is the API the command-line utility calls that stored our vanity URL and mapping data in the Azure SQL. I chose to use a Logic App, as it’s such a rapid way to prototype. An Azure Function would perhaps be slightly faster, but less maintainable at the same time.

First, this is how the completed Logic App looks:

It’s HTTP triggered, and expecting the JSON payload. Once it gets that, it populates three variables to make it easier to work with the data:

And then, it’s a simple matter of submitting that to the database:

Executing this workflow takes about 800 ms, so relatively fast.

Querying the database reveals that mappings have been stored successfully:

Building the web app

Adding new vanity URLs and their mapping data works now end-to-end – from the command-line utility to the database, with help from the Logic App. What is left now is a simple Web App, that redirects users.

The idea is to expose a single Web App, such as https://shorturl2020.azurewebsites.net/, and any calls to that would be picked up. If a user opens https://shorturl2020.azurewebsites.net/ex1, then ex1 is the vanity URL that needs to be resolved (to https://example.com, as seen in the database snapshot above, see vanity_id == 2).

I first created the redirection logic. As I’m using ASP.NET Core, I knew I could rely on web.config for this. It took a bit of fiddling, but I ended up with this rule:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <rewrite>
            <rules>
                <rule name="Vanity">
                     <match url="(.*)" />
                         <conditions logicalGrouping="MatchAll">
                             <add input="{R:0}" negate="true" pattern="^Index*." ignoreCase="true" />
                        </conditions>
                          <action type="Redirect" url="/Index?vanity={R:0}" appendQueryString="false" redirectType="Permanent" />
                </rule>
            </rules>
        </rewrite>
</configuration>

It matches all URLs (.*) but exempts any URLs beginning with /Index. This is my redirection page. All other URLs I assume, are vanity URLs that must be resolved. So I’ll pick up the desired URI part and redirect the user to https://shorturl2020.azurewebsites.net/Index?vanity={parameter}.

Now that this critical piece is completed, let’s take a look at the Web App. It’s ASP.NET Core, and has very little complexity in it.

In Index.cshtml.cs, which is the code file for the default Index page, I added this in the OnGet() method:

StringValues queryVal;

	bool v = HttpContext.Request.Query.TryGetValue("vanity", out queryVal);
	if (v)
	{
		string vanity = queryVal.FirstOrDefault();

		string pattern = @"^[a-zA-Z0-9]*$";
		Regex regex = new Regex(pattern);
		Match match = regex.Match(vanity);

		if (match.Success)
		{
			SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();

			builder.DataSource = "AZURE-SQL-SERVER";
			builder.UserID = "DBADMIN";
			builder.Password = "PASSWORD";
			builder.InitialCatalog = "DATABASE";

			using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
			{
				connection.Open();
				StringBuilder sb = new StringBuilder();
				sb.Append("SELECT destination_URL FROM vanityurls WHERE vanity_URL='" + vanity + "'");
				String sql = sb.ToString();

				using (SqlCommand command = new SqlCommand(sql, connection))
				{
					using (SqlDataReader reader = command.ExecuteReader())
					{
						while (reader.Read())
						{
							Response.Redirect(reader.GetString(0));
						}
					}
				}
			}
		}
	}

I’m first checking if a query string (?vanity={ID}) exists. I’ll then do a quick RegEx match to filter out anything that shouldn’t be there – like SQL injections. And finally, I’ll do a query against the database, and get the vanity URL – and perform a redirection.

Does it work?

See for yourself:

In conclusion

This build consisted of a few moving parts. Admittedly, the trickiest was the web.config redirection, as I haven’t touched those in almost a decade now. It’s rudimentary, in that it doesn’t reveal too much logic outside the basic vanity ID, but it’s a nice skeleton that I can use to build a better version in the future.

The Azure SQL database is the one thing that actually costs a little bit of money – all other components are either running locally or utilizing a free tier in Azure. The Logic App will eventually cost money also, but for now, it’s not used enough to warrant any worries.

I feel it’s highly useful to learn about different frameworks, programming languages, and platforms by building solutions like this – even if certain parts of the solution are very bare-bones!