Creating a URL shortener using ASP.NET WepAPI and MVC: implementing the business layer

In my previsious post I discussed the first implementation of this application. In this post I’m explained how to implement the business layer.

First of all you make sure the Business project references the Data, Entities and Exceptions projects. In the Exceptions project you add two new classes:

ShorturlConflictException

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PSC.Shorturl.Web.Exceptions
{
    public class ShorturlConflictException : Exception	
    {
    }
}

ShorturlNotFoundException

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PSC.Shorturl.Web.Exceptions
{
    public class ShorturlNotFoundException : Exception
    {
    }
}

We’ve implemented a stub for a business manager. The manager actually doesn’t do anything, but since we’ve implemented the database layer, it’s now possible to do some advanced stuff. Below you’ll see the new IUrlManager and UrlManager.

IUrlManager

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PSC.Shorturl.Web.Entities;

public interface IUrlManager
{
    Task<ShortUrl> ShortenUrl(string longUrl, string ip, string segment = "");
    Task<Stat> Click(string segment, string referer, string ip);
}

UrlManager

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PSC.Shorturl.Web.Data;
using PSC.Shorturl.Web.Entities;
using PSC.Shorturl.Web.Exceptions;

namespace PSC.Shorturl.Web.Business.Implementations
{
    public class UrlManager : IUrlManager
    {
        public Task<ShortUrl> ShortenUrl(string longUrl, string ip, string segment = "")
        {
            return Task.Run(() =>
            {
                using (var ctx = new ShorturlContext())
                {
                    ShortUrl url;

                    url = ctx.ShortUrls.Where(u => u.LongUrl == longUrl).FirstOrDefault();
                    if (url != null)
                    {
                        return url;
                    }

                    if (!string.IsNullOrEmpty(segment))
                    {
                        if (ctx.ShortUrls.Where(u => u.Segment == segment).Any())
                        {
                            throw new ShorturlConflictException();
                        }
                    }
                    else
                    {
                        segment = this.NewSegment();
                    }

                    if (string.IsNullOrEmpty(segment))
                    {
                        throw new ArgumentException("Segment is empty");
                    }

                    url = new ShortUrl()
                    {
                        Added = DateTime.Now,
                        Ip = ip,
                        LongUrl = longUrl,
                        NumOfClicks = 0,
                        Segment = segment
                    };

                    ctx.ShortUrls.Add(url);
                    ctx.SaveChanges();

                    return url;
                }
            });
        }

        public Task<Stat> Click(string segment, string referer, string ip)
        {
            return Task.Run(() =>
            {
                using (var ctx = new ShorturlContext())
                {
                    ShortUrl url = ctx.ShortUrls.Where(u => u.Segment == segment).FirstOrDefault();
                    if (url == null)
                    {
                        throw new ShorturlNotFoundException();
                    }

                    url.NumOfClicks = url.NumOfClicks + 1;

                    Stat stat = new Stat()
                    {
                        ClickDate = DateTime.Now,
                        Ip = ip,
                        Referer = referer,
                        ShortUrl = url
                    };

                    ctx.Stats.Add(stat);
                    ctx.SaveChanges();

                    return stat;
                }
            });
        }

        private string NewSegment()
        {
            using (var ctx = new ShorturlContext())
            {
                int i = 0;
                while (true)
                {
                    string segment = Guid.NewGuid().ToString().Substring(0, 6);
                    if (!ctx.ShortUrls.Where(u => u.Segment == segment).Any())
                    {
                        return segment;
                    }
                    if (i > 30)
                    {
                        break;
                    }
                    i++;
                }
                return string.Empty;
            }
        }
    }
}

You’ll see two new methods here: Click and NewSegment. The Click method will be executed any time anyone clicks on a short URL; some data (like the refering website) will be stored in the database. The NewSegment method creates a unique segment for our short URL. If it hasn’t found a valid segment in 30 loops, an empty string will be returned (this situation will be very rare though). The ShortenUrl method now actually shortens a long URL and stores it, together with a generated segment, in the database.

As you can see, any time a database action is executed, a new context object is created. When there are changes to the data, SaveChanges() is executed on the context at the end.

Putting it in the UrlController

Now we have a working business layer, we can call the business methods in the URL controller. Below, you see the updated Index() method:

public async Task<ActionResult> Index(Url url)
{
    if (ModelState.IsValid)
    {
        ShortUrl shortUrl = await this._urlManager.ShortenUrl(url.LongURL, Request.UserHostAddress);
        url.ShortURL = string.Format("{0}://{1}{2}{3}", Request.Url.Scheme, 
                                     Request.Url.Authority, Url.Content("~"), 
                                     shortUrl.Segment);
    }
    return View(url);
}

The controller asks the UrlManager for a new ShortUrl. When all went well, a full URL with the segment at the end will be created. At the moment there is only one problem; when we navigate to that URL, nothing happens, so we have to implement another method in the UrlController which handles the redirects. You’ll see this method below:

public async Task Click(string segment)
{
    string referer = Request.UrlReferrer != null ? Request.UrlReferrer.ToString() : string.Empty;
    Stat stat = await this._urlManager.Click(segment, referer, Request.UserHostAddress);
    return this.RedirectPermanent(stat.ShortUrl.LongUrl);
}

The only thing we have to do now is tell MVC that when we go to https://yourapp/segment, we wind up in that specific method. Below, you’ll see the new RegisterRoutes method of the class RouteConfig.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace PSC.Shorturl.Web
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Click",
                url: "{segment}",
                defaults: new { controller = "Url", action = "Click" }
            );

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

It’s very important that the “Click” route stands above the “Default” route.

Another nice thing our URL manager supports is adding a custom segment, for example https://yourapp/mysite. We have to modify three things for that:

1. Update the Url model

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;

namespace PSC.Shorturl.Web.Models
{
    public class Url
    {
        [Required]
        public string LongURL { get; set; }

        public string ShortURL { get; set; }

        public string CustomSegment { get; set; }
    }
}

2. Update the Index method

public async Task<ActionResult> Index(Url url)
{
    if (ModelState.IsValid)
    {
        ShortUrl shortUrl = await this._urlManager.ShortenUrl(url.LongURL, 
                                  Request.UserHostAddress, url.CustomSegment);
        url.ShortURL = string.Format("{0}://{1}{2}{3}", Request.Url.Scheme, 
                                     Request.Url.Authority, Url.Content("~"), 
                                     shortUrl.Segment);
    }
    return View(url);
}

3. Update the view

@model PSC.Shorturl.Web.Models.Url
@{
    ViewBag.Title = "URL shortener";
}

<h2>Shorten a URL</h2>

@Html.ValidationSummary()

@using (Html.BeginForm())
{
    <div class="form-group">
        @Html.TextBoxFor(model => model.LongURL, new { placeholder = "URL you would like to have shortened", 
                         @class = "form-control" })
    </div>

    <div class="form-group">
        @Html.TextBoxFor(model => model.CustomSegment, new { placeholder = "If you like, fill in a custom word for your short URL", 
                         @class = "form-control" })
    </div>

    if (!string.IsNullOrEmpty(Model.ShortURL))
    {
        <div>
            <h3><a href="@Model.ShortURL" target="_blank">@Model.ShortURL</a></h3>
        </div>
    }

    <input type="submit" class="btn btn-primary" value="Shorten" />
}

After this is done, it looks like we have a basic, and functioning, URL shortener. We still have to add some error handling; when an error is thrown from within the business layer, a big old ugly error page will be shown to the user. This is not something we want, this is what we’re going to fix next. In the next post.

Happy coding!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.