views:

40

answers:

1

I'll start here with a little bit of background. We have an ASP.Net MVC web application that sits upon a structure roughly based upon the Onion Architecture concept. Therefore, we have the following (simplified) vertical structure:

  • ASP.Net MVC controller layer
  • Application service layer
  • Business Service layer

NOTE: The above is simplified because it doesn't deal with the views, repositories, domain objects, etc which aren't relevant to this question.

For a horizontal structure, we have some major areas defined by what we call "Item Types" (for the sake of simplicity, this question will deal with two sample item types: "ItemTypeA", "ItemTypeB", etc).

We have a business service interface which has a separate implementation per item type:

public interface ISampleBusinessService
{
    string SampleMethod(string arg);
}

public class ItemTypeASampleBusinessService : ISampleBusinessService
{
    public string SampleMethod(string arg)
    {
        return "Item Type A: " + arg;
    }
}

public class ItemTypeBSampleBusinessService : ISampleBusinessService
{
    public string SampleMethod(string arg)
    {
        return "Item Type B: " + arg;
    }
}

Sitting above that is an application service that uses the business service:

public interface ISampleAppService
{
    string SampleMethod(string arg);
}

public class SampleAppService
{
    private readonly ISampleBusinessService service;

    public SampleAppService(ISampleBusinessService service)
    {
        this.service = service
    }

    public string SampleMethod(string arg)
    {
        return service.SampleMethod(arg);
    }
}

And sitting above that is our controller which uses the application service:

public class SampleController : Controller
{
    private ISampelAppService service

    public SampleController(ISampleAppService service)
    {
        this.service = service;
    }

    public PartialViewResult SampleAction(string arg)
    {
        return PartialView( service.SampleMethod(arg) );
    }
}

Note that the controller, application service interface and implementation, and business service interface are all generic - they don't care about which item type is being used. However, the business service implementations are specific to the item type. We know which item type we're dealing with at the time we call the action method on the controller (via RenderAction in the views) but we aren't sure what the best way to determine which business service implementation to use. There are a few options we've considered:

  1. Base class the controller and create item type-specific controller inheritors, then something similar with the app services. This feels like a weak solution - we would end up writing a few classes that add nothing in terms of functionality except to work out which item type we're dealing with.
  2. Pass a flag down to the service layer and create the service implementation in a factory (i.e. a SampleBusinessServiceFactory which takes an "itemType" argument in its CreateInstance method). The problem with this is that we're passing a variable down several layers just so that we can decide upon an implementation. We have used this approach so far.
  3. Generics - we haven't really thought this one through but it seems that there would be some difficulties with this as well (how would you call an Action method with generics from an ActionResult call in the view?). It would be similar, in a sense to passing a flag down, but would be based upon strongly typing object/services instead of using enums/magic strings.

What approach would be best suited to solving this problem? New options would be welcomed.

Any help provided will be much appreciated.

Cheers,
Zac

+1  A: 

This smells like Big Design Up Front ( http://en.wikipedia.org/wiki/Big_Design_Up_Front )

However its very possible to invoke both controllers and action methods generically: http://stackoverflow.com/questions/848904/in-asp-net-mvc-is-it-possible-to-make-a-generic-controller

Some info about invoking actions with generic action results ( which result in the same effect ).

http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/12/12/enabling-ioc-in-asp-net-actionresults-or-a-better-actionresult.aspx

I am a big fan of this approach and learning about these techniques is helpful to anybody who wants to keep their controllers extraordinarily slim.


Comment Answer:

You don't really need to call the generic action method, you just need a way to pass the generic parameter to your controller. The basic flow is to include your modelType as a route parameter. You could easily call generic render actions with the correct RouteValueDictionary. Here is some sample code from an older project of mine:

Start of my generic controller:

    public GenericController()
    {
        TypeTranslator.Configure("Brainnom.Web.Model", "Brainnom.Web");
    }

    [UrlRoute(Path="admin/{modelType}/add", Order=9000)]
    public virtual ActionResult Add()
    {
        return View( new MODEL() );
    }

    [HttpPost]
    [UrlRoute(Path = "admin/{modelType}/add",  Order = 9000)]
    public virtual ActionResult Add( MODEL model, string modelType)
    {
        if (!ModelState.IsValid)
            return View(model);

        var postedModel = new MODEL();

        UpdateModel(postedModel);

        using (var session = new MongoSession())
        {
            session.Add(postedModel);
        }

        return RedirectToRoute("Browse", new { modelType });
    }

and my GenericControllerFactory ( which I do need to refactor someday )

using System;
using System.Web.Mvc;
using System.Web.Routing;

namespace Brainnom.Web.Controllers
{
    public class GenericControllerFactory : DefaultControllerFactory
    {
        protected override Type GetControllerType(System.Web.Routing.RequestContext requestContext, string controllerName)
        {
            //the generic type parameter doesn't matter here
            if (controllerName.EndsWith("Co"))//assuming we don't have any other generic controllers here
                return typeof(GenericController<>);

            return base.GetControllerType(requestContext, controllerName);
        }

        protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
        {
            //are we asking for the generic controller?
            if (requestContext.RouteData.Values.ContainsKey("modelType"))
            {
                string typeName = requestContext.RouteData.Values["modelType"].ToString();
                //magic time
                return GetGenericControllerInstance(typeName, requestContext);
            }

            if (!typeof(IController).IsAssignableFrom(controllerType))
                throw new ArgumentException(string.Format("Type requested is not a controller: {0}",controllerType.Name),"controllerType");

            return base.GetControllerInstance(requestContext, controllerType);
        } 

        /// <summary>
        /// Returns the a generic IController tied to the typeName requested.  
        /// Since we only have a single generic controller the type is hardcoded for now
        /// </summary>
        /// <param name="typeName"></param>
        /// <returns></returns>
        private IController GetGenericControllerInstance(string typeName, RequestContext requestContext)
        {
            var actionName = requestContext.RouteData.Values["action"];

            //try and resolve a custom view model

            Type actionModelType = Type.GetType("Brainnom.Web.Models." + typeName + actionName + "ViewModel, Brainnom.Web", false, true) ?? 
                Type.GetType("Brainnom.Web.Models." + typeName + ",Brainnom.Web", false, true);

            Type controllerType = typeof(GenericController<>).MakeGenericType(actionModelType);

            var controllerBase = Activator.CreateInstance(controllerType, new object[0] {}) as IController;

            return controllerBase;
        }
    }
}
jfar
I can see how it might appear that the design is a bit over the top but in our case it is very much justified. We're working on a platform that will serve up around 30 websites and several other server-based apps - with a shared domain layer used across them all; we needed a really scalable design. The architecture we decided upon was chosen after a lot of discussion and investigation. As for the generics in controllers, have you come across a way to call a generic action method from a RenderAction call? This is what we need to be able to do... Cheers for your reply.
Zac