Colourblind

Welcome to Colourblind.

This is the personal web space of Tom Milsom. As much as possible everything is free (as in speech and as in beer).


Make text: Smaller Bigger

Making MSBuild in NANT act like MSBuild in Visual Studio

Posted by Tom on 21/12/2011 09:45:37

So I was making some NANT scripts for an ASP.NET MVC project.

Rather than going through the rigmarole of maintaining references and dependencies in two places (the VS project file and the NANT scripts), this time round I started using the <msbuild> task from nantcontrib instead of invoking the compiler directly using the plain old <csc> task. After making sure I set my OutDir I run NANT and get a failed build while copying a file, which is odd because I don't want MSBuild doing anything of the sort. It builds fine from Visual Studio, but after a little investigation it turns out that MSBuild is attempting to copy a file which is in the project but is missing from the file system. It's trying to drop it, along with the rest of the project contents, into _PublishedWebsites which has attaching itself to my output directory, like a warty blemish or tumour on my otherwise pristine build.

By setting the verbosity level of MSBuild to Diagnostic in both NANT and Visual Studio I could diff the two outputs and see where the processes started to diverge. The output of the NANT ran to a svelte 26k lines, but a quick search on '_PublishedWebsites' turns up the part of the log which includes the failing build. The copy operation is happening within a _CopyWebApplication target. Time to do a quick search in the Visual Studio MSBuild log . . .

Aha! Here's the slippery little devil:

Target "_CopyWebApplication" skipped, due to false condition; (!$(Disable_CopyWebApplication) And '$(OutDir)' != '$(OutputPath)') was evaluated as (!False And 'bin\' != 'bin\').

But why? Why would you do that? What is the OutputPath even supposed to be? According to MSBuild\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets . . .

<!--
============================================================
_CopyWebApplication

This target will copy the build outputs along with the content files into a 
_PublishedWebsites folder.

This Task is only necessary when $(OutDir) has been redirected to a folder 
other than ~\bin such as is the case with Team Build.

The original _CopyWebApplication is now a Legacy, you can still use it by 
setting $(UseWPP_CopyWebApplication) to true. By default, it now change to 
use _WPPCopyWebApplication target in Microsoft.Web.Publish.targets. It allow 
to leverage the web.config trsnaformation.
============================================================
-->

Fine. Whatever. Regardless of the rationale, if you set your OutDir and OutputPath properties to the same value in the NANT <msbuild> task:

<msbuild project="${dir.src}/${project::get-name()}/${project::get-name()}.csproj"
      target="Rebuild" verbosity="Minimal">
   <property name="Configuration" value="Release" />
   <property name="OutDir" value="../../${dir.build}/${project::get-name()}/bin/" />
   <property name="OutputPath" value="../../${dir.build}/${project::get-name()}/bin/" />
</msbuild>

Then your case of _PublishedWebsites should clear up in no time. (Alternatively you could set the Disable_CopyWebApplication property, but I'm attempting to get our NANT builds as close to our VS builds as possible.) As an addendum, once I knew what I was looking for I found quite a bit of confusion related to OutputPut, including a blog post from Mark Needham who also ran into problems related to OutputPath when tweaking OutDir and who turned up this nugget from MSDN:

OutputPath: This property is typically specified in the project file and resembles OutDir. OutputPath has been deprecated and OutDir should be used instead whenever possible.

At this point I stopped caring and reversed out of the rabbit hole.

Tags: ASP, NAnt

Comments (0)

Well I didn't know that

Posted by Tom on 11/09/2010 22:24:42

I like to think I know ASP.NET fairly well, but this is the first time I've come across this.

   1:  public void HopefullyRedirect()
   2:  {
   3:      try
   4:      {
   5:          SomeComplicatedCrap();
   6:          Response.Redirect("~/SomeplaceElse.aspx");
   7:      }
   8:      catch (Exception e)
   9:      {
  10:          ReportError(e);
  11:      }
  12:  }

Yeah, I know - catching Exception is bad. But SomeComplicatedCrap has some complicated crap in it. There were HttpWebRequests and XML parsing and all other kinds of stuff. Seriously. Stop looking at me like that. We've all done it. Jeez.

But that's not the point I'm trying to make. Response.Redirect calls Response.End, which calls Thread.Abort, which bubbles a ThreadAbortException. So now every time that method is called I get an email.

What I should have been doing is this.

   1:  public void HopefullyRedirect()
   2:  {
   3:      try
   4:      {
   5:          SomeComplicatedCrap();
   6:          Response.Redirect("~/SomeplaceElse.aspx");
   7:      }
   8:      catch (ThreadAbortException)
   9:      {
  10:          throw;
  11:      }
  12:      catch (Exception e)
  13:      {
  14:          ReportError(e);
  15:      }
  16:  }

I just find it odd that I've never come across this before. Others things I've discovered recently:

  • Wookey Hole has no wookiees
  • But Cheddar Gorge has shitloads of chedar
  • Catcher in the Rye was disappointing
  • However, Jim Beam Straight Rye is great

Edumacating!

Tags: ASP, dotNet

Comments (0)

The Perils of ASP.NET's JSON Serialiser

Posted by Tom on 08/03/2010 23:53:38

Promit Roy's weblog is on my feedreader (checkout his open source .NET profiler, by the way - it's a great piece of work), and his recent post about serialising interfaces reminded me of something I was going to write about ages ago and forgot about. A cautionary tale . . .

Picture the scene. We've got some users of several different kinds, all of which implement a common interface.

   1:  public interface IUser
   2:  {
   3:      string Username
   4:      {
   5:          get;
   6:      }
   7:  }
   8:   
   9:  public class User : IUser
  10:  {
  11:      public string Name { get; set; }
  12:   
  13:      public string Username
  14:      {
  15:          get { return Name; }
  16:      }
  17:   
  18:      public User()
  19:      {
  20:          Name = "foo";
  21:      }
  22:  }
  23:   
  24:  public class OtherUser : IUser
  25:  {
  26:      public string Username { get; set; }
  27:      public string Password { get; set; }
  28:      public string EmailAddress { get; set; }
  29:   
  30:      public OtherUser()
  31:      {
  32:          Username = "bar";
  33:          Password = "password";
  34:          EmailAddress = "a@a.com";
  35:      }
  36:  }

Say we've got a web service returning an IUser in JSON format. You might expect to see something like this come out of the client side:

{
    'Name' : "foo"
    'Username' : "foo"
}

Looks good. But wait - Name isn't a property on the IUser interface . . .

Whereas the .NET XML serialiser will throw a wobbler if you ask it to serialise an object referenced through an interface due to its slavish adherence to the only-serialise-what-you-can-deserialise philosophy, the JSON serialiser will happily GetType on the object and then serialise based on that. That's harmless enough for objects of class User, but what happens if an OtherUser manages to sneak out of the web service under the same interface?

{
    'Username' : "bar"
    'Password' : "password"
    'EmailAddress' : "a@a.com"
}

Ruh roh.

This may be a trivial example, but it's an easy mistake to make given a more involved system (in our case a list of comments was being returned from a web service, and each comment had an IUser property that was being filled by a factory based on the session context). The lesson to be learned? Re-wrap any objects before you send them out of your web service.

   1:  public class SafeUser
   2:  {
   3:      public string Username
   4:      {
   5:          get;
   6:          private set;
   7:      }
   8:   
   9:      public SafeUser(IUser user)
  10:      {
  11:          Username = user.Username;
  12:      }
  13:  }
Tags: ASP, dotNet

Comments (0)

On-the-fly Image Resizing

Posted by Tom on 07/02/2010 15:19:22

Designers, man. All web developers hate them, but as soon as you try and set one on fire someone always phones the police. Spoilsports.

Among my biggest gripes is inconsistant image sizes. It may be fine to design something using 8 different image sizes spread across 4 different aspect ratios when you're sitting in front of a copy of Photoshop, but that just ain't feasible once we start dealing with dynamic content. You can resize everything on upload, but as soon as someone tweaks the design slightly you're having to reach for IrfanView and cursing. Wouldn't it be better for all concerned if the images just came out the right size? That would be cool.

Wut?

Thankfully, resizing images on demand isn't all that hard. One HttpHandler is all it takes to leave our crayon-wielding brethren free to sling around whatever image sizes they want. Here's a quick list of the features I'll want:

  • The ability to resize the requested image from the querystring
  • To be able to specify if the image should be clamped to the specified size (think 'Image Size' vs 'Canvas Size' in Photoshop)
  • You should be able to constrain height or width
  • The option of providing a default image if the specified one can't be found
  • An optional cache for storing previously resized images
  • A limit on the size of the cache

Implementation

First up we'll start with a custom configuration section.

<ImageHandler
    imagePath="~/Resources"
    missingImageName="missing_image.png"
    cachingEnabled="true"
    cachePath="~/Resources/Cache"
    maxCacheSize="250000"
    backgroundColour="#000000"
/>

Most of the attribute names should be pretty self explanatory. If they're not then I've not done my job correctly, so feel free to leave a comment calling me a dick. The code to read the following looks like this:

   1:  public class ImageHandlerConfiguration : ConfigurationSection
   2:  {
   3:      [ConfigurationProperty("imagePath", IsRequired = true)]
   4:      public string ImagePath
   5:      {
   6:          get { return this["imagePath"].ToString(); }
   7:          set { this["imagePath"] = value; }
   8:      }
   9:   
  10:      [ConfigurationProperty("missingImageName", IsRequired = false)]
  11:      public string MissingImageName
  12:      {
  13:          get { return this["missingImageName"].ToString(); }
  14:          set { this["missingImageName"] = value; }
  15:      }
  16:   
  17:      [ConfigurationProperty("cachePath", IsRequired = false)]
  18:      public string CachePath
  19:      {
  20:          get { return this["cachePath"].ToString(); }
  21:          set { this["cachePath"] = value; }
  22:      }
  23:   
  24:      [ConfigurationProperty("cachingEnabled", IsRequired = false, DefaultValue = false)]
  25:      public bool CachingEnabled
  26:      {
  27:          get { return Convert.ToBoolean(this["cachingEnabled"]); }
  28:          set { this["cachingEnabled"] = value; }
  29:      }
  30:   
  31:      [ConfigurationProperty("maxCacheSize", IsRequired = false, DefaultValue = -1)]
  32:      public int MaxCacheSize
  33:      {
  34:          get { return Convert.ToInt32(this["maxCacheSize"]); }
  35:          set { this["maxCacheSize"] = value; }
  36:      }
  37:   
  38:      [ConfigurationProperty("backgroundColour", IsRequired = false)]
  39:      public string BackgroundColour
  40:      {
  41:          get { return this["backgroundColour"].ToString(); }
  42:          set { this["backgroundColour"] = value; }
  43:      }
  44:   
  45:      public static ImageHandlerConfiguration GetConfig()
  46:      {
  47:          return (ImageHandlerConfiguration)ConfigurationManager.GetSection("ImageHandler");
  48:      }
  49:  }

And now the code of the HttpHandler itself:

   1:  public class ImageHandler : IHttpHandler
   2:  {
   3:      #region Properties
   4:   
   5:      private static ImageHandlerConfiguration Config
   6:      {
   7:          get;
   8:          set;
   9:      }
  10:   
  11:      private static Dictionary<Guid, string> MimeTypes
  12:      {
  13:          get;
  14:          set;
  15:      }
  16:   
  17:      public bool IsReusable
  18:      {
  19:          get { return false; }
  20:      }
  21:   
  22:      #endregion
  23:   
  24:      #region Methods
  25:   
  26:      public void ProcessRequest(HttpContext context)
  27:      {
  28:          // Load list of image encoders for MIME type lookup
  29:          lock (this.GetType())
  30:          {
  31:              if (MimeTypes == null)
  32:                  Init();
  33:          }
  34:   
  35:          HttpRequest request = context.Request;
  36:          HttpResponse response = context.Response;
  37:   
  38:          int width = 0;
  39:          int height = 0;
  40:          bool clamp = true;
  41:          ImageFormat imageFormat = null;
  42:   
  43:          // Grab request details from the querystring
  44:          string filename = request.QueryString["i"];
  45:          string filePath = context.Server.MapPath(Config.ImagePath + "/" + filename);
  46:          Int32.TryParse(request.QueryString["w"], out width);
  47:          Int32.TryParse(request.QueryString["h"], out height);
  48:          clamp = String.IsNullOrEmpty(request.QueryString["c"]) 
  49:                  || Convert.ToInt32(request.QueryString["c"]) != 0;
  50:   
  51:          // Put together a path for the cached file
  52:          string fileExtension = filename.Substring(filename.LastIndexOf('.'));
  53:          string cacheFilename = filename.Replace(fileExtension, 
  54:                  String.Format("_{0}-{1}-{2}{3}", width, height, clamp ? "1" : "0", fileExtension));
  55:          string cacheFilePath = context.Server.MapPath(Config.CachePath + "/" + cacheFilename);
  56:   
  57:          Image resizedImage = null;
  58:          Image originalImage = null;
  59:   
  60:          try
  61:          {
  62:              if (File.Exists(cacheFilePath)) // A cached version exists, so use that
  63:              {
  64:                  resizedImage = Image.FromFile(cacheFilePath);
  65:                  imageFormat = resizedImage.RawFormat;
  66:              }
  67:              else
  68:              {
  69:                  if (File.Exists(filePath))
  70:                      originalImage = Image.FromFile(filePath);
  71:                  else if (!String.IsNullOrEmpty(Config.MissingImageName))
  72:                      originalImage = Image.FromFile(context.Server.MapPath(
  73:                                      Config.ImagePath + "/" + Config.MissingImageName));
  74:                  else
  75:                      throw new FileNotFoundException(filePath);
  76:   
  77:                  imageFormat = originalImage.RawFormat;
  78:                  if (width == 0 && height == 0)
  79:                  {
  80:                      resizedImage = (Image)originalImage.Clone();
  81:                  }
  82:                  else
  83:                  {
  84:                      Size size = GetImageDimensions(originalImage, width, height);
  85:   
  86:                      resizedImage = ResizeImage(originalImage, size, clamp);
  87:                      if (Config.CachingEnabled && (Config.MaxCacheSize < 0 
  88:                              || GetDirectorySize(context.Server.MapPath(Config.CachePath)) < Config.MaxCacheSize))
  89:                          resizedImage.Save(cacheFilePath, imageFormat);
  90:                  }
  91:              }
  92:   
  93:              response.ContentType = MimeTypes[imageFormat.Guid];
  94:              resizedImage.Save(response.OutputStream, imageFormat);
  95:          }
  96:          finally
  97:          {
  98:              if (originalImage != null)
  99:                  originalImage.Dispose();
 100:              if (resizedImage != null)
 101:                  resizedImage.Dispose();
 102:          }
 103:      }
 104:   
 105:      private Image ResizeImage(Image source, Size targetSize, bool clamp)
 106:      {
 107:          Image resizedImage = new Bitmap(targetSize.Width, targetSize.Height, source.PixelFormat);
 108:   
 109:          using (Graphics g = Graphics.FromImage(resizedImage))
 110:          {
 111:              g.CompositingQuality = CompositingQuality.HighQuality;
 112:              g.InterpolationMode = InterpolationMode.HighQualityBicubic;
 113:              g.SmoothingMode = SmoothingMode.HighQuality;
 114:   
 115:              Rectangle rec = Rectangle.Empty;
 116:              if (clamp)
 117:                  rec = new Rectangle(0, 0, targetSize.Width, targetSize.Height);
 118:              else
 119:                  rec = new Rectangle((targetSize.Width - source.Width) / 2, 
 120:                          (targetSize.Height - source.Height) / 2, source.Width, source.Height);
 121:   
 122:              if (!String.IsNullOrEmpty(Config.BackgroundColour))
 123:                  g.Clear(ColorTranslator.FromHtml(Config.BackgroundColour));
 124:   
 125:              g.DrawImage(source, rec);
 126:          }
 127:   
 128:          return resizedImage;
 129:      }
 130:   
 131:      private Size GetImageDimensions(Image sourceImage, int width, int height)
 132:      {
 133:          Size result = new Size(width, height);
 134:          if (width == 0) // Constrain to height
 135:          {
 136:              float scale = (float)height / sourceImage.Height;
 137:              result.Width = Convert.ToInt32(sourceImage.Width * scale);
 138:          }
 139:          else if (height == 0) // Constrain to width
 140:          {
 141:              float scale = (float)width / sourceImage.Width;
 142:              result.Height = Convert.ToInt32(sourceImage.Height * scale);
 143:          }
 144:          return result;
 145:      }
 146:   
 147:      private int GetDirectorySize(string path)
 148:      {
 149:          int result = 0;
 150:          DirectoryInfo dir = new DirectoryInfo(path);
 151:          foreach (FileInfo file in dir.GetFiles())
 152:              result += Convert.ToInt32(file.Length);
 153:          return result;
 154:      }
 155:   
 156:      private static void Init()
 157:      {
 158:          Config = ImageHandlerConfiguration.GetConfig();
 159:   
 160:          MimeTypes = new Dictionary<Guid, string>();
 161:          foreach (ImageCodecInfo info in ImageCodecInfo.GetImageEncoders())
 162:              MimeTypes.Add(info.FormatID, info.MimeType);
 163:      }
 164:   
 165:      #endregion
 166:  }

Usage

To start you need to set up your web.config. You'll want this line in your <configSections> config section:

<section name="ImageHandler" type="Colourblind.Web.ImageHandlerConfiguration, Colourblind.Web"/>

Next you'll need to hook up your image handler. Where this gets stuffed varies between different versions of IIS. Your best bet here is to get your Google on. Finally, to get a resized image you pass in the following parameters to your handler via the querystring.

i - image filename (required - must exist within the path specified in the config section)
w - width (leave blank if you want to constrain by height)
h - height (leave blank if you want to constrain by width)
c - clamp (set to zero if you wish to keep the image the original size and add borders)

Ultimately, your URL will look something like this:

/Img.aspx?i=pretty_picture.jpg&w=400&h=600&c=1

Examples

First the original, unmolested image.

And here are some examples of the image once molestation has taken place.


  1. Width set to 200
  2. Height set to 200
  3. Width and height both set to 400

And I'm spent

I think that just about covers it. I'll probably do a follow-up post in the future to revisit this stuff as there are already things in there that are making my fingers itch. But it'll do for now . . .

Tags: ASP, dotNet

Comments (0)

If an ASP.NET webservice errors in a forest and there is no-one around to hear it, does it make a sound?

Posted by Tom on 28/08/2009 21:38:19

Apparently not.

Try it yourself. Hook Application.OnError and write a webservice that throws an error.

It's cool. I'll wait.

Back? Splendid.

This was fine when no-one did anything particularly important with AJAX. Your AJAX fails and the user has to type in the whole address by themself? It's not ideal, but it shouldn't cause anyone to clear their basket in frustration and vow never to use the website again. However, if that same person now can't add anything to the basket then they don't have to clear their basket. You know, what with it already being empty and stuff.

Of course, other people have come across this charming idiosyncracy of .NET's web services, and it's pretty pitiful that however hard you shaft your application, as long as you do it inside a web service you'll never hear about it.

We've got a few options here as to how to proceed:

  1. Storm the Bastille. Personally I'm all for a march on Redmond, but I realise that practical considerations may make this unfeasible.
  2. Wrap every web service in a try catch block.
  3. Check the response code on Request.End. We'll still lose the error, since GetLastError never gets filled.
  4. Use a response filter, as laid out here by Daniel Richardson. At least with this we can parse the the response and grab the stack trace directly out of it.

If you have an existing error handling module then stuff this in. Failing that, putting it in Global.asax will work just as well.

   1:  public void Init(HttpApplication context)
   2:  {
   3:      context.PostRequestHandlerExecute += new EventHandler(context_PostRequestHandlerExecute);
   4:  }
   5:   
   6:  void context_PostRequestHandlerExecute(object sender, EventArgs e)
   7:  {
   8:      HttpContext context = HttpContext.Current;
   9:      if (context.Request.FilePath.Contains(".asmx") && context.Response.StatusCode == 500)
  10:      {
  11:          Stream foo = context.Response.Filter;
  12:          if (context.Response.ContentType == "application/json")
  13:          {
  14:              context.Response.Filter = new JsonErrorResponseFilter(context.Response.OutputStream);
  15:          }
  16:      }
  17:  }

The documentation can be found at the usual places, but what this does is places an event handler which catches the response on the way out, checks to see if the request is for a web service, if it's in JSON format (so we don't mess with any ye olde SOAP requests), and traps anything with with a 500 response code (or 'Server Error' to you and me).

Line 11 may raise some eyebrows, but there's is a method to the madness. There is a property lazy-loading fail in the HttpContext.Response.Filter property. It's one of those charming 'works fine when I debug it and step through but dies in it's arse every other time' bugs you encounter every now and then (hai2u Commerce Server!)

The ResponseFilter itself inherits from Stream and doesn't do a whole lot. Most of it is interface boilerplate.

   1:  class JsonErrorResponseFilter : Stream
   2:  {
   3:      private Stream _responseStream = null;
   4:   
   5:      public JsonErrorResponseFilter(Stream responseStream)
   6:      {
   7:          _responseStream = responseStream;
   8:      }
   9:   
  10:      public override bool CanRead
  11:      {
  12:          get { return true; }
  13:      }
  14:   
  15:      public override bool CanSeek
  16:      {
  17:          get { return true; }
  18:      }
  19:   
  20:      public override bool CanWrite
  21:      {
  22:          get { return true; }
  23:      }
  24:   
  25:      public override void Flush()
  26:      {
  27:          _responseStream.Flush();
  28:      }
  29:   
  30:      public override long Length
  31:      {
  32:          get { return _responseStream.Length; }
  33:      }
  34:   
  35:      public override long Position
  36:      {
  37:          get { return _responseStream.Position; }
  38:          set { _responseStream.Position = value; }
  39:      }
  40:   
  41:      public override int Read(byte[] buffer, int offset, int count)
  42:      {
  43:          return _responseStream.Read(buffer, offset, count);
  44:      }
  45:   
  46:      public override long Seek(long offset, SeekOrigin origin)
  47:      {
  48:          return _responseStream.Seek(offset, origin);
  49:      }
  50:   
  51:      public override void SetLength(long value)
  52:      {
  53:          _responseStream.SetLength(value);
  54:      }
  55:   
  56:      public override void Write(byte[] buffer, int offset, int count)
  57:      {
  58:          string response = Encoding.UTF8.GetString(buffer, offset, count);
  59:   
  60:          JsonException error = new JsonException(response);
  61:          Colourblind.Core.Log.Instance.Write(error);
  62:   
  63:          _responseStream.Write(buffer, offset, count);
  64:      }
  65:  }

Well, now we have a copy of the outgoing response it's up to us what we want to do with it. I chose to create a new exception and pass that to my logging code, but you can print it out on the nearest Laserjet or skywrite it if you want.

Super-shiny-turbo-tip: the JavaScriptSerializer is available for your parsing needs.

   1:  public class JsonException : Exception
   2:  {
   3:      private string _stackTrace;
   4:      private string _message;
   5:   
   6:      public override string StackTrace
   7:      {
   8:          get { return _stackTrace; }
   9:      }
  10:   
  11:      public override string Message
  12:      {
  13:          get { return _message; }
  14:      }
  15:   
  16:      public JsonException(string json)
  17:      {
  18:          JavaScriptSerializer serialiser = new JavaScriptSerializer();
  19:          Dictionary<string, string> data = serialiser.Deserialize<Dictionary<string, string>>(json);
  20:   
  21:          _message = data["Message"];
  22:          _stackTrace = data["StackTrace"];
  23:          Data.Add("OriginalExceptionType", data["ExceptionType"]);
  24:      }
  25:  }

I think that about covers everything. If anyone has a better way or any improvements to the code then please let me know. Although I'll probably claim your work as my own and then start plotting to kill so that my secret never comes to light. Just to warn you.

As an aside, if your web service is returning SOAP then you'll need another method. Best bet is probably to use a SOAP Extension to override the SOAP pipeline. As recommended here by that Atwood fellah and covered in more details here (scroll down to 'Unhandled Exceptions in ASP.NET Web Services').

Tags: ASP, dotNet

Comments (0)