Tuesday, April 19, 2016

A simple rules engine

I'm extracting data from some OCR'd letters and, in order to determine which type of letter I'm parsing, I'm using a method similar to this:

        public Letter Parse(string text)
        {
            Letter result;
            if (text.IndexOf("...", StringComparison.OrdinalIgnoreCase) >= 0)
                letter = new LetterA();
            else
                letter = new LetterB();

            //... additional processing
            return letter;
        }

If the letter contains a specific text, I know it's of one type; otherwise I'll default to the other type. Unfortunately that's going to get really complicated, really fast once I start adding new letter types. I read somewhere that "you should move logic out of the code and into the data when possible"; it made sense and I never had a reason to regret it. So, let me try to do that here.

First I'll add a "rules list" class that will allow me to store the various criteria:

    public class RulesList<T, TResult>
    {
        public RulesList()
        {
            rules = new List<Tuple<Predicate<T>, Func<TResult>>>();
        }

        public void Add(Predicate<T> condition, Func<TResult> constructor)
        {
            rules.Add(Tuple.Create(condition, constructor));
        }

        public TResult Get(T criteria)
        {
            return rules
                .Where(rule => rule.Item1(criteria))
                .Select(rule => rule.Item2())
                .FirstOrDefault();
        }

        //

        private readonly List<Tuple<Predicate<T>, Func<TResult>>> rules;
    }

I've made this class more generic by replacing the string type with T; honestly, I don't think there will ever be a need for anything else in this project but… it wasn't a big "expense".

Using this is quite simple at the moment:

    public class LetterSelector
    {
        public LetterSelector()
        {
            rules = new RulesList<string, Letter>();

            rules.Add(s => s.IndexOf("...", StringComparison.OrdinalIgnoreCase) >= 0, () => new LetterA());
            rules.Add(_ => true, () => new LetterB());
        }

        public Letter Parse(string text)
        {
            var letter = rules.Get(text);
            //... additional processing

            return letter;
        }

        //

        private readonly RulesList<string, Letter> rules;
    }

Is this a big gain? Right now it doesn't look like I gained anything; however, bitter experience taught me that methods with many conditionals quickly become an unmaintainable mess (you haven't lived until you've had to fix a method with 700+ lines and a cyclomatic complexity over 200). This will allow me to separate those conditionals into their own lambdas or small private methods in the LetterSelector class.

Tuesday, April 12, 2016

Crystal Reports woes

This took me an hour to figure out so I thought I'd write it down in case it helps anyone else.

If you have a form that's going to display a Crystal Report and you want to zoom it by default, the "normal" way would be to do this in form_Shown:

    private void ReportViewer_Shown(object sender, EventArgs e)
    {
        viewer.Zoom(2); // 1 = page width, 2 = whole page, 25..400 = zoom factor
    }

(Where viewer is the CrystalReportViewer component.)

Unfortunately, it takes CR a while to compute and display the actual report; by the time that happens, the .Zoom() call has already been executed (and ignored).

I have tried a number of workarounds (including launching a thread, waiting for two seconds and then calling the Zoom method - it worked but it was a horrible hack) before I discovered that CR has a "hidden" PageChanged event (it has a [Browsable(false)] attribute). Use that event by assigning a handler in the constructor:

    viewer.PageChanged += Viewer_PageChanged;

and then add the Viewer_PageChanged method:

    private void Viewer_PageChanged(object sender, EventArgs e)
    {
        viewer.Zoom(2);

        viewer.PageChanged -= Viewer_PageChanged;
    }

(Note that, in order to avoid leaking references, I have removed the PageChanged handler immediately after calling the Zoom method.)

Saturday, March 26, 2016

POST-ing to an ASP.NET MVC form with an anti-forgery token

I've had some issues trying to write an acceptance test that was registering a new user by POST-ing the required information to a MVC site and I got back these messages:

  • The required anti-forgery cookie "__RequestVerificationToken" is not present.
  • The required anti-forgery form field "__RequestVerificationToken" is not present.
  • Validation of the provided anti-forgery token failed. The cookie "_RequestVerificationToken" and the form field "_RequestVerificationToken" were swapped.

Since it took me a bit of fiddling with the code before I managed to make it work, I thought I'd share the solution. The site is an ASP.NET MVC version 5 and I am trying to register a new user (POST-ing to the /Account/Register URL). The main issue you will encounter is having to extract two anti-forgery tokens, one from the cookies and one from the form, and then sending both of them in the appropriate places (cookies vs form field).

I have used LinqPad 5 with a reference to the HtmlAgilityPack NuGet package (to simplify extracting the needed information from the GET form). This is the code:

void Main()
{
  Cookie[] cookies;
  var html = Get("Account/Register", out cookies);
  var root = Parse(html);
  var formToken = GetFormToken(root);
  var cookieToken = GetCookieToken(cookies);
  
  var unique = Guid.NewGuid().ToString("N");
  
  Post("Account/Register", new
    {
      Email = unique + "@example.com",
      User = unique,
      Password = unique,
      ConfirmPassword = unique,
    },
      formToken, cookieToken);
}

  private const string BASE_URL = "http://localhost:5972"; // replace as needed  
  private const string TOKEN_NAME = "__RequestVerificationToken";
  
  private static string Get(string url, out Cookie[] responseCookies)
  {
    using (var web = new CookieAwareWebClient())
    {
      var result = web.DownloadString(BASE_URL + "/" + url);
      responseCookies = web.ResponseCookies.Cast<Cookie>().ToArray();

      return result;
    }
  }

  private static string Post(string url, object body, string formToken = null, string cookieToken = null)
  {
    using (var web = new WebClient())
    {
      var data = GetPostData(body);

      web.Headers["Content-Type"] = "application/x-www-form-urlencoded";
      if (formToken != null)
        data += "&" + TOKEN_NAME + "=" + formToken;
      if (cookieToken != null)
        web.Headers.Add(HttpRequestHeader.Cookie, TOKEN_NAME + "=" + cookieToken);

      return web.UploadString(BASE_URL + "/" + url, data);
    }
  }

  private static string GetPostData(object obj)
  {
    var kv = obj
      .GetType()
      .GetProperties(BindingFlags.Instance | BindingFlags.Public)
      .Select(prop => prop.Name + "=" + WebUtility.UrlEncode(prop.GetValue(obj) + ""))
      .ToArray();
    return string.Join("&", kv);
  }

  private static HtmlNode Parse(string html)
  {
    var doc = new HtmlDocument();
    doc.LoadHtml(html);
    return doc.DocumentNode;
  }

  private static string GetFormToken(HtmlNode root)
  {
    var formToken = root.SelectSingleNode("//*[@name='" + TOKEN_NAME + "']");
    return formToken.GetAttributeValue("value", null);
  }

  private static string GetCookieToken(IEnumerable<Cookie> cookies)
  {
    return cookies
      .Where(it => it.Name == TOKEN_NAME)
      .First()
      .Value;
  }

  // from http://stackoverflow.com/a/29479390
  public class CookieAwareWebClient : WebClient
  {
    public CookieContainer CookieContainer { get; }
    public CookieCollection ResponseCookies { get; set; }

    public CookieAwareWebClient()
    {
      CookieContainer = new CookieContainer();
      ResponseCookies = new CookieCollection();
    }

    protected override WebRequest GetWebRequest(Uri address)
    {
      var request = (HttpWebRequest) base.GetWebRequest(address);
      // ReSharper disable once PossibleNullReferenceException
      request.CookieContainer = CookieContainer;
      return request;
    }

    protected override WebResponse GetWebResponse(WebRequest request)
    {
      var response = (HttpWebResponse) base.GetWebResponse(request);
      // ReSharper disable once PossibleNullReferenceException
      ResponseCookies = response.Cookies;
      return response;
    }
  }

Hope this helps someone.