ASP.NET numeric formatting for financial apps

If you’ve been working with financial apps, you probably know this, but it’s not obvious if you’re trying to find it in the MSDN help.

You are probably aware of the use of “C” for a currency format string.  But, generally in the U.S. a “C” will include a dollar sign.  Often you will be asked to leave off the dollar sign, as financial people are accustomed to looking at currency values without them.  But, they often want the negative numbers to be formatted in parentheses rather than with a leading minus sign.  So,

Positive Numbers:       23,500.00

Negative Numbers:    (23,500.00)

Here is an example format string from a gridview BoundField:

DataFormatString="{0:#,###,##0.00;(#,###,##0.00)}"

The semicolon in the above is a separator between positive number formats and negative number formats.  The above will handle numeric values less than 10 million.  You can, of course, add more pound signs if you are dealing with larger numbers.

Like I said, this is not rocket science, but it should be easier to find this information.

Advertisements

Leave a comment

Filed under Computers and Internet, Web Development

VirtualPathProvider for Windows Azure: Hosting ASPX web pages in blob storage

I was doing some work on an ASP.NET web site that had an entire subdirectory tree of thousands of ASPX files whose content didn’t change often. The site is hosted in Windows Azure and took a fair amount of time to deploy, so it seemed like a good idea to put all these relatively static pages in BLOB storage and reference them there rather than include them in the project build.

The pages had no code-behind, but they did reference a master page and they did have <asp:xxx> tags in them, so they had to be processed by the ASP.NET server process. Clearly you can’t just reference the pages from their URL in blob storage. That would work for static HTML, media files or other static content, but not for ASPX content because it would not get processed by an ASP.NET web server.

After quite a bit of Googling I was researching how to write a custom HttpHandler class, but the problem with that was that I didn’t want to emit my own html, I wanted the ASP.NET process to do its usual thing once I provided the page’s content. I just needed a hook in somewhere to redirect whatever ASP.NET process was reading the files from disk on the web server.

As it turns out, sometime in the mid-2000’s Microsoft did add a means to hook into that process. You need to subclass VirtualPathProvider along with VirtualFile and VirtualDirectory, and you wire them up in your web application to allow you to serve up page content from wherever you like – zip file, database, blob storage, wherever! This knowledge base article provided me with most of what I needed to know. But there were still some snags, so I’ll go over my solution focusing on what’s different from the above page (in other words, you need to go read that page!).

When they first added support for this, Microsoft had relatively onerous permission requirements. But, in an update they relaxed the permissions slightly.

Here’s the VirtualFile class. Note the permissions attributes at top:

[AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
[AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
public class BlobAspxFile : VirtualFile
{
private BlobAspxProvider bp;
private string virPath;

public BlobAspxFile(string virtualPath, BlobAspxProvider provider)
: base(virtualPath)
{
this.bp = provider;
this.virPath = virtualPath;
}

public override Stream Open()
{
Stream stream = new MemoryStream();
try
{
bp.GetFileContents(virPath, ref stream);
}
catch (Exception)
{
// TODO: log your error messages here
}

return stream;
}
}

The VirtualDirectory class mimics what was done in the KB article:

public class BlobAspxVirtualDirectory : VirtualDirectory
{
BlobAspxProvider bp;

public BlobAspxVirtualDirectory(string virtualDir, BlobAspxProvider provider) : base(virtualDir)
{
bp = provider;
}

private ArrayList children = new ArrayList();
public override IEnumerable Children {get {return children;}}
private ArrayList directories = new ArrayList();
public override IEnumerable Directories{get {return directories;}}
private ArrayList files = new ArrayList();
public override IEnumerable Files{get { return files;}}
}

The VirtualPathProvider:

public class BlobAspxProvider : VirtualPathProvider
{
const string baseURL = "pathParent/virtualPathToHandle/"; // URL segment you are virtualizing

BlobUtility utility = null;
Hashtable fileList = null;

public BlobAspxProvider()
: base()
{
utility = new BlobUtility();
utility.BaseContainerPath = ConfigurationManager.AppSettings["BlobRoot"].ToString();
utility.VirtualPathRootSubpath = baseURL;

}

private bool IsPathVirtual(string virtualPath)
{
string checkPath = VirtualPathUtility.ToAppRelative(virtualPath).ToLower();

int iReplace = checkPath.IndexOf(baseURL);
return (iReplace > 0);

}

public override bool FileExists(string virtualPath)
{
if (IsPathVirtual(virtualPath))
{

BlobAspxFile file = (BlobAspxFile)GetFile(virtualPath);
if (utility.CheckIfFileExists(virtualPath))
return true;
}
return Previous.FileExists(virtualPath);
}

public override bool DirectoryExists(string virtualDir)
{
if (IsPathVirtual(virtualDir))
{
// For the moment we will always return TRUE here to speed processing
BlobAspxVirtualDirectory dir = (BlobAspxVirtualDirectory)GetDirectory(virtualDir);
return true;
}
else
return Previous.DirectoryExists(virtualDir);
}


//This method is used by the compilation system to obtain a VirtualFile instance to
//work with a given virtual file path.
public override VirtualFile GetFile(string virtualPath)
{
if (IsPathVirtual(virtualPath))
return new BlobAspxFile(virtualPath, this);
else
return Previous.GetFile(virtualPath);
}

//This method is used by the compilation system to obtain a VirtualDirectory
//instance to work with a given virtual directory.
public override VirtualDirectory GetDirectory(string virtualDir)
{
if (IsPathVirtual(virtualDir))
return new BlobAspxVirtualDirectory(virtualDir, this);
else
return Previous.GetDirectory(virtualDir);
}


public string GetFileContents(string virPath)
{
return ""; // utility.GetFileContents(virPath); initially not implemented
}

public void GetFileContents(string virPath, ref Stream strm)
{
utility.GetFileContents(virPath, strm);
}

public Hashtable GetVirtualData
{
get
{
if (fileList == null)
{
fileList = utility.GetVirtualFiles();
}
return fileList;
}
set { this.fileList = value; }
}

/// <summary>
/// This method is critical to making the provider work on Azure. It avoids an error that happens when the Azure
/// web role does not have a virtual directory under the web root. Without this you would have to include an actual
/// folder tree with a file in each folder to keep it from being optimized out of the deployment
/// </summary>
/// <param name="virtualPath"></param>
/// <param name="virtualPathDependencies"></param>
/// <param name="utcStart"></param>
/// <returns></returns>
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
return IsPathVirtual(virtualPath) ? null : base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}


public static void AppInitialize()
{
HostingEnvironment.RegisterVirtualPathProvider(new BlobAspxProvider());
}
}

Key points for the provider:

  1. The baseURL  represents the folder I was virtualizing.  I had the parent folder in there to make identification of the URL segment more reliable, but the parent folder is not being affected.
  2. BlobUtility.BaseContainerPath property is where to find the files in blob storage.
  3. GetCacheDependency() method was not explained clearly in the KB post, but without it my code worked fine locally when testing on the AppFabric, but failed when I deployed to Azure.

Lastly, all the BLOB work is actually done in BlobUtility class.  Providing this for the sake of completeness:

public class BlobUtility
{
CloudStorageAccount acct = null;
CloudBlobClient blobClnt = null;
CloudBlobContainer blobContainer = null;

string containerRoot = "";

/// <summary>
/// Constructor
/// </summary>
public BlobUtility()
{
BaseContainerPath = "";
VirtualPathRootSubpath = "";
}

// ==========================================================================
//
// Properties
//
// ==========================================================================

CloudStorageAccount CloudAccount
{
get
{
if (acct == null)
acct = CloudStorageAccount.Parse(ConfigurationManager.AppSettings["BlobStorageConnectInfo"]);
return acct;
}
}
CloudBlobClient BlobClient
{
get
{
if (blobClnt == null)
blobClnt = CloudAccount.CreateCloudBlobClient();
return blobClnt;
}
}
CloudBlobContainer BlobContainer
{
get
{
if (blobContainer == null)
blobContainer = BlobClient.GetContainerReference(BaseContainerPath);
return blobContainer;
}
}

public string BaseContainerPath
{
get
{
return containerRoot;
}
set
{
// If this has a subpath specified, we want to save that separately from the root.
// That's because in a blob only the base of the container path is actually a container. The
// rest are virtual folders in that container.
int iSlash = value.IndexOf('/');
if (iSlash == 0)
{
value = value.Substring(1); // ignore leading slash - doesn't belong there
iSlash = value.IndexOf('/');
}

if (iSlash > 0 && iSlash < value.Length - 1)
{
containerRoot = value.Substring(0, iSlash); // exclude the first slash
ContainerSubpath = value.Substring(iSlash + 1);
}
else
{
containerRoot = value;
ContainerSubpath = "";
}
}
}

public string ContainerSubpath
{
get;
set;
}
public string VirtualPathRootSubpath
{
get;
set;
}

// ==========================================================================
//
// Externally accessible Methods
//
// ==========================================================================


internal bool CheckIfFileExists(string virtualPath)
{
string blobPath = GetBlobPath(virtualPath);
CloudBlob blob = BlobContainer.GetBlobReference(blobPath);
return blob != null;
}

internal bool GetFileContents(string virtualPath, System.IO.Stream strm)
{
string blobPath = GetBlobPath(virtualPath);
CloudBlob blob = BlobContainer.GetBlobReference(blobPath);
if (blob != null)
{
blob.DownloadToStream(strm);
strm.Seek(0, SeekOrigin.Begin); // VERY IMPORTANT!!! You'll get no content without this.
return true;
}
return false;
}


internal System.Collections.Hashtable GetVirtualFiles()
{
return new System.Collections.Hashtable();
}


// ==========================================================================
//
// Private Methods
//
// ==========================================================================

/// <summary>
/// return just the part of the path relative to the encyclopedia root
/// </summary>
/// <param name="virtualPath"></param>
/// <returns></returns>
private string GetBlobPath(string virtualPath)
{
int iRoot = virtualPath.ToLower().IndexOf(VirtualPathRootSubpath);
if (iRoot < 1) // this should never happen! If it does, you did something wrong in BlobAspxProvider.IsPathVirtual()
throw new ApplicationException(string.Format("BlobAspxProvider reports Programmer Error: virtual path \"{0}\" is not supported!", virtualPath));

string blobPath = ContainerSubpath + virtualPath.Substring(iRoot + VirtualPathRootSubpath.Length);
return blobPath;
}

}

Of course, for BlobUtility you will need to include the following using statements in addition to the typical dotnet ones:

using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;

One last thing that wasn’t well explained: how to wire this up in your ASP.NET project:

1) Of course, add the above classes to your project and make sure they compile

2) In Global.ASAX in the Application_Start event handler, add the following code to enable your classes to be called:

HostingEnvironment hostingEnvironmentInstance = (HostingEnvironment)typeof(HostingEnvironment).InvokeMember("_theHostingEnvironment", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.GetField, null, null, null);
MethodInfo mi = typeof(HostingEnvironment).GetMethod("RegisterVirtualPathProviderInternal", BindingFlags.NonPublic | BindingFlags.Static);
mi.Invoke(hostingEnvironmentInstance, new object[] { new BlobAspxProvider() });

Thanks to MadPierre on http://stackoverflow.com/questions/8165854/using-virtualpathprovider-to-put-themes-in-azure-cdn for the above!!!

Lastly, I just excluded the entire folder subtree from the project.

I have not gone back and cleaned up or optimized, but this does work.  You can probably fine-tune it.  I didn’t actually make use of the Hashtable  like the KB article did, for example.

See Also: http://msdn.microsoft.com/en-us/library/aa479502.aspx

6 Comments

Filed under Computers and Internet, Web Development