im in ur web, enriching ur code

 
 

Sunday, July 06, 2008

What's wrong with Request.Headers["Accept-Encoding"].Contains("gzip")?

This post has been brewing for a little while. It stems from an inconsistency I've seen in code posted here and there over the web. Quite specifically this happens often when trying to detect which Accept-Encoding a browser can accept, so a GZIP or DEFLATE filter can be used to compress the content.

The Offending Code

The code in question goes something like this:

string encoding = Request.Headers["Accept-Encoding"];

if (encoding.Contains("gzip"))

{

  Response.AppendHeader("Content-Encoding", "gzip");

  Response.Filter = new GZipStream(Response.Filter, CompressionMode.Compress);

}

else if (encoding.Contains("deflate"))

{

  Response.AppendHeader("Content-Encoding", "deflate");

  Response.Filter = new DeflateStream(Response.Filter, CompressionMode.Compress);

}

What's wrong with it? It seems to be pretty standard and widely in use.

Well, consider the following possible values that can be sent in the Accept-Encoding header.

1: "gzip,deflate"

2: "deflate,gzip"

3: "gzip;q=.5,deflate"

4: "gzip;q=0,deflate"

5: "deflate;q=0.5,gzip;q=0.5,identity"

6: "*"

and here are some observations when running the above code listing, compared to what is expected by section 14 of RFC2616.

  1. "gzip,deflate":
    This one passes the example code and the rfc fine. As the browser, we're expecting gzip first, otherwise give us deflate.
  2. "deflate,gzip":
    Unfortunately, as the browser, we want deflate first, but gzip is okay if the server can't give us our first preference; we're served gzip in this case as the code just looks to see if "gzip" is contained within the Accept-Encoding header before it looks to see if "deflate" is there. This isn't such a big deal as we're still receiving compressed content, but we should have been served deflate.
  3. "gzip;q=.5,deflate":
    This one uses another valid method to tell the server (and your application) that the browser would prefer deflate encoding before gzip encoding. When a "q=" is specified after the encoding, with a value between 0 and 1, the preference is given that weight (or quality). When no "q=" is specified, it defaults to 1. So this rule tells the server the browser wants gzip only half as much as it wants deflate compression. Because our code is looking to see if gzip is contained within the header, it serves us gzip despite our preference. It's still not critical as we can still accept the gzip encoding.
  4. "gzip;q=0,deflate":
    Here is where we get into trouble. Now our header is telling the server "Do NOT give me gzip compressed content, but I'll take content encoded with deflate if you can". Again, because the code simply looks to see if gzip is contained within the header, regardless of its weight and being disallowed, we're still served gzip.
  5. "deflate;q=0.5,gzip;q=0.5,identity":
    This one is more of a trick question; its not as innocent as you might think. For accept encodings, "identity" is a special case. It means that it should be served without content encoding. Considering that the "q" defaults to "1" when not specified, this is the order of our preferred encodings "identity,deflate;q=0.5,gzip;q=0.5". This means that we shouldn't give a content-encoding. But once again, because the code simply looks to see if contains gzip, we get gzipped content.
  6. "*":
    The last one has said "We'll accept any encoding you want to serve.", and while we could (and perhaps should) be served compressed content, because the code can't see gzip or deflate in the header, it serves the normal content. This one isn't so critical, but it could have been easily solved and help in reducing bandwidth and download time.

In all fairness, its probably not likely now days to not accept gzip encoding so it probably effects very few requests - but it is incorrect. In my examples I've used an arbitrary ordering to illustrate the importance of the "q" part in regards to preference. Quite usually however, the most preferred values appear first in the list - but it's not guaranteed

To handle this situation we need to be able to find the preferred encoding (and this can be applied to any of the similar headers i.e. Accept-Encoding, Accept-Charset, Accept-Language & Accept) that is accepted.

Finding a preferred HTTP Header Value in C#

I decided to split the header value up into its relevant segments, sort it and then interrogate it in a generic list. There is possibly more overhead from parsing the value into a list and then looping over the values, but verses having an incorrect implementation as the first code listing does, its worth it. My implementation of a QValueList follows shortly. First, if you're interested, I'd encourage you to have a look at the built-in Microsoft "System.Web.Caching.OutputCacheModule" module via Lutz Roeder's .NET Reflector. It uses a parsing method to determine whether an encoding is allowed or not; and truthfully, its also where I encountered the "identity" encoding and investigated its impact on what we're trying to achieve So an alternative to this approach would be to parse similar to the OutputCache module.

Here's how we'll change the typical code from the start of the post:

/// load encodings from header

QValueList encodings = new QValueList(Request.Headers["Accept-Encoding"]);

 

/// get the types we can handle, can be accepted and

/// in the defined client preference

QValue preferred = encodings.FindPreferred("gzip", "deflate", "identity");

 

/// if none of the preferred values were found, but the

/// client can accept wildcard encodings, we'll default

/// to Gzip.

if(preferred.IsEmpty && encodings.AcceptWildcard && encodings.Find("gzip").IsEmpty)

  preferred = new QValue("gzip");

 

// handle the preferred encoding

switch(preferred.Name)

{

  case "gzip":

      Response.AppendHeader("Content-Encoding", "gzip");

      Response.Filter = new GZipStream(Response.Filter, CompressionMode.Compress);

    break;

  case "deflate":

      Response.AppendHeader("Content-Encoding", "deflate");

      Response.Filter = new DeflateStream(Response.Filter, CompressionMode.Compress);

    break;

  case "identity":

  default:

    break;

}

The first observation one might make is that there is quite a lot more code. However, it is required to get the correct behaviour. Besides, its not a lot if you factor it into your code/library/component correctly.

So why is it called "QValue" and not simply simply "AcceptEncoding"? Well, you can apply this to any of the headers that use the same qualified value convention including:

  • Request.Headers["Accept-Encoding"]
  • Request.Headers["Accept-Charset"]
  • Request.Headers["Accept-Language"]
  • Request.Headers["Accept"]

Source Code Listing & Download

Here's the code listing in line, or you can download the .cs file containing both QValue and QValueList.

using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.Text;

 

/// <summary>

/// Represents a weighted value (or quality value) from an http header e.g. gzip=0.9; deflate; x-gzip=0.5;

/// </summary>

/// <remarks>

/// accept-encoding spec:

///    http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

/// </remarks>

/// <example>

/// Accept:          text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5

/// Accept-Encoding: gzip,deflate

/// Accept-Charset:  ISO-8859-1,utf-8;q=0.7,*;q=0.7

/// Accept-Language: en-us,en;q=0.5

/// </example>

[DebuggerDisplay("QValue[{Name}, {Weight}]")]

public struct QValue : IComparable<QValue>

{

  static char[] delimiters = { ';', '=' };

  const float defaultWeight = 1;

 

  #region Fields

 

  string _name;

  float _weight;

  int _ordinal;

 

  #endregion

 

  #region Constructors

 

  /// <summary>

  /// Creates a new QValue by parsing the given value

  /// for name and weight (qvalue)

  /// </summary>

  /// <param name="value">The value to be parsed e.g. gzip=0.3</param>

  public QValue(string value)

    : this(value, 0)

  { }

 

  /// <summary>

  /// Creates a new QValue by parsing the given value

  /// for name and weight (qvalue) and assigns the given

  /// ordinal

  /// </summary>

  /// <param name="value">The value to be parsed e.g. gzip=0.3</param>

  /// <param name="ordinal">The ordinal/index where the item

  /// was found in the original list.</param>

  public QValue(string value, int ordinal)

  {

    _name = null;

    _weight = 0;

    _ordinal = ordinal;

 

    ParseInternal(ref this, value);

  }

 

  #endregion

 

  #region Properties

 

  /// <summary>

  /// The name of the value part

  /// </summary>

  public string Name

  {

    get { return _name; }

  }

 

  /// <summary>

  /// The weighting (or qvalue, quality value) of the encoding

  /// </summary>

  public float Weight

  {

    get { return _weight; }

  }

 

  /// <summary>

  /// Whether the value can be accepted

  /// i.e. it's weight is greater than zero

  /// </summary>

  public bool CanAccept

  {

    get { return _weight > 0; }

  }

 

  /// <summary>

  /// Whether the value is empty (i.e. has no name)

  /// </summary>

  public bool IsEmpty

  {

    get { return string.IsNullOrEmpty(_name); }

  }

 

  #endregion

 

  #region Methods

 

  /// <summary>

  /// Parses the given string for name and

  /// weigth (qvalue)

  /// </summary>

  /// <param name="value">The string to parse</param>

  public static QValue Parse(string value)

  {

    QValue item = new QValue();

    ParseInternal(ref item, value);

    return item;

  }

 

  /// <summary>

  /// Parses the given string for name and

  /// weigth (qvalue)

  /// </summary>

  /// <param name="value">The string to parse</param>

  /// <param name="ordinal">The order of item in sequence</param>

  /// <returns></returns>

  public static QValue Parse(string value, int ordinal)

  {

    QValue item = Parse(value);

    item._ordinal = ordinal;

    return item;

  }

 

  /// <summary>

  /// Parses the given string for name and

  /// weigth (qvalue)

  /// </summary>

  /// <param name="value">The string to parse</param>

  static void ParseInternal(ref QValue target, string value)

  {

    string[] parts = value.Split(delimiters, 3);

    if (parts.Length > 0)

    {

      target._name = parts[0].Trim();

      target._weight = defaultWeight;

    }

 

    if (parts.Length == 3)

    {

      float.TryParse(parts[2], out target._weight);

    }

  }

 

  #endregion

 

  #region IComparable<QValue> Members

 

  /// <summary>

  /// Compares this instance to another QValue by

  /// comparing first weights, then ordinals.

  /// </summary>

  /// <param name="other">The QValue to compare</param>

  /// <returns></returns>

  public int CompareTo(QValue other)

  {

    int value = _weight.CompareTo(other._weight);

    if (value == 0)

    {

      int ord = -_ordinal;

      value = ord.CompareTo(-other._ordinal);

    }

    return value;

  }

 

  #endregion

 

  #region CompareByWeight

 

  /// <summary>

  /// Compares two QValues in ascending order.

  /// </summary>

  /// <param name="x">The first QValue</param>

  /// <param name="y">The second QValue</param>

  /// <returns></returns>

  public static int CompareByWeightAsc(QValue x, QValue y)

  {

    return x.CompareTo(y);

  }

 

  /// <summary>

  /// Compares two QValues in descending order.

  /// </summary>

  /// <param name="x">The first QValue</param>

  /// <param name="y">The second QValue</param>

  /// <returns></returns>

  public static int CompareByWeightDesc(QValue x, QValue y)

  {

    return -x.CompareTo(y);

  }

 

  #endregion

 

}

 

/// <summary>

/// Provides a collection for working with qvalue http headers

/// </summary>

/// <remarks>

/// accept-encoding spec:

///    http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

/// </remarks>

[DebuggerDisplay("QValue[{Count}, {AcceptWildcard}]")]

public sealed class QValueList : List<QValue>

{

  static char[] delimiters = { ',' };

 

  #region Fields

 

  bool _acceptWildcard;

  bool _autoSort;

 

  #endregion

 

  #region Constructors

 

  /// <summary>

  /// Creates a new instance of an QValueList list from

  /// the given string of comma delimited values

  /// </summary>

  /// <param name="values">The raw string of qvalues to load</param>

  public QValueList(string values)

    : this(null == values ? new string[0] : values.Split(delimiters, StringSplitOptions.RemoveEmptyEntries))

  { }

 

  /// <summary>

  /// Creates a new instance of an QValueList from

  /// the given string array of qvalues

  /// </summary>

  /// <param name="values">The array of qvalue strings

  /// i.e. name(;q=[0-9\.]+)?</param>

  /// <remarks>

  /// Should AcceptWildcard include */* as well?

  /// What about other wildcard forms?

  /// </remarks>

  public QValueList(string[] values)

  {

    int ordinal = -1;

    foreach (string value in values)

    {

      QValue qvalue = QValue.Parse(value.Trim(), ++ordinal);

      if (qvalue.Name.Equals("*")) // wildcard

        _acceptWildcard = qvalue.CanAccept;

      Add(qvalue);

    }

 

    /// this list should be sorted by weight for

    /// methods like FindPreferred to work correctly

    DefaultSort();

    _autoSort = true;

  }

 

  #endregion

 

  #region Properties

 

  /// <summary>

  /// Whether or not the wildcarded encoding is available and allowed

  /// </summary>

  public bool AcceptWildcard

  {

    get { return _acceptWildcard; }

  }

 

  /// <summary>

  /// Whether, after an add operation, the list should be resorted

  /// </summary>

  public bool AutoSort

  {

    get { return _autoSort; }

    set { _autoSort = value; }

  }

 

  /// <summary>

  /// Synonym for FindPreferred

  /// </summary>

  /// <param name="candidates">The preferred order in which to return an encoding</param>

  /// <returns>An QValue based on weight, or null</returns>

  public QValue this[params string[] candidates]

  {

    get { return FindPreferred(candidates); }

  }

 

  #endregion

 

  #region Add

 

  /// <summary>

  /// Adds an item to the list, then applies sorting

  /// if AutoSort is enabled.

  /// </summary>

  /// <param name="item">The item to add</param>

  public new void Add(QValue item)

  {

    base.Add(item);

 

    applyAutoSort();

  }

 

  #endregion

 

  #region AddRange

 

  /// <summary>

  /// Adds a range of items to the list, then applies sorting

  /// if AutoSort is enabled.

  /// </summary>

  /// <param name="collection">The items to add</param>

  public new void AddRange(IEnumerable<QValue> collection)

  {

    bool state = _autoSort;

    _autoSort = false;

 

    base.AddRange(collection);

 

    _autoSort = state;

    applyAutoSort();

  }

 

  #endregion

 

  #region Find

 

  /// <summary>

  /// Finds the first QValue with the given name (case-insensitive)

  /// </summary>

  /// <param name="name">The name of the QValue to search for</param>

  /// <returns></returns>

  public QValue Find(string name)

  {

    Predicate<QValue> criteria = delegate(QValue item) { return item.Name.Equals(name, StringComparison.OrdinalIgnoreCase); };

    return Find(criteria);

  }

 

  #endregion

 

  #region FindHighestWeight

 

  /// <summary>

  /// Returns the first match found from the given candidates

  /// </summary>

  /// <param name="candidates">The list of QValue names to find</param>

  /// <returns>The first QValue match to be found</returns>

  /// <remarks>Loops from the first item in the list to the last and finds

  /// the first candidate - the list must be sorted for weight prior to

  /// calling this method.</remarks>

  public QValue FindHighestWeight(params string[] candidates)

  {

    Predicate<QValue> criteria = delegate(QValue item)

    {

      return isCandidate(item.Name, candidates);

    };

    return Find(criteria);

  }

 

  #endregion

 

  #region FindPreferred

 

  /// <summary>

  /// Returns the first match found from the given candidates that is accepted

  /// </summary>

  /// <param name="candidates">The list of names to find</param>

  /// <returns>The first QValue match to be found</returns>

  /// <remarks>Loops from the first item in the list to the last and finds the

  /// first candidate that can be accepted - the list must be sorted for weight

  /// prior to calling this method.</remarks>

  public QValue FindPreferred(params string[] candidates)

  {

    Predicate<QValue> criteria = delegate(QValue item)

    {

      return isCandidate(item.Name, candidates) && item.CanAccept;

    };

    return Find(criteria);

  }

 

  #endregion

 

  #region DefaultSort

 

  /// <summary>

  /// Sorts the list comparing by weight in

  /// descending order

  /// </summary>

  public void DefaultSort()

  {

    Sort(QValue.CompareByWeightDesc);

  }

 

  #endregion

 

  #region applyAutoSort

 

  /// <summary>

  /// Applies the default sorting method if

  /// the autosort field is currently enabled

  /// </summary>

  void applyAutoSort()

  {

    if (_autoSort)

      DefaultSort();

  }

 

  #endregion

 

  #region isCandidate

 

  /// <summary>

  /// Determines if the given item contained within the applied array

  /// (case-insensitive)

  /// </summary>

  /// <param name="item">The string to search for</param>

  /// <param name="candidates">The array to search in</param>

  /// <returns></returns>

  static bool isCandidate(string item, params string[] candidates)

  {

    foreach (string candidate in candidates)

    {

      if (candidate.Equals(item, StringComparison.OrdinalIgnoreCase))

        return true;

    }

    return false;

  }

 

  #endregion

 

}

 

kick it on DotNetKicks.com

posted @ Sunday, July 06, 2008 4:55 PM | Feedback (24)
Filed Under [ C#, Tips, .NET, Source Code ]

Wednesday, February 06, 2008

Drinking from the, erm... garden hose?

So, I finally signed up to dotnetkicks, and submitted my first story - it just happened to be my own "Build Providers: Strongly typed page urls in ASP.NET" post - which is an interesting take on improving productivity by using a build provider to create a strongly typed hierarchy of the files in your Visual Studio project, to avoid mis-typing page locations and improving intellisense.

On the same page

It turns out that Kirill Chilingarashvili was trying to solve the same issue with his solution last month - and while our methods in getting there differ, the end result is surprisingly similar. I only became aware of his method after submitting my post to dotnetkicks - I saw it a few entries down from mine just after I submitted it, and was a little jealous of his title "Auto generate strong typed navigation class for all user controls in ASP.NET web application" - how did I miss those very key words: auto generate and navigation class :)

It turns out that others thought the idea was interesting as well - and I feel the smallest amount of pride at the interested traffic dotnetkicks has sent my way. It's not exactly a deluge, like when you hear about sites getting slashdotted into the fiery pits of hell. No, but the near 500 views in a few days has totally outclassed my other posts which have been sitting there for some time, content rich of course, and earning visits from google.

Lets look at a few of my post stats...

Post Published Views Notes
The Smield: An unobtrusive javascript UI Helping Smart Field May 2007 652 Written 9 months ago, lots of content, indexed in google, most popular "all time" post... (but not for long?)
Building an Age Class in C# May 2007 552 Posted 9 months ago, not a lot of contention in search engines, gained a lot of popularity just before Christmas :)
Build Providers: Strongly typed page urls in ASP.NET Feb 2008 490 Published a few days ago, interesting content, submitted to community site (dotnetkicks).
Programmatically setting the SmtpClient pickup directory location at runtime Dec 2007 157 Recent addition, targeted content, good hit rate from search engines - pretty good popularity rise considering no outward push to gain popularity.

It's paltry figures by other popular bloggers standards (Haacked has almost 8,000 subscribers, egads!), but you have to start somewhere right?! It's about this time that you realise the power the community has, and how important it is if you want your 'hard work' and sharing to be seen and, erm, shared.

Originally, I wasn't going to 'push' or 'promote' articles out there. I thought that, if the content was good enough, readers would come to you (queue waynes world, if you build it, they will come, quote). It's still true that if you produce good content, you'll slowly gain readers and popularity. Search engines will slowly help you get there; Seeing that the build provider article posted to dotnetkicks has ousted my 'slow and steady' approach of my previous entries within the course of  couple of days, slow and steady seems a little too slow all of a sudden.

Initially, the thought of submitting a story I had also published seemed absurd, a little too much like 'gaming the system', but upon closer inspection, I saw a few bloggers I read regularly, posting their own content as well - some quite often.

Is this wrong?

From my perspective I want more people to read what I have to offer as I'm trying to fill a gap where I saw no previous content. The community is a great sounding board, it gives the ideas you have validation; how do you know if it's good, bad, works or not or is well-written if no one reads it? Google's certainly not going to tell me, "nice article Dave". It's a robot! At least not yet... I think there was a badly re-constituted analogy about trees and sound, and falling in the woods in there somewhere... but I'm not sure. I'm not regurgitating other peoples work, like what was going down for a brief period on the main feed of weblogs.asp.net after they opened up the blogs, but I wouldn't mind trying to get some of my older posts validated as it may be useful to others - or not. How will we know?

So, when does submitting your own stories to community sites become obnoxious and frowned upon? Technically, the submission is only the first minor step; the content itself draws the votes or 'kicks' and propels it up the list. Validation by popularity certainly sounds above board. Is there etiquette around such things?

It's not a community without you... and you.... and yeah, you too

I'm not sure why it took me so long to join dotnetkicks, but I'm glad I'm there now. If you're a .NET developer, I'd encourage you to sign up as well by shouting you a prized chocolate fish from my youth, but you won't need it once you realise what a great resource you've signed up to. And remember to kick things when you like them!

 

kick it on DotNetKicks.com

posted @ Wednesday, February 06, 2008 7:09 PM | Feedback (0)
Filed Under [ Off Topic, Community ]

Sunday, February 03, 2008

Build Providers: Strongly typed page urls in ASP.NET

I'm a fan of strong typing in .NET as you gain compile time validation of code, Visual Studio Intellisense support, not to mention less mucking around with type conversion or dealing with (usually inconsistently) entering strings for page names and configuration setting keys inline. Visual Studio provides intellisense and some degree of validation for pages (i.e. urls) within a project when using Design Mode, but using those urls in code, e.g..

Response.Redirect( "~/MyDir/MyPage.aspx" );

you're out of luck as there isn't any validation unless you write it yourself.

Perhaps a better syntax is to access urls within your project from a property somewhere, like so:

Response.Redirect( MyDir_MyPage );

This way, we reference a static variable or constant to retrieve the url we want to redirect the browser to. If the variable is spelt wrong, the project won't compile and we can fix the error. Being a member within the project means we're provided with intellisense support as well.

However, maintaining variables or constants for your pages can be tedious. If you change the name of a page from "MyPage.aspx" to "MyMovedPage.aspx" the actual value of the variable  "MyDir_MyPage" would also need to be updated. Once again, unless you have some automated tests to validate the page exists, the project will compile, but the if you forgot to update the reference, you'll get the wrong url.

Using a Build Provider to deal with changing file names

To get around these limitations, and to help lessen the maintenance within your project, we can use a build provider to generate a static class containing all the page's in our project. Once that's done, we get:

  • Intellisense - no more typing page address strings in code.
  • Compile time support - if a page is renamed, the build provider updates the generated class, and changes the name of the variable. Since the variable you referenced in your project no longer exists, you'll get compile errors wherever the variable is used. It's a simple hop, skip and a jump to go through and correct those errors.

So now we'll be able to use the following code, and know that if we change something, we'll get notified when when we build if something is amis.

Response.Redirect( Href.MyDir.MyPage );

The code for the build provider generates the following psudo code:

  • create a static root class called "Href"
  • all pages within the root of the project will be exposed as string constants
  • recurse subfolders, adding nested/inner classes and members to represent the hierarchy of the project

We'll look at the code for the build provider a little later, first lets look at what it will generate.

It's all Constants and Nested Classes

Using the previous example, the code the build provider generates looks something along the lines of...

public static class Href

{

  // ... repeat, one constant for each

  // file in the project root folder

  public const string Default = "~/";

 

  // ... repeat, one "nested class" for

  // each folder in the project root folder

  public static class MyDir

  {

    // ... repeat, one constant for each

    // file in the folder: /MyDir

    public const string MyPage = "~/mydir/mypage.aspx";

  }

}

Next: Setting up the build provider and configuration options.

Build providers must be compiled and referenced in the /bin folder of your project. You can't simply add the code for the build provider to the App_Code folder, as it will give a "Could not load type ''MyBuildProvider" error.

Once we've added the assembly reference to our project, we can add the relevant configuration information to the web.config.

<system.web>

  <compilation>

    <buildProviders>

      <add extension=".hrefs" type="CSharpVitamins.Compilation.HrefBuildProvider, CSharpVitamins"/>

    </buildProviders>

  </compilation>

</system.web>

Here I've chosen to associate files with the ".hrefs" extension with my HrefBuildProvider. You could easily choose another extension, as it's simply a mapping for a file extension found within the App_Code folder and the build provider. 

Now we can create a file with that extension in the App_Code folder (let's use "Site.hrefs" for now, but you could use any name. You could even have multiple .hrefs files with different class names, one for aspx pages, another for your images, javascript and css files :), and add the following configuration - then tailor to suit your preferences.

<?xml version="1.0" encoding="utf-8" ?>

<settings

  namespace=""

  className="Href"

  maxDepth="100"

  lowercaseUrls="true"

  includeExtension="false"

  makePartial="false">

 

  <files include="\.(as[pcmh]x|html?)$" exclude="" />

  <folders include="" exclude="App_|Bin" />

</settings>


Setting Description
namespace The namespace the generated class will be placed within. Leave blank to add to the global namespace. 
className The actual class name that forms the stub of all urls. Defaults to "Href".
maxDepth The maximum depth of recursion for processing. Enter a value of 1 for root files only, or higher to include subfolders. Defaults to 100.
lowercaseUrls When true, forces the values of urls to lowercase. Handy of you want to have to pages named in PascalCase but want urls all lowercase. Property names follow the same casing as the file name. Defaults to false.
includeExtension When true, the extension is appended to the property name separated by an underscore. Defaults to false.
makePartial When true, the root class uses the partial modifier, enabling you to add additional members through a non-auto-generated class file.
files Contains two regular expression patterns, once each for files to include and exclude. Include defaults to "\.aspx$", Exclude defaults to empty (none are explicitly excluded).
folders Same as files, but applied to folders. Include default to empty (all are included), Exclude defaults to "App_|Bin".

Apart from the juicy code, which is coming next, that's it. Now you'll have intellisense for your file locations and compile time checking. It's worth noting that the generated class from the build provider will regenerate when the .hrefs file is modified, or you rebuild. If you want to see the generated class (it's stored in the "Temporary ASP.NET Files" folder), I'd suggest simply to right click on "Href" from your code, and choose, "Go to definition" - it will bring up the temporary file for you to peruse.

Source code for the HrefBuildProvider

Rather than walking through the step by step of creating a build provider, I'll just point you to a couple of other articles on the subject if you wish to learn more, then I can just display the fully commented source code for the class below :)

You can download the source code for the HrefBuildProvider or look at the class definition below.

using System;

using System.CodeDom;

using System.Collections.Generic;

using System.IO;

using System.Text.RegularExpressions;

using System.Web.Compilation;

using System.Web.Hosting;

using System.Xml;

 

namespace CSharpVitamins.Compilation

{

/// <summary>

/// Generates a static class for urls within an ASP.NET project

/// </summary>

public class HrefBuildProvider : BuildProvider

{

  #region Fields

 

  /// <summary>

  /// Path to the web root for determining urls within the site

  /// </summary>

  string _basePath;

  /// <summary>

  /// Whether to convert urls to lowercase - based on your preference

  /// </summary>

  bool _useLowerCaseUrls;

  /// <summary>

  /// Whether to include the extension of the file in the name

  /// of the member

  /// </summary>

  bool _includeExtension;

  /// <summary>

  /// Whether to create the root class as 'partial', allowing

  /// additional members to be added to the resultant class.

  /// </summary>

  bool _makePartial;

  /// <summary>

  /// Maximum recurring depth for the project - 1 for top level,

  /// 2 for first level of subfolders etc...

  /// </summary>

  int _maxDepth = 100;

  /// <summary>

  /// Namespace the generated class will be placed within.

  /// Leave empty to add to the global namespace e.g. global::Href

  /// </summary>

  string _namespace;

  /// <summary>

  /// The name of the main class e.g. Href

  /// </summary>

  string _className;

 

  /// <summary>

  /// Current depth of recursion

  /// </summary>

  int _depth = 0;

  /// <summary>

  /// Predicate to determine if a file should be included

  /// </summary>

  Predicate<FileSystemInfo> isValidFile;

  /// <summary>

  /// Predicate to determine if a folder should be included

  /// </summary>

  Predicate<FileSystemInfo> isValidFolder;

 

  #endregion

 

  #region GenerateCode

 

  /// <summary>

  /// Initialises settings from the BuildProvider file, and

  /// generates the appropriate code.

  /// </summary>

  /// <param name="assemblyBuilder"></param>

  public override void GenerateCode(AssemblyBuilder assemblyBuilder)

  {

    // init settings from xml source file

    init();

 

    // create root class e.g. global::Href

    CodeTypeDeclaration root = createStaticClass(_className, _makePartial);

    addSummary(root, "Provides access to urls within the project.");

    build(new DirectoryInfo(_basePath), root);

 

    CodeNamespace ns = new CodeNamespace();

    ns.Types.Add(root);

 

    // leave blank for global::namespace

    if (!string.IsNullOrEmpty(_namespace))

      ns.Name = _namespace;

 

    CodeCompileUnit unit = new CodeCompileUnit();

    unit.Namespaces.Add(ns);

    assemblyBuilder.AddCodeCompileUnit(this, unit);

  }

 

  #endregion

 

  #region build

 

  /// <summary>

  /// Iterates over the given directory, adding the files as members to

  /// the parent type, then recurse down the folder tree until the max

  /// depth is reached.

  /// </summary>

  /// <param name="dir">The directory to process</param>

  /// <param name="parent">Parent class to add members too.</param>

  void build(DirectoryInfo dir, CodeTypeDeclaration parent)

  {

    if (_depth >= _maxDepth)

      return;

 

    ++_depth;

 

    /// 'members' keeps a record of the number of times a

    /// member name is repeated within the parent type/class.

    /// Pass this to 'ensureUniqueMemberName' to get the name

    /// with an index number appended to the end e.g. the second

    /// occurrence of "MyProperty" becomes "MyProperty1"

    Dictionary<string, int> members = new Dictionary<string, int>();

 

    ///

    /// process files:

    /// iterate over files and add the member

    /// public const string MyfileName = "~/myfileName.aspx";

    ///

    FileInfo[] files = dir.GetFiles();

    foreach (FileInfo file in files)

    {

      if (isValidFile(file))

      {

        CodeMemberField field = new CodeMemberField();

        field.Name = getName(file);

        field.Type = new CodeTypeReference(typeof(string));

        field.Attributes = MemberAttributes.Public | MemberAttributes.Const;

        field.InitExpression = getInitExpression(getUrl(file));

        addSummary(field, getUrl(file, false));

        ensureUniqueMemberName(members, field);

 

        parent.Members.Add(field);

      }

    }

 

    ///

    /// process subfolders:

    /// iterate over folders and add a nested class

    ///

    DirectoryInfo[] subfolders = dir.GetDirectories();

    foreach (DirectoryInfo folder in subfolders)

    {

      if (isValidFolder(folder))

      {

        CodeTypeDeclaration nested = createStaticClass(getName(folder), false);

        addSummary(nested, "Provides access to urls under: {0}", getUrl(folder, false).TrimStart('~'));

        ensureUniqueMemberName(members, nested);

        build(folder, nested);

 

        /// .ctor will have already been added to members

        /// so only add to parent if there are additional

        /// members present

        if (nested.Members.Count > 1)

          parent.Members.Add(nested);

      }

    }

 

    --_depth;

  }

 

  #endregion

 

  #region init

 

  /// <summary>

  /// Initialises settings from the config file.

  /// </summary>

  /// <example>

  ///

  /// Sample configuration - all nodes and elements are optional, but the file

  /// needs a root node to load as an xml document.

  /// <?xml version="1.0" encoding="utf-8" ?>

  /// <settings

  ///    namespace=""

  ///    className="Href"

  ///    maxDepth="100"

  ///    lowercaseUrls="true"

  ///    includeExtension="false">

  ///   

  ///    <files include="\.(aspx|html)$" exclude="\.htm$" />

  ///    <folders include="" exclude="App_|Bin|Templates" />

  ///  </settings>

  ///

  /// </example>

  void init()

  {

    _basePath = HostingEnvironment.ApplicationPhysicalPath;

 

    XmlDocument xml = new XmlDocument();

    using (Stream stream = VirtualPathProvider.OpenFile(base.VirtualPath))

      xml.Load(stream);

 

    /// code below uses syntax,

    ///      xml["elementName"] ?? xml.CreateElement( "elementName" );

    /// as a lazy initialisation technique, rather than checking for

    /// the node and performing alternate initialisation - this keeps

    /// it within the same reading context.

 

    ///

    /// general settings

    ///

    XmlElement settings = xml["settings"] ?? xml.CreateElement("settings");

    _maxDepth = int.Parse(getAttributeValue(settings, "maxDepth", "100"));

    _useLowerCaseUrls = bool.Parse(getAttributeValue(settings, "lowercaseUrls", "false"));

    _includeExtension = bool.Parse(getAttributeValue(settings, "includeExtension", "false"));

    _makePartial = bool.Parse(getAttributeValue(settings, "makePartial", "false"));

    _namespace = getAttributeValue(settings, "namespace");

    _className = getAttributeValue(settings, "className", "Href");

 

    ///

    /// files settings

    ///

    XmlElement files = settings["files"] ?? xml.CreateElement("files");

    isValidFile = createFilter(

      getAttributeValue(files, "include", @"\.aspx$"),

      getAttributeValue(files, "exclude")

      );

 

    ///

    /// folders settings

    ///

    XmlElement folders = settings["folders"] ?? xml.CreateElement("folders");

    isValidFolder = createFilter(

      getAttributeValue(folders, "include"),

      getAttributeValue(folders, "exclude", "App_|Bin")

      );

  }

 

  #endregion

 

  #region getAttributeValue

 

  /// <summary>

  ///

  /// </summary>

  /// <param name="parent"></param>

  /// <param name="name"></param>

  /// <param name="default"></param>

  /// <returns></returns>

  static string getAttributeValue(XmlElement parent, string name, string @default)

  {

    XmlAttribute attribute = parent.Attributes[name];

    return null == attribute ? @default : attribute.Value.Trim();

  }

 

  /// <summary>

  ///

  /// </summary>

  /// <param name="parent"></param>

  /// <param name="name"></param>

  /// <returns></returns>

  static string getAttributeValue(XmlElement parent, string name)

  {

    return getAttributeValue(parent, name, null);

  }

 

  #endregion

 

  #region createFilter

 

  /// <summary>

  /// Creates a Predicate for filtering based on the given regex patterns

  /// </summary>

  /// <param name="includePattern"></param>

  /// <param name="excludePattern"></param>

  /// <returns></returns>

  static Predicate<FileSystemInfo> createFilter(string includePattern, string excludePattern)

  {

    Regex include = null, exclude = null;

 

    if (!string.IsNullOrEmpty(includePattern))

      include = new Regex(includePattern,

        RegexOptions.Compiled | RegexOptions.IgnoreCase);

 

    if (!string.IsNullOrEmpty(excludePattern))

      exclude = new Regex(excludePattern,

        RegexOptions.Compiled | RegexOptions.IgnoreCase);

 

    return createFilter(include, exclude);

  }

 

  /// <summary>

  /// Creates a Predicate for filtering based on the given regex

  /// </summary>

  /// <param name="include"></param>

  /// <param name="exclude"></param>

  /// <returns></returns>

  static Predicate<FileSystemInfo> createFilter(Regex include, Regex exclude)

  {

    if (null != include && null != exclude)

      return delegate(FileSystemInfo info) { return include.IsMatch(info.FullName) && !exclude.IsMatch(info.FullName); };

    else if (null != include)

      return delegate(FileSystemInfo info) { return include.IsMatch(info.FullName); };

    else if (null != exclude)

      return delegate(FileSystemInfo info) { return !exclude.IsMatch(info.FullName); };

    else

      return delegate(FileSystemInfo info) { return true; };

  }

 

  #endregion

 

  #region getInitExpression

 

  /// <summary>

  /// Returns the given value surrounded with quotes.

  /// </summary>

  /// <param name="value"></param>

  /// <returns></returns>

  static CodeSnippetExpression getInitExpression(string value)

  {

    return new CodeSnippetExpression(string.Concat("\"", value, "\""));

  }

 

  #endregion

 

  #region getName

 

  /// <summary>

  /// Gets the normalised/escaped member name for the given file

  /// </summary>

  /// <param name="file"></param>

  /// <returns></returns>

  string getName(FileSystemInfo file)

  {

    string name = Path.GetFileNameWithoutExtension(file.Name);

 

    if (_includeExtension)

      name += file.Extension.Replace('.', '_');

 

    name = Regex.Replace(name, @"[^a-z0-9_]*", string.Empty,

      RegexOptions.Compiled | RegexOptions.IgnoreCase);

 

    // Ensure pascal casing of the name - not really required

    //name = TextHelper.PascalCase( name );

 

    // Escape names not starting with a letter

    if (name.Length > 0 && !char.IsLetter(name[0]))

      name = "_" + name;

 

    // Escape C# keywords

    // included in separate library, omitted here for clarity

    //name = CodeHelper.CSharp.EscapeWord(name);

 

    return name;

  }

 

  #endregion

 

  #region getUrl

 

  /// <summary>

  /// Gets the url of the file, relative to the app root

  /// </summary>

  /// <param name="file"></param>

  /// <returns></returns>

  string getUrl(FileSystemInfo file)

  {

    return getUrl(file, true);

  }

 

  /// <summary>

  /// Gets the url of the file, relative to the app root

  /// </summary>

  /// <param name="file"></param>

  /// <param name="allowNormalisation">When true, allows lowercasing of the

  /// url and stripping of default.aspx</param>

  /// <returns></returns>

  string getUrl(FileSystemInfo file, bool allowNormalisation)

  {

    string url = file.FullName.Substring(_basePath.Length);

 

    if (allowNormalisation)

    {

      if (_useLowerCaseUrls)

        url = url.ToLower();

 

      if (url.EndsWith("default.aspx", StringComparison.OrdinalIgnoreCase))

        url = url.Substring(0, url.Length - 12);

    }

 

    return string.Concat("~/", url.Replace("\\", "/"));

  }

 

  #endregion

 

  #region addSummary

 

  /// <summary>

  /// Adds a summary doc comment to the type's comment collection

  /// </summary>

  /// <param name="type"></param>

  /// <param name="format"></param>

  /// <param name="args"></param>

  void addSummary(CodeTypeMember type, string format, params object[] args)

  {

    addSummary(type.Comments, format, args);

  }

 

  /// <summary>

  /// Adds a summary doc comment to the collection

  /// </summary>

  /// <param name="comments"></param>

  /// <param name="format"></param>

  /// <param name="args"></param>

  void addSummary(CodeCommentStatementCollection comments, string format, params object[] args)

  {

    comments.Add(new CodeCommentStatement("<summary>", true));

    comments.Add(new CodeCommentStatement(string.Format(format, args), true));

    comments.Add(new CodeCommentStatement("</summary>", true));

  }

 

  #endregion

 

  #region createStaticClass

 

  /// <summary>

  /// Creates a static class type with the given name

  /// </summary>

  /// <param name="name"></param>

  /// <param name="partial"></param>

  /// <returns></returns>

  static CodeTypeDeclaration createStaticClass(string name, bool partial)

  {

    CodeTypeDeclaration type = new CodeTypeDeclaration(name);

    type.TypeAttributes |= System.Reflection.TypeAttributes.Sealed;

    type.Attributes = MemberAttributes.Public | MemberAttributes.Static;

    type.IsClass = true;

    type.IsPartial = partial;

 

    CodeConstructor ctor = new CodeConstructor();

    ctor.Attributes = MemberAttributes.Private;

    type.Members.Add(ctor);

 

    return type;

  }

 

  #endregion

 

  #region ensureUniqueMemberName

 

  /// <summary>

  /// Ensures a unique name within the dictionary to get the name

  /// with an index number appended to the end e.g. the second

  /// occurrence of "MyProperty" becomes "MyProperty1"

  /// </summary>

  /// <remarks>A caveat of this method is where you may have two members

  /// like MyPage.aspx, MyPage.html as well as MyPage1.aspx. A conflict

  /// will occur giving duplicate members i.e. in the same order MyPage,

  /// MyPage1 and MyPage1 a second time. The likelihood of such conflicts

  /// is possible, with one possible solution to recurse using

  /// ensureUniqueMemberName with the newly generated name, or enumerate

  /// the parent type's Members to see whether there will be a conflict.

  /// </remarks>

  /// <param name="members">Dictionary containing the name of the members

  /// in the current namespace as the key + the number of appearances as

  /// the value.</param>

  /// <param name="type">The next member to add to the current namespace.</param>

  static void ensureUniqueMemberName(Dictionary<string, int> members, CodeTypeMember type)

  {

    string key = type.Name;

    int count;

    if (members.TryGetValue(key, out count))

    {

      type.Name = string.Concat(key, ++count);

      members[key] = count;

    }

    else

    {

      members[key] = 0;

    }

  }

 

  #endregion

}

}

 

kick it on DotNetKicks.com

posted @ Sunday, February 03, 2008 3:51 PM | Feedback (12)
Filed Under [ C#, Utilities, Tips, ASP.NET, Source Code ]

Thursday, December 20, 2007

ShortGuid - A shorter and url friendly GUID class in C#

I like Mads Kristensen, he's often coming up with useful code snippets which he shares with the community AND he heads up the BlogEngine.NET project. He's produced the GuidEncoder helper class, which takes a standard guid like this:

c9a646d3-9c61-4cb7-bfcd-ee2522c8f633

And shortens it to a smaller string like this:

00amyWGct0y_ze4lIsj2Mw

Which is a huge help if you happen to be using guids in your URL's. You can read his article, A shorter and URL friendly GUID, on his website.

I've gone one step further than Mads, and created a struct which encapsulates the GuidEncoder functionality. I've called mine the "ShortGuid", but you could equally call it a "Sguid" if you like how that sounds (and if you pronounce GUID like "squid" and not "goo-id"). Mads's code for en/decoding the guid is still in there as static methods, which you can see further down - I'm pretty sure it hasn't been changed.

I'll jump right into the usage, if you're interested in the code, jump to the bottom and download the source.

Using the ShortGuid

The ShortGuid is compatible with normal Guid's and other ShortGuid strings. Let's see an example:

Guid guid = Guid.NewGuid();

ShortGuid sguid1 = guid; // implicitly cast the guid as a shortguid

Console.WriteLine( sguid1 );

Console.WriteLine( sguid1.Guid );

This produces a new guid, uses that guid to create a ShortGuid, and displays the two equivalent values in the console. Results would be something along the lines of:

FEx1sZbSD0ugmgMAF_RGHw
b1754c14-d296-4b0f-a09a-030017f4461f

Or you can implicitly cast a string to a ShortGuid as well.

string code = "Xy0MVKupFES9NpmZ9TiHcw";

ShortGuid sguid2 = code; // implicitly cast the string as a shortguid

Console.WriteLine( sguid2 );

Console.WriteLine( sguid2.Guid );

Which produces the following:

Xy0MVKupFES9NpmZ9TiHcw
540c2d5f-a9ab-4414-bd36-9999f5388773

Flexible with your other data types

The ShortGuid is made to be easily used with the different types, so you can simplify your code. Take note of the following examples:

// for a new ShortGuid, just like Guid.NewGuid()

ShortGuid sguid = ShortGuid.NewGuid();

 

// to cast the string "myString" as a ShortGuid,

string myString = "Xy0MVKupFES9NpmZ9TiHcw";

 

// the following 3 lines are equivilent

ShortGuid sguid = new ShortGuid( myString ); // traditional

ShortGuid sguid = (ShortGuid)myString; // explicit cast

ShortGuid sguid = myString; // implicit cast

 

// Likewise, to cast the Guid "myGuid" as a ShortGuid

Guid myGuid = new Guid( "540c2d5f-a9ab-4414-bd36-9999f5388773" );

 

// the following 3 lines are equivilent

ShortGuid sguid = new ShortGuid( myGuid ); // traditional

ShortGuid sguid = (ShortGuid)myGuid; // explicit cast

ShortGuid sguid = myGuid; // implicit cast

After you've created your ShortGuid's the 3 members of most interest are the original Guid value, the new short string (the short encoded guid string), and the ToString() method, which also returns the short encoded guid string.

sguid.Guid; // gets the Guid part

sguid.Value; // gets the encoded Guid as a string

sguid.ToString(); // same as sguid.Value

Easy comparison with guid's and strings

You can also do equals comparison against the three types, Guid, string and ShortGuid like in the following example:

Guid myGuid = new Guid( "540c2d5f-a9ab-4414-bd36-9999f5388773" );

ShortGuid sguid = (ShortGuid)"Xy0MVKupFES9NpmZ9TiHcw";

 

if( sguid == myGuid )

  // logic if guid and sguid are equal

 

if( sguid == "Xy0MVKupFES9NpmZ9TiHcw" )

  // logic if string and sguid are equal

ShortGuid Source Code

Following is the full listing in C#, or you can download the source code for the ShortGuid struct.

using System;

 

namespace CSharpVitamins

{

  /// <summary>

  /// Represents a globally unique identifier (GUID) with a

  /// shorter string value. Sguid

  /// </summary>

  public struct ShortGuid

  {

    #region Static

 

    /// <summary>

    /// A read-only instance of the ShortGuid class whose value

    /// is guaranteed to be all zeroes.

    /// </summary>

    public static readonly ShortGuid Empty = new ShortGuid(Guid.Empty);

 

    #endregion

 

    #region Fields

 

    Guid _guid;

    string _value;

 

    #endregion

 

    #region Contructors

 

    /// <summary>

    /// Creates a ShortGuid from a base64 encoded string

    /// </summary>

    /// <param name="value">The encoded guid as a

    /// base64 string</param>

    public ShortGuid(string value)

    {

      _value = value;

      _guid = Decode(value);

    }

 

    /// <summary>

    /// Creates a ShortGuid from a Guid

    /// </summary>

    /// <param name="guid">The Guid to encode</param>

    public ShortGuid(Guid guid)

    {

      _value = Encode(guid);

      _guid = guid;

    }

 

    #endregion

 

    #region Properties

 

    /// <summary>

    /// Gets/sets the underlying Guid

    /// </summary>

    public Guid Guid

    {

      get { return _guid; }

      set

      {

        if (value != _guid)

        {

          _guid = value;

          _value = Encode(value);

        }

      }

    }

 

    /// <summary>

    /// Gets/sets the underlying base64 encoded string

    /// </summary>

    public string Value

    {

      get { return _value; }

      set

      {

        if (value != _value)

        {

          _value = value;

          _guid = Decode(value);

        }

      }

    }

 

    #endregion

 

    #region ToString

 

    /// <summary>

    /// Returns the base64 encoded guid as a string

    /// </summary>

    /// <returns></returns>

    public override string ToString()

    {

      return _value;

    }

 

    #endregion

 

    #region Equals

 

    /// <summary>

    /// Returns a value indicating whether this instance and a

    /// specified Object represent the same type and value.

    /// </summary>

    /// <param name="obj">The object to compare</param>

    /// <returns></returns>

    public override bool Equals(object obj)

    {

      if (obj is ShortGuid)

        return _guid.Equals(((ShortGuid)obj)._guid);

      if (obj is Guid)

        return _guid.Equals((Guid)obj);

      if (obj is string)

        return _guid.Equals(((ShortGuid)obj)._guid);

      return false;

    }

 

    #endregion

 

    #region GetHashCode

 

    /// <summary>

    /// Returns the HashCode for underlying Guid.

    /// </summary>

    /// <returns></returns>

    public override int GetHashCode()

    {

      return _guid.GetHashCode();

    }

 

    #endregion

 

    #region NewGuid

 

    /// <summary>

    /// Initialises a new instance of the ShortGuid class

    /// </summary>

    /// <returns></returns>

    public static ShortGuid NewGuid()

    {

      return new ShortGuid(Guid.NewGuid());

    }

 

    #endregion

 

    #region Encode

 

    /// <summary>

    /// Creates a new instance of a Guid using the string value,

    /// then returns the base64 encoded version of the Guid.

    /// </summary>

    /// <param name="value">An actual Guid string (i.e. not a ShortGuid)</param>

    /// <returns></returns>

    public static string Encode(string value)

    {

      Guid guid = new Guid(value);

      return Encode(guid);

    }

 

    /// <summary>

    /// Encodes the given Guid as a base64 string that is 22

    /// characters long.

    /// </summary>

    /// <param name="guid">The Guid to encode</param>

    /// <returns></returns>

    public static string Encode(Guid guid)

    {

      string encoded = Convert.ToBase64String(guid.ToByteArray());

      encoded = encoded

        .Replace("/", "_")

        .Replace("+", "-");

      return encoded.Substring(0, 22);

    }

 

    #endregion

 

    #region Decode

 

    /// <summary>

    /// Decodes the given base64 string

    /// </summary>

    /// <param name="value">The base64 encoded string of a Guid</param>

    /// <returns>A new Guid</returns>

    public static Guid Decode(string value)

    {

      value = value

        .Replace("_", "/")

        .Replace("-", "+");

      byte[] buffer = Convert.FromBase64String(value + "==");

      return new Guid(buffer);

    }

 

    #endregion

 

    #region Operators

 

    /// <summary>

    /// Determines if both ShortGuids have the same underlying

    /// Guid value.

    /// </summary>

    /// <param name="x"></param>

    /// <param name="y"></param>

    /// <returns></returns>

    public static bool operator ==(ShortGuid x, ShortGuid y)

    {

      if ((object)x == null) return (object)y == null;

      return x._guid == y._guid;

    }

 

    /// <summary>

    /// Determines if both ShortGuids do not have the

    /// same underlying Guid value.

    /// </summary>

    /// <param name="x"></param>

    /// <param name="y"></param>

    /// <returns></returns>

    public static bool operator !=(ShortGuid x, ShortGuid y)

    {

      return !(x == y);

    }

 

    /// <summary>

    /// Implicitly converts the ShortGuid to it's string equivilent

    /// </summary>

    /// <param name="shortGuid"></param>

    /// <returns></returns>

    public static implicit operator string(ShortGuid shortGuid)

    {

      return shortGuid._value;

    }

 

    /// <summary>

    /// Implicitly converts the ShortGuid to it's Guid equivilent

    /// </summary>

    /// <param name="shortGuid"></param>

    /// <returns></returns>

    public static implicit operator Guid(ShortGuid shortGuid)

    {

      return shortGuid._guid;

    }

 

    /// <summary>

    /// Implicitly converts the string to a ShortGuid

    /// </summary>

    /// <param name="shortGuid"></param>

    /// <returns></returns>

    public static implicit operator ShortGuid(string shortGuid)

    {

      return new ShortGuid(shortGuid);

    }

 

    /// <summary>

    /// Implicitly converts the Guid to a ShortGuid

    /// </summary>

    /// <param name="guid"></param>

    /// <returns></returns>

    public static implicit operator ShortGuid(Guid guid)

    {

      return new ShortGuid(guid);

    }

 

    #endregion

  }

}

 

kick it on DotNetKicks.com

posted @ Thursday, December 20, 2007 5:15 PM | Feedback (40)
Filed Under [ C#, Tips, .NET, Source Code ]

Wednesday, December 19, 2007

Programmatically setting the SmtpClient pickup directory location at runtime

In my last post on using an smtp pickup directory for ASP.NET development, I explained some of the reasons to use a pickup folder instead of an SMTP server during ASP.NET development. One of the caveats of that configuration is that you have to enter the full path of the folder you want to store the application's mail in. This is fine if you're working solo, or can agree on a common folder/path use amongst your colleagues but it reduces the instant portability of your application, since you'll have the full path of the folder set in the web.config.

You can always use the configSource property for your web.config/system.net configuration element, and specify the 'per machine details' in a separate file which isn't source controlled. Not a bad idea.

However, it still doesn't solve the 'portability' issue. ASP.NET 2.0 (Visual Studio 2005) introduced the new Web Project model, where you can simply open a folder from the file system as a website and start coding. You can copy or move the folder to another location, or another machine, and open the folder in VS2005, and once again you're away.

With that in mind, configuring the SpecifiedPickupDirectory PickupDirectoryLocation as an absolute path (which is required by the framework) isn't the best solution as it ties the mail drop folder to a fixed path and you'll have to reconfigure if you move your project.

Changing the mailSettings through the configuration model

It's not entirely straight forward to change the location of the pickup folder at runtime. Initially, I thought of using the frameworks built-in functionality to change the application's mailSettings. Which looks something like this: 

Configuration config = WebConfigurationManager.OpenWebConfiguration( "~/web.config" );

MailSettingsSectionGroup mail = (MailSettingsSectionGroup)config.GetSectionGroup( "system.net/mailSettings" );

if( mail.Smtp.DeliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory )

{

  string path = Path.Combine( HttpRuntime.AppDomainAppPath, @"..\Mail" );

  mail.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation = path;

}

But on closer inspection of the SmtpClient class using Reflector, you'll find that the default smtp settings utilise a static instance of an internal class called MailSettingsSectionGroupInternal, and is initialised from the default web.config settings. So changes to the runtime configuration don't effect the actual values the SmtpClient uses. That pretty much means the settings can only be 'set' when the configuration is loaded.

The only way we can get our changes to be applied is to save the configuration (with a call to config.Save()), which means writing to the web.config file. This in turn triggers the application to reload the changes. It's not ideal, since the app will be restarted and it's bad because it updates the web.config file with the new absolute path to the pickup folder. It's pretty much back to square one.

Reflection to the rescue

In .NET, nothing is too far from our reach if you know how to go about it. To programmatically set the PickupDirectoryLocation at runtime, all we need to do is use reflection to navigate our way down a path of interal classes and properties, to the single private variable that needs to change, and then simply set it.

The hierarchy that we need to change looks like this: System.Net.Mail.SmtpClient.MailConfiguration.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation

Since we can access the public member SmtpClient, we'll start there and get the first internal (and static) property, MailConfiguration. This should be called or defined in the Application_Start method of your Global.asax.

First, let's define some variables for working with, as well as the path we want to use for our drop folder. In this instance, I've set the path to the folder above the current application root, in a folder called "Mail".

BindingFlags instanceFlags = BindingFlags.Instance | BindingFlags.NonPublic;

PropertyInfo prop;

object mailConfiguration, smtp, specifiedPickupDirectory;

string path = Path.Combine( HttpRuntime.AppDomainAppPath, @"..\Mail" );

Then get the first static property and object from the SmtpClient

// get static internal property: MailConfiguration

prop = typeof( SmtpClient ).GetProperty( "MailConfiguration", BindingFlags.Static | BindingFlags.NonPublic );

mailConfiguration = prop.GetValue( null, null );

Continue down the hierarchy from the object we just assigned to mailConfiguration, getting the properties, and then the value, for each instance.

// get internal property: Smtp

prop = mailConfiguration.GetType().GetProperty( "Smtp", instanceFlags );

smtp = prop.GetValue( mailConfiguration, null );

 

// get internal property: SpecifiedPickupDirectory

prop = smtp.GetType().GetProperty( "SpecifiedPickupDirectory", instanceFlags );

specifiedPickupDirectory = prop.GetValue( smtp, null );

Lastly, get the field we want to set, as the corresponding property doesn't provide a setter.

// get private field: pickupDirectoryLocation, then set it to the supplied path

FieldInfo field = specifiedPickupDirectory.GetType().GetField( "pickupDirectoryLocation", instanceFlags );

field.SetValue( specifiedPickupDirectory, path );

And now, whenever an instance of SmtpClient is created, it will be initialised with the new pickup folder path.

A C# helper class, for your convenience.

So now you know how to set the property, here is the full listing of the helper class I use to achieve it.

How to use the CSharpVitamins.MailHelper class.

void Application_Start(object sender, EventArgs e)

{

  // set drop folder for mail

  if( MailHelper.IsUsingPickupDirectory )

    MailHelper.SetRelativePickupDirectoryLocation( @"..\Mail" );

}

Source code for the CSharpVitamins.MailHelper class.

using System;

using System.Configuration;

using System.Web;

using System.IO;

using System.Reflection;

using System.Net.Mail;

using System.Net.Configuration;

 

namespace CSharpVitamins

{

  public static class MailHelper

  {

    static bool? _isUsingPickupDirectory;

 

    /// <summary>

    /// Gets a value to indicate if the default SMTP Delivery

    /// method is SpecifiedPickupDirectory

    /// </summary>

    public static bool IsUsingPickupDirectory

    {

      get

      {

        if( !_isUsingPickupDirectory.HasValue )

        {

          Configuration config = WebConfigurationManager.OpenWebConfiguration( "~/web.config" );

          MailSettingsSectionGroup mail = (MailSettingsSectionGroup)config.GetSectionGroup( "system.net/mailSettings" );

          _isUsingPickupDirectory = mail.Smtp.DeliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory;

        }

        return _isUsingPickupDirectory.Value;

      }

    }

 

    /// <summary>

    /// Sets the default PickupDirectoryLocation for the SmtpClient.

    /// </summary>

    /// <remarks>

    /// This method should be called to set the PickupDirectoryLocation

    /// for the SmtpClient at runtime (Application_Start)

    ///

    /// Reflection is used to set the private variable located in the

    /// internal class for the SmtpClient's mail configuration:

    /// System.Net.Mail.SmtpClient.MailConfiguration.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation

    ///

    /// The folder must exist.

    /// </remarks>

    /// <param name="path"></param>

    public static void SetPickupDirectoryLocation( string path )

    {

      BindingFlags instanceFlags = BindingFlags.Instance | BindingFlags.NonPublic;

      PropertyInfo prop;

      object mailConfiguration, smtp, specifiedPickupDirectory;

 

      // get static internal property: MailConfiguration

      prop = typeof( SmtpClient ).GetProperty( "MailConfiguration", BindingFlags.Static | BindingFlags.NonPublic );

      mailConfiguration = prop.GetValue( null, null );

 

      // get internal property: Smtp

      prop = mailConfiguration.GetType().GetProperty( "Smtp", instanceFlags );

      smtp = prop.GetValue( mailConfiguration, null );

 

      // get internal property: SpecifiedPickupDirectory

      prop = smtp.GetType().GetProperty( "SpecifiedPickupDirectory", instanceFlags );

      specifiedPickupDirectory = prop.GetValue( smtp, null );

 

      // get private field: pickupDirectoryLocation, then set it to the supplied path

      FieldInfo field = specifiedPickupDirectory.GetType().GetField( "pickupDirectoryLocation", instanceFlags );

      field.SetValue( specifiedPickupDirectory, path );

    }

 

    /// <summary>

    /// Sets the default PickupDirectoryLocation for the SmtpClient

    /// to the relative path from the current web root.

    /// </summary>

    /// <param name="path">Relative path to the web root</param>

    public static void SetRelativePickupDirectoryLocation( string path )

    {

      SetPickupDirectoryLocation( HttpRuntime.AppDomainAppPath, path );

    }

 

    /// <summary>

    /// Sets the default PickupDirectoryLocation for the SmtpClient.

    /// </summary>

    /// <remarks>

    /// This is a shortcut for passing in two paths, which are then

    /// combined to set the pickup directory.

    /// </remarks>

    /// <param name="path1">Base path</param>

    /// <param name="path3">Relative path to be combined with </param>

    public static void SetPickupDirectoryLocation( string path1, string path3 )

    {

      SetPickupDirectoryLocation( Path.Combine( path1, path3 ) );

    }

  }

}

Click here to download the source code for the MailHelper class.

Conclusion

In summary, it appears that we need to jump through a few hoops to achieve something fairly small in nature. Although encapsulating our code into a helper class improves the process of applying our desired setting, I couldn't find anything more straight forward to accomplish the task.

kick it on DotNetKicks.com

posted @ Wednesday, December 19, 2007 6:23 PM | Feedback (13)
Filed Under [ C#, Utilities, Tips, .NET, ASP.NET, Source Code ]

Wednesday, November 28, 2007

Using an smtp pickup directory for ASP.NET development

I don't know why this never occurred to me before, but using SpecifiedPickupDirectory as the SmtpDeliveryMethod for sending email while developing ASP.NET websites is a very good idea.

I was recently 'forced' to start using a mail pickup directory for development when I upgraded to Vista (yes I said upgraded) and found out that IIS7 doesn't ship with an SMTP Server. Many blog posts out there recommend using third party software like SmarterMail, Free SMTP Server or other cheap shareware products, but if it's for your development machine I say: don't do it!

Why not simply configure your mail settings to use a pickup directory?

  • All email is written as an .eml file to the folder you specify.
  • Peace of mind that your system in development isn't going to send your client an unintended email.
  • No more waiting for your test emails to be delivered . Network problems and latency disappear.
  • You can monitor every email your application is sending via a folder. Okay, you have log files for this in SMTP, but peeking into a folder is pretty convenient during development.
  • Your inbox won't get cluttered with numerous development emails, they just sit in the pickup folder ready for you to view and/or delete.
  • No more fake or catch-all email addresses to maintain. Because each email is dumped to the folder, you just open it from the file system.
  • Store your mail in a separate folder for each website for easier use.
  • Order the files in your pickup directory by "Date Modified" to see the latest messages that have arrived as you'll most probably want to check the last few messages as you develop.

And of course we have some cons to our pros... well, a "con" at least.

  • Files use a GUID as their filename - not very helpful as you have to open the message (or wait for the summary to be extracted as a tool tip) to view the subject line, sender and recipient.

Of course, this concept really only applies to your development machine. You may want set up an SMTP server for your staging servers (and obviously your production servers ;), but your dev PC is personal - keep it that way.

Need further convincing?

Argument #1: Mass Mail

Imagine developing a newsletter application, and you need a few thousand addresses to test load (and that funky status widget you built to monitor the send progress). You don't want to have to go and download the few thousand emails to verify them. Look in your pickup folder. Count the number of emails produced. Open a few and inspect the contents. Look for differences in file size; it may give you a clue that something has gone awry.

Argument #2: Testing with a copy of the production database, or at least, some semi-real data

So you've decided to download a copy of the production database so you can test your new features with some real data. You don't want all that new email functionality sending the real recipient your half completed development emails. Look at the pickup directory for the emails of the user your testing against - exactly the way they would see them.

Argument #3: Waiting

From my experience, sending mail from within our development environment, to an outside network takes a wee bit of time. Usually, production environment is fairly instant for sending mail, and that's great. But in dev mode, you might have to hit your send & receive button a few times. Maybe you receive the email, maybe you don't. You hit send & receive a few more times, come to the conclusion something is b0rked, the mail didn't send or the outside network went down. Execute the step to send the email again, hit send & receive only to get both emails. sigh. Again, it's simple, after you've completed the step that sends the email, have a peek at the new file in the pickup folder.

If you're now convinced, you may pass go, collect $200 and move on to the configuration settings.

Mail Settings Configuration

Configuration is simple, just change the delivery method to SpecifiedPickupDirectory, and add the location to the folder you want to use to store your mail in as per the example below. You do have to enter the full path of the pickup directory, and the directory must exist, but that's really the only caveat.

<system.net>
  <mailSettings>
    <smtp deliveryMethod="SpecifiedPickupDirectory" from="no-reply@mydomain.com">
      <specifiedPickupDirectory pickupDirectoryLocation="C:\Development\MyWebApp\Mail" />
    </smtp>
  </mailSettings>
</system.net>


And that's about all there is to it. Once you let go of your old ways and adopt the pickup directory, it becomes a pretty reliable way to get your development done. I hope this helps.

[UPDATE] See my follow on post Programmatically setting the SmtpClient pickup directory location at runtime, for another helpful development technique.


kick it on DotNetKicks.com

posted @ Wednesday, November 28, 2007 3:34 AM | Feedback (17)
Filed Under [ C#, Tips, .NET, ASP.NET ]

Monday, May 14, 2007

The Smield: An unobtrusive javascript UI Helping Smart Field

The 'smield', or smart field, is a useful little user interface widget written in javascript. It combines the values of other fields on the page to allow for easier input of repeated content. Based on some javascript configuration, the smield will insert an unordered list of label and radio button pairs under the target input field (of the smield).

A few common examples might be:

  • You have a list of customers. Some of those customers might be known by a their company name, others might use first and last name combined, and others again might use a nick name.
  • You've just uploaded a file to the web server, and you need an intuitive option to leave the file as the original file name, or select an option and specify a new name.

The smield allows the user to easily select the value for the new field based on the information already entered in the form. Don't want to enter your new username because you want to use your email? Click the "Use Email" option below the username field, or check the option that combines your first and last names with a dot.

The next image (an animated gif no less!) gives you an idea of how it works (thanks Cropper).

Animated gif showing the Smield in action

Example and Source Code

View the Smield Demo Page for all the info (source code, example setup, etc...) and a working example. Or just grab the javascript source for the smield (or minified smield source).

Features

  • Unobtrusively extends input fields in HTML forms
  • Combine fields to populate another field
  • Doesn't require any additional frameworks or libraries.
  • Plays nicely with with 3 great frameworks: YUI, Prototype and Microsoft ASP.NET AJAX Extensions (erm, Atlas)
  • Easily apply custom filtering of input; useful for stripping unwanted characters, normalising usernames etc...
  • Minimal setup
  • Supports form resetting
  • Minified javascript source <7Kb
  • Saves your users time and helps lead data entry
  • Makes you happy *

* individual results may vary

Configuration

Configuration isn't difficult, although it could be shorter.

If you want to use a smield, this is what you need:

for the smield:

new CSharpVitamins.Smield(
   [input field (String or HTMLElement)]
,
   [choices (SmieldChoice array)],
   [options (Object, optional)]
   );

and for the choices of a smield:

new CSharpVitamins.SmieldChoice(
   [name (String)]
,
   [label (String)],
   [fields (String or HTMLElement array)],
   [separator (String, optional)]
   );

A simple code example for two options, say to specify a value or use the user's first and last name, would be:

var myChoices = [
    new CSharpVitamins.SmieldChoice( "other", "Specify" ), 
    new CSharpVitamins.SmieldChoice( "name", "Your Name", [ "txtFirstName", "txtLastName" ], " " ) 
];
new CSharpVitamins.Smield( "txtKnownAs", myChoices );

There are some options that can be passed in as the smield's third argument. They are as follows:

var myOptions = {
    group: "auto-generated if emtpy|name of group",
    listClass: "disabled|unordered list class",
    disabledClass: "input field class when the input field is readonly/disabled", 
// for the filter, specify any function that accepts and returns a string
    filter: function(value){return value.replace(/[^a-z]/g, "");}, 
    position: "below|above",
    protection: "readonly|disabled"
};

so our example becomes:

new CSharpVitamins.Smield( "myTextbox", myChoices, myOptions );

Note the "filter" option. In the example above, we provide our own custom function that strips out anything that is not a lowercase letter. Mmmm, Handy!

You might get a better idea of the configuration and how it works by looking at the source of the smield example page.

Question and Answer time

That's amazing! What doesn't the smield do?

It is amazing what something so simple can do. Alas, the smield does nothing beyond helping you create a slightly more intuitive user interface. It's quite superficial really.

The smield does not:

  • make use of AJAX. (It's DHTML, whether it's Web Two Point Ohhh is up to you.)
  • remove your original input field. (It's still a plain 'input of type equals text' field. Nothing funky is done to it; the field's value is submitted as any other form element without a smield.)
  • improve your sex life (well, it could, but I doubt it)

What do you mean "plays nicely" with other libraries?

Good question, I'm glad you asked.

In the interest of making the smield independent of any particular library, we chose to use a fairly standard implementation for adding event handlers to elements in the DOM. Originally, the smield was created using Prototype, but things have moved on and we use a variety of frameworks to get the job done these days.

Initially, given that most of the time we'd be using one library or another, I shied away from adding another set of event handling routines to the smield, bloating the code and potentially breaking functionality of those libraries. But instead, for the sake of portability, we decided to make the most of both worlds, and go with a default implementation as well - after all it would be silly not to for such a small piece of the overall pie.

What is meant by "plays nicely" is that, if it can, the smield will use the library method to add, remove and purge events, so that all the expected behaviour of those libraries stays in-tact.

For example, the chosen libraries handle adding and removing events in different ways:

Prototype YUI Microsoft ASP.NET AJAX
Adds event handlers to a global cache object, which is automatically cleared when the page unloads (helping to prevent memory leaks in IE) Also keeps track of handlers via a global cache and clears them on page unload. Can purge an element of all attached listeners. Adds event handlers to an element and caches them on the element itself, but dosen't have automatic purging. Can purge an element of the attached handlers.

So if we added handlers by another means than the library's method, and then tried to get or purge an element's events, because the smield didn't use the library, the smield's events would not be included and would still be left attached.

It's a border-line argument, but still a valid one. It introduces ambiguous behaviour if you're relying on the framework you use every day, and could be difficult to troubleshoot/debug. Whether anyone would be affected by it is another story. So for the sake of a little more code, we can play nice with YUI, Prototype and MS AJAX.

It's worth noting that the chosen library should be including before the smield. If the smield can't detect one of the libraries when it's loaded, it'll use it's own implementation.

But where do you draw the line? If we support "playing nicely" for every library out there, we might have a file that is a lot bigger than the functionality warrants.

We could take the Ext approach, and introduce an 'adapter'  or 'bridge' for the library that overrides the smields default implementation, but then that gives us two files to include - and isn't really worth the hassle for a small widget.

Thinking forward, a better way might be to simply provide a default method, so the smield can work in a stand-alone fashion. Then allow the smield's methods to be overridden as desired from your project.

Maybe we'll do this in a future version.

Summing up

So there you have it. The Smield, a helpful little UI tool to improve on the monotony of web forms. It's unobtrusive, simple to implement, has a small footprint and plays nicely with the 'big kid' libraries out there. If you have comments or questions, please leave your message below; or use the contact form.

 

kick it on DotNetKicks.com

posted @ Monday, May 14, 2007 6:25 PM | Feedback (1)
Filed Under [ Javascript, Source Code, User Interface ]

Recently on C# Vitamins...

Powered By Subtext

 

About C# Vitamins

Dave has been working in the industry for around 14 years, and has a focus on Javascript, C#, ASP.NET and SQL Server web development; not to mention being a standards driven type of guy.

C# Vitamins is the result of his findings while working in the web industry and a desire to share with the community; and if it was traced back far enough, you might say it might not have existed if he hadn't taken such an interest in id Software's original Quake.

Related Links

Below is a list of related links of Dave's other sites.