ASP.NET MVC 4 - Making Asynchronous Calls to External Services from an Action Method

While developing applications, developers often face the challenge of retrieving data from more than one external sources. Traditional multi-threaded programming can be often complex and hard to get right. Prior to .Net 4.5, asynchronous controllers used to inherit from the AsyncController class for Async support. However with the introduction of async and await keywords in .Net 4.5, MVC 4 Controllers now fully support async Action methods. More in Task based WCF Services in .NET 4.5.

Let’s consider a scenario where you are developing a Web application that needs to access several web services to retrieve data for a single dashboard. This scenario is made simple in .NET 4.5 with C# new keywords async and await. I have already explained these keywords here and so has Filip Ekberg in Async & Await in C# 5.0

In ASP.NET MVC 4, when we target the project to .NET 4.5 we can easily make async parallel calls to external WCF services.

Building the Sample WCF Service

Step 1: Open VS 2012 and create a new blank solution, name it as ‘MVC40_Parallel_Download’. In this solution add 3 WCF service application projects, name them as ‘WCF_IPD’, ‘WCF_Pathology’ and ‘WCF_Radiology’. (Note: You can make use of WEB API instead of WCF.)


Step 2: Open WCF_Radiology. In the IService1.cs, add the following Service Contract and Data Contract.

using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;

namespace WCF_Radiology
{
[ServiceContract]
public interface IRadiology
{
  [OperationContract]
  [WebGet(UriTemplate="/Radiology",ResponseFormat=WebMessageFormat.Xml)]
  RadiologyDetails[] GetDetails();
}

[DataContract(Name = "RadiologyDetails", Namespace = "")]
public class RadiologyDetails
{
  [DataMember]
  public int PatientId { get; set; }
  [DataMember]
  public string PatientRdiologyDetails { get; set; }
  [DataMember]
  public int RadiologyBill { get; set; }
}
}

The above code defines ‘RadiologyDetails’ data contract. The Service Contract, IRadiology defines an operation contract ‘GetDetails’ which has the [WebGet] attribute. This implies that this is a WCF REST service which will expose data in the form of XML.

Step 3: Rename Service1.svc to Radiology.Svc, and implement the class as shown below in Radiology.svc.cs.

public class Radiology : IRadiology
{
RadiologyDetails[] radiologyDetails;
public Radiology()
{
  radiologyDetails = new RadiologyDetails[]{
   new RadiologyDetails(){PatientId=1,PatientRdiologyDetails="X-Ray Chest",RadiologyBill=4000},
   new RadiologyDetails(){PatientId=2,PatientRdiologyDetails="MRI-SCAN",RadiologyBill=7000},
   new RadiologyDetails(){PatientId=3,PatientRdiologyDetails="Fluoroscopy", RadiologyBill=3000},
   new RadiologyDetails(){PatientId=4,PatientRdiologyDetails="Ultrasound", RadiologyBill=6000},
   new RadiologyDetails(){PatientId=5,PatientRdiologyDetails="X-Ray Legs",RadiologyBill=3000},
  };
}
public RadiologyDetails[] GetDetails()
{
  return radiologyDetails;
}
}

Step 4: Right click on Radiology.svc and select ‘View Markup’ and change the ServiceHost directive as below:

<%@ ServiceHost Language="C#" Debug="true" Service="WCF_Radiology.Radiology" CodeBehind="Radiology.svc.cs"
     Factory="System.ServiceModel.Activation.WebServiceHostFactory"%>

Step 5: Open Web.Config file and under the <system.ServiceModel> tag, add the protocol mapping for ‘webHttpBinding’ and also define endpoint behavior for the Help page in REST services as below:

<system.serviceModel>
<protocolMapping>
  <add binding="webHttpBinding" scheme="http"/>
</protocolMapping>
<behaviors>
  <serviceBehaviors>
   <behavior>
    <serviceMetadata httpGetEnabled="true"/>
    <serviceDebug includeExceptionDetailInFaults="false"/>
   </behavior>
  </serviceBehaviors>
  <endpointBehaviors>
   <behavior>
    <webHttp helpEnabled="True"/>
   </behavior>
  </endpointBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>

Step 6: You can test the Radiology.svc in the browser and with the URL like:
http://MyServer/Radiology.svc/Radiology

You should get the data in the form of XML.

Step 7: Open WCF_Pathology and add the following code in IService.cs

using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;

namespace WCF_Pathology
{
[ServiceContract]
public interface IPathology
{
  [OperationContract]
  [WebGet(UriTemplate="/Pathology",ResponseFormat=WebMessageFormat.Xml)]
  PathologyDetils [] GetDetails();
}
[DataContract(Name="PathologyDetils",Namespace="")]
public class PathologyDetils
{
  [DataMember]
  public int PatientId { get; set; }
  [DataMember]
  public string PathologyDetails { get; set; }
  [DataMember]
  public int PathologyBill { get; set; }
}
}

The above Service Contract, Operation contract and Data Contract defines the details for Pathology.

Step 8: Rename Service1.svc to Pathology.svc and write the code as shown below:

public class Pathology : IPathology
{
PathologyDetils[] pathologyDetails;
public Pathology()
{
  pathologyDetails = new PathologyDetils[]{
  new PathologyDetils() {PatientId=1,PathologyDetails="Cholestrol",PathologyBill=2000},
  new PathologyDetils() {PatientId=2,PathologyDetails="Sugar",PathologyBill=1500},
  new PathologyDetils() {PatientId=3,PathologyDetails="TB",PathologyBill=4000},
  new PathologyDetils() {PatientId=4,PathologyDetails="Maleria",PathologyBill=1000},
  new PathologyDetils() {PatientId=5,PathologyDetails="Swine Flue",PathologyBill=2500}
  };
}
public PathologyDetils[] GetDetails()
{
  return pathologyDetails;
}
}

As mentioned in Step 4, add the code in Pathology.svc in Markup for the Factory attribute and the Service attribute value as shown below:

<%@ ServiceHost Language="C#" Debug="true" Service="WCF_Pathology.Pathology" CodeBehind="Pathology.svc.cs"
    Factory="System.ServiceModel.Activation.WebServiceHostFactory"%>

   
As mentioned in Step 5, make necessary changes in Web.Config file and as per Step 6, test the service with following URL:

http://MyServer/Pathology.svc/Pathology

Step 9: Open WCF_IPD project and in the IService1.cs, add the following code:

using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;

namespace WCF_IPD
{
[ServiceContract]
public interface IIPD
{
  [OperationContract]
  [WebGet(UriTemplate="/IPD",ResponseFormat=WebMessageFormat.Xml)]
        IPDDetails[] GetDetails();
}
[DataContract(Name = "IPDDetails",Namespace="")]
public class IPDDetails
{
  [DataMember]
  public int PatientId { get; set; }
  [DataMember]
  public string IPDCheckDetails { get; set; }
  [DataMember]
  public int IPDBill { get; set; }
}
}

The above code specifies the details for IPD.

Step 10: Rename Service1.svc to IPD.svc and in IPD.svc.cs add the below code:

public class IPD : IIPD
{
IPDDetails[] ipdDetails;
public IPD()
{
  ipdDetails = new IPDDetails[]{
  new IPDDetails() {PatientId=1,IPDCheckDetails="B.P. Check",IPDBill=200},
  new IPDDetails() {PatientId=2,IPDCheckDetails="Tempreture Check",IPDBill=300},
  new IPDDetails() {PatientId=3,IPDCheckDetails="Injection",IPDBill=130},
  new IPDDetails() {PatientId=4,IPDCheckDetails="General Checkup",IPDBill=400},
  new IPDDetails() {PatientId=5,IPDCheckDetails="Saline",IPDBill=200}
  };
}
public IPDDetails[] GetDetails()
{
  return ipdDetails;
}
}

As mentioned in Step 4, add the code in IPD.svc in Markup for the Factory attribute and the Service attribute value as below:

<%@ ServiceHost Language="C#" Debug="true" Service="WCF_IPD.IPD" CodeBehind="IPD.svc.cs"
     Factory="System.ServiceModel.Activation.WebServiceHostFactory"%>

  
As mentioned in Step 5, make necessary changes in Web.Config file and as per Step 6 test the service with below URL:

http://localhost:2126/IPD.svc/IPD

This completes the WCF Service creations, you can also host them on IIS web server. Now it’s time for us to add a new MVC 4.0 application targeted to .NET 4.5.

Setting up a client for the Service and Retrieving data asynchronously

Step 1: In the same solution, add a new MVC 4 application targeting .NET 4.5, name it as ‘MVC40_Medical_App’. Select the ‘Empty’ MVC project template.

Step 2: Since we are going to make call to the WCF REST services, we will fetch the data in the form of XML so we need concrete type to store this XML in the form of CLR object. To do this, in the Models folder add a new class file, name it as ‘ModeClassRespository.cs’, add the following classes in it:

namespace MVC40_Medical_App.Models
{
public class RadiologyDetails
{
  public int PatientId { get; set; }
  public string PatientRdiologyDetails { get; set; }
  public int RadiologyBill { get; set; }
}
public class PathologyDetils
{
  public int PatientId { get; set; }
  public string PathologyDetails { get; set; }
  public int PathologyBill { get; set; }
}
public class IPDDetails
{
  public int PatientId { get; set; }
  public string IPDCheckDetails { get; set; }
  public int IPDBill { get; set; }
}
}

Step 3: In the Controllers folder, add a new Empty MVC Controller of name ‘PatientInfoController.cs’. In the controller class, add the following code:

using MVC40_Medical_App.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Xml.Linq;

namespace MVC40_Medical_App.Controllers
{
public class PatientInfoController : Controller
{
  //
  // GET: /PatietInfo/
public  async Task<ActionResult> Index()
{
  List<Task> patientTasks = new List<Task>();
  //Make Call to Radiology Service
  using (WebClient webclientRadiology = new WebClient())
  {
   Uri uriRadiology = new   
    Uri("
http://localhost:2125/Radiology.svc/Radiology");
   patientTasks.Add(webclientRadiology.DownloadStringTaskAsync( uriRadiology));
  }

//Make Call to Pathology Service
using (WebClient webclientPathology = new WebClient())
{
  Uri uriPathology = new  
   Uri("
http://localhost:2119/Pathology.svc/Pathology");
  patientTasks.Add(webclientPathology.DownloadStringTaskAsync( uriPathology));
}
//Make Call to IPD Service
using (WebClient webclientIPD = new WebClient())
{
  Uri uriIPD = new Uri("
http://localhost:2126/IPD.svc/IPD");
              patientTasks.Add(webclientIPD.DownloadStringTaskAsync(uriIPD));
}

//This is Executed when all the Asynchronous Calls are completed.
//This will pass the data to the new List<Task> of task
await Task.WhenAll(patientTasks);

//S1: Start Working on the Result -> Radiology
var RadiologyDetails = ((Task<string>)patientTasks[0]).Result; //The Result at index 0 in List
XDocument xdocRadiology = XDocument.Parse(RadiologyDetails);

var RadiologyData = from rd in xdocRadiology.Descendants("RadiologyDetails")
  select
   new RadiologyDetails
   {
    PatientId = Convert.ToInt32(rd.Descendants("PatientId").First().Value),
    PatientRdiologyDetails = rd.Descendants("PatientRdiologyDetails").First().Value,
    RadiologyBill = Convert.ToInt32(rd.Descendants("RadiologyBill").First().Value)
   };
  ViewBag.Radiology = RadiologyData;
//S2: Start Working on the Result -> Pathology
var PathologyDetails = ((Task<string>)patientTasks[1]).Result; //The Result at index 1 in List
XDocument xdocPathology = XDocument.Parse(PathologyDetails);

var PathologyData = from pd in xdocPathology.Descendants("PathologyDetils")
  select
  new PathologyDetils()
  {
   PatientId = Convert.ToInt32(pd.Descendants("PatientId").First().Value),
   PathologyDetails = pd.Descendants("PathologyDetails").First().Value,
   PathologyBill = Convert.ToInt32(pd.Descendants("PathologyBill").First().Value)
  };
  ViewBag.Pathology = PathologyData;
  //S3: Start Working on result -> IPD
  var IPDDetails = ((Task<string>)patientTasks[2]).Result; //The Result at index 2 in List
  XDocument xdocIPD = XDocument.Parse(IPDDetails);
  var IPDData = from ipd in xdocIPD.Descendants("IPDDetails")
   select
    new IPDDetails()
    {
     PatientId =
      Convert.ToInt32(ipd.Descendants("PatientId").First().Value),
     IPDCheckDetails = ipd.Descendants("IPDCheckDetails").First().Value,
     IPDBill = Convert.ToInt32(ipd.Descendants("IPDBill").First().Value)
    };
   ViewBag.IPD = IPDData;
   return View();
  }
}
}

The Index Action method is very interesting here. The first thing is, it is an async method and the return type is Task<ActionResult>. This indicates that, the method performs Task based asynchronous operations which returns value. To make parallel asynchronous calls to the above created external WCF services, the WebClient class is used. The DownloadStringTaskAsync() method downloads the resource as a string from the URI specified as an asynchronous operation using task object. Here in the code, each received Task object is added into the List<Task> object declared into the beginning of the method.

There are 3 parallel asynchronous calls made to the external services. Now after parallel calls are completed, to process the data, the Task.WhenAll() method is used. This is executed when all asynchronous call are completed. The return values i.e. Task object from each call is then passed to this method so that the data can be further processed.

The rest of the code of this method reads the Task result from each index of the List<Task> object and using LINQ to XML, the Xml response is stored into the List of the Model classes we created in MVC project Step 2. This data is now passed to view using ViewBag object.

Step 4: Add a new Empty view using the Index action method and add the following code in it:

<h2>Hospital Daily Statistics</h2>
<table border="1">
<thead>
  <tr>
   <td>Radiology Department</td>
   <td>Pathology Department</td>
   <td>IPD Department</td>
  </tr>
</thead>
<tr>
  <td>
  @{
   var gridRadiology = new WebGrid(ViewBag.Radiology);
   @gridRadiology.GetHtml(columns:gridRadiology.Columns
    (gridRadiology.Column("PatientId","Patient Id"),
     gridRadiology.Column("PatientRdiologyDetails","Radiology Details"),
     gridRadiology.Column("RadiologyBill","Bill to Pay")));
   }
  </td>
  <td>
  @{
    var gridPathology = new WebGrid(ViewBag.Pathology);
    @gridPathology.GetHtml(
     columns:gridPathology.Columns(
     gridPathology.Column("PatientId","Patient Id"),
     gridPathology.Column("PathologyDetails","Pathology Details"),
     gridPathology.Column("PathologyBill","Bill to Pay")
    ));
   }
   </td>
   <td>
   @{
    var gridIPD = new WebGrid(ViewBag.IPD);
    @gridIPD.GetHtml(
     columns:gridIPD.Columns(
     gridIPD.Column("PatientId","Patient Id"),
     gridIPD.Column("IPDCheckDetails","IPD Checking Details"),
     gridIPD.Column("IPDBill","Bill to Pay")
    ));
   }
  </td>
</tr>
</table>

  
The above code makes use of WebGrid helper method to which the ViewBag object defined in the controller is passed.

Step 5: Run the application, the result will be as below:

final-dashboard-view

Conclusion

While developing WEB Applications, when you have a requirement to retrieve data from multiple services at the same time, then it is a good idea to use Asynchronous action methods in .NET 4.5. This will make your code less complex and easy to maintain.

Download the entire source code of this article (Github)

2 comments:

  1. good article..can you suggest or give me some code to find backlinks to my site in asp.net(C#).

    ReplyDelete
  2. Dear Mahesh,

    Really informative. This will be quite useful when building travel portal application using multiple GDS.

    Thanks Mahesh.

    Regards,
    Suresh

    ReplyDelete