Referencing different resources via configuration

Minifying and obfuscating your resources (CSS/JS) is a great practice.  It reduces bandwidth and makes it harder for users to look at what your JS is doing.  However, you don’t want that in your dev or QA environments where you probably need to look at the JS to debug an issue.  And, chances are, staging and/or prod resources get pushed to a CDN so your paths change when you get into the higher environments, and the path to the files, or the CDN hostname, probably changes between the higher environments.  Furthermore, Microsoft’s bundling, though not awesomely implemented, is a good idea to reference groups of files that you might need in certain places around your application.

Basically, what you have is the following requirements:

  1. Choose whether to pull minified/obfuscated resources by environment
  2. Choose whether to pull local or CDN resources by environment
  3. If using CDN, provide different CDN paths by environment
  4. Resource bundling

This can be quite laborious to figure out how to get something like this to work.  So, let’s get to it.

Configuration

First, let’s get configuration figured out as that will impact everything else.  With the wishlist defined above, the configuration class is pretty much defined.

Using Microsoft’s Unity (though you can certainly use another DI), you can configure the configuration using DI and design time configuration, meaning you set this all up in your application’s web.config.  Then, using web.config transformations, you can change the values on an environmental basis.

As is best practices, and to get this working with Unity, let’s define our resource configuration interface.

public interface IResourceConfiguration
{
    /// <summary>
    /// Gets or sets a value indicating whether [use CDN].
    /// </summary>
    /// <value>
    ///   <c>true</c> if [use CDN]; otherwise, <c>false</c>.
    /// </value>
    bool UseCdn { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether [use minimum files].
    /// </summary>
    /// <value>
    ///   <c>true</c> if [use minimum files]; otherwise, <c>false</c>.
    /// </value>
    bool UseMinFiles { get; set; }

    /// <summary>
    /// Gets or sets the CDN prefix.
    /// </summary>
    /// <value>
    /// The CDN prefix.
    /// </value>
    string CdnPrefix { get; set; }

    /// <summary>
    /// Gets or sets the bundles.
    /// </summary>
    /// <value>
    /// The bundles.
    /// </value>
    ResourceBundle[] Bundles { get; set; }
}

The first three properties are self-explanatory, but the Bundles property requires some explaining.  A resource bundle, in this context, is a named group of resources (as a best practice, separate JS and CSS as the former should be at the foot of your page’s HTML and the latter should be at the top in the HEAD).  With that known, you can use the following JSON format to define bundles:

{
    "bundles":
    [
        "name": "",
        "include":
        [
            "",
            "",
            "..."
        ],
        "name": "",
        "include":
        [
            "..."
        ],
        "..."
    ]
}

What we ended doing, which has worked great, was a couple of conventions to make this easier on us:

  1. Prefix the bundle names with the type of resources.  JS bundles got the prefix “scripts.” and CSS bundles go the prefix “style.”
  2. We keep our page-related (non-global) JS in a “Pages” folder.  So, this would result in a contact us page JS being something like “/Scripts/Pages/contact-us.js.”  For page bundles, this would result in a “scipts.pages.” and “styles.pages.” naming convention

Also, the code took from MSFT’s bundling to require “~/” prefixes for all local resources.  This is important as non-local resources can be skipped and spit out as is onto the page.

A complete bundling JSON file might look like this:

{
    "bundles": [
        {
            "name": "scripts.core",
            "include": [
                "~/Scripts/jquery-3.1.1.min.js",
                "~/Scripts/bootstrap.min.js",
                "~/Scripts/respond.min.js",
                "~/Scripts/autotrack.js",
                "~/Scripts/analytics.js",
                "~/Scripts/underscore.min.js"
            ]
        },
        {
            "name": "scripts.forms",
            "include": [
                "~/Scripts/jquery.validate.min.js",
                "~/Scripts/jquery.maskedinput.min.js",
                "~/Scripts/bootstrap-datepicker.min.js"
            ]
        },
        {
            "name": "scripts.app",
            "include": [
                "~/Scripts/core.js",
                "~/Scripts/ajax.js",
                "~/Scripts/support.js",
                "~/Scripts/webparts.js"
            ]
        },
        {
            "name": "scripts.ko",
            "include": [
                "~/Scripts/knockout-3.4.0.js",
                "~/Scripts/knockout.validation.js"
            ]
        },
        {
            "name": "scripts.recaptcha",
            "include": [
                "~/Scripts/recaptcha.js"
            ]
        },
        {
            "name": "scripts.pages.bulk-mdu",
            "include": [
                "~/Scripts/jquery.ui.widget.min.js",
                "~/Scripts/jquery.ui.widget.parts.fileupload.min.js",
                "~/Scripts/Pages/bulk-mdu-form.js"
            ]
        },
        {
            "name": "style.core",
            "include": [
                "~/Content/normalize.css",
                "~/Content/bootstrap.css",
                "~/Content/core.css"
            ]
        },
        {
            "name": "style.pages.svp-contact-form",
            "include": [
                "~/Content/Pages/svp-contact-form.css"
            ]
        },
        {
            "name": "style.pages.bulk-mdu-form",
            "include": [
                "~/Content/Pages/bulk-mdu-form.css",
                "~/Content/bootstrap-datepicker3.min.css"
            ]
        }
    ]
}

Now we need a class that we can use Newtonsoft JSON to deserialize to.

public class ResourceBundle
{
    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    /// <value>
    /// The name.
    /// </value>
    public string Name { get; set; }
    /// <summary>
    /// Gets or sets the resources.
    /// </summary>
    /// <value>
    /// The resources.
    /// </value>
    [JsonProperty("include")]
    public string[] Resources { get; set; }
}

Finally, let’s implement IResourceConfiguration.

public class ResourceConfiguration : IResourceConfiguration
{
    /// <summary>
    /// Gets or sets a value indicating whether [use CDN].
    /// </summary>
    /// <value>
    /// <c>true</c> if [use CDN]; otherwise, <c>false</c>.
    /// </value>
    public bool UseCdn { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether [use minimum files].
    /// </summary>
    /// <value>
    /// <c>true</c> if [use minimum files]; otherwise, <c>false</c>.
    /// </value>
    public bool UseMinFiles { get; set; }

    /// <summary>
    /// Gets or sets the CDN prefix.
    /// </summary>
    /// <value>
    /// The CDN prefix.
    /// </value>
    public string CdnPrefix { get; set; }

    /// <summary>
    /// Gets or sets the bundles.
    /// </summary>
    /// <value>
    /// The bundles.
    /// </value>
    public ResourceBundle[] Bundles { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="ResourceConfiguration"/> class.
    /// </summary>
    public ResourceConfiguration() { }

    /// <summary>
    /// Initializes a new instance of the <see cref="ResourceConfiguration"/> class.
    /// </summary>
    /// <param name="configurationFileVirtualPath">The configuration file virtual path.</param>
    public ResourceConfiguration(string configurationFileVirtualPath)
    {
        var configFile = ParseConfigurationFile(configurationFileVirtualPath);
        Bundles = configFile.Bundles;
    }

    private static ResourceConfigurationFile ParseConfigurationFile(string virtualPath)
    {
        var serverPath = HostingEnvironment.MapPath(virtualPath);
        if (!File.Exists(serverPath))
            throw new FileNotFoundException("Missing required resource configuration file.", serverPath);
        return JsonConvert.DeserializeObject<ResourceConfigurationFile>(File.ReadAllText(serverPath));
    }
}

As you can see, the ResourceConfiguration takes in a string to point to the resources configuration JSON file.  This means you can change which resource file to look between environments using web.config transformations and DI.

The resource engine

Let’s get the resource engine up and running.  This engine needs to use the configuration we defined above to appropriately spit out the correct files and file paths and needs to split up configured bundles into individual file requests (you could make an HTTP handler that takes a bundle name and returns a stream with the files concatenated; we chose not to do that).  As such, the engine needs the following:

  1. Resolve a resource by its path
  2. Resolve an array of resources by a bundle name
  3. Determine if a minified version exists*
  4. Convert a resource path to a minified version of the path

*Minified version checks can only really be made when you’re not referencing CDN resources.  Otherwise, you’re making HTTP requests to the CDN to determine if the file exists.  You can do this, but we just decided to assume that if we’re turning on minifying, we’re assuming the file is up on the CDN.  Furthermore, we follow the pattern .min. for all minified files.

This results in an interface that looks like this:

public interface IResourceResolver
{
    /// <summary>
    /// Gets or sets the configuration.
    /// </summary>
    /// <value>
    /// The configuration.
    /// </value>
    IResourceConfiguration Configuration { get; set; }

    /// <summary>
    /// Resolves the resource.
    /// </summary>
    /// <param name="virtualPath">The virtual path.</param>
    /// <returns></returns>
    string ResolveResource(string virtualPath);

    /// <summary>
    /// Resolves the bundle.
    /// </summary>
    /// <param name="bundleName">Name of the bundle.</param>
    /// <returns></returns>
    IEnumerable<string> ResolveBundle(string bundleName);

    /// <summary>
    /// Does the min version exist.
    /// </summary>
    /// <param name="path">The path.</param>
    /// <returns></returns>
    bool DoesMinVersionExist(string path);

    /// <summary>
    /// Creates the min path.
    /// </summary>
    /// <param name="path">The path.</param>
    /// <returns></returns>
    string CreateMinPath(string path);
}

And an implementation would look like:

public class DefaultResourceResolver : IResourceResolver
{
    private IResourceConfiguration _configuration;

    /// <summary>
    /// Gets or sets the configuration.
    /// </summary>
    /// <value>
    /// The configuration.
    /// </value>
    public IResourceConfiguration Configuration
    {
        get { return _configuration ?? (_configuration = AbstractFactory.Factory.Create<IResourceConfiguration>()); }
        set { _configuration = value; }
    }

    /// <summary>
    /// Creates the min path.
    /// </summary>
    /// <param name="path">The path.</param>
    /// <returns></returns>
    public string CreateMinPath(string path)
    {
        if (path.Contains(".min")) return path;
        return path.Insert(path.LastIndexOf(".", path.Length), ".min");
    }

    /// <summary>
    /// Does the min version exist.
    /// </summary>
    /// <param name="path">The path.</param>
    /// <returns></returns>
    public bool DoesMinVersionExist(string path)
    {
        if (Configuration.UseCdn) return true;  // file is remote so this is useless
        var minPath = CreateMinPath(path);
        var serverPath = HostingEnvironment.MapPath(minPath);
        return File.Exists(serverPath);
    }

    /// <summary>
    /// Resolves the bundle.
    /// </summary>
    /// <param name="bundleName">Name of the bundle.</param>
    /// <returns></returns>
    public IEnumerable<string> ResolveBundle(string bundleName)
    {
        var bundle = Configuration.Bundles.FirstOrDefault(x => x.Name == bundleName);
        if (bundle == null)
            throw new System.NullReferenceException($"Bundle '{bundleName}' cannot be found.");
        return bundle.Resources;
    }

    /// <summary>
    /// Determines whether this instance [can be minified] the specified path.
    /// </summary>
    /// <param name="path">The path.</param>
    /// <returns>
    ///   <c>true</c> if this instance [can be minified] the specified path; otherwise, <c>false</c>.
    /// </returns>
    protected virtual bool CanBeMinified(string path)
    {
        var local = path.ToLower();
        local = VirtualPathUtility.RemoveTrailingSlash(local);
        if (local.Contains("?"))
            local = local.Substring(0, local.IndexOf('?'));
        return local.EndsWith("js") || local.EndsWith("css");
    }

    /// <summary>
    /// Resolves the resource.
    /// </summary>
    /// <param name="virtualPath">The virtual path.</param>
    /// <returns></returns>
    public string ResolveResource(string virtualPath)
    {
        virtualPath = virtualPath.Trim();
        if (Regex.IsMatch(virtualPath, "^(//|http://|https://)"))
            return virtualPath;
        var path = virtualPath;
        if (virtualPath.StartsWith("~/"))
            path = VirtualPathUtility.ToAbsolute(path);
        if (Configuration.UseMinFiles && CanBeMinified(path) && DoesMinVersionExist(path))
            path = CreateMinPath(path);
        if (Configuration.UseCdn && !string.IsNullOrWhiteSpace(Configuration.CdnPrefix))
        {
            var cdn = Configuration.CdnPrefix;
            if (path.StartsWith("/"))
                cdn = VirtualPathUtility.RemoveTrailingSlash(cdn);
            path = $"{cdn}{path}";
        }
        return path;
    }
}

Looking at this implementation, you’ll see that we have a Configuration property.  This property will accept any implementation of our IResourceConfiguration interface.  It also uses my abstract factory pattern, which you can remove if you do not wish to use that.  In any case, this allows you to inject configuration.

Now that we the configuration and the engine figured out, it’s time to get your resources onto the page.

Getting resources onto the page

To get resources onto the page, we created an MVC HtmlHelper extension that is pretty self-explanatory.

public static class ResourceHtmlHelper
{
    private static IResourceResolver _resourceResolver;

    /// <summary>
    /// Gets or sets the resource resolver.
    /// </summary>
    /// <value>
    /// The resource resolver.
    /// </value>
    public static IResourceResolver ResourceResolver
    {
        get { return _resourceResolver ?? (_resourceResolver = AbstractFactory.Factory.Create<IResourceResolver>());}
        set { _resourceResolver = value; }
    }

    /// <summary>
    /// Renders the style.
    /// </summary>
    /// <param name="helper">The helper.</param>
    /// <param name="virtualPath">The path.</param>
    /// <returns></returns>
    public static IHtmlString RenderStyle(this HtmlHelper helper, string virtualPath)
    {
        var resolvedPath = ResourceResolver.ResolveResource(virtualPath);
        var tag = HtmlElementHelper.CreateStylesheetElement(resolvedPath);
        return new HtmlString(tag);
    }

    /// <summary>
    /// Renders the styles.
    /// </summary>
    /// <param name="helper">The helper.</param>
    /// <param name="virtualPaths">The virtual paths.</param>
    /// <returns></returns>
    public static IHtmlString RenderStyles(this HtmlHelper helper, params string[] virtualPaths)
    {
        return RenderMultiple(helper, RenderStyle, virtualPaths);
    }

    /// <summary>
    /// Renders the style bundle.
    /// </summary>
    /// <param name="helper">The helper.</param>
    /// <param name="name">The name.</param>
    /// <returns></returns>
    public static IHtmlString RenderStyleBundle(this HtmlHelper helper, string name)
    {
        return RenderBundle(helper, name, RenderStyles);
    }

    /// <summary>
    /// Renders the script.
    /// </summary>
    /// <param name="helper">The helper.</param>
    /// <param name="virtualPath">The virtual path.</param>
    /// <returns></returns>
    public static IHtmlString RenderScript(this HtmlHelper helper, string virtualPath)
    {
        var resolvedPath = ResourceResolver.ResolveResource(virtualPath);
        var tag = HtmlElementHelper.CreateScriptElement(resolvedPath);
        return new HtmlString(tag);
    }

    /// <summary>
    /// Renders the scripts.
    /// </summary>
    /// <param name="helper">The helper.</param>
    /// <param name="virtualPaths">The virtual paths.</param>
    /// <returns></returns>
    public static IHtmlString RenderScripts(this HtmlHelper helper, params string[] virtualPaths)
    {
        return RenderMultiple(helper, RenderScript, virtualPaths);
    }

    /// <summary>
    /// Renders the script bundle.
    /// </summary>
    /// <param name="helper">The helper.</param>
    /// <param name="name">The name.</param>
    /// <returns></returns>
    public static IHtmlString RenderScriptBundle(this HtmlHelper helper, string name)
    {
        return RenderBundle(helper, name, RenderScripts);
    }

    private static IHtmlString RenderBundle(this HtmlHelper helper, string name, Func<HtmlHelper, string[], IHtmlString> render)
    {
        var resources = ResourceResolver.ResolveBundle(name);
        return render(helper, resources.ToArray());
    }

    private static IHtmlString RenderMultiple(HtmlHelper helper, Func<HtmlHelper, string, IHtmlString> render, params string[] paths)
    {
        return new HtmlString(string.Join(Environment.NewLine, from path in paths
                                                               select render(helper, path)));
    }
}

You will also need this helper class.

public static class HtmlElementHelper
{
    /// <summary>
    /// Creates the script element.
    /// </summary>
    /// <param name="src">The source.</param>
    /// <returns></returns>
    public static string CreateScriptElement(string src)
    {
        return $"http://src";
    }

    /// <summary>
    /// Creates the stylesheet element.
    /// </summary>
    /// <param name="href">The href.</param>
    /// <returns></returns>
    public static string CreateStylesheetElement(string href)
    {
        return $"<link rel=\"stylesheet\" href=\"{href}\"/>";
    }

    /// <summary>
    /// Creates an image HTML element.
    /// </summary>
    /// <param name="src">The src attribute value.</param>
    /// <param name="alt">The alt attribute value.</param>
    /// <returns></returns>
    public static string CreateImageElement(string src, string alt)
    {
        return $"<img src=\"{src}\" alt=\"{alt}\"/>";
    }

    /// <summary>
    /// Creates an image HTML element.
    /// </summary>
    /// <param name="src">The src attribute value.</param>
    /// <returns></returns>
    public static string CreateImageElement(string src)
    {
        return CreateImageElement(src, null);
    }
}

You can use this extension class in your views like so:

@Html.RenderScriptBundle("scripts.bundleName")
@Html.RenderScript("~/Scripts/Pages/page1.js")
@Html.RenderScript("https://code.jquery.com/jquery-3.2.1.min.js")

Wiring up

Now that the coding is set, it’s time to wire everything up.  As aforementioned, I use MSFT’s Unity for DI, so my examples will use that.  Unity also acts as my object factory.

My dev unity configuration looks something like this:

<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
  <!-- Enterprise -->
  <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" />
  <alias alias="Singleton" type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager, Microsoft.Practices.Unity" />
  <alias alias="TransparentProxy" type="Microsoft.Practices.Unity.InterceptionExtension.TransparentProxyInterceptor, Microsoft.Practices.Unity.Interception" />

  <alias alias="IResourceResolver" type="IResourceResolver, <the library>" />
  <alias alias="ResourceResolver" type="DefaultResourceResolver, <the library>" />
  <alias alias="IResourceConfiguration" type="IResourceConfiguration, <the library>" />
  <alias alias="ResourceConfiguration" type="ResourceConfiguration, <the library>" />
  <container>
	<register type="IResourceConfiguration" mapTo="ResourceConfiguration">
	  <lifetime type="Singleton" />
	  <interceptor type="InterfaceInterceptor" />
	  <policyInjection />
	  <constructor>
		<param name="configurationFileVirtualPath" value="~/App_Config/resources.json" />
	  </constructor>
	  <property name="UseCdn" value="false"></property>
	  <property name="UseMinFiles" value="false"></property>
	  <property name="CdnPrefix" value="dummy"></property>
	</register>
	<register type="IResourceResolver" mapTo="ResourceResolver">
	  <lifetime type="Singleton" />
	  <interceptor type="InterfaceInterceptor" />
	  <policyInjection />
	</register>
  </container>
</unity>

As you can see, we are passing all of our configuration using a web.config set up.  Using web.config transformation, we can then change any of these values during deployment.  You can turn CDN usage on and off (though, if on, you need to supply a CdnPrefix), you can target different bundle configuration files, and you can turn min file usage on and off.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a website or blog at WordPress.com

Up ↑

%d bloggers like this: