Mvc – ASP.NET MVC: where to put data-entry logic which hits the database

asp.net-mvcmvcvalidation

We do simple data entry validation with attributes on the viewmodel. Think format of an email address, mandatory clientname, format of a date, the housenumber being numeric etc.

Now there are also more complex validations which will hit the database and external services: check if the id is a valid one (external service), see if the person (probably) already is present in the database based on name/address (database action), check the address/zip code combination (external service).

In my opinion this logic should not be in the controller but in a separate validation component / validation service (which gets DI'd in the controller).

I would like your opinion on this.

Best Answer

Disclaimer: The following is my approach and not holy scripture. I'm at work so I wrote this swiftly and some code may not work/compile!

Controllers

They are components of the presentation layer so they shouldn't be concerned with business or database logic. Validation logic is a subset of business logic.

Services

I implement the Service Layer pattern which is basically a bunch of classes that are loosely associated with your domain models and act upon them. These service classes do all of the business logic. This includes the database stuff. If you're not too keen on putting database logic here you can implement the Repository pattern and inject the repository classes into your service classes.

So now the flow of things is becoming clear

Controller -> Service -> Repository

and this equates to

ViewModel to Model mapping -> Validation and other business logic -> CRUD operations

Every service class is described by an interface so now you can use DI to inject them into your Controllers like so:

readonly IClientService _clientService;

public ClientsController(IClientService clientService)
{
    _clientService = clientService;
}

It's now clear exactly what dependencies your Controller has since it's all there in the constructor. The Controllers are also cleaner since all of the business logic is in the service layer. Here's an insert example:

[HttpPost]
public ActionResult Insert(ClientViewModel viewModel)
{
    if (!ModelState.IsValid)
        return View(viewModel);

    var client = new Client();
    viewModel.MapTo(client);
    _clientService.Insert(client);

    return View(viewModel);
}

Now the validation part

The type of validation that constitutes checking the database is what I call deep validation. I put simple (shallow) validation in the ViewModel (like yourself) and deep validation in the business layer aka services.

So the deep validation that needs to take place when you insert a Client is found in the Insert method of the ClientService. All the database checks are done there, maybe by calling a repository or just directly in the service if you're not using repositories.

So if the client is invalid how do we return the validation errors back to the Controller?

We throw an exception since that's what exceptions are for. But it's going to be our own custom exception:

public class MyException : Exception
{
    public MyException()
    {
    }

    public MyException(string message)
        : base(message)
    {
    }

    public MyException(string message, Exception inner)
        : base(message, inner)
    {
    }
}

So when things are invalid (in our ClientService):

public void Insert(Client client)
{
    if (client == null)
    {
        throw new MyException("This thing's not right..");
    }

We've made sure the insert failed at the soonest possible moment, which is good.

Now all that's left is to catch these custom exceptions in our Controller. You can do this many ways. One is to use try/catch clauses:

try
{
    _clientService.Insert(client);
}
catch (MyException e)
{
    // Add error to ModelState perhaps
}

but they will thoroughly litter your Controller.

The proper MVC approach is to catch all the errors in the OnException method of your Controller:

protected override void OnException(ExceptionContext exceptionContext)
{
    exceptionContext.ExceptionHandled = true;

    if (exceptionContext.Exception is MyException)
    {
        // Perhaps add to ModelState
    }
    else
    {
        // Add a generic error message to ModelState and log error
    }

    filterContext.Result = new ViewResult
    {
        ViewName = ...
    };
}

You can put this code in a BaseController from which all of your other Controllers will inherit in order to avoid code duplication.


That's the basic framework I work in. I hope I didn't make things too complicated or confusing.