ASP.NET MVC: Moving code from controller action to service layer
- by DigiMortal
I fixed one controller action in my application that doesn’t seemed good enough for me. It wasn’t big move I did but worth to show to beginners how nice code you can write when using correct layering in your application. As an example I use code from my posting ASP.NET MVC: How to implement invitation codes support. Problematic controller action Although my controller action works well I don’t like how it looks. It is too much for controller action in my opinion. [HttpPost] public ActionResult GetAccess(string accessCode) { if(string.IsNullOrEmpty(accessCode.Trim())) { ModelState.AddModelError("accessCode", "Insert invitation code!"); return View(); } Guid accessGuid; try { accessGuid = Guid.Parse(accessCode); } catch { ModelState.AddModelError("accessCode", "Incorrect format of invitation code!"); return View(); } using(var ctx = new EventsEntities()) { var user = ctx.GetNewUserByAccessCode(accessGuid); if(user == null) { ModelState.AddModelError("accessCode", "Cannot find account with given invitation code!"); return View(); } user.UserToken = User.Identity.GetUserToken(); ctx.SaveChanges(); } Session["UserId"] = accessGuid; return Redirect("~/admin"); } Looking at this code my first idea is that all this access code stuff must be located somewhere else. We have working functionality in wrong place and we should do something about it. Service layer I add layers to my application very carefully because I don’t like to use hand grenade to kill a fly. When I see real need for some layer and it doesn’t add too much complexity I will add new layer. Right now it is good time to add service layer to my small application. After that it is time to move code to service layer and inject service class to controller. public interface IUserService { bool ClaimAccessCode(string accessCode, string userToken, out string errorMessage); // Other methods of user service } I need this interface when writing unit tests because I need fake service that doesn’t communicate with database and other external sources. public class UserService : IUserService { private readonly IDataContext _context; public UserService(IDataContext context) { _context = context; } public bool ClaimAccessCode(string accessCode, string userToken, out string errorMessage) { if (string.IsNullOrEmpty(accessCode.Trim())) { errorMessage = "Insert invitation code!"; return false; } Guid accessGuid; if (!Guid.TryParse(accessCode, out accessGuid)) { errorMessage = "Incorrect format of invitation code!"; return false; } var user = _context.GetNewUserByAccessCode(accessGuid); if (user == null) { errorMessage = "Cannot find account with given invitation code!"; return false; } user.UserToken = userToken; _context.SaveChanges(); errorMessage = string.Empty; return true; } } Right now I used simple solution for errors and made access code claiming method to follow usual TrySomething() methods pattern. This way I can keep error messages and their retrieval away from controller and in controller I just mediate error message from service to view. Controller Now all the code is moved to service layer and we need also some modifications to controller code so it makes use of users service. I don’t show here DI/IoC details about how to give service instance to controller. GetAccess() action of controller looks like this right now. [HttpPost] public ActionResult GetAccess(string accessCode) { var userToken = User.Identity.GetUserToken(); string errorMessage; if (!_userService.ClaimAccessCode(accessCode, userToken, out errorMessage)) { ModelState.AddModelError("accessCode", errorMessage); return View(); } Session["UserId"] = Guid.Parse(accessCode); return Redirect("~/admin"); } It’s short and nice now and it deals with web site part of access code claiming. In the case of error user is shown access code claiming view with error message that ClaimAccessCode() method returns as output parameter. If everything goes fine then access code is reserved for current user and user is authenticated. Conclusion When controller action grows big you have to move code to layers it actually belongs. In this posting I showed you how I moved access code claiming functionality from controller action to user service class that belongs to service layer of my application. As the result I have controller action that coordinates the user interaction when going through access code claiming process. Controller communicates with service layer and gets information about how access code claiming succeeded.