I'd really like to improve the current readability of my code by reducing the amount of null checks I'm doing. I get the feeling that although checking for certain conditions is a good thing, repeating those checks in various places isn't such a good idea. As an example, here's one method of my PostsController
:
public ActionResult ShowPost(int PostID, string slug)
{
PostViewModel viewModel = new PostViewModel();
Post model = postRepository.FindPost(PostID, filterByPublished: true);
if (model.PostID == 0)
return Redirect(Url.Home());
else if (model.Slug != slug)
return RedirectPermanent(Url.ShowPost(model.PostID, model.Slug));
postRepository.PostVisited(model);
Mapper.Map(model, viewModel);
return View(viewModel);
}
What I don't like
Firstly, the check to see if PostID
is 0. It can be 0 because of the way I set that method up in the repository:
public Post FindPost(int id, bool filterByPublished = false)
{
var query = db.Posts.Where(post => post.PostID == id);
if (filterByPublished)
query = query.Where(post => post.IsPublished == filterByPublished);
return query.Select(post => post).SingleOrDefault() ?? new Post { PostID = 0 };
}
I don't like that I've pushed that little hack in there just to cater for handling a null model. I also check for a null model in various strongly-typed views which require a PostViewModel
.
Possible solution
My first thought would be to create an action filter, override OnResultExecuting
and check for null models there. I actually quite like this idea and it does solve the problems of checking for null models in the controller and also the view. However, what it doesn't do is cater for situations like this:
else if (model.Slug != slug)
That'll give me a null reference exception as would passing the model to the postRepository
to update the view count. I'm guessing the call to Mapper.Map
would do the same.
So what can do I about it? Could I simply override OnActionExecuted
and check for exceptions there, logging them and then redirecting to a custom error view? Does that sound like a reasonable approach?
Thank you.
OK, let's try putting this controller on a diet.
We start by fixing your repository and leaving it return null instead of some default post with ID = 0 which was kind of weird as you noticed:
public Post FindPost(int id, bool filterByPublished = false)
{
var query = db.Posts.Where(post => post.PostID == id);
if (filterByPublished)
query = query.Where(post => post.IsPublished == filterByPublished);
return query.Select(post => post).SingleOrDefault();
}
then we could write a custom model binder for the Post
model:
public class PostModelBinder : IModelBinder
{
private readonly IPostsRepository _repository;
public PostModelBinder(IPostsRepository repository)
{
_repository = repository;
}
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var postIdValue = controllerContext.Controller.ValueProvider.GetValue("postid");
int postId;
if (postIdValue == null || !int.TryParse(postIdValue.AttemptedValue, out postId))
{
return null;
}
return _repository.FindPost(postId, true);
}
}
which could be associated with the Post
type in Application_Start
:
var postsRepository = DependencyResolver.Current.GetService<IPostsRepository>();
ModelBinders.Binders.Add(typeof(Post), new PostModelBinder(postsRepository));
and then a custom action filter that will take care to short-circuit the action in case the post is null or the post slug is different than the one passed as parameter:
public class PostActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var postParameter = filterContext
.ActionDescriptor
.GetParameters()
.Where(p => p.ParameterType == typeof(Post))
.FirstOrDefault();
if (postParameter == null)
{
return;
}
var post = (Post)filterContext.ActionParameters[postParameter.ParameterName];
if (post == null)
{
filterContext.Result = new RedirectResult(Url.Home());
}
var slug = filterContext.Controller.ValueProvider.GetValue("slug");
if (slug != null && post.Slug != slug.AttemptedValue)
{
filterContext.Result = new RedirectResult(
Url.ShowPost(post.PostID, post.Slug),
true
);
}
}
}
If you are using ASP.NET MVC 3 this custom attribute could be registered as a global one in Global.asax
:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new PostActionFilterAttribute());
...
}
and finally your controller action will become:
public ActionResult ShowPost(Post post)
{
postRepository.PostVisited(post);
var viewModel = Mapper.Map<Post, PostViewModel>(post);
return View(viewModel);
}
or even bringing this a step further and introducing a custom mapper action filter:
[AutoMap(typeof(Post), typeof(PsotViewModel))]
public ActionResult ShowPost(Post post)
{
postRepository.PostVisited(post);
return View(post);
}
The AutoMap
action filter is pretty easy to implement. You would subscribe to the OnActionExecuted
method and check whether the returned result from the controller action was a ViewResultBase
and then extract the model from it, pass it to AutoMapper given the 2 types and substitute the model with the view model.