ASP.NET MVC offers a great way of how to separate different application layers. View layer is responsible for data representation, the controller layer is responsible for receiving and replying to requests, and models are used as two-way information carriers between the previous two layers.
This separation of concerns is convenient for developers because there is no spaghetti mix of HTML layout and business logic. ASP.NET MVC welcomes developers to use dependency injection which leads to easier unit testing.
MVC assumes that: Model stores, View represents, Controller handles requests and returns a response. A natural question arises – Where do I put my domain specific logic? There are three ways that developers tend to go for:
- Model. In this case, the Model becomes rich and wise: it is able to request something from the data store, format it in an appropriate way and return to View. But there are disadvantages:
- View may be very large and complicated, and Model class becomes extended;
- Let Model have the method GetSomething() which requests something from a store. We can call this method in Controller. But if it is public (and often it has to be), we can call it in View and this is completely wrong because we lose the main point of MVC – separation of concerns. View should be responsible for formatting output, not for requesting it;
- Model may be used as the result of GET response and data of POST request. In POST request case we don’t need all the methods which exemplar of a Model has; we need only fields posted to a server – the information itself.
- Controller. If we put logic in Controller it becomes loaded with responsibilities it should not have – it is in charge of complex validation, business rules, helper calls, model generation, formatting and redirection. This approach also has disadvantages:
- Controller’s methods may become significantly extended. I’ve faced the problem of re-factoring GET action which was about 200 lines long and POST action which was about 150 lines long. Of course, the reason for such long actions is rather a previous developer’s lack of experience than the urgent need to have an all-in-one-action method, but still, business rules may change and then you are in trouble.
- Re-usability of code. This month we need to process text-file after POST request in MVC-application action, in a month we will need to do the same thing in WebAPI sub-system, in a year we may need to have a console application which handles a bunch of text files, and in all three cases, logic must stay the same.
- Helper classes. When talking about helpers, you need to bear in mind that they do not have a generic architecture, and usually, the amount of code located in helpers may grow out of control. It becomes really hard to find your logic scattered across dozen or more helper classes.
It is clear that three of these approaches violate basic OOP principles. Classes become bloated, objects are getting larger and larger, and when it comes down to testing, getting all the dependencies right is a pain in the neck. The purpose of this article is to address these problems.
And, please do not hesitate contacting us if you have any questions about that article
Business Service
It is reasonable to let Controller be in charge of MVC-specific things like model binding and redirection. If we do not put business logic in Controller, then we need to do that in another layer of an application and Controller should be dependent on it.
At the same time, business rules are usually dependent on data received from a store. Usual work-flow of GET-request processing: is request from store, filter using business logic, format, display.
The layer which holds business logic can be called Business Service and in this case layer hierarchy may be the following:
Each Entity (or table in database) has its individual Repository with simple methods like Get, Delete etc. Service serves a scope and holds all Repositories which are relevant to a scope. I.e., service AccountService serves scope Account and is related to entities like Account Type, Account itself, Account Role etc.
Scope Service is a data source for Business Service. Business Service is dependent on it and requests it if data are required.
Controller is dependent on Business Service. Having Business Service in place, there is no need to have long and fat Controller actions. Controller actions become thin and easily-readable:
public ActionResult Index() { if (_service.GetUserStatus(this.CurrentUser, this.CurrentMerchant) == UserStatusEnum.Admin) { return View(); } RedirectToAction("Login"); }
Business Service can be implemented with the following code sample:
public interface IBusinessServiceBase { void Register(IBaseController controller); } public abstract class BusinessServiceBase : IBusinessServiceBase { private IBaseController _controller; protected IBaseController Controller { get { return _controller; } } public void Register(IBaseController controller) { this._controller = controller; } }
To implement custom Business Service class must be derived from abstract BusinessServiceBase.
Validation
The class BusinessServiceBase is abstract and has a public method Register(IBaseController controller). The Register method is used to tie up a Business Service with a Controller. This relation is required to use built-in MVC validation functionality.
MVC Controller has a very important property ModelState of type ModelStateDictionary. This property is used to store Model validity flag and the list of errors, if they are present. I.e., during POST-request Model class, is bound to the received field list. If the property of the class is marked with data annotation attributes and it will not be valid, ModelState will contain property value validation errors.
After that validation is made, not only by annotation attributes, but also using some complicated logic which requires to request a store or compare with another Entity. In this case validation errors should be put into ModelState “manually”, obtrusively, during business logic work-flow. This requires us to have ModelState accessible for business logic. At the same time we should not have a direct reference to MVC Controller class because business logic should not be dependent on it. In this case, an interface may serve as a mediator.
Standard Controller should inherit from our custom abstract BaseController which implements IBaseController:
public interface IBaseController { ModelStateDictionary ModelState { get; } } public abstract class BaseController : Controller, IBaseController { }
After we have Base Controller we should implement Controller for our business needs, for example, to process Payment Analyzer requests. The simple code of how Payment Analyzer Controller may be bound to a Business Service may look the following way:
public class PaymentAnalyzerController : BaseController { private IPaymentAnalyzerService _service; public PaymentAnalyzerController(IPaymentAnalyzerService service) { _service = service; _service.Register(this); } [HttpGet] public ActionResult SubmitRefundToCustomer(string postGuid, string invoiceNumber) { return _service.SubmitRefundToCustomer(postGuid, invoiceNumber); } }
This inheritance allows to access and modify ModelState of a MVC Controller without the need to reference MVC Controller directly:
public class BillingSchemeService : BusinessServiceBase, IBillingSchemeService { public string SetScheme(BillingSchemeViewModel model) { if (String.IsNullOrWhiteSpace(model.BillingScheme.BillingSchemeCode)) { Controller.ModelState.AddModelError("BillingSchemeCode", Configuration.CodeMustHaveValue); } if (Controller.ModelState.IsValid) { model.BillingScheme.Save(); } return Controller.ModelState.IsValid ? null : Controller.ModelState.GetErrorMessageList().FirstOrDefault(); } }
We access it using getter Controller of type IControllerBase.
This form of referencing ModelState has the following effects:
- Using Business Services assumes they may be re-used in another application (WebAPI, WCF, Console, Windows Service etc.). If we leave this notation, each app where Business Service is used will need to reference System.Web.Mvc namespace because ModelStateDictionary is stored there. We can avoid this problem by making something like ModelState wrapper and this may entirely decouple Business Service class and ASP.NET MVC technology. But the description of how to do that is trivial and makes this article too long, so it is avoided.
- Getter Controller has name Controller, therefore each next application where Business Service is used will have to deal with Controller even if the application’s technology does not have such a notion.
Data Transfer Objects
Business Service contains business logic and returns objects processed by our rules. But do we need them to be sent directly to View? Using ASP.NET MVC View assumes we can put data annotation attributes over object properties; also we can have properties of some MVC-specific types, like System.Web.Mvc.SelectList. If we send to View an object constructed by Business Service, this may lead to the mix between business object features and representation object features, and this is not something we want.
To avoid this feature mix it may be appropriate to use DTOs instead. The sequence of how data transfer may look is described below:
In this scheme database stores, records are mapped to Entity Objects. Entity Objects are requested by appropriate Repository and sent to Business Service, where they are processed (filtered, combined, etc.) and converted to DTOs. DTOs are sent to a Controller. In Controller DTOs may be either mixed or converted to a Model and sent to a View.
Converter from DTO to a Model is specific to each View so it may be reasonable to implement it in private method/class under Controller.
There are cases when we don’t need to convert Entity Object to DTO. I.e., View must display a list of objects, and only specific few properties of object type should be displayed. Then it is worth to send Entity Object directly to a View in a list. But this simplicity may lead to unpredictable behaviour in case if our ORM is built upon Entity Framework base. Navigation fields of Entity Object are public and accessible in a View and may raise an exception if they are used somehow. On the other hand, if our ORM is built upon ADO.NET base, there is no navigation field at all, and no risk of catching an unpredictable error.
Conclusion
This article offers the way of how a developer can decouple such MVC-specific operations as model binding, re-directions, authorization, etc., and business logic using the Business Service layer. The business Service layer is bound to a Controller using custom interfaces and this binding allows to use of MVC validation functionality. Business Services are interface-referenced and interface-derived and this allows to make separate tests for business logic and request processing logic. This separation-of-concerns approach is already implemented in web-application designed for financial management our .NET developers released recently.
THANK YOU FOR VISITING US
Please use Contact Us form to connect with Diatom Enterprises developers!