At times it is desirable to create an absolute URL for a HTML element's href or src attribute. I failed to find a shrink wrapped mechanism to generate them in ASP.NET MVC, and some of the methods I found on the web were not quite what I wanted. So I created extension methods for the Uri class, which will generate absolute URLs relative to the current context's Request.Url property.

The class definition follows:

public static class UriHelperExtensions
{
   // Prepend the provided path with the scheme, host, and port of the request.
   public static string FormatAbsoluteUrl(this Uri url, string path)
   {
      return string.Format( 
         "{0}/{1}", url.FormatUrlStart(), path.TrimStart('/') );
   }

   // Generate a string with the scheme, host, and port if not 80.
   public static string FormatUrlStart(this Uri url)
   {
      return string.Format( "{0}://{1}{2}", url.Scheme, 
         url.Host, url.Port == 80 ? string.Empty : ":" + url.Port );
   }
}

The following snippet of code from an ASP.NET MVC Razor view demonstrates how to generate an absolute URL given the relative URL /images/img.jpg.

   <img src="@Request.Url.FormatAbsoluteUrl("/images/img.jpg")" alt="Alt text" />

For my blog, the HTML generated for the img element will be:

   <img src="http://www.nathanfox.net/images/img.jpg" alt="Alt text" />

I hope this helps :-)

Technical ASP.NET .NET ASP.NET MVC January 25, 2012

Rendering a page on my blog requires 12 JavaScript files and 3 CSS files. This is is a lot of HTTP request overhead. I decided to use AjaxMin to combine and minify my JS and CSS files. Now my pages only requires one JS and one CSS file.

AjaxMin can use an XML file to specify which files to minify and combine along with the target output file. The input file for my blog:


<root>
   <output path="..\..\web\site.js">
      <input path="..\..\web\syntax\scripts\shCore.js" />
      <input path="..\..\web\syntax\scripts\shLegacy.js" />
      <input path="..\..\web\syntax\scripts\shBrushCSharp.js" />
      <input path="..\..\web\syntax\scripts\shBrushXml.js" />
      <input path="..\..\web\syntax\scripts\shBrushPlain.js" />
      <input path="..\..\web\syntax\scripts\shBrushVb.js" />
      <input path="..\..\web\syntax\scripts\shBrushPowerShell.js" />
      <input path="..\..\web\scripts\jquery-1.7.1.min.js" />
      <input path="..\..\web\scripts\json2.js" />
      <input path="..\..\web\scripts\jquery.validate.min.js" />
      <input path="..\..\web\scripts\jquery.form.js" />
      <input path="..\..\web\scripts\FoxBlog.js" />
   </output>
   <output path="..\..\web\site.css">
      <input path="..\..\web\syntax\styles\shCore.css" />
      <input path="..\..\web\syntax\styles\shThemeDefault.css" />
      <input path="..\..\web\content\site.css" />
   </output>
</root>

The file name for the XML input file will be passed through the -xml argument to AjaxMin.

I'm running AjaxMin out of a folder off the root of my checkout tree, so the paths to all the files are relative paths that resolve to my web project folder structure. The folder structure of interest is shown below.

Web folder structure for my project using AjaxMin

AjaxMin is run out of the 3rdParty\AjaxMin folder. The input file is also located in the AjaxMin folder. The command to run AjaxMin:

ajaxmin -clobber -xml AjaxMin.xml

After executing AjaxMin, the files site.css and site.js will be created in the Web folder. These are the files that will be referenced in the website code.

AjaxMin can be run as a build event in Visual Studio, although technically it would only be necessary to run it if any of the CSS or JavsScript changes. A build event command for the above project structure:

$(ProjectDir)..\3rdParty\AjaxMin\ajaxmin -clobber -xml $(ProjectDir)..\3rdParty\AjaxMin\AjaxMin.xml

It might be better to set up a external tool in Visual Studio to run AjaxMin, a shortcut, or whatever is most convenient.

Keep in mind, some sort of versioning needs to be done with the CSS and JS files so the browser will download the latest version of the file. I have yet to implement a versioning mechanism for my blog, but I need to get to it at some point. Maybe a blog post about it as well :-)

Technical ASP.NET ASP.NET MVC November 30, 2011

I created some code which might save some time for others who want to generate XML sitemaps from .NET code. It is written in C#, but would likely be easily ported to other .NET languages. This code is used by my ASP.NET MVC blog engine to generate the sitemap for my blog entries. It could just as easily be used in ASP.NET WebForms, a console application, or from any .NET code framework.

The code for the helper class can be found here. You might want to change the namespace in the file.

The class SiteMapData is used to hold the data for each sitemap node.

[Serializable]
public class SiteMapData
{
   public string Loc { get; set; }
   public DateTime? Lastmod { get; set; }
   public string Changefreq { get; set; }
   public decimal? Priority { get; set; }
}

A list of SiteMapData instances is passed into the SiteMapHelper class function GenerateSiteMap, which will transform the list of SiteMapData objects into an XDocument.

public XDocument GenerateSiteMap(List<SiteMapData> dataRows)
{
   var xmlNodes =
      (from x in dataRows
         select CreateSiteMapUrlNode(x));

   XDocument siteMap = new XDocument(
      new XDeclaration("1.0", "utf-8", "yes"),
      new XElement(_xmlns + "urlset",
         new XAttribute(XNamespace.Xmlns + "xsd", "http://www.w3.org/2001/XMLSchema"),
         new XAttribute(XNamespace.Xmlns + "xsi", "http://www.w3.org/2001/XMLSchema-instance"), xmlNodes));

   return siteMap;
}

The resulting XDocument can be converted to a GZip'ed stream of bytes by calling the SiteMapHelper function GZipSiteMap.

public MemoryStream GZipSiteMap(XDocument siteMap)
{
   using (MemoryStream ms = new MemoryStream())
   {
      siteMap.Save(ms);
      ms.Seek(0, SeekOrigin.Begin);
      using (MemoryStream compressed = new MemoryStream())
      {
         using (GZipStream zip = 
            new GZipStream(compressed, System.IO.Compression.CompressionMode.Compress))
         {
            ms.CopyTo(zip, (int)ms.Length);
         }

         return new MemoryStream(compressed.ToArray());
      }
   }
}

The following code is an example of how to use the SiteMapHelper class to generate a sitemap from an ASP.NET MVC controller action. This is the code used to generate the sitemap for my blog.

public ActionResult SiteMap()
{
   EntityCollection<BlogEntryEntity> entities = 
      ServiceFactory.Beget<IEntityGenericService>().FetchAllEntities<EntityCollection<BlogEntryEntity>>();

   var mapData =
      from e in entities
      orderby e.DateInserted descending
      select new SiteMapData { 
         Loc = FormattingUtils.FormatUrlStart(Request.Url) + 
            Url.RouteUrl("Blog entry with id and title", new { BlogEntryId = e.BlogEntryId, blogEntrytitle = FormattingUtils.NormalizeTitle(e.Title) }), 
            Lastmod = e.LastTimeModified, 
            Priority = 0.5m, 
            Changefreq = "Weekly" };

   SiteMapHelper helper = new SiteMapHelper();

   XDocument siteMap = helper.GenerateSiteMap(mapData.ToList());
   MemoryStream compressedBytes = helper.GZipSiteMap(siteMap);

   FileStreamResult result = new FileStreamResult(compressedBytes, "gzip");
   result.FileDownloadName = "sitemap.xml.gz";
   return result;
}

The code used to fill in the SiteMapData list would change according to each specific situation. The use of the SiteMapHelper to the end of the function would be the same for most situations. Hopefully this illustrates the general idea.

One thing to keep in mind is that XML sitemaps can have at most 50,000 URLs and can be at most 10MB in size. So, if the list of SiteMapData nodes would generate a file that is larger than the given limits, then the list would have to be processed in chunks to generate separate sitemap files.

I share this with hope that it helps save someone a bit of time :-)

The entire SiteMapHelper code is shown below. It can be downloaded from the link at the top of this blog entry as well.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Xml.Linq;

namespace FoxBlog.Shared
{
   [Serializable]
   public class SiteMapData
   {
      public string Loc { get; set; }
      public DateTime? Lastmod { get; set; }
      public string Changefreq { get; set; }
      public decimal? Priority { get; set; }
   }

   public class SiteMapHelper
   {
      XNamespace _xmlns = "http://www.sitemaps.org/schemas/sitemap/0.9";

      public XDocument GenerateSiteMap(List<SiteMapData> dataRows)
      {
         var xmlNodes =
            (from x in dataRows
               select CreateSiteMapUrlNode(x));
   
         XDocument siteMap = new XDocument(
            new XDeclaration("1.0", "utf-8", "yes"),
            new XElement(_xmlns + "urlset",
               new XAttribute(XNamespace.Xmlns + "xsd", "http://www.w3.org/2001/XMLSchema"),
               new XAttribute(XNamespace.Xmlns + "xsi", "http://www.w3.org/2001/XMLSchema-instance"), xmlNodes));

         return siteMap;
      }

      public MemoryStream GZipSiteMap(XDocument siteMap)
      {
         using (MemoryStream ms = new MemoryStream())
         {
            siteMap.Save(ms);
            ms.Seek(0, SeekOrigin.Begin);
            using (MemoryStream compressed = new MemoryStream())
            {
               // The zip stream has to be closed to write out all the bytes. The underlying stream
               // is closed as well. To get the bytes of the the closed stream ToArray must be called.
               using (GZipStream zip = new GZipStream(compressed, System.IO.Compression.CompressionMode.Compress))
               {
                  ms.CopyTo(zip, (int)ms.Length);
               }

               return new MemoryStream(compressed.ToArray());
            }
         }
      }

      private XElement CreateSiteMapUrlNode(SiteMapData data)
      {
         XElement node = new XElement(_xmlns + "url", new XElement(_xmlns + "loc", data.Loc));

         if ( data.Lastmod.HasValue )
         {
            node.Add(new XElement(_xmlns + "lastmod", data.Lastmod.Value.ToString("yyyy-MM-dd")));
         }
         
         if ( data.Changefreq != null )
         {
            node.Add(new XElement(_xmlns + "changefreq", data.Changefreq));
         }

         if ( data.Priority.HasValue )
         {
            node.Add(new XElement(_xmlns + "priority", data.Priority.ToString()));
         }

         return node;
      }
   }
}
Technical .NET ASP.NET MVC November 23, 2011