2

In trying to reduce the size of my MVC controllers, I am refactoring a lot of logic out into services (though this same issue would apply if it was being factored into models). I'm often finding that I was directly setting ViewData and/or TempData with information I wanted to be displayed to the user, like:

var repoUser = new UserRepository();
var foundUser = repoUser.GetUser(userId);
if (foundUser == null) {
    ViewData["ErrorMessage"] = "Could not find user with ID {0}.".FormatWith(userId);
    return View("UsersRegister");
}

Of course, as soon as you move into a service class, you lose direct access to ViewData, TempData, and methods like View() or RedirectToAction(), so I'm trying to figure out the best practice for dealing with it. Two solutions come to mind:

  1. Create a class containing various pieces of information about what the controller should do, to be passed back from the services, like:

    public class ResponseInfo {
        public string Name { get; set; }
        public bool DoRedirect { get; set; }
        public object RedirectRouteValues { get; set; }
        public string InfoMessage { get; set; }
        public string ErrorMessage { get; set; }
    }
    
  2. Try and allow the service methods to have direct access to things like ViewData and TempData, eg.:

    public ActionResult ToggleUserAttended(int eventId, int userId, HttpRequestBase request, TempDataDictionary tempData, ViewDataDictionary viewData, Func<string, object, ActionResult> redirectAction) {
        //...
        var repoUser = new UserRepository();
        var foundUser = repoUser.GetUser(userId);
        if (foundUser == null) {
            tempData["ErrorMessage"] = "Could not find user with ID {0}.".FormatWith(userId);
            return redirectAction("UsersRegister", new { id = eventId, returnUrl = request.QueryString["returnUrl"] });
        }
        //...
    }
    

    ... and then in the controller:

    return _svcEvent.ToggleUserAttended(123, 234, Request, TempData, ViewData, (name, routeVals) => RedirectToAction(name, routeVals));
    

For number 1, the controller would have more logic to do in looking at the ResponseInfo object and determining whether to redirect, display a view, plugging the error or info message into TempData or ViewData, etc. Number 2 would pretty much allow a one-liner controller, but you're making the service very aware of controller-specific stuff. However, the service is always going to be closely tied in with the controller anyway so is this a problem? Would 1 or 2 be best practice, or something else I haven't listed?

Jez
  • 27,951
  • 32
  • 136
  • 233
  • 1
    GOD NO. Move that sort of thing into `ActionFilter`s... Just don't, please don't allow your services access to front-end concepts like `ViewData` and `TempData`.. – Simon Whitehead Mar 19 '13 at 10:50
  • @SimonWhitehead So are you basically saying to leave this stuff in the controller? What about the error messages themselves? A fuller answer would be appreciated. – Jez Mar 19 '13 at 11:00
  • Is there a reason you haven't considered using the `ModelState` for displaying these errors? You can have your UI layer map between service layer error lists and a `ModelStateDictionary`. This decouples your Service from your UI. You could even write your own `ActionResult` so that it returns it all nicely and brings your actions down significantly in size.. I'm not near a PC right now to type up an example. – Simon Whitehead Mar 19 '13 at 11:34
  • I've never heard of `ModelState` and its [documentation](http://msdn.microsoft.com/en-us/library/dd460334%28v=vs.108%29.aspx) seems to be woefully inadequate. – Jez Mar 19 '13 at 11:37
  • Here: http://msdn.microsoft.com/en-au/library/system.web.mvc.modelstatedictionary.addmodelerror(v=vs.90).aspx. Have a read of that.. see what you think. If you provide an empty key to the `ModelStateDictionary`, it won't be bound to a specific entity property.. but will still show up in a `ValidationSummary`. – Simon Whitehead Mar 19 '13 at 11:39
  • Still not much of a general description of the use of `ModelState`. – Jez Mar 19 '13 at 11:56
  • One problem with `ModelState` is that it seems to pertain to the state of the model being used by the 'parent' view rendered. I'm using several forms in the same view, and rendering them using partials so that I can pass a different model for that form. So errors for those mini-forms would presumably not be appropriate for the `ModelState` for the view containing the partials. – Jez Mar 19 '13 at 11:58

1 Answers1

1

Maybe I misunderstood something, but I find it strange that you want to provide information related to presentation concerns to the services. IMO, it is a very bad idea to do that as services are business logic and should not depend on presentation layer.

I don't really like neither of approaches you suggested as it seems to me that they blur the line between presentation and services.

As an example, a service should throw if it can't find the user, and the controller should then handle this error with appropriate UI mechanism: an error message, HTTP error or whatever fits your app. Similarly, how can the service know where to redirect? It is aware of GUI? How do you plan to use it in other contexts (API, non-web GUI, whatever)?

What I usually do is create a command/dto based on form/parameters and provide that to service. Then any service client can use the same command. If I need to show data, I ask services/repos for whatever I need, and map that to presentation form (if needed) and put that into ViewData/TempData/Model/Whatever.

Here are some examples (treat it as pseudo-code):

[HttpPost]
public ActionResult ChangeUserInfo(UserInfoModel model)
{
  var cmd = new ChangeUserInfoCommand(CurrentUserId, model.FirstName, model.LastName);
  try {
      userSvc.UpdateUser(cmd); 
      return RedirectToAction(...);
  }
  catch(XxxxException e) { // e.g. something wrong (business logic)
      TempData["Error"] = "an error occurred: " + e.FriendlyError();
      return RedirectToAction(...); // error action
  }
  catch(SecurityException e) {
      throw new HttpException(404, "NotFound");
  }
}

public ActionResult ShowUsers()
{
  var userInfo = svc.GetUsers().Select(u => { Id = u.id, FullName = u.First + " " + u.Last);
  // or var userInfo = svc.GetUsers().Select(u => Mapper.To<UserDTO>(u));
  return View(userInfo);

  // or without model
  // ViewData["users"] = userInfo;
  // return View();
}

Please note that this is an illustration - I usually do it quite differently, but I have a lot of other infrastructure in place (e.g. I don't explicitly handle exceptions like this, my services sometimes have only one method Execute(IEnumerable<Command> cmds), I have nice support for anonymous classes as view models, etc)

Zdeslav Vojkovic
  • 14,391
  • 32
  • 45
  • Could you give an example of the command/dto based on form/parameters? – Jez Mar 19 '13 at 10:59
  • sure, just keep in mind that this is an illustration - I usually do it quite differently, but I have a lot of other infrastructure in place – Zdeslav Vojkovic Mar 19 '13 at 11:23
  • Hmm, so you're relying 100% on exceptions to convey that an error occurred in the service? That doesn't seem to be great practice either; some errors are common enough that they are predictable enough not to need an exception. – Jez Mar 19 '13 at 11:27
  • not sure I understood the last comment. If not having an exception, how do you denote an error? with an error code? This "predictable" sounds suspiciously like expected event and not really an error. – Zdeslav Vojkovic Mar 19 '13 at 11:30
  • I usually return a `bool` to indicate error and take an `out string errorMsg` param. – Jez Mar 19 '13 at 11:31
  • I believe that it is a very bad idea, as well. Some more details in this answer: http://stackoverflow.com/questions/15296993/error-communication-and-recovery-approaches-in-net/15298594#15298594 . These guys seem to agree: http://msdn.microsoft.com/en-us/library/vstudio/ms229014%28v=vs.100%29.aspx – Zdeslav Vojkovic Mar 19 '13 at 11:36
  • @ZdeslavVojkovic This is normally acceptable if you are double-validating the same business rules as the front end data-annotations did. If the front-end validation misses something and the service layer finds that it is invalid then that __is__ exceptional. I wouldn't say a user not being existant is all that exceptional though.. – Simon Whitehead Mar 19 '13 at 11:37
  • @SimonWhitehead, well *current user* not being existent is exceptional, IMO :) As service doesn't know about current users, it treats them all the same, thus it throws. Besides, using any normal application path, you shouldn't be able to update the non existing user (except in case of concurrent deletion of the user, where I also like to have an exception). Additionally, I rarely handle ValidationException, SecurityException and similar common errors explicitly like this - this is done by filters/interceptors/whatever. Besides, what if you forget to check the return value? – Zdeslav Vojkovic Mar 19 '13 at 11:47