views:

315

answers:

5

Here is my scenario:

I'm using nhibernate, openning and closing the session in an IHttpModule (PreRequestHandlerExecute and PostRequestHandlerExecute).

Ninject takes care of injecting my session in all my repositories and I'm very happy with it.

Now suppose an Update to one of my entities (code simplified):

Controller:

User user = _userService.GetUser(id);
user.Name = "foo";
user.Email = "[email protected]";
user.Group = _groupService.GetGroup(idGroup);
if(_userService.Edit(user)) {
   RedirectToAction("Index");
}
else {
   return View(user);
}

Service:

if(ValidateUser(user) {
   return _rep.Update(user);
}
return false;

ValidateUser is doing the validation logic and inserting any error in a IValidationDictionary that is a wrapper for the ModelState in the controller.

So far so good, I get all errors (if any) and I can show them in the view.


Here comes the problem:

When I try to save an user with an error (no name for instance), the method _rep.Update(user) is never called, but the user gets saved anyway.

Googling around it come to my knowledge the nhibernate AutoDirtyCheck, witch means that if I change an entity in memory, it will be persisted automatically in the database.

Very powerful feature I agree, but since my session is commited in PostRequestHandlerExecute, my invalid entity is saved anyway, something that I don't want.

I tried to remove this behavior using unhaddins, it worked, but then my child objects are not automatically saved when I save only the parent :(


So how to solve that?

Make ValidadeUser public and validade a copy before calling the _userService.GetUser(id)?

Put the validation logic elsewhere? Maybe in the entity class itself? (I like it so much separated!).

Thanks a lot in advance.

A: 

Personally, I call my validation code in mvc in the defaultmodelbinder. My viewmodel (posted data) is validated before I have done anything with it. I use one validator class for each validation concern.

public class MyController : Controller
{
  public ActionResult MyActionMethod(UserChangeModel model)
  {
     if (!ModelState.IsValid)
     {
        return RedirectToAction("Index");      
     }

     User user = _userService.GetUser(model.Id);
     user.Name = model.Name;
     user.Email = model.Email;
     user.Group = _groupService.GetGroup(model.IdGroup);     
     return View(user);
  }
}

public class MyDefaultModelBinder : DefaultModelBinder
{   
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var boundInstance = base.BindModel(controllerContext, bindingContext);
        if (boundInstance != null)
        {
            var validator = findValidator(bindingContext.ModelType);
            var errors = validator.Validate(boundinstance);
            addErrorsToTheModelState(bindingContext, errors);
        }
    }
}
Paco
I like your solution. I would like to see more opinions though. If nobody answers in one or two days you get the V :)
andrecarlucci
Man, a problem. With this I can't have a duplicated entity error in the same fashion (like trying to update an user with an existing username). How do you deal with this?
andrecarlucci
validator.Validate(object toValidate) (pseudo code) makes a call to a validator factory that creates a validator class instance, depending on the type of the object passed in (the viewmodel type in this case). The validator class is a class that inherits from validator<T>. I create a separate validator class for each model I need to validate. The validator factory resolves instances of the validator class with the IoC/Service locator. I can inject a dataacess class into the validator class to check if the username exists.
Paco
public class UserValidator : Validator<UserViewModel>{ private IUserDataAccess dataAccess; public UserValidator(IUserDataAccess dataAccess) { this.dataAccess = dataAccess; RuleFor(user => user.UserName) .When(username => dataAcess.GetUser(userName) != null) .Message("Choose a different username"); } }
Paco
Code formatting in comments does not work...
Paco
Cool, but what about another request inserting a new user between the username check and the insert itself?When adding new info, edit your answer instead of adding comments :)
andrecarlucci
That's a different question. That's about concurrency. There are several ways to implement concurrency in NHibernate.
Paco
A: 

I haven't tried this myself but the first thing that comes to mind is detaching the invalid user object. If you don't doing another query that would retrieve the same user would also return the same, now invalid, object.

Maurice
It would work, but my service has no idea what session.evict() is :(
andrecarlucci
As it already knows about a GetUser() and Update() something like CancelChanges() would not seem out of place.
Maurice
+2  A: 

FYI - you can set the Nhibernate session FlushMode property to FlushMode.Never to be totally in control of when NHibernate will flush updates to the database. It might be that you could cheat and if an action is not authorized - never do a flush and the nhibernate session will die when the response is over (if the session didnt go away you really should evict the modified object, tho)

HokieMike
A: 

Fabio Maulo has a very good writeup/solution

http://fabiomaulo.blogspot.com/2009/03/ensuring-updates-on-flush.html

gunteman
A: 

You can use ISession.Evict(obj) to remove the object from the session and that will prevent it from being automatically persisted. One thing to note is that this makes the object transient and attempting to load any lazy initialized child objects (typically collections) will cause NH to throw a LazyInitializationException.

ETA: I just read your comment to Maurice that you can't directly access the ISession. I manually inject the ISession into repository/service classes because it's needed for WinForms. There are several methods in ISession that I've had to access from time-to-time, particularly Evict, Merge, and Lock. I would expose the ISession or a wrapper so that you can use Evict.

Jamie Ide