Well, in this post I’ll present my approach on creating a Service Layer to be used within your project along with SharpArchitecture Library. As discussed in this post, I don’t think its a good practice to let the controller talk directly with the repository. The controllers are specific to the MVC pattern and the MVC library, which means that if we put validation rules within the controllers, it will be pretty hard to migrate our project to be used on winforms for example.
A service layer would control the business rules and validation logic, while being responsible for communicating with the repository.
I’ve changed the Northwind Project’s source code that comes with the SharpArch a little bit to show how I’ve implemented my own Service Layer.
The ServiceBase class:
using System.Collections.Generic;
using Northwind.Core.Validation;
using SharpArch.Core.DomainModel;
using SharpArch.Core.PersistenceSupport;
using SharpArch.Core.CommonValidator;
using SharpArch.Data.NHibernate;
namespace Northwind.Services
{
public abstract class ServiceBase<RT, T, IdT> : IRepositoryWithTypedId<T, IdT>
where RT : IRepositoryWithTypedId<T, IdT>
where T : IEntityWithTypedId<IdT>, IValidatable
{
public RT repository;
#region IRepositoryWithTypedId<T,IdT> Members
public virtual IDbContext DbContext
{
get { return repository.DbContext; }
}
public virtual void Delete(T entity)
{
OnActionExecuting();
repository.Delete(entity);
OnActionExecuted();
}
public T Get(IdT id)
{
return repository.Get(id);
}
public List<T> GetAll()
{
return repository.GetAll();
}
public List<T> FindAll(IDictionary<string, object> propertyValuePairs)
{
return repository.FindAll(propertyValuePairs);
}
public T FindOne(IDictionary<string, object> propertyValuePairs)
{
return repository.FindOne(propertyValuePairs);
}
public virtual T SaveOrUpdate(T entity)
{
OnActionExecuting();
try
{
Validate(entity);
entity = repository.SaveOrUpdate(entity);
OnActionExecuted();
}
catch
{
DbContext.RollbackTransaction();
throw;
}
return entity;
}
public virtual void Validate(T entity)
{
if (!entity.IsValid())
throw new InvalidModelStateException(entity.ValidationResults());
}
protected virtual void OnActionExecuting()
{
NHibernateSession.Current.BeginTransaction();
}
protected virtual void OnActionExecuted()
{
if (NHibernateSession.Current.Transaction.IsActive)
NHibernateSession.Current.Transaction.Commit();
}
#endregion
}
}
This class basically implements IRepositoryWithTypedId<T, IdT>, and it will be handy when your service is responsible for communicating with the repository, because it will have all
the repository methods.
&lt;pre&gt;public virtual void Validate(T entity)
{
if (!entity.IsValid())
throw new InvalidModelStateException(entity.ValidationResults());
}&lt;/pre&gt;
This is my way to validate. I do prefer to throw an exception when something is invalid. So I’ve implemented an InvalidModelStateException, which will transport validation results.
namespace Northwind.Core.Validation
{
public class InvalidModelStateException : Exception
{
public ICollection&lt;IValidationResult&gt; ValidationMessages { get; protected set; }
public InvalidModelStateException(ICollection&lt;IValidationResult&gt; validationMessages)
{
ValidationMessages = validationMessages;
}
}
}
I’ve also created my own ValidationResult as needed:
namespace Northwind.Core.Validation
{
public class ValidationResult : IValidationResult
{
public ValidationResult(System.Type classContext, string message, string propertyName)
{
Check.Require(message != null, "Validation result must have a message");
ClassContext = classContext;
Message = message;
PropertyName = propertyName;
}
#region IValidationResult Members
public System.Type ClassContext
{
get;
protected set;
}
public string Message
{
get;
protected set;
}
public string PropertyName
{
get;
protected set;
}
#endregion
}
}
That’s because I wanted to let the class context and the property null sometimes.
In the case when something is invalid but when it’s some business rule, not related
with just one property.
I don’t think the OnActionExecuting/Executed are a good approach to access the NHibernateSession…
There should be a better way, but I couldn’t notice. Tell me if you have any idea.
I though about implementing a new TransactionAttribute to decorate the Service methods when needed,
but I don’t know if it would be good.
Northwind.Services.ServiceInterface.IEmployeeService
namespace Northwind.Services.ServiceInterface
{
public interface IEmployeeService : IRepository&lt;Employee&gt;
{
void Delete(int id);
Employee Edit(int id, Employee employee);
}
}
Northwind.Services.Organization.EmployeeService:
namespace Northwind.Services.Organization
{
public class EmployeeService : ServiceBase&lt;IRepository&lt;Employee&gt;, Employee, int&gt;, IEmployeeService
{
public EmployeeService(IRepository&lt;Employee&gt; repository)
{
Check.Require(repository != null, "Employee repository cannot be null");
this.repository = repository;
}
public override void Delete(Employee entity)
{
repository.Delete(entity);
try
{
base.Delete(entity);
}
catch(Exception e)
{
DbContext.RollbackTransaction();
throw new Exception("The employee couldn't be deleted; likely due to a foreign key " +
"reference. You could cascade the deletion or you could inform " +
"the user better on where the foreign dependencies are and what needs to be " +
"done before the employee can be deleted.", e);
}
}
public void Delete(int id)
{
Employee employeeToDelete = Get(id);
if (employeeToDelete == null)
throw new Exception("An employee with the ID of " + id + " could not be found for deletion.");
Delete(employeeToDelete);
}
public Employee Edit(int id, Employee employee)
{
Employee employeeToUpdate = repository.Get(id);
//This will call IsValid() and throw an InvalidModelStateException if it fails.
Validate(employee);
OnActionExecuting();
TransferFormValuesTo(employeeToUpdate, employee);
OnActionExecuted();
return employeeToUpdate;
}
private static void TransferFormValuesTo(Employee employeeToUpdate, Employee employeeFromForm)
{
employeeToUpdate.FirstName = employeeFromForm.FirstName;
employeeToUpdate.LastName = employeeFromForm.LastName;
employeeToUpdate.PhoneExtension = employeeFromForm.PhoneExtension;
}
}
}
This is the class responsible to communicate with the EmployeeRepository. We took out the sweet candy from the controller and gave it to our beauty Service. So the controller will have an instance of IEmployeeService, don’t forget to add the EmployeeService in the Castle Windsor Container:
//...
container.AddComponent("employeeService",
typeof (IEmployeeService), typeof (EmployeeService));
//...
And finnaly how our new EmployeeController looks like:
namespace Northwind.Web.Controllers.Organization
{
[HandleError]
public class EmployeesController : Controller
{
public EmployeesController(IEmployeeService employeesService)
{
Check.Require(employeesService != null, "employeeRepository may not be null");
this.employeesService = employeesService;
}
/// &lt;summary&gt;
/// The transaction on this action is optional, but recommended for performance reasons
/// &lt;/summary&gt;
[Transaction]
public ActionResult Index() {
List&lt;Employee&gt; employees = employeesService.GetAll();
return View(employees);
}
/// &lt;summary&gt;
/// The transaction on this action is optional, but recommended for performance reasons
/// &lt;/summary&gt;
[Transaction]
public ActionResult Show(int id) {
Employee employee = employeesService.Get(id);
return View(employee);
}
public ActionResult Create() {
return View();
}
[ValidateAntiForgeryToken] // Helps avoid CSRF attacks
[Transaction] // Wraps a transaction around the action
[AcceptVerbs(HttpVerbs.Post)] // Limits the method to only accept post requests
public ActionResult Create(Employee employee) {
try
{
employeesService.SaveOrUpdate(employee);
TempData["message"] = employee.FullName + " was successfully created.";
return RedirectToAction("Index");
}
catch(InvalidModelStateException e)
{
MvcValidationAdapter.TransferValidationMessagesTo(ViewData.ModelState, e.ValidationMessages);
// If it wasn't valid, go back to the input page
return View();
}
}
/// &lt;summary&gt;
/// The transaction on this action is optional, but recommended for performance reasons
/// &lt;/summary&gt;
[Transaction]
public ActionResult Edit(int id) {
Employee employee = employeesService.Get(id);
return View(employee);
}
/// &lt;summary&gt;
/// Accepts the form submission to update an existing item. This uses
/// &lt;see cref="DefaultModelBinder" /&gt; since we're going to try to update the persisted
/// entity and ask it for its validation state rather than relying on the employee
/// from the form to report validation issues. This is particularly important when verifying
/// if the updated persistent object is unique when compared to other entities in the DB.
/// &lt;/summary&gt;
[ValidateAntiForgeryToken]
[Transaction]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, [ModelBinder(typeof(DefaultModelBinder))] Employee employee)
{
try
{
var employeeToUpdate = employeesService.Edit(id, employee);
TempData["message"] = employeeToUpdate.FullName + " was successfully updated.";
return RedirectToAction("Index");
}
catch (InvalidModelStateException e)
{
MvcValidationAdapter.TransferValidationMessagesTo(ViewData.ModelState,
e.ValidationMessages);
}
return View(employee);
}
/// &lt;summary&gt;
/// As described at http://stephenwalther.com/blog/archive/2009/01/21/asp.net-mvc-tip-46-ndash-donrsquot-use-delete-links-because.aspx
/// there are a lot of arguments for doing a delete via a GET request. This addresses that, accordingly.
/// &lt;/summary&gt;
[ValidateAntiForgeryToken]
[Transaction]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(int id)
{
string resultMessage = null;
try
{
employeesService.Delete(id);
resultMessage = "The employee was successfully deleted.";
}
catch (Exception e)
{
resultMessage = e.Message;
}
TempData["Message"] = resultMessage;
return RedirectToAction("Index");
}
private readonly IEmployeeService employeesService;
}
}
Notice that the service is responsible to take care of the values sent by the form. The controller will only call the service’s methods and present validation and result messages.
Basically that’s it.
I’m using this approach in our project here, and it’s working fine. Sorry for taking to long to post this. But I’m working a lot, so I was a bit out of time for it.
Any advices or sugestions are very welcome.
I’m working on a project using the awesome SharpArchitecture library, and I’m amazed. I’ve never used TDD (Test Driven Development) before, and I can admit it improves the code you write, making it clean and brief.
Congratulations Billy McCafferty and all the staff working on it for this amazing library.
Here is the link to download the source code, binary files and information about it as well.
Regards, Rodrigo Leote.