Some thoughts on global expression methods

In the past, I’ve sometimes found it useful to place some very generic extension methods in the global namespace, and/or at a visibility level corresponding to the classes extended. This can ease reuse, as the extension methods don’t require developers to take extra steps or have extra knowledge to use them; they’re just immediately available in type-ahead. Anyone who has suffered through using a namespace-happy API with multiple locations for extension methods may thank you for this approach.

As a brief example, consider the extension methods below. These mainly provide a bit of syntactic sugar for working with regular expressions, by converting what would often be multi-line calls into a single expression. The hiding of the System.Text.RegularExpressions namespace would be complete, except that in a few cases Match objects are exposed, as useful bundles of values and indices in the original string; System.Text.RegularExpressions would never need to be explicitly used even with those methods, if the var keyword were used.

using System.Collections.Generic;
using System.Text.RegularExpressions;

public static partial class StringExtensions
{
    private static MatchCollection EmptyMatchCollection = Regex.Matches("", "0");

    private static Match UnsuccessfulMatch = Regex.Match("a", "b");

    public static bool IsMatch(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return false;
        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;
        return Regex.IsMatch(s, pattern, options);
    }

    public static int IndexOfPattern(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return -1;
        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;
        Match match = Regex.Match(s, pattern, options);
        return match.Success ? match.Index : -1;
    }

    public static int LastIndexOfPattern(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return -1;
        RegexOptions options = RegexOptions.CultureInvariant | RegexOptions.RightToLeft;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;
        Match match = Regex.Match(s, pattern, options);
        return match.Success ? match.Index : -1;
    }

    public static MatchCollection Matches(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return EmptyMatchCollection;

        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;

        return Regex.Matches(s, pattern, options);
    }

    public static IEnumerable FindAll(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return new List();

        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;
        
        return Regex.Matches(s, pattern, options).Select(m => m.Value);
    }

    public static Match Match(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return UnsuccessfulMatch;

        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;

        return Regex.Match(s, pattern, options);
    }

    public static Match MatchLast(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return UnsuccessfulMatch;

        RegexOptions options = RegexOptions.CultureInvariant | RegexOptions.RightToLeft;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;

        return Regex.Match(s, pattern, options);
    }

    public static string Find(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return "";

        RegexOptions options = RegexOptions.CultureInvariant;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;

        Match match = Regex.Match(s, pattern, options);
        return match.Success ? match.Value : "";
    }

    public static string FindLast(this string s, string pattern, bool isCompiled = false, bool ignoreCase = false)
    {
        if (s == null || pattern == null) return "";

        RegexOptions options = RegexOptions.CultureInvariant | RegexOptions.RightToLeft;
        if (ignoreCase)
            options |= RegexOptions.IgnoreCase;
        if (isCompiled)
            options |= RegexOptions.Compiled;
        Match match = Regex.Match(s, pattern, options);
        return match.Success ? match.Value : "";
    }

}

The MatchCollection class itself is kind of an odd duck, as it’s a useful collection that was never fully updated for use with Linq. If one makes frequent use of regular expressions it might be useful to also put a few simple Linq-enabling extension methods on MatchCollection itself; it’s up to the user whether to make them global. I leave it to the reader to implement the rest of the Linq methods as deemed useful in a particular case.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

public static partial class MatchCollectionExtensions
{
    public static bool Any(this MatchCollection source, Func<Match, bool> predicate)
    {
        return source.Cast<Match>().Any(predicate);
    }

    public static Match First(this MatchCollection source)
    {
        return (source == null || source.Count == 0) ? null : source[0];
    }

    public static Match First(this MatchCollection source, Func<Match, bool> predicate) {
        return source.Cast<Match>().First(predicate);
    }

    public static Match FirstOrDefault(this MatchCollection source)
    {
        return (source == null || source.Count == 0) ? null : source[0];
    }

    public static Match FirstOrDefault<T>(this MatchCollection source, Func<Match, bool> predicate)
    {
        return source.Cast<Match>().FirstOrDefault(predicate);
    }

    public static Match Last<T>(this MatchCollection source)
    {
        return (source == null || source.Count == 0) ? null : source[0];
    }

    public static Match Last(this MatchCollection source, Func<Match, bool> predicate)
    {
        return source.Cast<Match>().Last(predicate);
    }

    public static Match LastOrDefault(this MatchCollection source)
    {
        return (source == null || source.Count == 0) ? null : source[source.Count - 1];
    }

    public static Match LastOrDefault<T>(this MatchCollection source, Func<Match, bool> predicate)
    {
        return source.Cast<Match>().LastOrDefault(predicate);
    }

    public static IOrderedEnumerable<Match> OrderBy<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector)
    {
        return source.Cast<Match>().OrderBy(keySelector);
    }

    public static IOrderedEnumerable<Match> OrderBy<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector, IComparer<TKey> comparer)
    {
        return source.Cast<Match>().OrderBy(keySelector, comparer);
    }

    public static IOrderedEnumerable<Match> OrderByDescending<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector)
    {
        return source.Cast<Match>().OrderByDescending(keySelector);
    }

    public static IOrderedEnumerable<Match> OrderByDescending<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector, IComparer<TKey> comparer)
    {
        return source.Cast<Match>().OrderByDescending(keySelector, comparer);
    }

    public static IEnumerable<Match> Reverse(this MatchCollection source)
    {
        return source.Cast<Match>().Reverse();
    }

    public static IEnumerable<TResult> Select<TResult>(this MatchCollection source, Func<Match, TResult> selector)
    {
        return source.Cast<Match>().Select(selector);
    }

    public static IEnumerable<TResult> Select<TResult>(this MatchCollection source, Func<Match, int, TResult> selector)
    {
        return source.Cast<Match>().Select(selector);
    }

    public static IEnumerable<Match> Skip(this MatchCollection source, int count)
    {
        return source.Cast<Match>().Skip(count);
    }

    public static IEnumerable<Match> Take(this MatchCollection source, int count)
    {
        return source.Cast<Match>().Take(count);
    }

    public static Dictionary<TKey, Match> ToDictionary<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector)
    {
        return source.Cast<Match>().ToDictionary(keySelector);
    }

    public static Dictionary<TKey, Match> ToDictionary<Match, TKey>(this MatchCollection source, Func<Match, TKey> keySelector, IEqualityComparer<TKey> comparer)
    {
        return source.Cast<Match>().ToDictionary(keySelector, comparer);
    }

    public static Dictionary<TKey, TElement> ToDictionary<Match, TKey, TElement>(this MatchCollection source, Func<Match, TKey> keySelector, Func<Match, TElement> elementSelector)
    {
        return source.Cast<Match>().ToDictionary(keySelector, elementSelector);
    }

    public static Dictionary<TKey, TElement> ToDictionary<Match, TKey, TElement>(this MatchCollection source, Func<Match, TKey> keySelector, Func<Match, TElement> elementSelector, IEqualityComparer<TKey> comparer)
    {
        return source.Cast<Match>().ToDictionary(keySelector, elementSelector, comparer);
    }

    public static List<Match> ToList<T>(this MatchCollection source)
    {
        return source.Cast<Match>().ToList();
    }

    public static IEnumerable<Match> Where<T>(this MatchCollection source, Func<Match, bool> predicate)
    {
        return source.Cast<Match>().Where(predicate);
    }
}

The MemoryCache class is another fundamentally useful .NET class that has a bit of a clumsy API, which can be remedied by providing facades to the clunkier methods. Very often one merely wants to set a cache duration when setting an object, and get an object of a specified type safely.

using System;
using System.Runtime.Caching;

public static partial class MemoryCacheExtensions
{
    public static void Set(this MemoryCache cache, string key, object value, long durationMilliseconds)
    {
        if (cache == null || key == null) throw new ArgumentNullException();
        else if (durationMilliseconds <= 0) return;
        else if (value == null)
            try { cache.Remove(key); } catch {}

        DateTimeOffset expiration = DateTime.Now.AddMilliseconds(durationMilliseconds);
        cache.Set(key, value, expiration);
    }

    public static T Get<T>(this MemoryCache cache, string key, T defaultValue = default(T))
    {
        object value = cache.Get(key);

        if (value == null || !(value is T))
            return defaultValue;
        else
            return (T)value;
    }
}

The common thread in these examples is that even in the .NET FCL (and CoreFX for that matter), API design is not always optimal. A primary use case for extension methods is where a developer desires to add useful functionality to an API, but without the ability to control the API itself, and this applies to .NET fundamentals as easily as anything else. When one is adding those methods to a core class, it may make sense to make the extension methods themselves as visible as the core classes so extended–and here the efficiency/usability gains of broadening visibility can be tainting in a sense, as above where the MatchCollection extensions are made global by virtue of the fact they may be returned from the result of another global extension method on the String class.

This may not be everyone’s cup of tea, and in fact I don’t tend to do this for seldom-used, specialty classes ever. The String class is obviously a different case from even MatchCollection and the like. And whenever creating extension methods, but increasing with their visibility, one should strongly consider placing them in partial, wisely named classes to avoid naming collisions in the future.

A tiny, cross-browser script to intercept third-party JavaScript injection via document.write()

Many of us have encountered third-party scripts which use document.write(), particularly for script injection. Certain older analytics scripts use this approach ({cough} {cough} {Omniture!}). If you’re stuck maintaining an older site with a projected end-of-life, you may not have the time to upgrade those old bad scripts away, especially if they’re hosted externally and dynamically generated.

Unfortunately, many of those same scripts were written without async loading in mind, and by render blocking, blocking on the DOM or CSSOM, etc. can greatly decrease performance of your site; a second-plus of unnecessary page-load time is not uncommon. The problem is so pervasive that Google finally put its metaphorical foot down, ever so lightly, by decreasing support for third-party document.write() statements; as an immediate effect of this change, certain scripts may not even be loaded on mobile, defeating the purpose of loading the scripts in the first place.

This can leave us in a pickle: how to intercept document.write() calls by these third-party scripts without disabling analytics or other functionality for which they were included in the first place, ideally while decreasing page-load times through the use of async/defer loading?

This can be done by replacing document.write with a function that intercepts attempts to inject scripts in this way, and instead adds them dynamically to the DOM, while passing through all other non-script parameters to the original document.write() function:



// 1. Moves the document.write() method, for safekeeping
document.writeText = document.write;

// 2. Assigns a new function to document.write(), to serve as a middleman
document.write = function(parameter) {
    if (!parameter) return;
	
	var scriptPattern = /<script.*?src=['|"](.*?)['|"]/;
	if (scriptPattern.test(parameter)) {
		// Get the value of the src attribute
		var srcAttribute = scriptPattern.exec(parameter)[1]; 
		// Add the script to the DOM 
		var script = document.createElement('script');
		script.src = srcAttribute;
		document.head.appendChild(script); 
	}
	else {
		document.writeText(parameter);
	}	
};

The last piece of the puzzle, if you’re also interested in decreasing page-load time with these older libraries, is to load them asynchronously. Options here would include the following:

1. Use of libraries such as postscribe , jQuery etc., to help with async-loading external scripts in general.

2. Using the async and/or defer properties directly on the script tag including a legacy library (I’m unaware as of this writing of a way to do this cleanly with cross-browser support using the DOM and avoiding use of innerHTML). This may be recommended against, as discussed further below after #3.

3. Especially in the case of a legacy script which is relatively lightweight itself but uses document.write() extensively to inject script tags, you could allow the first script to load synchronously, but use the technique above to write the script tags yourself, adding async and defer as desired. This may significantly decrease page load time, because the bulk of render-blocking script loads are avoided. To use this technique, modify the code sample to use document.writeText()–or whatever you’ve called your placeholder for the old document.write()–to emit an HTML script tag, using the regex-parsed src value and adding adding “async defer” directly.

Adobe in particular has consistently recommended against loading DTM libraries async, and this legacy can be seen stretching back to the days of the fore-mentioned Omniture, which unfortunately does use document.write() extensively to load other scripts from dynamically generated code. Thus a working approach to get at least some such scripts loaded asynchronously, while avoiding page flicker, can be to leave the script-loading legacy script in place as-is, but intercept its calls to document.write().

Global.asax, Keeping the Magic Alive

In my efforts to retrofit an old Sitecore Web Forms application for caching which is safe for use with postback, etc. in an elegant way, I needed to review the full set of “magic” methods available in the Global.asax application file, which ASP.NET wires up at runtime.

The application- and session-specific event methods are:

Application_Start
Application_End
Application_Error
Session_Start
Session_End

The request-specific events are:

Application_BeginRequest
Application_AuthenticateRequest
Application_AuthorizeRequest
Application_ResolveRequestCache
Application_AcquireRequestState
Application_PreRequestHandlerExecute
Application_PreSendRequestHeaders
Application_PreSendRequestContent
Application_PostRequestHandlerExecute
Application_ReleaseRequestState
Application_UpdateRequestCache
Application_EndRequest

Sources:
https://web.archive.org/web/20071223170129/http://articles.techrepublic.com.com/5100-10878_11-5771721.html
http://sandblogaspnet.blogspot.com/2008/03/methods-in-globalasax.html

An Aspect-Oriented Programming (AOP) Approach to Logging

Logging is a topic near and dear to my heart, having in an earlier version of .NET created a logging package tuned for high performance and used many others since. Today, with multiple popular offerings available to the .NET developer with different strengths and weaknesses (NLog, Log4Net, Serilog et al.) it’s not unusual to see adapters in local codebases to allow configuration and use of different packages as desired. This is actually a practice I recommend, to decouple local APIs from the implementation of a solution to a common and cross-cutting concern.

This naturally leads to thoughts of simplifying access in one’s API to the logging code. Aspect-oriented logging has in the past included attribute-based approaches, such as in PostSharp. But what if one hasn’t adopted such a library, or would like to log statements from code inside existing methods?

Suppose that one has logging classes presumably configured using some IoC implementation, and wants to decorate an API with logging functionality without undue clutter. One can use C# extension methods and weak references together with a marker interface to achieve the desired effect. Here are the steps:

1. Create an interface with which to decorate classes that will generate log information. (In the linked code sample, see the ILogSource interface.)

2. Add extensions to the log-source interface to support logging messages and/or events, corresponding with the desired use of the logging API, and to get and set a logger using a weak reference. (In the linked code sample, see the ILogSourceExtensions static class, stored with ILogSource in ILogSource.cs).

3. Decorate any desired class with logging functionality by implementing the marker interface, and configuring it with a logger as desired, then calling its logging methods within its other code. (See the code sample for more.)

This approach still allows an adapter to a target logging API to be used, and run-time configuration of the implementation as desired. It merely provides syntactic sugar to avoid littering your API with logging substructure in base classes and the like, by reducing the necessary plumbing to a single interface marked on the logging client class. The overhead of looking up the weak-referenced logger turns out to be minimal, at several nanoseconds per call. I’m still thinking through how best to wire this together with injection; constructor injection seems to obviously be out of the question.

Cure YSOD in the Sitecore Template Inheritance Tab

I recently encountered a Yellow Screen O’ Death (YSOD) error when attempting to use the template inheritance tab while viewing a template in the Sitecore Content Editor. As it turned out, the culprit was a template field with a blank type. To find such fields, run the following query to find the fields, then set their types:

/sitecore/templates/User Defined//*[@@TemplateKey = 'template field' and @Type='']

Extension methods + weak references = extension pseudo-properties in C#

Extension methods can be helpful for adding functionality onto existing classes, especially where one doesn’t have the ability to control a class definition and thus can’t add the methods directly. Adding extension properties to C# could be just as useful in some programming scenarios, but so far isn’t slated for definite release. Yet when working with third-party code such as the Sitecore API, it can be especially useful to add on both post-hoc functionality and state. And while extension methods can’t precisely duplicate the syntax of properties in C#, they can come close through the use of getter/setter methods as in Java, if some facility is used to store data on the object.

The System.Runtime.CompilerServices.ConditionalWeakTable class is ideal for such use, as a collection specifically made to contain weak references, i.e. to allow any referred-to object to be garbage collected (releasing the weak reference as well) if all strong references have been removed. What this means on a practical basis is that one can provide ancillary state for an object by using such a weak-reference collection in a static, and ideally thread-safe, way. It’s exactly what’s needed to dummy up extension properties in C#, while we wait for the real deal from Microsoft.

Here’s a basic example:

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

public static class ExtendedDataExtensions
{
    ///<summary>Stores extended data for objects</summary>
    private static ConditionalWeakTable<object, object> extendedData = new ConditionalWeakTable<object, object>();

    /// <summary>
    /// Creates a collection of extended pseudo-property values
    /// </summary>
    /// <param name="o">The object to receive the tacked-on data values</param>
    /// <returns>A new dictionary</returns>
    internal static IDictionary<string, object> CreateObjectExtendedDataCache(object o)
    {
        return new Dictionary<string, object>();
    }

    /// <summary>
    /// Sets an extended pseudo-property value on this object
    /// </summary>
    /// <param name="o">this object</param>
    /// <param name="name">The pseudo-property name</param>
    /// <param name="value">The value to set (if null, any value for the name will be removed)</param>
    public static void SetExtendedDataValue(this object o, string name, object value)
    {
        if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
        name = name.Trim();

        // Gets the key-value collection serving as extended "properties" for this object
        IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, CreateObjectExtendedDataCache);

        if (value != null)
            values[name] = value;
        else
            values.Remove(name);
    }

    /// <summary>
    /// Gets a pseudo-property value stored for this object
    /// </summary>
    /// <typeparam name="T">The type to return</typeparam>
    /// <param name="o">this object</param>
    /// <param name="name">The pseudo-property name</param>
    /// <returns>A value of the indicated type, or the type default if not found or of a different type</returns>
    public static T GetExtendedDataValue<T>(this object o, string name)
    {
        if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
        name = name.Trim();

        IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, CreateObjectExtendedDataCache);
        object value = null;
        if (values.TryGetValue(name, out value)) 
        {
            if (value is T)
                return (T)value;
            else
                return default(T); // or throw an exception, as desired
        }
        else
            return default(T);
    }

    /// <summary>
    /// Gets a pseudo-property value stored for this object
    /// </summary>
    /// <param name="o">this object</param>
    /// <param name="name">The pseudo-property name</param>
    /// <returns>A value if found for the specified name, otherwise null</returns>
    public static object GetExtendedDataValue(this object o, string name)
    {
        if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
        name = name.Trim();

        IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, CreateObjectExtendedDataCache);
        object value = null;
        if (values.TryGetValue(name, out value))
            return value;
        else
            return null;
    }
}

A brief example of calling the methods:

Item item = Sitecore.Context.Item;
item.SetExtendedDataValue("Tweet Count", 300);
// ...
int tweetCount = item.GetExtendedDataValue<int>("Tweet Count");

… after which one’s thoughts naturally turn to wrapping things up a bit more nicely, seasoning to taste for such issues as null and range checking. An inane example, for want of a better one:

public static class ItemExtensions
{
    public static int GetTweetCount(this Item item)
    {
        return item.GetExtendedDataValue<int>("Tweet Count");
    }

    public static void SetTweetCount(this Item item, int tweetCount)
    {
        item.SetExtendedDataValue("Tweet Count", tweetCount);
    }
}

And, of course, if it’s desired to make the pseudo-properties thread-safe, merely replace the dictionary storage with ConcurrentDictionary, with nearly identical performance.

Mixing in social media while avoiding mixed content warnings

If you’ve ever had the thrill of integrating external content into your website, you’ve likely run across the mixed content warning issue. In short, one can link to non-secure content from a secure page, but anything that would result in content being loaded from a non-secure source (a common example being an image URL) will likely cause a mixed content warning of some type in a user’s browser, when the page is served over HTTPS.

It’s generally fine to load HTTPS content in an HTTP page, of course. This means that when including content in a page, one can consider either replacing HTTP links with HTTPS ones, or using protocol/scheme-relative URLs. If a resource can be served over HTTPS, it’s a good practice to use HTTPS URLs at all times in website content. This avoids the problem of protocol-relative URLs when a resource either cannot be served over HTTPS (or, sometimes, HTTP), or where the URL is different depending on the protocol.

That last problem is rare, but unfortunately not non-existent. A prominent example occurs with Pinterest, which serves each pin’s images over both HTTP and HTTPS–but when using the latter, one must include an extra “s-“, for example:

https://s-some-really-long-url-stuff/and-more-stuff-etc.jpg

Unfortunately, when retrieving results using the Pinterest API, URLs for images (for both avatars and pins) are returned only in the non-secure flavor. Thus for Pinterest-API content included in a page presented over HTTPS, URLs should have the protocols switched to HTTPS, but also the extra “s-” must be added.

Luckily, most of the other big social media sites (Facebook, Twitter, YouTube) serve images at URLs returned by their various APIs just fine via HTTPS, with no funky differences between URL formats for HTTP and HTTPS.

Avoid a FOUCed content delivery experience using CSS and JavaScript

A Flash of Unstyled Content (FOUC) problem occurs when unstyled content is displayed in its raw form during page load, then later laid out as desired. This can be a problem in a Sitecore site as in any other; recently we encountered this while using the jQuery Exposure plugin.

To avoid this, try placing the following, or a functional equivalent, in the <head> section of your layout, after the script tag for jQuery itself:

<style>
.no-fouc {
    visibility : hidden ;
}
</style>

<script>
    $(function () { $( ".no-fouc" ).removeClass("no-fouc"); });
</script>

Then simply apply the no-fouc class to an appropriate container of the elements causing the problem.

Easily construct site-resolving URLs in Sitecore using extension methods

The LinkManager class is familiar to anyone who has had to construct URLs in Sitecore. It’s useful, especially with the site resolution feature, designed to help one construct cross-domain links. However, it’s important to remember to set the targetHostName attribute in the site configuration for best results.

As one would expect, given the high degree of extensibility of the Sitecore platform, one can extend and change the default link provider mechanism used to generate links. However, in a large number of cases, writing a custom class and configuring it properly, or even constructing a UrlOptions object explicitly and using it in calls to LinkManager, can be avoided by using a few extension methods. Just as the LinkManager.GetItemUrl() method was designed to make link generation easier, a single-shot method for constructing URLs, right from any item, would make things easier in many programming scenarios.

Below is sample code for such an approach. Features include the ability to programmatically set URL options as the default for a site or container, and a method to call from an Item instance to generate a URL, with the option to override the URL option defaults. Site resolution is performed for any content item in a site configured in Web.config with the targetHostName property, even if in a different site from the context item. If desired, one can tweak this approach further, such as by constructing the default URL options used here from configuration settings dealing with links and site resolution.

There’s nothing wrong with the LinkManager implementation, but a bit of sugar such as this would make it easier to use the framework. It wouldn’t prevent switching to a different link provider implementation, either, since the static extension methods simply front-end the built-in API.

public static class SitecoreDataItemExtensions
{
   static SitecoreDataItemExtensions()
   {
      defaultUrlOptions = new Sitecore.Links.UrlOptions();
      defaultUrlOptions.AddAspxExtension = false;
      defaultUrlOptions.AlwaysIncludeServerUrl = true;
      defaultUrlOptions.LanguageEmbedding = Sitecore.Links.LanguageEmbedding.Never;
      defaultUrlOptions.LowercaseUrls = true;
      defaultUrlOptions.ShortenUrls = true;
      defaultUrlOptions.SiteResolving = true;
      defaultUrlOptions.UseDisplayName = false;   
   }

   private static System.Collections.Concurrent.ConcurrentDictionary<string, Sitecore.Links.UrlOptions> siteUrlOptionsMap = new System.Collections.Concurrent.ConcurrentDictionary<string,Sitecore.Links.UrlOptions>();
   private static Sitecore.Links.UrlOptions defaultUrlOptions;

   /// <summary>
   /// Constructs and returns a clone of the default URL options for the specified site
   /// </summary>
   /// <param name="siteContext">The site for which to get the default URL options</param>
   /// <returns>A clone of the URL options, using the container-wide default as a fallback, with the specified site context applied</returns>
   private static Sitecore.Links.UrlOptions GetDefaultUrlOptions(Sitecore.Sites.SiteContext siteContext)
   {
      Sitecore.Links.UrlOptions urlOptions = defaultUrlOptions;
      try { siteUrlOptionsMap.TryGetValue(siteContext.Name, out urlOptions); } catch {}
      if (urlOptions == null)
         urlOptions = defaultUrlOptions;

      urlOptions = (Sitecore.Links.UrlOptions)urlOptions.Clone();
      urlOptions.Site = siteContext;
      return urlOptions;
   }

   /// <summary>
   /// Sets a clone of these UrlOptions as the default to use for all sites in this container
   /// </summary>
   public static void SetAsDefault(this Sitecore.Links.UrlOptions urlOptions)
   {
      if (urlOptions == null) return;

      Sitecore.Links.UrlOptions clone = (Sitecore.Links.UrlOptions)urlOptions.Clone();
      clone.Site = null;
      defaultUrlOptions = clone;
   }

   /// <summary>
   /// Sets a clone of this set of URL options as the default for the specified site
   /// </summary>
   /// <param name="siteContext">The optional site for which to set these UrlOptions as the default, overriding any which are set on the UrlOptions directly</param>
   public static void SetAsSiteDefault(this Sitecore.Links.UrlOptions urlOptions, Sitecore.Sites.SiteContext siteContext = null)
   {
      if (urlOptions == null) return;
      else if (siteContext == null)
      {
         urlOptions.SetAsDefault();
         return;
      }

      urlOptions = (Sitecore.Links.UrlOptions)urlOptions.Clone();
      urlOptions.Site = siteContext;
      urlOptions.SiteResolving = true;
      
      siteUrlOptionsMap.AddOrUpdate(siteContext.Name, urlOptions, (k, v) => urlOptions);
   }

   /// <summary>
   /// Sets a clone of this set of URL options as the default for the specified site
   /// </summary>
   /// <param name="siteName">The optional name of the site for which to set these UrlOptions as the default, overriding any which are set on the UrlOptions directly</param>
   public static void SetAsSiteDefault(this Sitecore.Links.UrlOptions urlOptions, string siteName = null)
   {
      if (urlOptions == null) return;

      Sitecore.Sites.SiteContext siteContext = null;
      if (!string.IsNullOrWhiteSpace(siteName))
      {
         try
         {
            siteContext = Sitecore.Sites.SiteContextFactory.GetSiteContext(siteName);
         } 
         catch (Exception e) {
            throw new InvalidOperationException("Could not find site '" + siteName + "' (Check Web.config <sites>); " + e.Message);
         }
         if (siteContext == null) 
            throw new InvalidOperationException("Could not find site '" + siteName + "' (Check Web.config <sites>)");
      }
      
      if (siteContext == null)
         siteContext = urlOptions.Site;

      urlOptions.SetAsSiteDefault(siteContext);
   }

   private static List<Sitecore.Sites.SiteContext> siteContexts = null;

   /// <summary>
   /// Gets a list of site contexts for this container
   /// </summary>
   private static List<Sitecore.Sites.SiteContext> SiteContexts
   {
      get 
      {
         if (siteContexts == null)
         {
            List<Sitecore.Sites.SiteContext> siteContextList = new List<SC.Sites.SiteContext>();

            Sitecore.Sites.SiteContext sc;
            foreach (string siteName in Sitecore.Sites.SiteContextFactory.GetSiteNames())
            {
               sc = Sitecore.Sites.SiteContextFactory.GetSiteContext(siteName);
               if (sc != null) siteContextList.Add(sc);
            }

            siteContexts = siteContextList;
         }

         return siteContexts;
      }
   }

   /// <summary>
   /// Gets the site context, if any, for this item
   /// </summary>
   public static Sitecore.Sites.SiteContext GetSiteContext(this Sitecore.Data.Items.Item item)
   {
      if (item == null) return null;
      string itemPath = item.Paths.FullPath.ToLower();

      foreach (Sitecore.Sites.SiteContext siteContext in SiteContexts)
         if (!string.IsNullOrWhiteSpace(siteContext.RootPath) &&
            siteContext.RootPath.StartsWith("/sitecore/content/") &&
            (siteContext.RootPath.Length > 18) &&
            itemPath.StartsWith(siteContext.RootPath.ToLower()))
            return siteContext;

      return null;
   }

   /// <summary>
   /// Gets a URL for this item; if no optional URL options are supplied, uses site-resolving default URL options
   /// </summary>
   public static string GetUrl(this Sitecore.Data.Items.Item item, Sitecore.Links.UrlOptions urlOptions = null)
   {
      if (item == null) throw new ArgumentNullException();
      Sitecore.Sites.SiteContext siteContext = item.GetSiteContext();

      if (urlOptions == null)
      {
         if (siteContext != null)
            urlOptions = GetDefaultUrlOptions(siteContext);
      }
      else if (siteContext != null)
      {
         urlOptions.Site = siteContext;
      }

      if (urlOptions == null)
         return Sitecore.Links.LinkManager.GetItemUrl(item);
      else
         return Sitecore.Links.LinkManager.GetItemUrl(item, urlOptions);
   }

}