ASP.NET MVC: How can I get my business rule validation to bubble up to the presentation layer?
For each of my business entities I have a corresponding view model.
I have a generic CRUD controller that works like this:
[HttpPost]
public virtual ActionResult Create(TViewModel model, int? id)
{
// Validate input
if (!ModelState.IsValid)
return Json(Failure(createView, model.SelectLists(repository)));
// Prepare Model
var entity = new TModel();
// Add to repository
UpdateModel(entity);
repository.Add(entity);
repository.Save();
return Json(CreateSuccess(entity));
}
I use data annotations on my view model properties and this works great for simple input validation.
Now I have a case where I want to make sure a duplicate record isn't created by accident.
My first instinct is to put this logic in the repository's Add method. This implementation would be easy, but how do I get the repository to add a model开发者_运维知识库 state error and return some useful information to the user? I feel like there has to be a solution out there, but I haven't had much luck searching.
Thanks for any help!
I would use exceptions.
- Throw your custom application exception at the Add method, if an enity is doubled.
- Wrap the Add method in a try block to catch this specific exception at the Create method.
Add a model state error based on the exception data at the catch block
try { repository.Add(entity); } catch(MyRepositoryException ex) { ViewData.ModelState.AddModelError(ex.Key, ex.Value.ToString(), ex.Message) } if (!ModelState.IsValid) return Json(Failure(createView, model.SelectLists(repository)));
An alternative to your approach would be to use the idea of a ModelStateWrapper
implementing IValidationDictionary
. It basically decouples the modelState, but still lets your repository/service interact with errors dictionary. This way error handling is all done via an interface and there's no need to reference any MVC-specific data object.
There's a good writeup on it here: http://www.asp.net/mvc/tutorials/validating-with-a-service-layer-cs, but the basic idea is:
1) Pass an instance of your ModelStateWrapper to your repository during the controller's initialization:
public MyController()
{
repository = new MyRepository(new ModelStateWrapper(this.ModelState));
}
2) Add errors to it inside your repository:
_validatonDictionary.AddError("Name", "Name is required.");
3) Handle errors like you normally would in your controller:
if (!repository.Save())
return View();
I hate answering my own questions, but I think I stumbled across the answer I was looking for while searching for something else:
http://nerddinnerbook.s3.amazonaws.com/Part3.htm
Looks like it was time for a back-to-basics review! I should have thought to go back and review my first tutorial as there was no way I was absorbing everything when I was first starting out.
Part 3 of the tutorial talks about implementing domain model validation that returns errors with property name and error message strings that are to be added to the controller's ModelState, which allows for this kind of validation:
if (ModelState.IsValid) {
try {
dinner.HostedBy = "SomeUser";
dinnerRepository.Add(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new{id=dinner.DinnerID});
}
catch {
ModelState.AddModelErrors(dinner.GetRuleViolations());
}
}
I don't know if I like the idea of raising exceptions for business rule violations, but the basic pattern will work well for my project. Hope this helps someone else!
精彩评论