I've spent some time working on a strategy to implement validation in my ASP.Net MVC site. At the risk of over-engineering, I'm attempting to develop a loosely couple implementation that could be rolled out consistently for any of my projects. Given all the moving parts, I thought I would ask the folks at SO to see if they have any input or thoughts on improvement. The code is obviously contrived, I just wanted to give a sense of how everything hangs together.
The moving parts of interest:
- Repository layer with EF for data access
- Model Data Annotations for input validation
- Service layer for business rule validation
- Unity for DI
Given that I want to use the same EF context during a single Controller action, I'm using the Unit of Work pattern to inject the same DataContect into multiple services within the controller:
public class OrderController : Controller
{
private IUnitOfWork _unitOfWork;
private IOrderService _recipeService;
private IInventoryService _inventoryService;
public OrderController(IUnitOfWork unitOfWork, IOrderService orderService, IInventoryService inventoryService)
{
_unitOfWork = unitOfWork;
_orderService = orderService;
_inventoryService = inventoryService
//Use property injection to apply the Unit of Work context and validation state to our services
_orderService.Context = _unitOfWork;
_orderService.ValidationState = new ModelStateWrapper(this.ModelState);
_inventoryService.Context = _unitOfWork;
_inventoryService.ValidationState = new ModelStateWrapper(this.ModelState);
}
Continuing with some more contrived code, let's say in my Create action, I want to create an order for a product, and also remove the product from inventory:
public ActionResult Create(CreateEditOrderViewModel model)
{
try
{
Product product = Mapper.Map<ProductDTO, Product>(model.ProductDTO);
if(_orderService.Insert(product) &&
_inventoryService.Remove(product) &&
ModelState.IsValid)
{
_unitOfWork.Save();
return RedirectToAction("Index");
}
}
catch (DataException exc)
{
//Log the error (add a variable name after DataException)
ModelState.AddModelError("", "Unable to save changes, please check the log for errors.");
}
return View(model);
}
In my service, I do some business rule validation per http://www.asp.net/mvc/tutorials/older-versions/models-(data)/validating-with-a-service-layer-cs:
public class OrderService : IOrderService
{
public bool Insert(Recipe orderToCreate)
{
// Validation logic
if (!ValidateOrder(orderToCreate))
return false;
// Database logic
try
{
_context.OrderRepository.Insert(orderToCreate);
}
catch
{
return false;
}
return true;
}
protected bool ValidateOrder(Order orderToValidate)
{
Product p = orderToValidate.Product;
//Ensure inventory has product before creating order
if (_context.InventoryRepository.HasProduct(p)
_validationState.AddError("Product", "That product cannot be added to the order as we don't have it in stock");
return _validationState.IsValid;
}
public IUnitOfWork Context
{
get
{
return _context;
}
set
{
_context = value;
}
}
public IValidationDictionary ValidationState
{
get
{
return _validationState;
}
set
{
_validationState = value;
}
}
}
And a simple order model would look like this:
public class Order: IModel
{
[Key]
public int ID { get; set; }
[Required(ErrorMessage="A buyer is required.")]
public string Buyer { get; set; }
public virtual ICollection<Product> Products{ get; set; }
}
So, as it stands, validation on the data annotations occurs during model binding, and business rule validation occurs when the service's CRUD methods are invoked. The services use the same Unit of Work object that contains references to the repositories, so all service CRUD methods execute within the same EF context, which provides me goodies like transactions and concurrency.
In my controller, I'm making calls to multiple services within my Create action. Would it be preferable to instead make a single call to OrderService, which then makes a call to the InventoryService itself?
Is there a way to attach the Unit of Work object into the service via Unity, given I need the same UoA object for each service? I couldn't think of a way to do it which wouldn't end up with a different instance for each service.
If anyone has any thoughts or suggestions, I would love to hear them!
Thanks!
Chris