Thursday, March 18, 2010 10:01 PM Kazi Manzur Rashid

Maintaining shared service in ASP.NET MVC Application

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?

Shout it
Filed under: , , , ,

Comments

# re: Maintaining shared service in ASP.NET MVC Application

Thursday, March 18, 2010 2:57 PM by Paul

Very cool, thanks for sharing.

One quick question, though.  Your context is a "hard" dependency, by which I mean that your methods internally assume that it's there, and assume that it has non-null values (e.g. for the Context.Blog.Id calls).  Is there a reason that you didn't just decide to go w/ constructor injection of the dependency to make sure that invariant was met?  I like the AOP-ness of attributes, but given that you'll throw w/o a statisfied Blog property on the Context, it seems like it's more appropriate to require that in the constructor.  Or, for that matter, directly require the bits you need (i.e. the blog itself) to avoid the whole "feature envy" code smell.

# re: Maintaining shared service in ASP.NET MVC Application

Thursday, March 18, 2010 4:30 PM by Kazi Manzur Rashid

@Paul : The Controller methods will never get executed when Blog is null, check the line 40-43 of PopulateBlogContextAttribute, it is simply setting the ActionResult to a 404.

Apart from that the whole idea was to minimize the duplicate codes, getting the service in each method call and passing it back to the viewmodel. If ctor injection is used by some intelleigent IoC container you still have to pass it back the view model.

# re: Maintaining shared service in ASP.NET MVC Application

Friday, March 19, 2010 3:28 PM by Kevin Jensen

cool post.  Do you have a sample download app?

# re: Maintaining shared service in ASP.NET MVC Application

Wednesday, March 24, 2010 1:10 PM by brom38

You can inject dependencies in ActionFilterAttributes using ControllerActionInvoker.

   public class UnityControllerActionInvoker : ControllerActionInvoker

   {

       private IUnityContainer UnityContainer { get; set; }

       public UnityControllerActionInvoker(IUnityContainer unityContainer)

       {

           UnityContainer = unityContainer;

       }

       protected override ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, System.Collections.Generic.IList<IActionFilter> filters, ActionDescriptor actionDescriptor, System.Collections.Generic.IDictionary<string, object> parameters)

       {

           foreach (var filter in filters)

           {

               var sessionAttribute = filter as SessionAttribute;

               if (sessionAttribute != null)

               {

                   UnityContainer.BuildUp(sessionAttribute);

               }

           }

           return base.InvokeActionMethodWithFilters(controllerContext, filters, actionDescriptor, parameters);

       }

   }

And assign ActionInvoker in you ControllerFactory:

       protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)

       {

           if (controllerType != null)

           {

               var controller = (Controller) Container.Resolve(controllerType);

               controller.ActionInvoker = Container.Resolve<UnityControllerActionInvoker>();

               return controller;

           }

           return null;

       }

# re: Maintaining shared service in ASP.NET MVC Application

Wednesday, March 24, 2010 2:21 PM by Kazi Manzur Rashid

@brom38: Pls check the AspNetMvcExtensibility codebase, it shows how you can write container independent code, yes that also includes filter dependency injection as well as fluent filter registration.