DotNetCurry Logo

Practical Use of ASP.NET Web API Attribute Routing in an MVC application

Posted by: Mahesh Sabnis , on 8/26/2014, in Category ASP.NET MVC
Views: 44168
Abstract: In ASP.NET Web API 2 a feature called Attribute Routing was introduced. This article demonstrates uses of the Attribute Routing in an ASP.NET MVC and Knockout.js application.

ASP.NET MVC is now widely used for web development on the .NET platform. It can be easily be integrated with the client-side JavaScript Frameworks and Libraries. The framework has incrementally progressed from one version to the other. In the latest stable release of MVC 5 and WEB API 2 a relatively new feature named ‘Attribute Routing’ was introduced. This feature provides more control over the URIs in ASP.NET WebAPI for performing HTTP Operations. In earlier versions of WEB API we had conventional routing and separate action methods for Http GET,POST, PUT and DELETE which made it challenging to define action methods for similar Http operations (like multiple Http GET actions). In WEB API 2 with the advantage of the attribute routing, we can have multiple similar actions in the same API controller class.

 

Note: This article shows uses of the Attribute Routing in WEB API 2. You can take an overview of the other features on WebAPI in What's New in ASP.NET Web API 2.0 .

To understand the context better, check a previous article of mine titled Customizing ASP.NET Web API Routing for the User Defined methods in ApiController class to understand the challenges of having similar Http actions in a previous version of WEB API, v1.0.

Practical need of attribute routing in ASP.NET WEB API 2.0

Consider a scenario where you need to retrieve the Sales information of each Sales Agent in each Territory. In the database, the Sales table has references of AgentId and TerritoryId as shown here:

webapi-db-relations

The above design clearly specifies that Sales is a logical child of Agent and Territory (and Product). This means if we need to access Sales data with logical combinations of Agent and the Territory using HTTP, then the URL can be really complex. For e.g. if we want to retrieve Sales data based on AgentId for a specific Territory then the expected URL will be as below:

Agent/1/AND/Territory/1 or Agent/1/OR/Territory/1,

In the earlier version, implementing such a complex URL in WEB API (v1.0) actions was difficult. In WEB API 2, attribute routing can be applied as seen below:

[Route(Agent/1/AND/Territory/1)]

Using WEB API 2 in an ASP.NET MVC Application

In this ASP.NET MVC application, we will be using WEB API 2, EntityFramework, jQuery and Knockout.js.

Step 1: Open Visual Studio 2013 and create a new Empty ASP.NET MVC Application, name it as ‘MVC_FIlteringData_WEBAPI’. In the App_Data Folder of the application, add a new SQL Server Database of the name ‘Company’ and create the following tables:

CREATE TABLE [dbo].[Agent] (
    [AgentId]   INT          IDENTITY (1, 1) NOT NULL,
    [AgentName] VARCHAR (50) NOT NULL,
    PRIMARY KEY CLUSTERED ([AgentId] ASC)
);

CREATE TABLE [dbo].[Territory] (
    [TerritoryId]   INT          IDENTITY (1, 1) NOT NULL,
    [TerritoryName] VARCHAR (50) NOT NULL,
    PRIMARY KEY CLUSTERED ([TerritoryId] ASC)
);


CREATE TABLE [dbo].[Product] (
    [ProductId]   INT          IDENTITY (1, 1) NOT NULL,
    [ProductName] VARCHAR (50) NOT NULL,
    PRIMARY KEY CLUSTERED ([ProductId] ASC)
);

CREATE TABLE [dbo].[Sales] (
    [SalesRecordId] INT IDENTITY (1, 1) NOT NULL,
    [AgentId]       INT NOT NULL,
    [TerritoryId]   INT NOT NULL,
    [ProductId]     INT NOT NULL,
    [Quantity]      INT NOT NULL,
    PRIMARY KEY CLUSTERED ([SalesRecordId] ASC),
    FOREIGN KEY ([AgentId]) REFERENCES [dbo].[Agent] ([AgentId]),
    FOREIGN KEY ([TerritoryId]) REFERENCES [dbo].[Territory] ([TerritoryId]),
    FOREIGN KEY ([ProductId]) REFERENCES [dbo].[Product] ([ProductId])
);

Step 2: In the project, in the Models folder, add the ADO.NET EF and select the Company database created above. After the completion of the wizard of EntityFramework, the mapping will be displayed as seen here:

webapi-db-relations

Step 3: In the Models folder add a new class:

public class SalesInfo
{
    public int SalesRecordId { get; set; }
    public string AgentName { get; set; }
    public string TerritoryName { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
}

The above class is an arrangement for specifying the format of sales data returned to the client.

Step 4: In the Controllers folder add a new Empty WEB API 2 controller of the name ‘SalesInfoAPIController’. In the controller add the following code:

using MVC_FIlteringData.Models;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace MVC_FIlteringData.Controllers
{
    public class SalesInfoAPIController : ApiController
    {
        CompanyEntities ctx;
        public SalesInfoAPIController()
        {
            ctx = new CompanyEntities(); 
        }

        [Route("Agents")]
        public IEnumerable<Agent> GetAgents()
        {
            List<Agent> Agents = new List<Agent>();  

            var result = (from a in ctx.Agents
                          select new  { 
                           AgentId = a.AgentId,
                            AgentName = a.AgentName
                          }).ToList();


            foreach (var item in result)
            {
                Agents.Add(new Agent() {AgentId = item.AgentId,AgentName = item.AgentName });
            }

            return Agents;
        }

        [Route("Territories")]
        public IEnumerable<Territory> GetTerritories()
        {
            List<Territory> Territories = new List<Territory>();

            var result = (from a in ctx.Territories
                          select new
                          {
                              TerritoryId = a.TerritoryId,
                              TerritoryName = a.TerritoryName
                          }).ToList();


            foreach (var item in result)
            {
                Territories.Add(new Territory() {TerritoryId  = item.TerritoryId, TerritoryName = item.TerritoryName });
            }

            return Territories;
        }

        [Route("SalesInfo")]
        public IEnumerable<SalesInfo> GetSales()
        {
            List<SalesInfo> Sales = new List<SalesInfo>();

            var result = from s in ctx.Sales
                         select new { 
                            SalesRecordId = s.SalesRecordId,
                            AgentId = s.AgentId,
                            TerritoryId = s.TerritoryId,
                            ProductId = s.ProductId,
                            Quantity = s.Quantity
                         };

            foreach (var item in result)
            {
                Sales.Add(new SalesInfo()
                  {
                      SalesRecordId = item.SalesRecordId,
                      AgentName =  ctx.Agents.Where(id=>id.AgentId==item.AgentId).First().AgentName,
                      TerritoryName = ctx.Territories.Where(id=>id.TerritoryId==item.TerritoryId).First().TerritoryName,
                      ProductName = ctx.Products.Where(id=>id.ProductId==item.ProductId).First().ProductName,
                      Quantity = item.Quantity
                  });
            }

            return Sales;
        }


        /// <summary>
        /// The Method works with Multiple Parameters in the URL
        /// Here filter parameter is 'AND' or 'OR'
        /// </summary>
        /// <param name="agentId"></param>
        /// <param name="filter"></param>
        /// <param name="territoryId"></param>
        /// <returns></returns>
        [Route("Agents/{agentId}/{filter}/Territories/{territoryId}")]
        public IEnumerable<SalesInfo> GetSales(int agentId,string filter,int territoryId)
        {
            List<SalesInfo> Sales = new List<SalesInfo>();
            filter = filter.ToUpper();
             
            switch (filter)
            {
                case "AND":
                    var result1 = from s in ctx.Sales
                                 where s.AgentId == agentId && s.TerritoryId == territoryId
                                 select new
                                 {
                                     SalesRecordId = s.SalesRecordId,
                                     AgentId = s.AgentId,
                                     TerritoryId = s.TerritoryId,
                                     ProductId = s.ProductId,
                                     Quantity = s.Quantity
                                 };

                    foreach (var item in result1)
                    {
                        Sales.Add(new SalesInfo()
                        {
                            SalesRecordId = item.SalesRecordId,
                            AgentName = ctx.Agents.Where(id => id.AgentId == item.AgentId).First().AgentName,
                            TerritoryName = ctx.Territories.Where(id => id.TerritoryId == item.TerritoryId).First().TerritoryName,
                            ProductName = ctx.Products.Where(id => id.ProductId == item.ProductId).First().ProductName,
                            Quantity = item.Quantity
                        });
                    }

                    break;
                case "OR":
                   var  result2 = from s in ctx.Sales
                                 where s.AgentId == agentId || s.TerritoryId == territoryId
                                 select new
                                 {
                                     SalesRecordId = s.SalesRecordId,
                                     AgentId = s.AgentId,
                                     TerritoryId = s.TerritoryId,
                                     ProductId = s.ProductId,
                                     Quantity = s.Quantity
                                 };

                   foreach (var item in result2)
                   {
                       Sales.Add(new SalesInfo()
                       {
                           SalesRecordId = item.SalesRecordId,
                           AgentName = ctx.Agents.Where(id => id.AgentId == item.AgentId).First().AgentName,
                           TerritoryName = ctx.Territories.Where(id => id.TerritoryId == item.TerritoryId).First().TerritoryName,
                           ProductName = ctx.Products.Where(id => id.ProductId == item.ProductId).First().ProductName,
                           Quantity = item.Quantity
                       });
                   }

                    break;
            }

            return Sales;
        }
    }
}

The above implementation is very important; the API controller class has all action methods for HTTP GET operations. All the GET actions are differentiated using the Route Attributes applied on them.

· The method ‘GetAgents()’ is applied with Route Attribute as [Route(“Agents”)], so the Url for the method will be:

http://server/WebSite/Agents

· One of the major advantages of the Attribute Routing is the support provided for multi-parameters passed to the action method. In the above class, the method GetSales() accepts 3 parameters of name agentId, filter and territoryId which are of the type int, string, int respectively. The Routing for this method is defined using

[Route("Agents/{agentId}/{filter}/Territories/{territoryId}")]

In the above route, the input parameters for the method are defined using the ‘{}’ expressions. The Url for the GetSales() will look as below:

http://server/WebSite/Agents/1/AND/Territories/1

Here agentid is 1, filter is ‘AND’ and territoryId is 1. This means that for the specific AgentId, and for a specific TerritoryId, the data will be retrieved based upon the filter value like All, AND, OR.

As per the Attribute Route applied on actions of the API controller class, the URL for action will be as below:

Method Name

Attribute Route

URL

GetAgents()

[Route(Agents)]

http://Server/WebSite/Agents

GetTerritories()

[Route(Territories)]

http://Server/WebSite/Territories

GetSales()

[Route(SalesInfo)]

http://Server/WebSite/SalesInfo

GetSales()

[Route("Agents/{agentId}/{filter}/Territories/{territoryId}")]

http://Server/WebSites/Agents/1

/OR/Territories/1

Note: Filter can be ‘AND’, ‘All’, etc.

To support and enable the Attribute Routing for ASP.NET WEB API 2, the Register method from the WebApiConfig class has been used as follows:

 

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Step 5: Using the Index Action method of the Home Controller, add a new Empty View in the project. In this Index.cshtml add the following html:

<table id="tbl1">
    <tr>
        <td>
            Agent
        </td>
        <td>
            <select id="lstagents"></select>
        </td>
        <td>
            Filter
        </td>
        <td>
            <select id="lstoption">
                 
            </select>
        </td>
        <td>
            Territory
        </td>
        <td>
            <select id="lstterritories"></select>
        </td>
    </tr>
</table>
<div></div>

<table class="clstable">
    <thead>
        <tr>
            <th class="clstable">SalesRecordId</th>
            <th class="clstable">AgentName</th>
            <th class="clstable">TerritoryName</th>
            <th class="clstable">ProductName</th>
            <th class="clstable">Quantity</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td class="clstable"> <span></span></td>
            <td class="clstable"> <span></span></td>
            <td class="clstable"> <span></span></td>
            <td class="clstable"> <span></span></td>
            <td class="clstable"> <span></span></td>
        </tr>
    </tbody>
</table>

Step 6: For this application to consume the WEB APIs, we will be using jQuery and Knockout libraries. I am using Knockout because of it’s excellent databinding features. Add these libraries in the project using NuGet Package Manager. In the Index.cshtml add the script references for jQuery and Knockout.js and styles as shown here:

<script src="~/Scripts/jquery-2.1.1.min.js"></script>
<script src="~/Scripts/knockout-3.2.0.js"></script>

<style type="text/css">
    .clstable{
    border:double;
    }
</style>

<script type="text/javascript">
    var ViewModel = function (){
        var self = this;

        //Observable Arrays for Agents, Territories, Sales and Filters
        
        self.Agents = ko.observableArray([]);
        self.Territories = ko.observableArray([]);
        self.Sales = ko.observableArray([]);
        self.FilterOptions = ko.observableArray([
            "All", "AND", "OR"
        ]);

        //Observable declaration for Selected Agent, Territory and Filters

        self.SelectedAgent = ko.observable();
        self.SelectedTerritory = ko.observable();
        self.SelectedFilter = ko.observable();

        var filter;

        loadagents();
        loadterritories();


        //The Function used to Load Agents
        function loadagents() {
            $.ajax({
                url: "/Agents",
                type: "GET"
            }).done(function (resp) {
                ko.utils.arrayForEach(resp, function (item) {
                    self.Agents.push({AgentId:item.AgentId,AgentName:item.AgentName});
                });
            }).error(function (err) {
                alert("Error " + err.status + "  " + err.statusText);
            });
        }

        //The Function used to Load Terrotories
        function loadterritories() {
            $.ajax({
                url: "/Territories",
                type: "GET"
            }).done(function (resp) {
                ko.utils.arrayForEach(resp, function (item) {
                    self.Territories.push({TerritoryId:item.TerritoryId,TerritoryName:item.TerritoryName});
                });
            }).error(function (err) {
                alert("Error " + err.status + "  " + err.statusText);
            });
        }

        


        self.SelectedAgent.subscribe(function (val) {
            
            if (self.SelectedTerritory() !== 'undefined') {
                loadSalesData(val, filter, self.SelectedTerritory());
            }
            else {
                alert("Please Select the Territory Name");
            }

        });

        self.SelectedTerritory.subscribe(function (val) {
            
            if (self.SelectedAgent() !== 'undefined') {
                loadSalesData(self.SelectedAgent(),filter,val);
            }
            else {
                alert("Please Select the Agent Name");
            }
        });


        self.SelectedFilter.subscribe(function (v) {
            filter = v;
           
            loadSalesData(self.SelectedAgent(), filter, self.SelectedTerritory());
        });

        //Function to load Sales Information
        //If the filter is 'All' the complete Sales information will be loaded
        //Else the Sales information based upon 'AND' and 'OR' condition combination of the
        //Agent and territory will be loaded
        function loadSalesData(agentId,filter,territoryId) {
            var url;
            if (filter === 'All') {
                url = "/SalesInfo";
            }
            else {
                url = "/Agents/" + agentId + "/" + filter + "/Territories/" + territoryId;
            }

            $.ajax({
                url: url,
                type:"GET"
            }).done(function (resp) {
                self.Sales(resp);
            }).error(function (err) {
                alert("Error " + err.status + "  " + err.statusText);
            });
        }
       
    };
    ko.applyBindings(new ViewModel());
</script>

In the above JavaScript, the ViewModel code has the following specification:

· The code defines observable arrays for storing Agents, Territories, Sales and Filter Options. The FilterOptions observable array defines search conditions e.g. ALL, will return all sales records, whereas AND and OR return sales records based upon the logical conditions of Agent and Territory.

· Functions ‘loadagents()’ and ‘loadterritories()’ will make call to WEB API and retrieve the respective data from the server which will be pushed in the respective observable array.

· Function ‘loadSalesData()’ accepts agentId, filter and territoryId as input parameters. If the filter is ‘All’ then all sales data will be retrieved using ‘/SalesInfo’ URL. If the value of the filter is either ‘AND’ or ‘OR’ then the URL will be formed based upon the ‘AND’ or ‘OR’ combination of agentId and territoryId and the sales data will be fetched.

Step 7: Since we have used Knockout library, we need to bind the Observables and ObservableArray to HTML UI as below:

<table id="tbl1">
    <tr>
        <td>
            Agent
        </td>
        <td>
            <select id="lstagents" data-bind="options:Agents,optionsCaption:'Choose Agent Name',value:SelectedAgent,optionsText:'AgentName',optionsValue:'AgentId'"></select>
        </td>
        <td>
            Filter
        </td>
        <td>
            <select id="lstoption" data-bind="options:FilterOptions,value:SelectedFilter">
                 
            </select>
        </td>
        <td>
            Territory
        </td>
        <td>
            <select id="lstterritories" data-bind="options:Territories,optionsCaption:'Choose Territory Name',value:SelectedTerritory,optionsText:'TerritoryName',optionsValue:'TerritoryId'"></select>
        </td>
    </tr>
</table>
<div></div>

<table class="clstable">
    <thead>
        <tr>
            <th class="clstable">SalesRecordId</th>
            <th class="clstable">AgentName</th>
            <th class="clstable">TerritoryName</th>
            <th class="clstable">ProductName</th>
            <th class="clstable">Quantity</th>
        </tr>
    </thead>
    <tbody data-bind="foreach:Sales">
        <tr>
            <td class="clstable"> <span data-bind="text:SalesRecordId"></span></td>
            <td class="clstable"> <span data-bind="text:AgentName"></span></td>
            <td class="clstable"> <span data-bind="text:TerritoryName"></span></td>
            <td class="clstable"> <span data-bind="text:ProductName"></span></td>
            <td class="clstable"> <span data-bind="text:Quantity"></span></td>
        </tr>
    </tbody>
</table>

In the above code, the html < select > element is bound with the ObservableArray using ‘options’ binding, the value binding is set to the Observable declared in the ViewModel. The optionsText and optionsValue corresponds to the selectedtext and selectedvalue from the < select > element.

Step 8: Run the application and the result will be as seen here:

When the page is loaded, it will show all records from sales because the filter is set to value ‘All’ and the result will be as seen here:

filter-all

Select the AgentName and the TerritoryName from the List and change the Filter to ‘AND’, the result will be as below:

 

sales-record-knockout-webapi

The above screenshot shows the sale by Agent Deepak in Maharashtra Territory.

Change the filter to OR, the result will be as below:

filter-all

Conclusion: The Attribute Routing provided with ASP.NET WEB API 2 makes it easy to define clear URLs with multiple parameter for action methods. The major advantage here is that in one API controller class, we can have multiple methods of same types (multiple HTTP Get etc.)

Download the entire source code of this article (Github)

Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+
Further Reading - Articles You May Like!
Author
Mahesh Sabnis is a DotNetCurry author and Microsoft MVP having over 17 years of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions). Follow him on twitter @maheshdotnet


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!
Comment posted by Yogendra Sharma on Tuesday, September 9, 2014 7:01 AM
llml
Comment posted by Jacobus Terhorst on Wednesday, October 1, 2014 9:56 AM
You can use this:

            var agents = (from a in ctx.Agents
                          select new Agent {
                              a.AgentId,
                            a.AgentName
                          }).ToList();

            return agents;

instead of this:

List<Agent> Agents = new List<Agent>();

            var result = (from a in ctx.Agents
                          select new  {
                           AgentId = a.AgentId,
                            AgentName = a.AgentName
                          }).ToList();


            foreach (var item in result)
            {
                Agents.Add(new Agent() {AgentId = item.AgentId,AgentName = item.AgentName });
            }

            return Agents;
Comment posted by MAhesh Sabnis on Wednesday, November 12, 2014 3:26 AM
Hi Jacobus Terhorst
Thanks for the suggestions.
Regards
Mahesh Sabnis
Comment posted by Srikanth on Monday, December 8, 2014 5:19 AM
Excellent article.. Thanks
Comment posted by Kenny Bright on Tuesday, May 12, 2015 5:52 PM
Nice article. thanks for sharing