Thursday, February 26, 2009

SolrNet 0.2.1 released

SolrNet is a Solr client for .NET

Here's the changelog from 0.2.0:

Added a couple of Solr 1.3 features:

Other enhancements:

  • "Has any value" queries:
    It's often convenient to see what documents have a field defined or not:
    var q = new SolrHasValueQuery("name"); // translates to name:[* TO *]
  • Fluent interface for query building:
    Query.Simple("name:solr"); // translates to name:solr
    Query.Field("name").Is("solr"); // name:solr
    Query.Field("price").From(10).To(20); // price:[10 TO 20]
    Query.Field("price").In(10, 20, 30); // price:10 OR price:20 OR price:30
    Query.Field("name").HasAnyValue(); // name:[* TO *]

You can see the spell checker and random sorting in action in the sample web app:

spellcheck

Download links:

Thursday, February 19, 2009

SolrNet 0.2 released

SolrNet is a Solr client for .Net.

Changelog from version 0.1:

The sample application is pretty basic right now, it includes only the basic features, but it shows how it all fits together. Here's a screenshot with the features highlighted:

For the next releases I'll look into adding Solr 1.3 features like multi-core and spell-checking, as well as making it easier to express queries (maybe build a LINQ provider).

Code is hosted at Google code.

Direct download links:

Tuesday, February 17, 2009

ASP.NET MVC postback support

In my unwanted quest to port a MonoRail app to ASP.NET MVC, I have to use existing master pages and user controls.

Bad news is, some of these user controls post back and then all hell breaks loose. Specifically, I started getting this exception on every postback:

Line 338: <% if (Model.PaymentOptions.Count > 0) {%>
Line 339: <div class="fleft" style="margin:2px 20px 2px 0px; padding: 0px;">
Line 340: <% Html.RenderPartial("ListingDetailPaymentOption", Model.PaymentOptions[0]);%>
Line 341: </div>
Line 342: <% }%>
[HttpException]: Unable to validate data.
   at System.Web.Configuration.MachineKeySection.GetDecodedData(Byte[] buf, Byte[] modifier, Int32 start, Int32 length, Int32& dataLength)
   at System.Web.UI.ObjectStateFormatter.Deserialize(String inputString)
[ViewStateException]: Invalid viewstate. 
	Client IP: 127.0.0.1
	Port: 18559
	User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.0.6) Gecko/2009011913 Firefox/3.0.6 (.NET CLR 3.5.30729)
	ViewState: /wEPDwUKLTc0NTAwNTg1NA9kFgJmD2QWAmYPZBYCZg9kFgICBw9kFgICBQ9kFgICAw9kFgICAw9kFgICAg9kFgJmDxYCHgtfIUl0ZW1Db3VudAIFFgpmD2QWAmYPFQk8LzIwMDZfTGFuZF9Sb3Zlcl9SYW5nZV9Sb3Zlcl9TcG9ydF9TdXBlcmNoYXJnZWRfMTEyNjg3LnhodG1sBDIwMDYpTGFuZCBSb3ZlciBSYW5nZSBSb3ZlciBTcG9ydCBTdXBlcmNoYXJnZWQ1L3Bob3Rvcy9pbWdzaG93Uy5hc3B4P2lkPTI0MTgmc2l6ZT10XzEyMHg5MCZZZWFyPTIwMDY8LzIwMDZfTGFuZF9Sb3Zlcl9SYW5nZV9Sb3Zlcl9TcG9ydF9TdXBlcmNoYXJnZWRfMTEyNjg3LnhodG1sBDIwMDYpTGFuZCBSb3ZlciBSYW5nZSBSb3ZlciBTcG9ydCBTdXBlcmNoYXJnZWQCMTkHJDk1MC4wMGQCAQ9kFgJmDxUJJy8yMDA2X01lcmNlZGVzX0UzNTBfU2VkYW5fXzExMjk4MC54aHRtbAQyMDA2FE1lcmNlZGVzIEUzNTAgU2VkYW4gKy9waG90b3MvaW1nc2hvdy5hc3B4P2lkPTY4MjE4JnNpemU9dF8xMjB4OTAnLzIwMDZfTWVyY2VkZXNfRTM1MF9TZWRhbl9fMTEyOTgwLnhodG1sBDIwMDYUTWVyY2VkZXMgRTM1MCBTZWRhbiACMTIHJDQ4OC4wMGQCAg9kFgJmDxUJLy8yMDA2X01lcmNlZGVzX0NMUzUwMF9Db3VwZV80X0Rvb3JfMTA4NjI4LnhodG1s...
[HttpException]: Validation of viewstate MAC failed. If this application is hosted by a Web Farm or cluster, ensure that <machineKey> configuration specifies the same validationKey and validation algorithm. AutoGenerate cannot be used in a cluster.
   at System.Web.UI.ViewStateException.ThrowError(Exception inner, String persistedState, String errorPageMessage, Boolean macValidationError)
   at System.Web.UI.ViewStateException.ThrowMacValidationError(Exception inner, String persistedState)
   at System.Web.UI.ObjectStateFormatter.Deserialize(String inputString)
   at System.Web.UI.ObjectStateFormatter.System.Web.UI.IStateFormatter.Deserialize(String serializedState)
   at System.Web.UI.Util.DeserializeWithAssert(IStateFormatter formatter, String serializedState)
   at System.Web.UI.HiddenFieldPageStatePersister.Load()
   at System.Web.UI.Page.LoadPageStateFromPersistenceMedium()
   at System.Web.UI.Page.LoadAllState()
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest()
   at System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context)
   at System.Web.UI.Page.ProcessRequest(HttpContext context)
   at System.Web.Mvc.ViewPage.RenderView(ViewContext viewContext)
   at System.Web.Mvc.ViewUserControl.RenderViewAndRestoreContentType(ViewPage containerPage, ViewContext viewContext)
   at System.Web.Mvc.ViewUserControl.RenderView(ViewContext viewContext)
   at System.Web.Mvc.WebFormView.RenderViewUserControl(ViewContext context, ViewUserControl control)
   at System.Web.Mvc.WebFormView.Render(ViewContext viewContext, TextWriter writer)
   at System.Web.Mvc.HtmlHelper.RenderPartialInternal(String partialViewName, ViewDataDictionary viewData, Object model, ViewEngineCollection viewEngineCollection)
   at System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper htmlHelper, String partialViewName, Object model)
   at ASP.views_listingdetail_index_aspx.__Render__control2(HtmlTextWriter __w, Control parameterContainer) in c:\trabajo\web\WEB2\Views\ListingDetail\Index.aspx:line 340
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.HtmlControls.HtmlForm.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.HtmlControls.HtmlContainerControl.Render(HtmlTextWriter writer)
   at System.Web.UI.HtmlControls.HtmlForm.Render(HtmlTextWriter output)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.HtmlControls.HtmlForm.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at Templates.master.Render(HtmlTextWriter writer) in C:\trabajo\web\WEB2\templates\master.master.vb:line 29
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Page.Render(HtmlTextWriter writer)
   at System.Web.Mvc.ViewPage.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest()
   at System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context)
   at System.Web.UI.Page.ProcessRequest(HttpContext context)
   at ASP.views_listingdetail_index_aspx.ProcessRequest(HttpContext context) in c:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\root\ffa83e12\a51f62df\App_Web_index.aspx.1cc8d514.vtl-v5gs.0.cs:line 0
   at System.Web.Mvc.ViewPage.RenderView(ViewContext viewContext)
   at System.Web.Mvc.WebFormView.RenderViewPage(ViewContext context, ViewPage page)
   at System.Web.Mvc.WebFormView.Render(ViewContext viewContext, TextWriter writer)
   at System.Web.Mvc.ViewResultBase.ExecuteResult(ControllerContext context)
   at System.Web.Mvc.ControllerActionInvoker.InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
   at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass11.b__e()
   at System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilter(IResultFilter filter, ResultExecutingContext preContext, Func`1 continuation)
   at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass11.<>c__DisplayClass13.b__10()
   at System.Web.Mvc.ControllerActionInvoker.InvokeActionResultWithFilters(ControllerContext controllerContext, IList`1 filters, ActionResult actionResult)
   at System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName)
   at System.Web.Mvc.Controller.ExecuteCore()
   at System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext)
   at System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext requestContext)
   at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContextBase httpContext)
   at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContext httpContext)
   at System.Web.Mvc.MvcHandler.System.Web.IHttpHandler.ProcessRequest(HttpContext httpContext)
   at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

As usual, first thing I did was googling the exception, but the standard answer was: "Postback is not supported in MVC". I also found this Microsoft Connect issue that confirmed that this is a bug.

What I found weird is that the exception was actually throwing in the ViewUserControl (MVC's user control), not the legacy WebForms user controls. I took a look at the source of ViewUserControl.RenderView() and found that the rendering is actually done by creating a fake ViewPage, and that the ViewState validation in this ViewPage was the one failing. But, since in MVC we don't use ViewState, we can just turn it off. So I created this little base class to do that, just inherit from this one instead of ViewUserControl<T>:

public class ViewUserControlWithoutViewState<T> : ViewUserControl<T> where T : class {
    protected override void LoadViewState(object savedState) {}

    protected override object SaveControlState() {
        return null;
    }

    protected override void LoadControlState(object savedState) {}

    protected override object SaveViewState() {
        return null;
    }

    /// <summary>
    /// extracted from System.Web.Mvc.ViewUserControl
    /// </summary>
    /// <param name="viewContext"></param>
    public override void RenderView(ViewContext viewContext) {
        viewContext.HttpContext.Response.Cache.SetExpires(DateTime.Now);
        var containerPage = new ViewUserControlContainerPage(this);
        ID = Guid.NewGuid().ToString();
        RenderViewAndRestoreContentType(containerPage, viewContext);
    }

    /// <summary>
    /// extracted from System.Web.Mvc.ViewUserControl
    /// </summary>
    /// <param name="containerPage"></param>
    /// <param name="viewContext"></param>
    public static void RenderViewAndRestoreContentType(ViewPage containerPage, ViewContext viewContext) {
        string contentType = viewContext.HttpContext.Response.ContentType;
        containerPage.RenderView(viewContext);
        viewContext.HttpContext.Response.ContentType = contentType;
    }

    /// <summary>
    /// Extracted from System.Web.Mvc.ViewUserControl+ViewUserControlContainerPage
    /// </summary>
    private sealed class ViewUserControlContainerPage : ViewPage {
        // Methods
        public ViewUserControlContainerPage(ViewUserControl userControl) {
            Controls.Add(userControl);
            EnableViewState = false;
        }

        protected override object LoadPageStateFromPersistenceMedium() {
            return null;
        }

        protected override void SavePageStateToPersistenceMedium(object state) {}
    }
}
DISCLAIMER: it worked for my specific case, I'm not claiming this covers all possible cases of this exception! Hopefully this problem will be addressed soon by Phil et al...

Tuesday, February 10, 2009

ARFetch for ASP.NET MVC

I'm currently in the unfortunate position of having to merge part of a MonoRail + ActiveRecord + Windsor (the full Castle stack) project with a huge existing Webforms project. I have to use existing master pages and lots of user controls, and MonoRail's support for these is rather scarce, so I thought of using ASP.NET MVC instead of trying to cram things into webforms. I already had Windsor working on the webforms project, setting up ActiveRecord is a snap, and integrating ASP.NET MVC with Windsor is a breeze thanks to MvcContrib. But I couldn't find anything to integrate ASP.NET MVC with ActiveRecord.

Luckily, MonoRail's IParameterBinder and ASP.NET MVC's IModelBinder aren't that different in spirit. In order to implement ARFetching, I only had to make a couple of minor changes to the ARFetcher and ARFetchAttribute classes.

Here's what a simple CRUD controller would look like using ARFetch:

public class PersonController : Controller {
  //
  // GET: /Person/

  public ActionResult Index() {
      return View(ActiveRecordMediator<Person>.FindAll());
  }

  //
  // GET: /Person/Details/5

  public ActionResult Details([ARFetch("id")] Person person) {
      if (person == null)
          return RedirectToAction("Index");
      return View(person);
  }

  //
  // GET: /Person/Create

  public ActionResult Create() {
      return View();
  }

  //
  // POST: /Person/Create

  [AcceptVerbs(HttpVerbs.Post)]
  public ActionResult Create(FormCollection collection) {
      try {
          var p = new Person();
          UpdateModel(p, collection.ToValueProvider());
          ActiveRecordMediator<Person>.Save(p);
          return RedirectToAction("Index");
      } catch {
          return View();
      }
  }

  //
  // GET: /Person/Edit/5

  public ActionResult Edit([ARFetch("id")] Person person) {
      return View(person);
  }

  //
  // POST: /Person/Edit/5

  [AcceptVerbs(HttpVerbs.Post)]
  public ActionResult Edit([ARFetch("id")] Person person, FormCollection collection) {
      try {
          UpdateModel(person, collection.ToValueProvider());
          ActiveRecordMediator<Person>.Update(person);
          return RedirectToAction("Index");
      } catch {
          return View();
      }
  }
}

Here's the full source code, all merit goes to the Castle team, any bugs are mine:

Monday, February 2, 2009

Repeater with separator for ASP.NET MVC

Today I needed to repeat over a list of items in my views, separating each item with some html. A simple foreach is no good in this case. I really liked Phil Haack's code-based repeater syntax, but it handles only alternating items, not separators. I didn't need the alternating items, so I just wrote this extension with separator support:

public static class HtmlHelperRepeatExtensions {
    private static Func<T, U> ActionToFunc<T, U>(Action<T> a) {
        return t => {
            a(t);
            return default(U);
        };
    }

    private static Func<T> ActionToFunc<T>(Action a) {
        return () => {
            a();
            return default(T);
        };
    }

    public static void Repeat<T>(this HtmlHelper html, IEnumerable<T> items, Action<T> render, Action separator) {
        var frender = ActionToFunc<T, int>(render);
        var fseparator = ActionToFunc<int>(separator);
        items.Select<T, Func<int>>(e => () => frender(e)).Intersperse(fseparator).Select(f => f()).ToArray();
    }

    // From http://blogs.msdn.com/wesdyer/archive/2007/03/09/extending-the-world.aspx
    private static IEnumerable<T> Intersperse<T>(this IEnumerable<T> sequence, T value) {
        bool first = true;
        foreach (var item in sequence) {
            if (first)
                first = false;
            else
                yield return value;
            yield return item;
        }
    }
}

Sample usage:

<% Html.Repeat(Model.Products, p => { %> 
    <div><%= p.Id%></div>
    <div><%= p.Name%></div>
<%}, () => { %> 
    <br /> <%-- separator --%>
<% }); %>

It's worth mentioning that I had to convert the rendering Actions into Funcs in order to write this in a functional style... in F# this would definitely look prettier :-)