Depending on the application sometimes we have to maintain some shared service throughout our application. Let’s say you are developing a multi-blog supported blog engine where both the controller and view must know the currently visiting blog, it’s setting , user information and url generation service. In this post, I will show you how you can handle this kind of case in most convenient way. First, let see the most basic way, we can create our PostController in the following way: public class PostController : Controller
{
public PostController(dependencies...) { }
public ActionResult Index(string blogName, int? page)
{
BlogInfo blog = blogSerivce.FindByName(blogName);
if (blog == null)
{
return new NotFoundResult();
}
IEnumerable<PostInfo> posts = postService.FindPublished(blog.Id, PagingCalculator.StartIndex(page, blog.PostPerPage), blog.PostPerPage);
int count = postService.GetPublishedCount(blog.Id);
UserInfo user = null;
if (HttpContext.User.Identity.IsAuthenticated)
{
user = userService.FindByName(HttpContext.User.Identity.Name);
}
return View(new IndexViewModel(urlResolver, user, blog, posts, count, page));
}
public ActionResult Archive(string blogName, int? page, ArchiveDate archiveDate)
{
BlogInfo blog = blogSerivce.FindByName(blogName);
if (blog == null)
{
return new NotFoundResult();
}
IEnumerable<PostInfo> posts = postService.FindArchived(blog.Id, archiveDate, PagingCalculator.StartIndex(page, blog.PostPerPage), blog.PostPerPage);
int count = postService.GetArchivedCount(blog.Id, archiveDate);
UserInfo user = null;
if (HttpContext.User.Identity.IsAuthenticated)
{
user = userService.FindByName(HttpContext.User.Identity.Name);
}
return View(new ArchiveViewModel(urlResolver, user, blog, posts, count, page, achiveDate));
}
public ActionResult Tag(string blogName, string tagSlug, int? page)
{
BlogInfo blog = blogSerivce.FindByName(blogName);
if (blog == null)
{
return new NotFoundResult();
}
TagInfo tag = tagService.FindBySlug(blog.Id, tagSlug);
if (tag == null)
{
return new NotFoundResult();
}
IEnumerable<PostInfo> posts = postService.FindPublishedByTag(blog.Id, tag.Id, PagingCalculator.StartIndex(page, blog.PostPerPage), blog.PostPerPage);
int count = postService.GetPublishedCountByTag(tag.Id);
UserInfo user = null;
if (HttpContext.User.Identity.IsAuthenticated)
{
user = userService.FindByName(HttpContext.User.Identity.Name);
}
return View(new TagViewModel(urlResolver, user, blog, posts, count, page, tag));
}
}
As you can see the above code heavily depends upon the current blog and the blog retrieval code is duplicated in all of the action methods, once the blog is retrieved the same blog is passed in the view model. Other than the blog the view also needs the current user and url resolver to render it properly. One way to remove the duplicate blog retrieval code is to create a custom model binder which converts the blog from a blog name and use the blog a parameter in the action methods instead of the string blog name, but it only helps the first half in the above scenario, the action methods still have to pass the blog, user and url resolver etc in the view model.
Now lets try to improve the the above code, first lets create a new class which would contain the shared services, lets name it as BlogContext:
public class BlogContext
{
public BlogInfo Blog { get; set; }
public UserInfo User { get; set; }
public IUrlResolver UrlResolver { get; set; }
}
Next, we will create an interface, IContextAwareService:
public interface IContextAwareService
{
BlogContext Context { get; set; }
}
The idea is, whoever needs these shared services needs to implement this interface, in our case both the controller and the view model, now we will create an action filter which will be responsible for populating the context:
public class PopulateBlogContextAttribute : FilterAttribute, IActionFilter
{
private static string blogNameRouteParameter = "blogName";
private readonly IBlogService blogService;
private readonly IUserService userService;
private readonly BlogContext context;
public PopulateBlogContextAttribute(IBlogService blogService, IUserService userService, IUrlResolver urlResolver)
{
Invariant.IsNotNull(blogService, "blogService");
Invariant.IsNotNull(userService, "userService");
Invariant.IsNotNull(urlResolver, "urlResolver");
this.blogService = blogService;
this.userService = userService;
context = new BlogContext { UrlResolver = urlResolver };
}
public static string BlogNameRouteParameter
{
[DebuggerStepThrough]
get { return blogNameRouteParameter; }
[DebuggerStepThrough]
set { blogNameRouteParameter = value; }
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
string blogName = (string) filterContext.Controller.ValueProvider.GetValue(BlogNameRouteParameter).ConvertTo(typeof(string), Culture.Current);
if (!string.IsNullOrWhiteSpace(blogName))
{
context.Blog = blogService.FindByName(blogName);
}
if (context.Blog == null)
{
filterContext.Result = new NotFoundResult();
return;
}
if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
context.User = userService.FindByName(filterContext.HttpContext.User.Identity.Name);
}
IContextAwareService controller = filterContext.Controller as IContextAwareService;
if (controller != null)
{
controller.Context = context;
}
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
Invariant.IsNotNull(filterContext, "filterContext");
if ((filterContext.Exception == null) || filterContext.ExceptionHandled)
{
IContextAwareService model = filterContext.Controller.ViewData.Model as IContextAwareService;
if (model != null)
{
model.Context = context;
}
}
}
}
As you can see we are populating the context in the OnActionExecuting, which executes just before the controllers action methods executes, so by the time our action methods executes the context is already populated, next we are are assigning the same context in the view model in OnActionExecuted method which executes just after we set the model and return the view in our action methods.
Now, lets change the view models so that it implements this interface:
public class IndexViewModel : IContextAwareService
{
// More Codes
}
public class ArchiveViewModel : IContextAwareService
{
// More Codes
}
public class TagViewModel : IContextAwareService
{
// More Codes
}
and the controller:
public class PostController : Controller, IContextAwareService
{
public PostController(dependencies...) { }
public BlogContext Context
{
get;
set;
}
public ActionResult Index(int? page)
{
IEnumerable<PostInfo> posts = postService.FindPublished(Context.Blog.Id, PagingCalculator.StartIndex(page, Context.Blog.PostPerPage), Context.Blog.PostPerPage);
int count = postService.GetPublishedCount(Context.Blog.Id);
return View(new IndexViewModel(posts, count, page));
}
public ActionResult Archive(int? page, ArchiveDate archiveDate)
{
IEnumerable<PostInfo> posts = postService.FindArchived(Context.Blog.Id, archiveDate, PagingCalculator.StartIndex(page, Context.Blog.PostPerPage), Context.Blog.PostPerPage);
int count = postService.GetArchivedCount(Context.Blog.Id, archiveDate);
return View(new ArchiveViewModel(posts, count, page, achiveDate));
}
public ActionResult Tag(string blogName, string tagSlug, int? page)
{
TagInfo tag = tagService.FindBySlug(Context.Blog.Id, tagSlug);
if (tag == null)
{
return new NotFoundResult();
}
IEnumerable<PostInfo> posts = postService.FindPublishedByTag(Context.Blog.Id, tag.Id, PagingCalculator.StartIndex(page, Context.Blog.PostPerPage), Context.Blog.PostPerPage);
int count = postService.GetPublishedCountByTag(tag.Id);
return View(new TagViewModel(posts, count, page, tag));
}
}
Now, the last thing where we have to glue everything, I will be using the AspNetMvcExtensibility to register the action filter (as there is no better way to inject the dependencies in action filters).
public class RegisterFilters : RegisterFiltersBase
{
private static readonly Type controllerType = typeof(Controller);
private static readonly Type contextAwareType = typeof(IContextAwareService);
protected override void Register(IFilterRegistry registry)
{
TypeCatalog controllers = new TypeCatalogBuilder()
.Add(GetType().Assembly)
.Include(type => controllerType.IsAssignableFrom(type) && contextAwareType.IsAssignableFrom(type));
registry.Register<PopulateBlogContextAttribute>(controllers);
}
}
Thoughts and Comments?