“Chishiki” is Japanese for “knowledge.” e-chishiki.com aims to bring software developers, information security professionals, IT executives and other IT pros a rich body of knowledge in the form of articles, interviews, tutorials and technical discussions. Our contributors are among the biggest names in the Indian IT industry and include noted authors, educators and practitioners.
Weekly Programming Series - A Web Services Primer
A Web Services Primer (9/10): Until We REST!
Yashavant Kanetkar and Asang Dani
REST Mechanisms In ASP.NET
To represent the APIs in a Web Service as URLs, we need to use a feature in the ASP.NET framework to provide our own virtual paths within the web server. It will require us to parse the URL for incoming requests and map it to existing Web Service APIs. We will extend two abstract framework classes for this purpose:
- CurrencyPathProvider – extends abstract class VirtualPathProvider
- CurrencyVirtualFile – extends abstract class VirtualFile
Overriding CurrencyPathProvider will allow us to get hooked in the URL parsing process and crease a "virtual" namespace of our own. CurrencyFile class will stream the data for a specific URL and is the heart of the Web Service implementation. The path provider class must be registered with the ASP.NET hosting environment in the Application_Start event handler. You can add Global.asax file in your ASP.Net project by choosing Add New Item and choosing Global Application Class. This code is given below.
Global.asax
[AspNetHostingPermission(
System.Security.Permissions.SecurityAction.Demand,
Level = AspNetHostingPermissionLevel.High )]
void Application_Start( object sender, EventArgs e )
{
CurrencyPathProvider provider = new CurrencyPathProvider( ) ;
System.Web.Hosting.HostingEnvironment
.RegisterVirtualPathProvider ( provider ) ;
}
As you can notice from this code, we have created an object of our class CurrencyPathProvider, and registered it with the Web Hosting environment.
Let us now see the implementation of CurrencyPathProvider. This class should be in the App_Code folder of your ASP.NET Web Service.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Security.Permissions;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;
[AspNetHostingPermission ( SecurityAction.Demand,
Level = AspNetHostingPermissionLevel.Medium ) ]
[AspNetHostingPermission ( SecurityAction.InheritanceDemand,
Level = AspNetHostingPermissionLevel.High ) ]
public class CurrencyPathProvider : VirtualPathProvider
{
private CurrencyVirtualFile m_file ;
private CurrencyService m_Service ;
private CountryCurrency[ ] m_CountryCurrencies ;
private List<String> m_nouns ;
public const string cNounSymbol = "~/symbol" ;
public const string cNounCountry = "~/country" ;
public CurrencyPathProvider ( )
{
m_Service = new CurrencyService ( ) ;
m_Service.sHeader = new SubscriberHeader ( ) ;
m_Service.sHeader.id = "95e62420-d529-49f4-ba79-151df61ac788" ;
m_CountryCurrencies = m_Service.GetCountryCurrencies ( ) ;
m_nouns = new List<string>( ) ;
m_nouns.Add ( cNounSymbol ) ;
m_nouns.Add ( cNounCountry ) ;
}
public override bool FileExists ( string virtualPath )
{
bool result ;
// If the path is virtual, see if the file exists.
if ( isPathVirtual( virtualPath ) )
{
// Create the file and return the value of Exists property.
m_file = ( CurrencyVirtualFile )GetFile( virtualPath );
result = m_file.Exists ;
}
else
{
// if the path is not virtual, use the default path provider.
result = Previous.FileExists ( virtualPath ) ;
}
return result ;
}
public override VirtualFile GetFile ( string virtualPath )
{
VirtualFile file ;
// If the path is virtual, get the file from virtual filesystem
if ( isPathVirtual( virtualPath ) )
{
// If file has already been created,
// return the existing instance
if ( m_file != null &&
m_file.VirtualPath.StartsWith ( virtualPath ,
StringComparison.InvariantCultureIgnoreCase ) )
{
file = m_file;
}
else
file = new CurrencyVirtualFile ( virtualPath, this ) ;
}
else
{
// If the file is not virtual, use the default path provider.
file = Previous.GetFile( virtualPath );
}
return file;
}
public override CacheDependency GetCacheDependency (
string virtualPath,
IEnumerable virtualPathDependencies,
DateTime utcStart )
{
if ( isPathVirtual( virtualPath ) )
{
// If the path is virtual, there is no cache dependancy.
// You could put a cache dependancy here on a file,
// a SQL table, or some other cache trigger.
return null;
}
else
{
// If the file is not virtual, use the default path provider.
return Previous.GetCacheDependency(
virtualPath,
virtualPathDependencies,
utcStart
);
}
}
public CurrencyService Service
{
get
{
return m_Service ;
}
}
// Potential Virtual URLs
// ~/symbol/ - LIST / GET
// ~/country/ - LIST / GET
// ~/country/India - GET
// ~/symbol/INR/USD - GET
// ~/country/India/United%20States - GET
private bool isPathVirtual( string virtualPath )
{
bool result = false;
string ckPath = VirtualPathUtility.ToAppRelative( virtualPath ) ;
foreach ( string noun in m_nouns )
{
if ( ckPath.StartsWith( noun,
StringComparison.InvariantCultureIgnoreCase ) )
{
result = true;
break ;
}
}
return result ;
}
public bool isValidCountry ( string n )
{
foreach ( CountryCurrency c in m_CountryCurrencies )
if ( string.Compare( c.country, n, true ) == 0 )
return true ;
return false ;
}
public bool isValidSymbol( string n )
{
foreach ( CountryCurrency c in m_CountryCurrencies )
{
if ( string.Compare( c.symbol, n, true ) == 0 )
return true;
}
return false;
}
public bool isValidName( string n )
{
return isValidCountry( n ) || isValidSymbol( n );
}
public string NounRemove( string path )
{
foreach ( string noun in m_nouns )
{
if ( path.StartsWith( noun,
StringComparison.InvariantCultureIgnoreCase ) )
return path.Replace( noun + "/", "" ).Replace( noun, "" );
}
return path;
}
public string NounGet ( string path )
{
foreach ( string noun in m_nouns )
{
if ( path.StartsWith( noun,
StringComparison.InvariantCultureIgnoreCase ) )
return noun;
}
return null;
}
}
Let us understand this implementation one step at a time.
- The constructor creates an instance of CurrencyService class and initializes the subscriber header field. It also stores the list of currencies by invoking the GetCountryCurrencies( ) API. This will enable us to validate the virtual URL paths. Finally, it adds the "nouns" i.e. URL components that we support, viz: country and symbol in a collection of acceptable URL starting points.
- FileExists( ) is a base class function that we must override. This function has the responsibility of checking if the incoming request is a valid REST URL. It relies on private function isPathVirtual( ) to do the real job. If the nouns are not in our list, then it invokes the default provider by calling Previous.FileExists ( virtualPath ).
- GetFile( ) is another base class function that we must override. It constructs a CurrencyVirtualFile object for the specified virtual path and returns it to the caller. It checks if the CurrencyVirtualFile object already exists before calling its constructor.
- GetCacheDependency( ) function returns null for all virtual paths that we handle. Otherwise, it uses the default path provider's implementation.
- Finally, we have provided numerous helper functions to extract noun from the virtual path, to check if the country name and symbol components are valid etc. We will show how to use of these functions, when we implement the CurrencyVirtualFile class.
- Implementing CurrencyVirtualFile
This class implements "virtual file" abstraction. This creates an illusion that the virtual path present in the URL actually exists. The data from this file object will be read by ASP.NET to generate the response. In order to generate a correct response, we have to parse each virtual path to determine the Web Service API used and extract parameters for the request.
using System.IO ;
using System.Security.Permissions ;
using System.Text ;
using System.Web ;
using System.Web.Hosting ;
enum RestReqType
{
Invalid = 0,
ListAllCurrencies,
ListAllCountries,
GetCountryCurrency,
GetRateBySymbol,
GetRateByCountry
}
[AspNetHostingPermission ( SecurityAction.Demand,
Level = AspNetHostingPermissionLevel.Minimal ) ]
[AspNetHostingPermission ( SecurityAction.InheritanceDemand,
Level = AspNetHostingPermissionLevel.Minimal ) ]
public class CurrencyVirtualFile : VirtualFile
{
private bool m_exists = false ;
private string m_virtualPath ;
private string m_Noun ;
private string[ ] m_Parts = null ;
private RestReqType m_reqType = RestReqType.Invalid ;
private CurrencyPathProvider m_Provider ;
public CurrencyVirtualFile ( string virtualPath,
CurrencyPathProvider provider ) : base ( virtualPath )
{
m_Provider = provider ;
m_virtualPath = VirtualPathUtility.ToAppRelative (
virtualPath.TrimEnd( '/' ) ) ;
string cmdPath = m_Provider.NounRemove( m_virtualPath ) ;
if ( !string.IsNullOrEmpty( cmdPath ) )
m_Parts = cmdPath.Split( '/' ) ;
m_Noun = m_Provider.NounGet ( m_virtualPath ) ;
m_exists = false ;
string reqType = HttpContext.Current.Request.RequestType ;
if ( m_Parts == null || m_Parts.Length == 0 )
{
if ( reqType == "LIST" || reqType == "GET" )
{
switch ( m_Noun )
{
case CurrencyPathProvider.cNounCountry:
m_reqType = RestReqType.ListAllCountries ;
m_exists = true ;
break ;
case CurrencyPathProvider.cNounSymbol:
m_reqType = RestReqType.ListAllCurrencies ;
m_exits = true ;
break ;
}
}
}
else if ( m_Parts.Length > 0 )
{
if ( m_Parts.Length == 1 )
{
if ( reqType == "GET" &&
m_Noun == CurrencyPathProvider.cNounCountry &&
m_Provider.isValidCountry( m_Parts[0] ) )
{
m_reqType = RestReqType.GetCountryCurrency ;
m_exists = true ;
}
}
else if ( m_Parts.Length == 2 && reqType == "GET" )
{
switch ( m_Noun )
{
case CurrencyPathProvider.cNounCountry:
if ( m_Provider.isValidCountry( m_Parts[0] ) &&
m_Provider.isValidCountry( m_Parts[1] ) )
{
m_exists = true ;
m_reqType = RestReqType.GetRateByCountry ;
}
break ;
case CurrencyPathProvider.cNounSymbol:
if ( m_Provider.isValidSymbol( m_Parts[0] ) &&
m_Provider.isValidSymbol( m_Parts[1] ) )
{
m_exists = true ;
m_reqType = RestReqType.GetRateBySymbol ;
}
break ;
}
}
}
}
public bool Exists
{
get
{
return m_exists;
}
}
public override Stream Open( )
{
switch ( m_reqType )
{
case RestReqType.Invalid:
break ;
case RestReqType.GetCountryCurrency:
return GetCountryCurrency( m_Parts[0] ) ;
case RestReqType.GetRateBySymbol:
return GetRateBySymbol( m_Parts[0], m_Parts[1] ) ;
case RestReqType.GetRateByCountry:
return GetRateByCountry( m_Parts[0], m_Parts[1] ) ;
case RestReqType.ListAllCountries:
return ListAllCountries( ) ;
case RestReqType.ListAllCurrencies:
return ListAllCurrencies( ) ;
default:
break ;
}
return null ;
}
private Stream ListAllCurrencies( )
{
MemoryStream s = null ;
CountryCurrency[ ] cc =
m_Provider.Service.GetCountryCurrencies ( ) ;
StringBuilder sb = new StringBuilder( ) ;
sb.Append( "<?xml version=\"1.0\"?>" ) ;
sb.Append( "<Countries>" ) ;
foreach ( CountryCurrency c in cc )
{
sb.Append( "<Country>" ) ;
sb.Append( "<Name>" + c.country + "</Name>" ) ;
sb.Append( "<Currency>" + c.currency.Trim() + "</Currency>" ) ;
sb.Append( "<Symbol>" + c.symbol + "</Symbol>" ) ;
sb.Append( "</Country>" ) ;
}
sb.Append( "</Countries>" ) ;
s = new MemoryStream( ) ;
byte[ ] data = Encoding.UTF8.GetBytes( sb.ToString( ) ) ;
s.Write( data, 0, data.Length ) ;
s.Position = 0 ;
return s ;
}
private Stream ListAllCountries( )
{
MemoryStream s = null ;
string[] countries = m_Provider.Service.GetCountryNames( ) ;
StringBuilder sb = new StringBuilder( ) ;
sb.Append( "<?xml version=\"1.0\"?>" ) ;
sb.Append( "<Countries>" );
foreach ( string name in countries )
{
sb.Append( "<Country>" ) ;
sb.Append( "<Name>" + name + "</Name>" ) ;
sb.Append( "</Country>" ) ;
}
sb.Append( "</Countries>" ) ;
s = new MemoryStream( );
byte[ ] data = Encoding.UTF8.GetBytes( sb.ToString( ) ) ;
s.Write( data, 0, data.Length ) ;
s.Position = 0 ;
return s ;
}
private Stream GetCountryCurrency( string p )
{
MemoryStream s = null;
CountryCurrency c = m_Provider.Service.GetCurrency( p );
StringBuilder sb = new StringBuilder( );
sb.Append( "<?xml version=\"1.0\"?>" );
sb.Append( "<Country>" );
sb.Append( "<Name>" + c.country + "</Name>" );
sb.Append( "<Currency>" + c.currency.Trim() + "</Currency>" );
sb.Append( "<Symbol>" + c.symbol + "</Symbol>" );
sb.Append( "</Country>" ) ;
s = new MemoryStream( ) ;
byte[ ] data = Encoding.UTF8.GetBytes( sb.ToString( ) );
s.Write( data, 0, data.Length ) ;
s.Position = 0 ;
return s ;
}
private Stream GetRateBySymbol( string from, string to )
{
MemoryStream s = null;
float rate = m_Provider.Service.GetRateBySymbol( from, to );
StringBuilder sb = new StringBuilder( );
sb.Append( "<?xml version=\"1.0\"?>" );
sb.Append( "<Rate>" + rate + "</Rate>" );
s = new MemoryStream( );
HttpContext.Current.Response.ContentType = "text/xml";
byte[ ] data = Encoding.UTF8.GetBytes( sb.ToString( ) );
s.Write( data, 0, data.Length );
s.Position = 0;
return s;
}
private Stream GetRateByCountry( string from, string to )
{
MemoryStream s = null;
float rate = m_Provider.Service.GetRateByCountry( from, to );
StringBuilder sb = new StringBuilder( );
sb.Append( "<?xml version=\"1.0\"?>" );
sb.Append( "<Rate>" + rate + "</Rate>" );
s = new MemoryStream( );
byte[ ] data = Encoding.UTF8.GetBytes( sb.ToString( ) );
s.Write( data, 0, data.Length );
s.Position = 0;
return s;
}
}
Let us understand this implementation one step at a time.
-
The constructor stores a reference to the CurrencyPathProvider object. The virtual path is converted from its original form which is application relative - /CurrencyService/symbol/INR/USD to ~/symbol/INR/USD. This path is then split using separate / character and analyzed. HTTP Request type is found using HttpContext class. We only support two HTTP verbs – GET and LIST. Our request types are mapped into five operations:
・ListAllCountries
・ListAllCurrencies
・GetCountryCurrency
・GetRateBySymbol
・GetRateByCountry
The consturctor does the necessary parsing and sets m_exists field to true if the virtual path is found to be correct. This field is used by CurrencyPathProvider to determine if the virtual path exists or not. - If the virtual path exists, then ASP.NET framework will use the object of CurrencyVirtualFile class to send the response to REST based Web Service client. To support this, we must override Open( ) method in our abstract base class VirtualFile. The function must return a reference to a readonly Stream object. Depending on the virtual path, five cases arise. Each function returns a stream corresponding to five Web Service APIs explained above.
- The data format used in reponse to REST Web Service call is not governed by any standard. We have chosen XML as format due to its universal appeal.
- Private functions like ListAllCurrencies( ) in this class simply use the existing implementation in CurrencyService class. The result of API call is converted into XML format and written to a StringBuilder object. This object is then used to construct MemoryStream object, reference to which is then returned to the caller. It's important to reset the Position in the returned stream to 0.
For every virtual path that exists, a CurrentyVirtualFile object is created. Open( ) call on this object will return the XML representation of response to the client.
This completes the implementation of REST based access to our CurrenyWebService. As you will notice, both SOAP and REST based approaches can co-exist happily. In the next article, we will show you how easy it is to modify our client application to use REST instead of SOAP, so stay tuned!



