Welcome Guest, you are in: Login

Castle Project

RSS RSS

Navigation (Active Record)





Search the wiki
»

PoweredBy

MonoRail ActiveRecord integration

RSS
Modified on 2011/01/03 02:23 by Jan Wilson Categorized as Uncategorized
If you are using ActiveRecord you may consider using the integration we developed fo it. In order to do so, first of all, add a reference to the following assembly:
  • Castle.MonoRail.ActiveRecordSupport

ARSmartDispatcherController

What we are about to discuss only works if you are using ARSmartDispatcherController instead of SmartDispatcherController. The ARSmartDispatcherController offers a CustomBindObject method that is ActiveRecord-aware.

ARDataBindAttribute and ARDataBinder class

So, imagine that you are creating a CRUD page for a Customer object. Creation is really simple, and the DataBindAttribute attribute is enough:

public class CustomerController : ARSmartDispatcherController 
{
    public void Index()
    {
    }

    public void New()
    { 
    }

    public void Create([DataBind("customer")] Customer customer)
    { 
        try
        {
            customer.Create();

            RedirectToAction("index");
        }
        catch(Exception ex)
        {
            Flash["error"] = ex.Message;

            RedirectToAction("new", Request.Form);
        }
    }
}

Now editing is tricky. You must load the Customer, and populate the changes with the form data. Enter ARDataBindAttribute:

public class CustomerController : ARSmartDispatcherController 
{
    ...

    public void Edit(int id)
    { 
        PropertyBag.Add("customer", Customer.Find(id));
    }

    public void Update([ARDataBind("customer", AutoLoad=AutoLoadBehavior.Always)] Customer customer)
    { 
        try
        {
            customer.Update();

            RedirectToAction("index");
        }
        catch(Exception ex)
        {
            Flash["error"] = ex.Message;

            RedirectToAction("edit", Request.Form);
        }
    }
}

The ARDataBindAttribute extends the DataBindAttribute so the Exclude and Allow properties are still there.

However, as you can see, we used AutoLoad=AutoLoadBehavior.Always. This tells the binder to collect the primary key value for the customer and load it, then populate the object. So all you have to do next is to invoke Save or Update method.

The AutoLoad property

It is very important that you know what the AutoLoad property means and the behavior it governs.

Enum fieldDescription
NeverMeans that no autoload should be performed on the target type or on nested types.
AlwaysMeans that autoload should be used for the target type and the nested types (if present). This demands that the primary key be present on the http request for the root type and nested.
OnlyNestedDoes not load the root type, but loads nested types if the primary key is present. If not present, sets null on nested type. This is useful for insertions.
NewInstanceIfInvalidKeyMeans that we should autoload, but if the key is invalid, like null, 0 or an empty string, then just create a new instance of the target type.
NullIfInvalidKeyMeans that we should autoload, but if the key is invalid, like null, 0 or an empty string, then just set null on the nested type.

ActiveRecord DataBinding Issues

The combination of databinding and ActiveRecord opens the possibility for an error that is most often hit by unexperienced users, especially when using the recommended Session Per Request configuration for ActiveRecord. Consider the code from above:

public void Update([ARDataBind("customer", AutoLoad=AutoLoadBehavior.Always)] Customer customer)
{ 
    try
    {
        customer.Update();

        RedirectToAction("index");
    }
    catch(Exception ex)
    {
        Flash["error"] = ex.Message;

        RedirectToAction("edit", Request.Form);
    }
}


Now, what happens if there is an exception? The exception is caught as intended and the error message added to the Flash container. But actually, you will get an ASP.NET errorpage nonetheless. The reason for this behaviour is simple once you know how the request is processed in the controller:

  1. An ActiveRecord SessionScope is created in OnBeginRequest()
  2. The customer object will be looked up from database by its primary key.
  3. The properties of the customer object are updated through databinding.
  4. customer.Update() is called and throws an exception.
  5. The catch block executes.
  6. The ActiveRecord SessionScope is disposed in OnEndRequest():
    1. NHibernate checks whether there are pending changes.
    2. The customer object is marked dirty, because its property values have been changed during databinding.
    3. NHibernate flushes the session. During the flush, the changes of customer object are written back to the database.
    4. An unhandled exception occurs.
  7. The exception cannot be handled in your controller and an errorpage is shown.

The same issue appears when using ActiveRecord's validation support. Behaviour and reasons are identical in that case: The object is not saved but changed by the DataBinder and will be saved by NHibernate when the session is disposed.

The same issue appears when using ActiveRecord's validation support. Behaviour and reasons are identical in that case: The object is not saved but changed by the DataBinder and will be saved by NHibernate when the session is disposed.


So, now that the problem is known, how can it be handled? There are plenty possible solutions for this, depending on the user's needs:
  1. Make the session readonly and always flush it explicitly.
  2. Remove the offending object from the session.
  3. Create a TransactionScope and roll it back.
  4. Use the Validate option of ARDataBind.

Using a Read-Only-Session

If you change the SessionScope to not automatically flush, changes are not flushed on disposal of the scope. You can setup the scope as shown below (based on this article):

// GlobalApplication.cs
public void OnBeginRequest(object sender, EventArgs e)
{
    HttpContext.Current.Items.Add("nh.sessionscope", new SessionScope(FlushAction.Never);
}


Doing so requires you to flush your sesssion manually in every controller that changes existing objects or introduces new objects to the database. There are two possibilities to get the session object to flush:

// Use this when the session was added to the HttpContext in OnBeginRequest
((SessionScope)Context.Items["nh.sessionscope"]).Flush();

// Gets the session, if it is not stored in a known place.
ActiveRecordMediator
    .GetSessionFactoryHolder()
    .CreateSession(typeof(Customer))
    .Flush();

Removing Invalid Objects From Session

Another possibility is to keep the default behaviour and only assure that invalid objects are not flushed on disposal of the session. This strategy is recommended when there are objects that need to be saved to the database even when one object is invalid and must not be stored.

You might also want to use this strategy when you are using Windsor integration and the ActiveRecordIntegration facility, because in this case you cannot change the session to be read-only within your code.

To remove the conflicting object from the SessionScope, you must Evict it:

ActiveRecordMediator
    .GetSessionFactoryHolder()
    .CreateSession(typeof(Customer))
    .Evict(customer);

By using the ActiveRecordMediator, you will get access to the session regardless where it as been originally created.

Using a TransactionScope

Another method is to wrap the validation in a transaction. If the validation fails, the transaction must be rolled back:

using (TransactionScope tx = new TransactionScope())
{
    try
    {
        customer.Update();
        RedirectToAction("index");
    }
    catch(Exception ex)
    {
        Flash["error"] = ex.Message;
        tx.Rollback();
        RedirectToAction("edit", Request.Form);
    }
}

You might wonder how this works because I wrote above that the DataBinder makes the offending changes outside of the TransactionScope. The answer is that TransactionScopes can be nested and that there is an implicit transaction started during the creation of the SessionScope. The rollback is then propagated to that transaction and all changes are discarded.

However, that means that if you have other objects that must be saved, you should instead evict the invalid object as descibed above.

Use Validation during DataBinding

The most elegant method to circumvent such problems is the use of the Castle Validation component. By setting the parameter Validate of the ARDataBind-attribute to true, MonoRail performs validation of the input data before changing the properties based on the ValidateXXX-Attributes of the ActiveRecord model classes.

The drawback is that invalid data is completely discarded and will not redisplayed to the client by FormHelper. In order to access the data, Request.Params must be used.

public void Update(
    [ARDataBind("customer", 
        AutoLoadBehavior.NewRootInstanceIfInvalidKey, 
        Validate = true)] Customer customer)
{
    if (ValidationSummaryPerInstance[info].ErrorsCount > 0)
    {
        string msg = "Please correct errors:";
        foreach (string p in ValidationSummaryPerInstance[customer].InvalidProperties)
        {
            msg += "<p>" + p + ":</p>";
            foreach (string m in ValidationSummaryPerInstance[customer].GetErrorsForProperty(p))
            {
                msg += string.Format("<p>{0}</p>", m);
            }
        }
        Flash["message"] = msg;
        Flash["customer"] = customer;
        RedirectToReferrer();
        return;
    }
    // "Normal" application flow
    // ...
}

ARFetchAttribute

The ARFetchAttribute is a simpler version of ARDataBinder. It is in charge of loading the instance from the database and nothing more.

public class CustomerController : ARSmartDispatcherController 
{
    ...

    public void SetPassword([ARFetch("customer.id")] Customer customer, String newPassword)
    { 
        try
        {
            customer.Password = newPassword;
            customer.Save();

            RedirectToAction("index");
        }
        catch(Exception ex)
        {
            Flash["error"] = ex.Message;

            RedirectToAction("changepassword", Request.Form);
        }
    }
}

The optional parameter passed to ARFetch tells it which form field has the primary key value. If you don't specify it, it will use the parameter name (for the example above it would be customer)

You can also specify Required=true which will force it to throw an exception if the record is not found:

public class CustomerController : ARSmartDispatcherController 
{
    ...

    public void SetPassword([ARFetch("customer.id", Required=true)] Customer customer, String newPassword)
    { 
        ...
    }
}

You can also specify Create=true, which will create a new object if the primary key form field is empty:

public class CustomerController : ARSmartDispatcherController 
{
    ...

    public void CreateOrModifyCustomer([ARFetch("customer.id", Create=true)] Customer customer, String name, ...)
    { 
        customer.Name = name;
        customer.Save();
    }
}

ScrewTurn Wiki version 3.0.4.560. Some of the icons created by FamFamFam.