Adding an external Microsoft login to IdentityServer4

This article shows how to implement a Microsoft Account as an external provider in an IdentityServer4 project using ASP.NET Core Identity with a SQLite database.

Setting up the App Platform for the Microsoft Account

To setup the app, login using your Microsoft account and open the My Applications link

https://apps.dev.microsoft.com/?mkt=en-gb#/appList

id4-microsoft-apps

Click the Add an app button. Give the application a name and add your email. This app is called microsoft_id4_enrico.

id4-microsoft-apps-registration

After you clicked the create button, you need to generate a new password. Save this somewhere for the application configuration. This will be the client secret when configuring the application.

id4-microsoft-apps-myapp

Now Add a new platform. Choose a Web type.

id4-microsoft-apps-platform

Now add the redirect URL for you application. This will be the https://YOUR_URL/signin-microsoft

id4-microsoft-apps-platform2

Add the Permissions as required

id4-microsoft-apps-permission

id4-microsoft-apps-permission-list

pplication configuration

Note: The samples are at present not updated to ASP.NET Core 2.0

Clone the IdentityServer4 samples and use the 6_AspNetIdentity project from the quickstarts.
Add the Microsoft.AspNetCore.Authentication.MicrosoftAccount package using Nuget as well as the ASP.NET Core Identity and EFCore packages required to the IdentityServer4 server project.

The application uses SQLite with Identity. This is configured in the Startup class in the ConfigureServices method.

services.AddDbContext<ApplicationDbContext>(options =>
       options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
 
services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    .AddIdentityServer();

Now the AddMicrosoftAccount extension method can be use to add the Microsoft Account external provider middleware in the Configure method in the Startup class. The SignInScheme is set to “Identity.External” because the application is using ASP.NET Core Identity. The ClientId is the Id from the app ‘microsoft_id4_damienbod’ which was configured on the my applications website. The ClientSecret is the generated password.

services.AddAuthentication()
     .AddMicrosoftAccount(options => {
          options.ClientId = _clientId;
          options.SignInScheme = "Identity.External";
          options.ClientSecret = _clientSecret;
      });
 
services.AddMvc();
 
...
 
services.AddIdentityServer()
     .AddSigningCredential(cert)
     .AddInMemoryIdentityResources(Config.GetIdentityResources())
     .AddInMemoryApiResources(Config.GetApiResources())
     .AddInMemoryClients(Config.GetClients())
     .AddAspNetIdentity<ApplicationUser>()
     .AddProfileService<IdentityWithAdditionalClaimsProfileService>();

And the Configure method also needs to be configured correctly.

If you receive an error like "unauthorize_access", remember that RedirectUri is required in IdentityServer configuration and clients.

Adding Swagger to Web API project

swagger_help_pages

Adding Swagger to your Web API does not replace ASP.NET Web API help pages (here the nuget package for Microsoft ASP.NET Web Api Help Page). You can have both running side by side, if desired.

To add Swagger to an ASP.NET Web Api, we will install an open source project called Swashbuckle via nuget.

Install-Package Swashbuckle –Version 5.2.1

After the package is installed, navigate to App_Start in the Solution Explorer. You’ll notice a new file called SwaggerConfig.cs. This file is where Swagger is enabled and any configuration options should be set here.

swagger_config-1

Configuring Swagger

At minimum you’ll need this line to enable Swagger and Swagger UI.

GlobalConfiguration.Configuration
  .EnableSwagger(c => c.SingleApiVersion("v1", "A title for your API"))
  .EnableSwaggerUi();

Start a new debugging session (F5) and navigate to http://localhost:[PORT_NUM]/swagger. You should see Swagger UI help pages for your APIs.

swagger_ui

Expanding an api and clicking the “Try it out!” button will make a call to that specific API and return results.

swagger_get_superhero-1

And then you see the response:

swagger_get_response

Enable Swagger to use XML comments

The minimum configuration is nice to get started but let’s add some more customization. We can tell Swashbuckle to use XML comments to add more details to the Swagger metadata. These are the same XML comments that ASP.NET Help Pages uses.

First, enable XML documentation file creation during build. In Solution Explorer right-click on the Web API project and click Properties. Click the Build tab and navigate to Output. Make sure XML documentation file is checked. You can leave the default file path. In my case its bin\SwaggerDemoApi.XML

build_xml_docs

Next, we need to tell Swashbuckle to include our XML comments in the Swagger metadata. Add the following line to SwaggerConfig.cs. Make sure to change the file path to the path of your XML documentation file.

GlobalConfiguration.Configuration
  .EnableSwagger(c =>
    {
      c.SingleApiVersion("v1", "SwaggerDemoApi");
      c.IncludeXmlComments(string.Format(@"{0}\bin\SwaggerDemoApi.XML",           
                           System.AppDomain.CurrentDomain.BaseDirectory));
    })
  .EnableSwaggerUi();

Finally, if you haven’t already, add XML comments to your Models and API methods.

xml_comments

Run the project and navigate back to /swagger. You should see more details added to your API documentation. I’ve highlighted a few below with their corresponding XML comment.

swagger_xml_comments

Under Response Class, click Model. You should see any XML comments added to your models.

swagger_xml_comments_model

Describing Enums As Strings

My Superhero class contains an Enum property called Universe which represents which comic universe they belong to.

universe_enum

By default, Swagger displays these Enum values as their integer value. This is not very descriptive. Let’s change it to display the string representation.

GlobalConfiguration.Configuration
  .EnableSwagger(c =>
  {
    c.SingleApiVersion("v1", "SwaggerDemoApi");
    c.IncludeXmlComments(string.Format(@"{0}\bin\SwaggerDemoApi.XML", 
                         System.AppDomain.CurrentDomain.BaseDirectory));
    c.DescribeAllEnumsAsStrings();
  })
  .EnableSwaggerUi();

If I look at Swagger now, the Universe Enum values are displayed as strings.

swagger_xml_comments_enum

These are just a few of the many configuration options you can specify in Swashbuckle to create your Swagger metadata. I encourage you to review the other options on Swashbuckle’s GitHub.

Happy coding!

Connect ASP.NET MVC 4.6.2 project to IdentityServer4

I have a website running on ASP.NET MVC 4.5.2. I have an IdentityServer4 server running but when I try and authenticate against it I get an:

invalid_request

I googled a bit but I can’t find a solution. Finally, I found the way.

First, in your IdentityServer4 you have to create a new client:

public static IEnumerable GetClients() {
    return new List<client> {
        new Client {
            ClientId = "yourid",
            AllowedScopes = new List<string> { "openid" },
            AllowedGrantTypes = GrantTypes.Hybrid,
            RedirectUris = new List { "http://yoururl/signin-oidc" },
        }
    }
}

When you added the new client, you can update your other MVC project. Under App_Start open Startup.Auth.cs and add this code:

using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;

namespace PSC
{
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "Cookies"
            });

            app.UseOpenIdConnectAuthentication(
              new OpenIdConnectAuthenticationOptions
              {
                Authority = "https://yourIdentityServerUrl",
                ClientId = "yourid",
                ResponseType = "id_token code",
                SignInAsAuthenticationType = "Cookies",
                RedirectUri = "http://yoururl/signin-oidc",
                Scope = "openid",
              });
        }
    }
}

You have to add a Nuget package called Microsoft.Owin.Security.OpenIdConnect.

Happy coding!

Connect ASP.NET MVC 4.6.2 project to IdentityServer4

I have a website running on ASP.NET MVC 4.5.2. I have an IdentityServer4 server running but when I try and authenticate against it I get an:

invalid_request

I googled a bit but I can’t find a solution. Finally, I found the way.

First, in your IdentityServer4 you have to create a new client:

public static IEnumerable GetClients() {
    return new List<client> {
        new Client {
            ClientId = "yourid",
            AllowedScopes = new List<string> { "openid" },
            AllowedGrantTypes = GrantTypes.Hybrid,
            RedirectUris = new List { "http://yoururl/signin-oidc" },
        }
    }
}

When you added the new client, you can update your other MVC project. Under App_Start open Startup.Auth.cs and add this code:

using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;

namespace PSC
{
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "Cookies"
            });

            app.UseOpenIdConnectAuthentication(
              new OpenIdConnectAuthenticationOptions
              {
                Authority = "https://yourIdentityServerUrl",
                ClientId = "yourid",
                ResponseType = "id_token code",
                SignInAsAuthenticationType = "Cookies",
                RedirectUri = "http://yoururl/signin-oidc",
                Scope = "openid",
              });
        }
    }
}

You have to add a Nuget package called Microsoft.Owin.Security.OpenIdConnect.

Happy coding!

Render in MVC a link with image and text

Hi guys, I want in MVC to render a text with an image as an ActionLink. For creating a simple anchor tag, we use Html.ActionLink() helper which generates anchor tag for us.

If you want to create something a bit more complicated, you must create you own component. For this reason, I created the following code. It allows you to create an anchor with an image and a text.

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

namespace PSC.MVC.Helpers
{
    public static class CustomHtmlHelpers
    {
        /// 
        /// Enum ImageAndText
        /// 
        public enum ImageAndText
        {
            /// 
            /// Image on the left, text on the right
            /// 
            ImageOnTheLeft,

            /// 
            /// Text on the left, image on the right
            /// 
            ImageOnTheRight
        }

        /// 
        /// Images the action link.
        /// 
        /// The HTML helper.
        /// The link text.
        /// The action.
        /// The controller.
        /// The route values.
        /// The HTML attributes.
        /// The image source.
        /// The alternate text.
        /// The text style.
        /// The image style.
        /// The image position.
        /// IHtmlString.
        public static IHtmlString ImageActionLink(
               this HtmlHelper htmlHelper, string linkText, string action, 
               string controller, object routeValues, object htmlAttributes, 
               string imageSrc, string alternateText = "", 
               string textStyle = "", string imageStyle = "", 
               ImageAndText imagePosition = ImageAndText.ImageOnTheLeft)
        {
            var urlHelper = new UrlHelper(
                    htmlHelper.ViewContext.RequestContext);

            // create the image
            var img = new TagBuilder("img");
            img.Attributes.Add("src", 
                    VirtualPathUtility.ToAbsolute(imageSrc));
            if (!string.IsNullOrEmpty(alternateText))
                img.Attributes.Add("alt", alternateText.Trim());
            if (!string.IsNullOrEmpty(imageStyle))
                img.Attributes.Add("style", imageStyle);

            // create a render for the image and the text
            string render = "";
            if (imagePosition == ImageAndText.ImageOnTheLeft)
                render = img.ToString(TagRenderMode.SelfClosing) + linkText;
            else
                render = linkText + img.ToString(TagRenderMode.SelfClosing);

            // create the anchor with image and text
            var anchor = new TagBuilder("a") {
                InnerHtml = render
            };
            if (!string.IsNullOrEmpty(textStyle))
                anchor.AddCssClass(textStyle);

            // add reference to the anchor
            anchor.Attributes["href"] = urlHelper.Action(action, 
                                                         controller, 
                                                         routeValues);
            anchor.MergeAttributes(new RouteValueDictionary(htmlAttributes));

            return MvcHtmlString.Create(anchor.ToString());
        }
    }
}

In your MVC code you have to add:

@using PSC.MVC.Helpers;

and then you can call your component:
@Html.ImageActionLink("Your text", "Index", "Home", null, null, 
                      "~/Content/images/img.png", "Logo", "navbar-brand", 
                      "width: 40px;")

C# and HTML5: drag and Drop elements

HTML5 API includes Drag and Drop (DnD) native functionality.

The event listener methods for all the drag and drop events accept Event object which has a readonly attribute called dataTransfer. The event.dataTransfer returns DataTransfer object associated with the event

This is the list of events fired during the different stages:

Event Description
dragstart Fires when the user starts dragging of the object.
dragenter Fired when the mouse is first moved over the target element while a drag is occuring. A listener for this event should indicate whether a drop is allowed over this location. If there are no listeners, or the listeners perform no operations, then a drop is not allowed by default.
dragover This event is fired as the mouse is moved over an element when a drag is occuring. Much of the time, the operation that occurs during a listener will be the same as the dragenter event.
dragleave This event is fired when the mouse leaves an element while a drag is occuring. Listeners should remove any highlighting or insertion markers used for drop feedback.
drag Fires every time the mouse is moved while the object is being dragged.
drop The drop event is fired on the element where the drop was occured at the end of the drag operation. A listener would be responsible for retrieving the data being dragged and inserting it at the drop location.
dragend Fires when the user releases the mouse button while dragging an object.

 

In this post we develop an application to handle the drag and drop events between two elements, and launch a HttpPost method in the server which will ends inserting the dragged value in database.

First of all we create an MVC project in C#. Under Controller folder in the HomeController we add this code:

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

namespace HTML5DragAndDrop.Controllers {
   public class HomeController : Controller {
      ITestDatabase _testDatabase = new TestDatabase();

      public ActionResult Index() {
         return View();
      }

      public ActionResult About() {
         ViewBag.Message = "Your application description page.";

         return View();
      }

      public ActionResult Contact() {
         ViewBag.Message = "Your contact page.";

         return View();
      }

      [HttpPost]
      public JsonResult InsertTestElements(string testElement) {
         int? insertResult = null;

         if (!string.IsNullOrEmpty(testElement)) {
            string[] elementData = testElement.Split(':');

            string symbol = elementData[0].Trim();
            string name = elementData[1].Trim();

            insertResult = _testDatabase.InsertElement(symbol, name);
         }

         return Json(insertResult);
      }
   }
}

Now we create a folder in the root of the project called Code. Under this folder we create two new files:

ITestDatabase.cs

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

public interface ITestDatabase {
   int InsertElement(string symbol, string name);
}   
 

TestDatabase.cs

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

public class TestDatabase : ITestDatabase {
   /// <summary>
   /// Save an element in the database and return the new ID
   /// </summary>
   /// <param name="symbol"></param>
   /// <param name="name"></param>
   /// <returns></returns>
   public int InsertElement(string symbol, string name) {
      // save the element in database
      // now it returns just a number: it will be the Id of the record
      return 1;
   }
}

The first step is the definition of the UXinterface, in sequence the display is this

DragAndDrop_App

We are adding h5utils.js file, with an implementation of AddEvent function to simplify our code:

h5utils.js

var AddEvent = (function () {
   if (document.addEventListener) {
      return function (el, type, fn) {
         if (el && el.nodeName || el === window) {
            el.addEventListener(type, fn, false);
         } else if (el && el.length) {
            for (var i = 0; i < el.length; i++) {
               AddEvent(el[i], type, fn);
            }
         }
      };
   } else {
      return function (el, type, fn) {
         if (el && el.nodeName || el === window) {
            el.attachEvent('on' + type, function () 
                  { 
                    return fn.call(el, window.event); 
                  });
         } else if (el && el.length) {
            for (var i = 0; i < el.length; i++) {
               AddEvent(el[i], type, fn);
            }
         }
      };
   }
})();

var pDragElement = document.createElement('p');
var chemicalElements = document.querySelectorAll('div > p'), el = null;
for (var i = 0; i < chemicalElements.length; i++) {
   el = chemicalElements[i];
   el.setAttribute('draggable', 'true');
   AddEvent(el, 'dragstart', dragStartElement);
   AddEvent(el, 'dragend', dragEndElement);
}

function dragStartElement(e) {
   e.dataTransfer.effectAllowed = 'copy';
   e.dataTransfer.setData('Text', this.id);
   e.dataTransfer.setData('Type', this.innerHTML);

   this.style.backgroundColor = "#ffa31a";
}

function dragEndElement(e) {
   this.style.backgroundColor = "#fff9f0";
}

var divBoxElements = document.querySelector('#divBoxElements');

AddEvent(divBoxElements, 'dragover', function (e) {
   if (e.preventDefault) e.preventDefault();
   e.dataTransfer.dropEffect = 'copy';
   return false;
});

AddEvent(divBoxElements, 'drop', function (e) {
   if (e.stopPropagation) e.stopPropagation();

   var element = e.dataTransfer.getData('Type');
   pDragElement.innerHTML = "Adding " + element + " element";

   var pClone = pDragElement.cloneNode(true);
   var newDiv = document.createElement("div");
   newDiv.appendChild(pClone);
   divBoxElements.appendChild(newDiv);
   InsertTestElement(element);

   return false;
});

In the Index.cshtml we change as following code:

Index.cshtml

@{
   ViewBag.Title = "Home Page";
}

<div class="jumbotron">
    <h1>Drag and drop example</h1>
    <p class="lead">HTML5 API includes Drag and Drop (DnD) 
    native functionality. The event listener methods for all the drag and 
    drop events accept Event object which has a readonly attribute called 
    dataTransfer. The event.dataTransfer returns DataTransfer object 
    associated with the event.</p>
    <p><a href="http://puresourcecode.com" 
    class="btn btn-primary btn-lg">Learn more »</a></p>
</div>

<div class="row">
    <p id="1A">1: A</p>
    <p id="2B">2: B</p>
    <p id="3C">3: C</p>
    <p id="4D">4: D</p>
    <p id="5E">5: E</p>
    <div id="divBoxElements 
       style="border: 1px solid #dcdcdc; width: 300px; height: 300px;"
    ></div>
</div>

@section scripts {
    <script src="~/Scripts/html5utils.js"></script>
    <script>
        <!--
        function InsertTestElement(element) {
            var url = '@Url.Action("InsertTestElements", "Home")';

            $.post(url, { testElement: element }, function (data) {
                switch (data) {
                    case 1:
                        divBoxElements.innerHTML = 
                             element + " inserted OK";
                        setTimeout(function () { 
                             divBoxElements.innerHTML = ""; }, 2000);
                        break;

                    default:
                        alert("Error inserting the element");
                }
            });
        }
        //-->
    </script>
}

Download or fork tthis code on Github.

Happy coding!

C# MVC: HttpContextBase from HttpContext.Current

It's easy by using the HttpContextWrapper

HttpContextBase basecontext = (new HttpContextWrapper(HttpContext.Current));
Happy coding!

How to upload a file in an ASP.Net MVC page

For this example, I’m using a controller named “HomeController”, with the view “Index”. When you click the “Submit” button on the view, it will be submitted to the “Save” method of HomeController.

Index.cshtml

@using(Html.BeginForm("Save", "Home", FormMethod.Post, 
               new {enctype = "multipart/form-data"}))
{
    <table>
        <tr>
            <td>File:</td>
            <td>
                <input type="file" name="UploadedFile" />
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" name="Submit" value="Submit"/>
            </td>
        </tr>
    </table>
}

Notice the two additional parameters for BeginForm

FormMethod.Post, new {enctype = "multipart/form-data"}

You need those to have the file included in the HTTP Post action to the Save method on the controller. Other than that, you just need a file input control, with a unique name attribute set.

HomeController

using System;
using System.Web;
using System.Web.Mvc;

namespace FileUpload.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult Save(FormCollection formCollection)
        {
            if(Request != null)
            {
                HttpPostedFileBase file = Request.Files["UploadedFile"];

                if((file != null) && (file.ContentLength > 0) && 
                   !string.IsNullOrEmpty(file.FileName))
                {
                    string fileName = file.FileName;
                    string fileContentType = file.ContentType;
                    byte[] fileBytes = new byte[file.ContentLength];
                    file.InputStream.Read(fileBytes, 0, 
                                       Convert.ToInt32(file.ContentLength));

                    string pth = Path.GetFileName(fileName);
                    // change the folder
                    file.SaveAs(Server.MapPath("~/Uploads") + "/" + pth);
                }
            }

            return View("Index");
        }
    }
}

The Request object is a base object for handling HTTP requests. Since we set the FormMethod and enctype in the view, we have access to the file that was submitted on the page.

If the Request object isn’t null, we can check for the file by getting the HttpPostedFileBase from the Request object’s list of files, using its name (which is why a unique name was required for the file input control on the view).

Then we do some checking, to make sure the file actually has data, and get its information.

If you’re going to save the files somewhere (which is probably why you’re letting people upload files), then you’re going to need a way to display it for them later. To display a file within a browser, you need to know its ContentType. This will let the browser display it in the correct way.

With the byte array of the file’s contents, you can either save it to the database, in a varbinary column [for SQL Server], or write it out to disk, or some other document storage location.

Where you should save the files depends on what you’re going to do with them, how large they are, and what type of file they are.

If you see an error like

The SaveAs method is configured to require a rooted path, and the path '~/App_data/Uploads/1.pdf' is not rooted.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.Web.HttpException: The SaveAs method is configured to require a rooted path, and the path '~/App_data/Uploads/1.pdf' is not rooted.

the problem is in the SaveAs function. You must pass a valid full path such as “C:\Uploads\Test\1.png”. For this reason I created in the root of the project a folder called Uploads and use Server.MapPath to find the full path.

Happy coding!

Creating a URL shortener using ASP.NET WepAPI and MVC: error handling

In the two previous post I discussed about the first step to creare this application and the implementation of the business logic. Now we can implement the error handling.

We have two custom exception classes:

  • ShorturlConflictException (when a segment already exists) and
  • ShorturlNotFoundException (when a segment isn’t found in the database).

There is also a third type: an unexpected exception, for example when there is no database connection. Normally, the user will see these errors. We have to build a mechanism where these exceptions are caught and a nice error page is shown, corresponding to the type of error, with the corresponding HTTP status code.

We need to add a filter to the Web project. Add a new folder called “Filters” and add a “ShorturlExceptionFilter” class.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using PSC.Shorturl.Web.Exceptions;

namespace PSC.Shorturl.Web.Filters
{
    public class ShorturlErrorFilter : HandleErrorAttribute
    {
        public override void OnException(ExceptionContext filterContext)
        {
            HttpStatusCode code = HttpStatusCode.InternalServerError;
            var ex = filterContext.Exception;
            string viewName = "Error500";

            if (ex is ShorturlNotFoundException)
            {
                code = HttpStatusCode.NotFound;
                viewName = "Error404";
            }
            if (ex is ShorturlConflictException)
            {
                code = HttpStatusCode.Conflict;
                viewName = "Error409";
            }

            filterContext.Result = new ViewResult()
            {
                ViewName = viewName
            };

            filterContext.ExceptionHandled = true;
            filterContext.HttpContext.Response.Clear();
            filterContext.HttpContext.Response.StatusCode = (int)code;
            filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
        }
    }
}

This filter extends from the HandleErrorAttribute, and overrides the OnException method. In this method we do our own exception handling. If the exception is one of our own exceptions, a 404 or 409 is returned, else a 500 is returned. At the bottom of the method we tell the context that the exception is handled, and the current response (the ugly error page) will be cleared and our custom views (which are defined below) will be returned.

Add the following views in the “Views\Shared” folder:

Error404.cshtml

@{
    ViewBag.Title = "Not Found";
}

<h2>Not Found</h2>

<p>The resource you've requested isn't found.</p>

Error409.cshtml

@{
    ViewBag.Title = "Conflict";
}

<h2>Conflict</h2>

<p>The name you've chosen already exists.</p>

Error500.cshtml

@{
    ViewBag.Title = "Error";
}

<h2>Error</h2>

<p>An unexpected error occured.</p>

The only thing we have to do now is add the filter to our FilterConfig class (under App_Start). In the FilterConfig class, replace the existing method with this method:

using System.Web;
using System.Web.Mvc;
using PSC.Shorturl.Web.Filters;

namespace PSC.Shorturl.Web
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new ShorturlErrorFilter());
            filters.Add(new HandleErrorAttribute());
        }
    }
}

Now, when there is an exception, a nice error page will be shown. Next post about WebApi integration.

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 http://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 http://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!

Advertsing

125X125_06

Planet Xamarin

Planet Xamarin




TagCloud

MonthList