Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Building a simple URL shorten service with Redis

0.00/5 (No votes)
16 Sep 2014 1  
In the article, I'll show you how to build a simple URL shorten service with Redis and ASP.NET MVC

 

Before reading this article, I suggest you download the source code first.

What's Redis

Similar to memcached, Redis is a in-memory database. Apart from this, Redis also allows you to save your data to a hard drive, which makes Redis not only a cache but also a NoSQL database.

Building the web

First, create an ASP.NET MVC project in Visual Studio with no authentication.

Then add a class to store the URL data in Models folder

public class MicroUrlItem
{
    [Display(Name = "Shorten URL")]
    public string ShortenUrl { get; set; }

    [Required]
    [Display(Name = "Origin URL")]
    [Url]
    public string OrignUrl { get; set; }

    [Range(1, double.MaxValue)]
    [Display(Name = "Expire in Days")]
    public int ExpireInDays { get; set; }

    [Display(Name = "Expire Mode")]
    public ExpireMode ExpireMode { get; set; }
}

ExpireMode is a custom enum so we need to declarate it.

public enum ExpireMode
{
    [Display(Name = "By Created")]
    ByCreated = 0,
    [Display(Name = "By Last Accessed")]
    ByLastAccessed = 1,
    [Display(Name = "Never")]
    Never = 2
}

After that, add a Home page

@model MicroUrl.Models.MicroUrlItem

@{
    ViewBag.Title = "Home Page";
}
<h3>Get you micro URL here</h3>
<div class="row">
    @using (Html.BeginForm("Index", "Home", FormMethod.Post, new { role = "form", @class = "col-lg-6" }))
    {
        <div class="form-group">
            @Html.LabelFor(m => m.OrignUrl)
            @Html.TextBoxFor(m => m.OrignUrl, new { @class = "form-control" })
            @Html.ValidationMessageFor(m => m.OrignUrl, "", new { @class = "text-danger" })
        </div>
        <div class="form-group">
            @Html.LabelFor(m => m.ShortenUrl)
            @Html.TextBoxFor(m => m.ShortenUrl, new { @class = "form-control", placeHolder = "Keep it empty to generate automatically" })
            @Html.ValidationMessageFor(m => m.ShortenUrl, "", new { @class = "text-danger" })
        </div>
        <div class="form-group">
            @Html.LabelFor(m => m.ExpireMode)
            @Html.EnumDropDownListFor(m => m.ExpireMode, new { @class = "form-control" })
            @Html.ValidationMessageFor(m => m.ExpireMode, "", new { @class = "text-danger" })
        </div>
        <div class="form-group">
            @Html.LabelFor(m => m.ExpireInDays)
            @Html.TextBoxFor(m => m.ExpireInDays, new { @class = "form-control" })
            @Html.ValidationMessageFor(m => m.ExpireInDays, "", new { @class = "text-danger" })
        </div>
        <button type="submit" class="btn btn-default">Create</button>
    }
</div>

and it will look like this

One interesting thing in this code snippet is the EnumDropDownListFor method in HtmlHeper, which in not available until ASP.NET MVC 5.2.

In previous version of ASP.NET MVC, without this method, we had to create the dropdown list items manually, which was a dirty work.

But now we can use this method to generate items from an enum, and use the Display attribute to set its display name.

Then we need a page to tell the user that the operation was succeed in Success.cshtml, that's pretty simple.

@model MicroUrl.Models.MicroUrlItem

@{
    ViewBag.Title = "Success";
}

<h2>Success</h2>
<h3>
    @{
        var url = string.Format("http://{0}:{1}/{2}", Request.Url.Host, Request.Url.Port, Model.ShortenUrl);
    }
    Your Micro URL is <a href="@url">@url</a>
</h3>

Saving objects to Redis

With those preparations above, we can now start writing code to store data in Redis.

First a Nuget package called ServiceStack.Redis is needed. We can simply search for redis in Package Manager, or install in Console using this following command:

PM> Install-Package ServiceStack.Redis

BTW, you can install ServiceStack.Redis.Signed if a reference to a signed assembly, which has a strong name, is required.

Then we can create a redis client with a using statement, and get a typed client to deal with MicroUrlItems

using (IRedisClient client = new RedisClient())
{
    var typedClient = client.As<MicroUrlItem>();
    //...
}

Before saving objects in Redis, we need to mark the ShortenUrl property as the primary key with ServiceStack.DataAnnotations.PrimaryKey attribute, so finally the ShortenUrl property in MicroUrlItem should be

public class MicroUrlItem
{
    [ServiceStack.DataAnnotations.PrimaryKey]
    [Display(Name = "Shorten URL")]
    public string ShortenUrl { get; set; }
    
    //other properties
    //...
}

Another thing we need to do is to generate a random url when the ShortenUrl is not specified.

if (string.IsNullOrWhiteSpace(model.ShortenUrl))
{
    string url;
    do
    {
        url = GetRandomUrl(6);
    } while (typedClient.GetById(url) != null);
    model.ShortenUrl = url;
}

And here is how I generate a random url

private static string randomList = "0123456789abcdefghijklmnopqrstuwxyz";

private static string GetRandomUrl(int length)
{
    var sb = new StringBuilder(length);
    var random = new Random();
    for (int i = 0; i < length; i++)
    {
        var index = random.Next(0, randomList.Length);
        sb.Append(randomList[index]);
    }
    return sb.ToString();
}

Now the model is ready to be saved to Redis

typedClient.Store(model);

Finally, we need to tell Redis when the object will expire if the expiration is specified user.

if (model.ExpireMode != ExpireMode.Never)
    typedClient.ExpireIn(model.ShortenUrl, TimeSpan.FromDays(model.ExpireInDays));

Getting objects from Redis

[Route("{shortenUrl}")]
public ActionResult Index(string shortenUrl)
{
    using (IRedisClient client = new RedisClient())
    {
        var typedClient = client.As<MicroUrlItem>();
        var microUrlItem = typedClient.GetById(shortenUrl);
        if (microUrlItem != null)
        {
            //renew the record
            if (microUrlItem.ExpireMode == ExpireMode.ByLastAccessed)
                typedClient.ExpireIn(microUrlItem.ShortenUrl, TimeSpan.FromDays(microUrlItem.ExpireInDays));
            return Redirect(microUrlItem.OrignUrl);
        }
    }
    return HttpNotFound();
}

Now we can create a typed client in the same way above, and return a response with HTTP 302 if a matching record was found. When a shorten url is accessed, wo should renew it when its ExpireMode property is set to ByLastAccessed.

Another important thing is to make the Route attribute work, just simply map the AttributeRoutes to the ASP.NET MVC route system when application starts.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    //map the AttributeRoutes
    routes.MapMvcAttributeRoutes();

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

Run the application

Before running this application, we need to start Redis, to get Redis, you can go to http://redis.io/download to download it.

For Windows, start a command prompt and go to the Redis folder, and run 

redis-server.exe --maxheap 1gb

and we can also launch a command line client to start a monitor

Then we can start the website in Visual Studio, and create a shorten url.

Here is the custom url

And here is the url generated by the service

After we've got the micro url, we can see what happened in redis in the monitor.

Thanks for reading!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here