Tag Archives: nuget

Inside RazorScriptManager

22 Jun

When you add the RazorScriptManager NuGet package to your project, three things happen: Script.cshtml is added to /App_Code, RazorScriptManager.cs is added to /Handlers, and a web.config transform is performed. The first file provides the Razor helper methods used in your views. The second provides the HttpHandler that handles the combining/compression and responds to requests for scripts.axd. The web.config transform adds a couple settings and registers the HttpHandler.

Script.cshtml

There are four Razor helpers added as part of the NuGet package. Two for adding script references to the response, and two for writing out script tags for the response. One of each type is provided for CSS and JavaScript.

Inside the two Add methods (AddCss() and AddJavaScript()), a new ScriptInfo object is created for the referenced script. That ScriptInfo object contains the script type, local path, CDN path, and whether the script is used site-wide. The ScriptInfo object is then added to a List<ScriptInfo> that’s kept in Session.

@helper AddJavaScript(string localPath, string cdnPath = null, bool siteWide = false) {
	var scriptType = ScriptType.JavaScript;
	//create a session key specifically for javascript ScriptInfo objects
	var key = "__rsm__" + scriptType.ToString();
	//If the List doesn't exist, create it
	if (Session[key] == null) {
		Session[key] = new List();
	}
	//pull out the current (or new) list - it may already have other ScriptInfo objects
	var scripts = Session[key] as List;
	//add the current ScriptInfo
	scripts.Add(new ScriptInfo(Server.MapPath(localPath), cdnPath, scriptType, siteWide));
	//put the list back in Session
	Session[key] = scripts;
}

In the Output methods (OutputCss() and OutputJavaScript()), the List<ScriptInfo> is extracted from Session. Based on web.config settings, a list of CDN-hosted scripts may be extracted. The helper then writes out <script> or <link> tags for CDN-hosted scripts (if any) and for the HttpHandler path. An MD5 hash is generated from the filenames of the referenced local scripts and appended to the HttpHandler path. This MD5 is used to cache the combined/compressed output in the HttpHandler, and will be explained more in that section.

@helper OutputJavaScript() {
	var scriptType = ScriptType.JavaScript;
	//create a session key specifically for javascript ScriptInfo objects
	var key = "__rsm__" + scriptType.ToString();
	//if no scripts have been added, don't do anything
	if (Session[key] == null) return;
	//pull out the current list from Session
	var scripts = Session[key] as List;
	var cdnScripts = new List();
	//if the web.config says to use CDN-hosted scripts, extract then from the list into cdnScripts
	if (bool.Parse(System.Configuration.ConfigurationManager.AppSettings["UseCDNScripts"])) {
		//get all scripts without a CDN path
		var localScripts = scripts.Where(s => string.IsNullOrWhiteSpace(s.CDNPath)).ToList().ToList();
		//get all scripts that aren't local-only scripts
		cdnScripts = scripts.Except(localScripts).ToList();
		//put the local scripts back into session (CDN scripts are handled here, not in the HttpHandler)
		Session[key] = localScripts;
	}

	//write out the CDN scripts to the response
	foreach (var cdnScript in cdnScripts) {
<script type="text/javascript" src="@cdnScript.CDNPath"></script>}

	//generate a unique hash based on the filenames
	var hash = HttpUtility.UrlEncode(RazorScriptManager.GetHash(scripts));
	//write out a script tag for the HttpHandler using the script type and hash<script type="text/javascript" src="/scripts.axd?type=@scriptType.ToString()&hash=@hash"></script>
}

RazorScriptManager.cs

The class file for the HttpHandler also contains the definitions for ScriptInfo, ScriptType and ScriptInfoComparer. These classes represent a script reference, the type of script, and a way of comparing two scripts. The comparer is used later to eliminate duplicate script references (e.g. if you have a reference to the same jQuery file on your Layout and a partial view, it will only use one). The RazorScriptManager class itself (which is the actual HttpHandler) provides an instance method for responding to requests (ProcessRequest()) and a static method for generating a hash (GetHash()). GetHash() works by appending the distinct list of script paths into a single string, then generating a standard MD5 hash of that string.

public static string GetHash(IEnumerable scripts) {
	var input = string.Join(string.Empty, scripts.Select(s => s.LocalPath).Distinct());
	var hash = System.Security.Cryptography.MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(input));
	var sb = new StringBuilder();
	for (int i = 0; i < hash.Length; i++)
		sb.Append(hash[i].ToString("X2"));
	return sb.ToString();
}

ProcessRequest() is a little more involved. First, it determines the type of script being requested from the querystring value. Based on the type, it sets the response’s content type appropriately.

var scriptType = (ScriptType)Enum.Parse(typeof(ScriptType), context.Request.Params["type"]);

switch (scriptType) {
	case ScriptType.JavaScript:
		context.Response.ContentType = @"application/javascript";
		break;
	case ScriptType.Stylesheet:
		context.Response.ContentType = @"text/css";
		break;
}

After setting content type, the method checks the application cache to see if a combined/compress script has already been generated for that particular set of files. To do this, it uses the hash as a cache key. If the script output already exists, the method immediately returns the cached output and no further processing is required.

var hashString = context.Request.Params["hash"];
if (!String.IsNullOrWhiteSpace(hashString)) {
	var result = cache[HttpUtility.UrlDecode(hashString)] as string;
	if (!string.IsNullOrWhiteSpace(result)) {
		context.Response.Write(result);
		return;
	}
}

If the output wasn’t already cached, the method pulls the List<ScriptInfo> for the current type out of Session. It then obtains the distinct scripts from the list using the ScriptInfoComparer and reorders them based on whether or not they were marked as site-wide. Site-wide scripts (like jQuery) need to be loaded first, so that other script can take advantage of their methods. At this point, the contents of each file are added to a single string. This string will become the combined and compressed single script that’s returned by the handler.

var scripts = context.Session["__rsm__" + scriptType.ToString()] as IEnumerable;
context.Session["__rsm__" + scriptType.ToString()] = null;
if (scripts == null) return;
var scriptbody = new StringBuilder();

scripts = scripts.Distinct(new ScriptInfoComparer());

//add sitewide scripts FIRST, so they're accessible to local scripts
var siteScripts = scripts.Where(s => s.SiteWide);
var localScripts = scripts.Where(s => !s.SiteWide).Except(siteScripts, new ScriptInfoComparer());
var scriptPaths = siteScripts.Concat(localScripts).Select(s => s.LocalPath);
var minify = bool.Parse(ConfigurationManager.AppSettings["CompressScripts"]);

foreach (var script in scriptPaths) {
	if (!String.IsNullOrWhiteSpace(script)) {
		using (var file = new System.IO.StreamReader(script)) {
			var fileContent = file.ReadToEnd();
			if (scriptType == ScriptType.Stylesheet) {
				var fromUri = new Uri(context.Server.MapPath("~/"));
				var toUri = new Uri(new FileInfo(script).DirectoryName);
				fileContent = fileContent.Replace("url(", "url(/" + fromUri.MakeRelativeUri(toUri).ToString() + "/");
			}
			if (!minify) scriptbody.AppendLine(String.Format("/* {0} */", script));
			scriptbody.AppendLine(fileContent);
		}
	}
}
string scriptOutput = scriptbody.ToString();

If CompressScripts is set to true in the web.config, run the appropriate minifier for the current script type. Side note: there’s some interesting asymmetry within the YUI Compressor: for JavaScript the compress method is an instance method, while for CSS the compress method is a static method.

string scriptOutput = scriptbody.ToString();
if (minify) {
	switch (scriptType) {
		case ScriptType.JavaScript:
			var jscompressor = new Yahoo.Yui.Compressor.JavaScriptCompressor(scriptOutput);
			scriptOutput = jscompressor.Compress();
			break;
		case ScriptType.Stylesheet:
			scriptOutput = Yahoo.Yui.Compressor.CssCompressor.Compress(scriptOutput);
			break;
	}
}

Finally, save the output to the cache (for next time!) and send the output as the reponse.

var hash = GetHash(scripts);
cache[hash] = scriptOutput;
context.Response.Write(scriptOutput);

web.config.transform

Two appSettings are added to the web.config: UseCDNScripts and CompressScripts. The first determines whether or not the Output Razor helpers write out tags for the CDN paths. The second determines whether or not the <code>HttpHandler</code> compresses the combined output before returning the response.

<appSettings>
  <add key="UseCDNScripts" value="false" />
  <add key="CompressScripts" value="false" />
</appSettings>

The HttpHandler is also registered in the web.config. One version for IIS6, another for IIS7.

<system.web>
  <httpHandlers>
    <add verb="*" path="scripts.axd" type="RazorScriptManager.RazorScriptManager"/>
  </httpHandlers>
</system.web>
<system.webServer>
  <handlers>
    <add name="ScriptManager" verb="*" path="scripts.axd" type="RazorScriptManager.RazorScriptManager"/>
  </handlers>
</system.webServer>
Advertisements

Pushing Your First NuGet Package

13 Jun

I absolutely love NuGet. Having a package management system in place makes it so much easier not only to use shared code, but it also makes it easier to share. For instance, say you had a class or assembly that you think other people might find useful. The old way to do it was to pick one of the many project-hosting sites, build a deployable assembly, upload the assembly, and hope people found it. And even if someone did find it, you were either limited in how you could integrate with their project or you had to include long, drawn out integration instructions. But no longer!

With NuGet, you have the ability to control your shared code’s integration with a project. You can add files to the directory tree, transform the web.config, or even run a PowerShell script. The PowerShell script can even be used to add commands to the Package Console, so your shared code can include tooling along with it. But for your first foray into the world of NuGet, you’ll probably want to start as simply as possible. So that’s the scenario I’ll cover here—packaging a single file.


If you’re following along and would like an example, the current source for my actual first NuGet package is on GitHub: https://github.com/davecowart/AutoMapperPagedList

Step 1: Write Your Code

You’re on your own here.

Step 2: Build Your Package Folder Structure

Create a folder for your package. Inside that folder, create another folder named ‘content’. Anything that goes inside content will be placed in the root of the project folder, so inside that content folder you can simulate the folder structure for any files you want to include in the project. In this instance, I have a single class I’d like to include in the Models folder, so I created the folder structure [package root]/content/Models and placed my file there.

folderstructure

Step 3: Build Your NuSpec File

The glue that holds it all together is the NuSpec file. This XML file contains all the package information that NuGet needs in order to identify, share and deploy your package. For this step, you’ll need to install nuget.exe. If you have NuGet installed, you probably already have it. Run nuget in the command line to see if you have it, and if you do run nuget update to make sure you have the latest version.

Once you have the NuGet executable straightened up, navigate to your package’s root folder in the command line. Run nuget [projectname] to create the .nuspec file, and open it up in a text editor. For this package, only a minimal amount of information is needed:

<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
  <metadata>
    <id>AutoMapperPagedList</id>
    <version>0.6</version>
    <authors>Dave Cowart</authors>
    <owners>Dave Cowart</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Adds an implementation of PagedList that uses AutoMapper to emit ViewModels</description>
    <tags>PagedList, AutoMapper, ASP.NET</tags>
    <dependencies>
      <dependency id="AutoMapper" version="1.0" />
    </dependencies>
  </metadata>
</package>

In this case, the package has a dependency on another package—AutoMapper. The id used in the dependency element is the same as the name used in AutoMapper’s NuSpec file. That id is used as the name pretty much throughout NuGet, so it’s easy to find if you need it. Just make sure that your chosen id is unique, descriptive, and free of any crazy special characters.

Step 4: Package It Up

Head back to the command line and run the command nuget pack [packagename].nuspec. This will create the .nupkg file you’ll need to upload to NuGet.

Step 5: Upload to NuGet

Before you actually upload to the NuGet library, you’ll need to register and get your API key. Register for an account at http://nuget.organd go to My Account. Copy the access key and head back to the command line once again. Run nuget setApiKey [apikey] to set your API key (you’ll only need to do this one time, even if you’re creating multiple packages).

Once your API key is set, run nuget push [packagename].nupkg. This will upload your package to the library, and it’ll be available in just a couple minutes. To check on it, go back to http://nuget.org and go to Contribute > Manage My Packages. From this page, you’ll get a list of all the packages you’ve shared, along with the total number of reviews and downloads.


And that’s it! The first one can be the hardest, mostly because it’s unfamiliar and requires setting up an account and your API key. But once all that’s in place, it’s simple to create additional packages, and even simpler to push updates to an existing package (just make sure to increment your version number in the NuSpec file). Be sure to check out the source for AutoMapperPagedList if you have any questions, and if you feel like giving it a try in your projects, let me know!

P.S. – For a package that includes a web.config transform, you can check out the source for RazorScriptManager, another package I created that I’ll be blogging about soon.

AutoMapperPagedList

9 Jun

If you’re just looking for an example of how to use AutoMapperPagedList, drop down to the bottom and look at the last code sample.

A couple months ago (while at MIX 11, actually) I was playing with some of the new MVC3 Tooling, including EF4.1. To play with it, I was trying to build a simple blog, and for no real reason at all decided to use both AutoMapper and Scott Guthrie’s PagedList. For those unfamiliar with either, AutoMapper is a great way to transform your data access objects into view models, and PagedList makes it really simple to hook up an IQueryable to server-side paging.

First, an explanation of AutoMapper. In creating AutoMapper, Jimmy Bogard provided a super-useful way to automatically map one object to another type. The mapping is primarily based on conventions, so if you name the properties on the classes properly, you only have to do minimal configuration. For more details, see the examples on AutoMapper’s CodePlex site. For my simple blog, I wanted to use AutoMapper to flatten my Post model into a PostViewModel. Because PostViewModel was set up to easily work with AutoMapper, the only configuration I had to do was make sure that this line was called in my Global.asax:

AutoMapper.Mapper.CreateMap<Post, PostViewModel>();

Then when I want to convert a Post to a PostViewModel, I can just call this:

AutoMapper.Mapper.Map<Post, PostViewModel>(post);

Next, an explanation of PagedList. The Gu created a helpful class and extension methods for working with server-side paging. The extension method extends an IQueryable<T> and converts it to a PagedList<T>. PagedList<T> inherits from List<T>, so it has all the normal features of a List<T>, but it adds the interface IPagedList:

public interface IPagedList {
	int TotalCount { get; set; }
	int PageIndex { get; set; }
	int PageSize { get; set; }
	bool IsPreviousPage { get; }
	bool IsNextPage { get; }
}

The properties added by IPagedList can be used in your view to easily control Previous/Next buttons. Additionally, the constructor of PagedList calls Skip() and Take() on the IQueryable before calling ToList() and adding the resultant items to itself, meaning that the execution of the IQueryable happens inside of PagedList. This is important because AutoMapper won’t work as part of deferred execution, which is what is going on with the IQueryable. This means that the trying to use AutoMapper to map an IQueryable<Post> to an IQueryable<PostViewModel> just can’t happen.

So since I can’t use AutoMapper to send objects to PagedList, I had to use AutoMapper inside PagedList, after the IQueryable was executed and returned as a List. However, I didn’t want to muck around too much with the regular PagedList class, so I created a new class that inherits from it instead. This new class is called (quite creatively) MappedPagedList. It works the exact same way as PagedList, but takes one additional generic type and one additional parameter. It requires two generic types because AutoMapper needs a source type and destination type. The extra parameter, however, is the key. As parameter types go, it’s a doozy:

public static MappedPagedList<TSource, TOutput> ToPagedList<TSource, TOutput>(this IQueryable<TSource> source, int index, Func<IEnumerable<TSource>, IEnumerable<TOutput>> mapper, int pageSize = 10) {
	return new MappedPagedList<TSource, TOutput>(source, index, pageSize, mapper);
}

Let me break it down a little.
Func<IEnumerable<TSource>, IEnumerable<TOutput>>
looks intimidating, but it’s really just a delegate for a call to AutoMapper.Map<TSource, TOutput>(). In simpler terms, the parameter takes a method that has one parameter of type IEnumerable<TSource> and returns IEnumerable<TOutput>. The best way to understand it is probably to see an example of the method being called:

IQueryable<Post> posts = context.Posts.OrderByDescending(p => p.Timestamp).AsQueryable();
<PostViewModel> pagedList = posts.ToPagedList<Post, PostViewModel>(2, Mapper.Map<IEnumerable<Post>, IEnumerable<PostViewModel>>, 10);
return View(pagedList);

Because MappedPagedList<T,O> inherits from PagedList<T> (and PagedList<T> is the only part that we actually need in the view), the View only needs to be passed an instance of Paged<T>. The magic happens inside the constructor of MappedPagedList, where it uses the passed-in Func to map the List<T> to a List<O>.

If you’ve read this far, you’re probably getting desperate for the part where I say “…and here’s how you use it in your site”, so here you go.

//Controller
public ViewResult Index(int page = 0) {
	int pageSize = 10;
	IQueryable<Post> posts = context.Posts.OrderByDescending(p => p.Timestamp).AsQueryable();
	PagedList<PostViewModel> pagedList = posts.ToPagedList<Post, PostViewModel>(page, Mapper.Map<IEnumerable<Post>, IEnumerable<PostViewModel>>, pageSize);
	return View(pagedList);
}

//View
@if (Model.IsPreviousPage) {
	@Html.ActionLink("Previous", "Index", new { page = Model.PageIndex - 1 })
}
@(Model.PageIndex + 1) of @(Model.TotalCount / Model.PageSize)
@if (Model.IsNextPage) {
	@Html.ActionLink("Next", "Index", new { page = Model.PageIndex + 1 })
}

This will generate a previous button, a page indicator, a next button. The previous and next button are hidden if there isn’t a previous or next page. The controller takes page as an optional parameter, and passes that to the MappedPagedList to use in Skip().

So why would you want to use it? If you need to use AutoMapper to send view models to your view, but want super-easy paging support, this single file will save you a bunch of time and effort. I’ve put it on NuGet at http://nuget.org/List/Packages/AutoMapperPagedList, so feel free to pull it down and give it a try. It’s a single file, and the source is hosted on GitHub at https://github.com/davecowart/AutoMapperPagedList, so feel free to clone it, fork it, or do whatever. I also have an example MVC project on GitHub at https://github.com/davecowart/AutoMapperPagedList.Demo if you need a full-site example.