Thursday, March 11, 2010

How to access the Targeting Context from within a Commerce Server Basket Pipeline

I fixed a bug yesterday where an order-level discount targeted against Targeting Context.Subtotal had an incorrect value—it was set in code before running the basket pipeline, but the subtotal was inaccurate before running the pipeline (newly added items to the basket had a price of zero prior to the pipeline execution, for example).

The solution was to update the subtotal in the targeting context during pipeline execution, after fetching the latest pricing data from the catalog and before running the OrderDiscount pipeline component. Here’s the code to get at the targeting context from within a pipeline:

public int Execute(object pdispOrder, object pdispContext, int lFlags)
{
    IDictionary context = (IDictionary)pdispContext;
    Decimal subtotal = (decimal)((Microsoft.CommerceServer.Interop.Profiles.IProfileObject)context["ContextProfile"]).Fields["Subtotal"].Value;
}

Thursday, March 04, 2010

Marinated Swordfish

Adapted from.

3 Tbsp lime juice
2 tsp rice vinegar
2.5 Tbsp olive oil
1 tsp salt
1/4 tsp fresh ground black pepper
1 tsp fresh grated ginger
3 or 4 fresh basil leaves, chopped
1/4 tsp fresh thyme leaves
1/4 fresh jalapeño, minced (seeds optional if you prefer spicier)
3/4 lbs swordfish

Whisk together all ingredients (except fish) in as small a pan as possible to hold the fish. Cover and marinate 2 to 3 hours, turning once. Broil or grill to desired doneness (130 to 140°F internal temperature).

Monday, February 08, 2010

Saag Something-or-other

This is my first attempt at a Saag Paneer. Except I didn't follow a saag paneer recipie; instead I loosely followed this Saag Aloo recipie. So it’s a curried spinach and paneer recipe. Next variation I want to add some cream, yogurt, coconut milk, or sour cream to the mix for a creamy texture.

2 medium Yukon Gold potatoes, cut into 3/4” cubes
9 oz fresh baby spinach leaves, tough stems removed
1 medium onion, diced (or 5 shallots)
1 Tbsp butter
1 jalapeño pepper, seeds removed, minced (optionally add back the seeds for a spicier dish)
1 clove pressed garlic
1” ginger root, peeled and minced
1 1/2 Tbsp curry powder
2 Tbsp brown sugar
8 oz paneer cut into 3/4” cubes (could substitute cooked chicken or tofu, or omit entirely)
7 oz canned chopped tomatoes
1 tsp salt, or to taste

In a large pot, bring a 2 quarts water to boil. Parboil the potatoes until starting to soften, 7 to 10 minutes. Remove potatoes from boiling water and put in cold water to stop the cooking; keep the boiling water hot.

Add the spinach leaves to the boiling water for two minutes to wilt. Remove from water and smoosh between paper towels on a plate to remove most of the water. Fold the spinach out of the paper towels and onto a cutting board. Chop until fine.

Add the butter, onions, potatoes and salt to a hot skillet and sauté. After 5 minutes, add the paneer. Continue sautéing until onions are translucent and the potatoes turn golden brown, about 7 minutes. Add the garlic, ginger, jalapeño, curry powder and brown sugar; stir until fragrant and well mixed, about 1 or 2 minutes.

Add tomatoes, combine well, and serve with naan or over rice. Serves 4.

Tuesday, January 26, 2010

Announcing ePubLib

I have published a project on CodePlex, ePubLib. ePub is quickly becoming the defacto standard for e readers, and after getting my hands on a (used and cheap) Sony Pocket Reader I wanted to be able to create documents to put on the device.

The format isn't particularly complicated, but there are a lot of little things that have to line up. This library makes it extremely easy. Create a Book object. Create 1 or more Chapter objects. Add the Chapter to Book.Chapters. Call Book.Save(). You can do the same thing for images as of the commit last night. The only knowledge that's required is basic XHTML skills and .NET to use the library.

I also wrote a utility to log me into The Economist's web site (I'm a paying subscriber) and scrape the Print Edition. It works great, a proper TOC, all of the articles and all of the images. I don't think I can upload that source to CodePlex due to perceived copyright concerns, but drop me a comment if you're interested.

Tuesday, December 29, 2009

Updated RSS and Atom Links

As part of the migration to Blogger, I have set up legacy redirect links from travis.pettijohn.com/blog/ to www.pettijohn.com (I'll add to them as I continue importing day by day). I will leave the redirects in place for a few months, at least until Google and Bing stop indexing the old URLs.

If you are a subscriber via RSS, please update your reader to this new link: RSS. Atom.

Saturday, December 26, 2009

Blogger Migration

I'm moving my blog to Blogger, formerly hosted in a custom ASP.NET app running at home. I want to get out of the business of running a server at home, and have migrated that box to Windows Home Server (I'll rave about that in another blog post).

Only one problem to migrating via the Blogger APIs: they rate limit you to fifty posts per day (the failure is an HTTP 400 with a plain text body "Blog has exceeded rate limit or otherwise requires word verification for new posts"). I'm not sure if that includes comments or not (which I'm importing, too). But either way, it will be somewhere between a week or two until I'm done (the code's all written, I just need to run it until it stops every day).


Home, sweet home.

Wednesday, December 16, 2009

Cookie Gotchas in ASP.NET

As a result of a recent security audit, we were asked to implement a more secure session identifier to help make session hijacking harder. Specifically, our requirements were:

  • Ensure that client-specified session IDs are system-generated
  • Generate a new session ID on login
  • Invalidate anonymous session IDs on login

We started following a strategy very similar to this solution on MSDN. If you don't click the link, the summary is, implement an HttpModule, in BeginRequest inspect the request cookie and extract & verify a hash from it, overwrite the Session ID cookie in the Request so that the underlying session implementation is unaware of any cookie mucking, and then on EndRequest write a new session cookie to the Response with a hash appended. The primary difference between that solution and ours is that ours encrypted the session ID and authentication information into the token so that we could meet the latter two requirements. (I'll blog about the specific solution at a later date.)

There's only one problem: it doesn't work. At all. You can't overwrite a Request cookie.

Sure, it works in trivial solution (including my POC), but if you modify the Response.Cookies collection, you lose your modifications to the Request.Cookies collection.

I cracked open Reflector to understand why. Take a look at System.Web.HttpCookieCollection.Remove(string):

public void Remove(string name)

{
    if (this._response != null)
    {

        this._response.BeforeCookieCollectionChange();
    }
    this.RemoveCookie(name);

    if (this._response != null)
    {
        this._response.OnCookieCollectionChange();

    }
}

And then System.Web.HttpResponse.OnCookieCollectionChange():

internal void OnCookieCollectionChange()
{

    this.Request.ResetCookies();
}

And as if this were necessary, System.Web.HttpRequest.ResetCookies():

internal void ResetCookies()

{
    if (this._cookies != null)
    {

        this._cookies.Reset();
        this.FillInCookiesCollection(this._cookies, true);

    }
    if (this._params != null)
    {

        this._params.MakeReadWrite();
        this._params.Reset();
        this.FillInParamsCollection();

        this._params.MakeReadOnly();
    }
}

Digging through the Analyzer, the cookies in the HttpRequest are reset by the following seemingly innocuous methods:

  • System.Web.HttpCookieCollection.Remove(String)
  • System.Web.HttpCookieCollection.Set(HttpCookie)
  • System.Web.HttpResponse.SetCookie(HttpCookie)

So if you ever try and overwrite a cookie in the HttpRequest, you had better not call any of those three methods, otherwise the cookies will get reloaded by parsing the original values from the raw request.

Final thoughts:

- You can use reflection to set HttpCookieCollection._response to null (from HttpResponse.Cookies), which then avoids the OnCookieCollectionChange call, but if you do, it will break other things, like automatic adding of cookies on accessing them. Like in System.Web.HttpCookieCollection.Get(string) (called by HttpCookieCollection this[string]):

public HttpCookie Get(string name)
{
    HttpCookie cookie = (HttpCookie) base.BaseGet(name);

    if ((cookie == null) && (this._response != null))

    {
        cookie = new HttpCookie(name);
        this.AddCookie(cookie, true);

        this._response.OnCookieAdd(cookie);
    }
    return cookie;

}

- I could only find one other person on the Internet who experience this problem.

- There's some other weirdness where adding a Response cookie resets the Request.Cookies collection, and adds the Response.Cookies to the Request.Cookies collection. You might be able to benefit from that; add a cookie to the Response.Cookies collection, call one of the methods that triggers resetting the Request cookies, and now your Response cookie will be one of the Request cookies. That didn't work in our case due to later cookie manipulation by the Session provider.

Wednesday, November 25, 2009

Troubleshooting a Custom Resource Provider in ASP.NET

My current project is a large multi-lingual eCommerce engagement. Due to business requirements (on-the-fly updates), we externalized resources into a database (following this MSDN article). Furthermore due to business requirements (uptime for a very high revenue site with regular mid-day content updates), they need to be able to publish markup (aspx & ascx) on the fly without restarting IIS.

We were seeing a sporadic issue where resources would disappear on the page. For example, there was custom control that looked like so:

<custom:CustomHyperLink ID="TL" runat="server" ImageUrl="[Header Image]" SiteMapNodeId="Basket" ToolTip="[Header]" meta:resourcekey="HeaderImage" />

When everything was working (which was 99% of the time), the ImageUrl would be looked up at runtime (via the control's HeaderImage.ImageUrl resource) and substituted in as ~/images/en/header_en.gif. But when it didn't work, we would see an <img src="[Header Image]" />, which obviously is a problem.

Adding to the confusion was that only some of the resources showed this problem. Also, it was seemingly random. Everything would be working one minute, broken the next, in the middle of the day. Putting together steps to reproduce was nearly impossible. It had something to do with loading resources and/or compiling the site and/or restarting iis and/or the planets aligning.

Here's what we didn't understand: Resources in ASP.NET can be declared either explicitly or implicitly. Explicit means code, GetGlobalResourceObject("keyName") or GetLocalResourceObject("keyName"). Implicit means markup, meta:resourcekey="keyName".

Explicit calls are evaluated at runtime, every time, because it is regular code that gets executed in the regular way.

Implicit calls are evaluated once at compile time, and the compiler (essentially) hooks up a binding to between the appropriate property and the explicit / GetLocalResourceObject()* call. To walk through the above control sample:

  • At ASP.NET compile time, that line is parsed. The compiler looks at what’s specified in meta:resourcekey.
  • That key is passed into IImplicitResourceProvider.GetImplicitResourceKeys. This method queries the resource provider for all valid keys starting with HeaderImage, and returns all of those keys in an ICollection. Our example thus returns {"HeaderImage.ImageUrl", "HeaderImage.ToolTip"}.
  • The compiler inspects all of the keys, parses them, and performs databinding. Our example matches HeaderImage.ImageUrl from our resource provider and hooks up a binding between the property and IImplicitResourceProvider.GetObject("HeaderImage.ImageUrl") ... *which I believe calls GetLocalResourceObject() to complete the circle.

If the resource datastore, whatever it may be, is unavailable (or empty!) at ASP.NET compile time, the call to GetImplicitResourceKeys will return nothing and no databinding will occur.

That's what was happening to our resources. We would push new markup and immediately afterwards import new resources. But our import process was naïve; it deleted all of the resources from the database, scanned the filesystem for .resx files, loaded them into memory, and then inserted them into the database. And because it was a live site, many of these controls would be hit by the ASP.NET compiler while the database was empty. This meant the compiler thought it had nothing to do because GetImplicitResourceKeys returned nothing and so would let the resources fall back to their default text, in our case, [Header Image].

The resources that never exhibited a problem were of course retrieved explicitly.

I want to give credit to Rick Strahl, whose blogged frustrations about this topic helped point me in the right direction. Unlike him, I did not have to implement IImplicitResourceProvider (which agrees with MSDN), I just had to trace down a race condition where it was being called at an inopportune time.

In summary, pushing new markup and then immediately emptying the resource datastore on a live site will break implicit (meta:resourcekey) resources due to how the ASP.NET compiler resolves implicit bindings.

Tuesday, October 20, 2009

Windows 7 Media Center WTV to iPhone / iPod Video Transcode

Below is my first pass at building a Powershell utility to monitor my Recorded TV folder from Windows 7 Media Center and automatically trim commercials and transcode them into iPhone format.

Some notes:

  • The version of ffmpeg.exe comes from iPodifier. I want it to use a more recent stock build.
  • Requires comskip and and drvcut. Built with Comskip 80_25.
  • Next version will also output Podcast XML, which will greatly clean up the experience on the iPhone.
  • Keep an eye on the transcode tag here for future updates.

$sourceFilesUnfiltered = "C:\Users\Public\Recorded TV\*.wtv"; $binDir = "C:\Users\Media\Downloads\comskip80_025"; $outputDir = "C:\iPodVideos2"; $keepWindowDays = 14; $sleepInterval = 600; while(1) { # Filter the input files $sourceFiles = Get-ChildItem $sourceFilesUnfiltered | Where-Object { $_.CreationTime -gt (Get-Date).AddDays(0 - $keepWindowDays) } | Sort-Object CreationTime; Write-Output ("Setting process priority to Idle."); [System.Diagnostics.Process]::GetCurrentProcess().PriorityClass = [System.Diagnostics.ProcessPriorityClass]::Idle; # Convert WTV to DVR-MS, Analyze afor commercials, trim commercials, and transcode to iPhone format. foreach($file in $sourceFiles) { #echo "Beginning $file"; # We don't want to process a currently recording show, so check last write time $file.Refresh(); #if(((Get-Date) - ($file.LastWriteTime)).TotalSeconds -lt 120) #{ # Write-Output ("Skipping $file because it is in use."); # continue; #} try { $handle = $file.OpenWrite(); } catch { Write-Output ("Skipping $file because it is in use."); continue; } finally { #TODO if handle isn't null $handle.Close(); } $baseName = $file.BaseName; $dvrmsFile = "$outputDir\$baseName.dvr-ms"; $cleanName = "$outputDir\$baseName" + "_clean.dvr-ms"; $m4vName = "$outputDir\$baseName" + ".m4v"; $trimFile = "$outputDir\$baseName" + "_dvrcut.bat"; #if ipod version does not exist if(!(Test-Path $m4vName)) { Write-Output "Beginning $file"; Write-Output ("Converting to DVR-MS."); & "C:\Windows\ehome\WTVConverter.exe" $file $dvrmsFile | Wait-Process; Write-Output ("Running ComSkip."); & "$binDir\comskip.exe" -q --ini="$binDir\comskip.ini" $dvrmsFile $outputDir | Wait-Process; Write-Output ("Trimming Commercials."); & Start-Process -Wait $trimFile;# | Wait-Process; Write-Output ("Compressing video."); & "$binDir\ffmpeg.exe" -y -i $cleanName -f mp4 -s 480x320 -acodec libfaac -async 4800 -dts_delta_threshold 1 -threads auto -vcodec libx264 -b 512k -level 21 -r 30000/1001 -bufsize 2000k -maxrate 768k -g 250 -coder 0 $m4vName | Wait-Process; del "$outputDir\$baseName.log", "$outputDir\$baseName.logo.txt", "$outputDir\$baseName.txt", $trimFile, $dvrmsFile, $cleanName; } else { Write-Output "Nothing to do for $file"; } #break; } # Delete extra (outside of the keep window) files foreach($m4vfile in Get-ChildItem "$outputDir\*.m4v") { $matchFound = [bool]0; foreach($wtvFile in $sourceFiles) { if($wtvFile.BaseName -eq $m4vFile.BaseName) { $matchFound = [bool]1; } } if(!$matchFound) { Write-Output ("Extra file found: $m4vFile"); } } Start-Sleep $sleepInterval; }

Sunday, September 06, 2009

Whoops

Had a hard drive die on the RAID5 and was down for a few weeks. Everything is back up, now with the web and SQL servers running on a Hyper-V VM. Next time the OS drive dies, all I have to do is remount the VHD and go.

Sunday, January 25, 2009

Bootable USB Flash Drive

Reposting these instructions on making a bootable flash drive.

  • On a command prompt, run diskpart.
  • list disk
  • select disk 2 (Replacing 2 with the correct number!)
  • clean
  • create partition primary
  • select partition 1
  • active
  • format fs=fat32 quick (Note that fs=ntfs will NOT be bootable!)
  • assign
  • exit
  • robocopy /MIR [source drive] [destination flash drive]

In short, you create an active, primary partition on the drive (and you have to do this in diskpart, because the Windows UI doesn't support it) and then you copy the files from your Windows installation media to the drive. And here I always thought that this was something difficult!

Windows 7 Media Center

I upgraded my Media Center box to Windows 7 Beta build 7000. Most everything is working; the native Clear-QAM support is most welcome! My channels actually say 5.1 instead of 1868. Good stuff. A little polish on the UI is also welcome. (I did have to manually change a registry flag that didn't upgrade automatically.)

The only issue I am having is with—surprise, surprise—the NVidia drivers. HDCP isn't working (solved with AnyDVD HD), switching resolutions is flaky and resizing the desktop (to correct overscan) isn't working. Oh, and that's with Vista drivers because the Windows 7 drivers crashed the system and had to be rolled back. But...I'm excited enough about testing it to deal with it.

Saturday, January 17, 2009

ShowMeCables.com

I have to post a rave about the customer service at ShowMeCables.com.

A while back, I decided to make a Y-adapter so that I could use an iPhone headset with my computer, which has the traditional separate mic and headphone jacks.

A few weeks ago, I got an email through my web contact form from John at ShowMeCables.com. He asked me if I knew why he was seeing so much web traffic from my domain. I told him about the adapter I had put together and shared links, and said that a post I had made on a forum had really led to it being noticed.

He replied and said he understood what I was trying to accomplish. He said that he would have one of the guys in his custom cable division whip me up a prototype and he'd send it to me. All he asked in return was that I let him know if it met my expectations.

A few days later I got a fedex with the connector. It was well made with durable-feeling construction. The best part was the handwritten-in-sharpie "mic" and "headphone" labels on the connectors. They sent me the very first prototype! I love it!

So to anyone looking for any custom cable connectors, do me a favor and check out ShowMeCables.com. If you have interest in the specific adapter, you can order it directly here. In fact, the adapter pictured there (at least as of now) is the actual unit they sent to me.

Is that customer service worth raving about, or what??

Saturday, January 10, 2009

Redoing my books for simplicity

I just completely restructured my personal finance books. I decided that it had become too much work to maintain them, yet I still feel a need to maintain them. So I restructured the hierarchy to be much shallower: no more Expenses:Transport:Car:Gas, now it's just Expenses:Transport, and it rolls up all of the sub-categories like taxis and public transit. In short, I collapsed probably 150 expense accounts into 10. Okay, I'm lying—there are many more than ten, but only for critical and automated transactions, like the tax line-items in my pay checks. But as far as day-to-day categories, where I have to put all of the transactions when I import my credit card and bank statement, there are only about ten.

This is quite a departure from my past. I remember a year ago deciding that I needed to create sub-accounts under Expenses:Gifts Given for Christmas, Birthdays and Weddings. But you know how many times I actually cared about those numbers? Zero. So today I collapsed them back together. And I collapsed, and collapsed and collapsed my accounts. The most interesting are Expenses:Living (lifestyle-related expenses and unaccounted-for cash) which includes sub-categories for Groceries and Personal Care (which includes clothes, haircuts and fitness club dues, though not as sub-accounts). There is also Expenses:Goodies, which I loosely define as "wants:" gadgets, movies, or books. Expenses:Housing is another important key account for improving and maintaining my household (could be toilet paper from Walgreens or paint for the bedroom), as well as sub-accounts for mortgage interest (kept as an account for tax purposes).

In addition, I restructured my hierarchy so that they are no longer sorted by account type (used to be Accounts:Bank:Savings, Accounts:Bank:Checking, Liabilities:Loans:Mortgage, Liabilities:Credit Cards:Diners Club, etc). Now they are sorted by how I want to aggregate the information. So now I have, Floating:Cash, Floating:Checking, and Floating:Credit Cards. That way I can look at the Floating level in the hierarchy and see if I have enough money in my Checking account to cover the credit card bill, or if I need to transfer money from Stable:Savings (a regular occurrence, because I keep checking as lean as possible so that I can earn better interest in Savings).

Phew, this is a wordy post that means little to anyone but me. Why are you still reading? I hope that this makes my life a little bit simpler and removes a little bit of stress. I also hope that it helps me, how shall I say, less resistant to dealing with my books and keeping them up-to-date and accurate. But not too accurate, because that's why I landed here in the first place! :)