2

Note: This question is indeed somewhat similar to this one, but I think I can put it in a simpler and more specific way.

I'm using Castle Windsor to intercept the URI passed to my Web API app to register the appropriate concrete class to the Controller's constructor.

What I want to be able to do is pass a "site number" on the URI, perhaps always as either the first or last arg. IOW, for site 42, instead of

http://localhost:28642/api/platypi/GetAll

...it would be:

http://localhost:28642/api/platypi/42/GetAll

-or:

http://localhost:28642/api/platypi/GetAll/42

When my Web API app first "sees"/intercepts the URI, I want to note that site number so that I can assign the desired concrete Repository to be registered by Castle Windsor. I want to be able to do this:

public class RepositoriesInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        if (siteNum == 42)
        {
            container.Register(
            Component.For<IDepartmentRepository>().ImplementedBy<DepartmentRepository42>().LifestylePerWebRequest(),
                Component.For<IInventoryItemRepository>().ImplementedBy<InventoryItemRepository42>().LifestylePerWebRequest(),
            . . .
        }
        else if (siteNum = 77)
        {
            container.Register(
        Component.For<IDepartmentRepository>().ImplementedBy<DepartmentRepository77>().LifestylePerWebRequest(),
                Component.For<IInventoryItemRepository>().ImplementedBy<InventoryItemRepository77>().LifestylePerWebRequest(),
            . . .
        }

In this way I can give site 42 its data, site 77 its data (each site uses a different database that share a common schema).

So: At which point in my Web API app's lifecycle can I hijack the URI, so as to assign the appropriate val to the global siteNum variable, so that it has been assigned before the IWindsorInstaller method runs?

UPDATE

Thanks to Mr. Slate, but if I were to do that, would this Controller code:

public DepartmentsController(IDepartmentRepository deptsRepository)
{
    if (deptsRepository == null)
    {
        throw new ArgumentNullException("deptsRepository");
    }
    _deptsRepository = deptsRepository;
}

...become:

public DepartmentsController(IDepartmentRepository deptsRepository)
{
    if (deptsRepository == null)
    {
        throw new ArgumentNullException("deptsRepository");
    }
    _deptsRepository = deptsRepository(siteNum);
}

...or???

And I'm still left with the problem of where do I intercept the incoming URI before Castle Windsor / the Controller gets it, so that I can set the appropriate value to the global / siteNum var?

Community
  • 1
  • 1
B. Clay Shannon-B. Crow Raven
  • 8,547
  • 144
  • 472
  • 862

3 Answers3

1

I'd change the constructor on DepartmentRepository() to pass the site num and use that to get the connectionstring. Then in your webconfig create a connectionstring for each site.

Doug Chamberlain
  • 11,192
  • 9
  • 51
  • 91
1

There are a number of extensions points that you can use to achieve this, personally I use this one for a similar result.

Create a custom model binder by extending IModelBinder something like this:

public class SiteManagerModelBinder : IModelBinder
    {
        #region IModelBinder Members

        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.Model != null)
            {
                throw new InvalidOperationException("Cannot update instances");
            }

            // Apply your condition to determine if site number is in Url.
            if (controllerContext.RouteData.Values['siteNum']!=null)
            {
               // probably want to resolve this from container just hard coding as example, assumption is that SiteManager, does the repository bits for you.
               return new SiteManager((int)controllerContext.RouteData.Values['siteNum']);
            }

            return null;
        }

        #endregion
    }

Okay so now we jus tneed to register our new ModelBinder:

    protected void Application_Start()
    { 
        ModelBinders.Binders.Add(typeof(SiteManager), new SiteManagerModelBinder ());

Okay and now in our controller all we do is add the SiteManager as a parameter of any Action and it will get filled in by our ModelBinder.

public class DepartmentsController: Controller {

    public ActionResult AnyAction(SiteManager siteManager, int whateverElse, ViewModel model)
    {

    }

}
shenku
  • 11,969
  • 12
  • 64
  • 118
  • Looks very promising, but is this a basic/generic MVC-only solution, or does it apply also to Web API, for which my Controller implements ApiController instead of Controller: public class DepartmentsController : ApiController – B. Clay Shannon-B. Crow Raven Jan 28 '14 at 22:36
  • Is the URI being passed / which is invoking the Web API app accessible from within Global.asax's Application_Start() handler? – B. Clay Shannon-B. Crow Raven Jan 28 '14 at 22:39
  • 1
    No, the URI is not accessible in the application_start, this is just the place where you will register application wide thigs. I am not too sure about the web API but as I understand it, it is largley the same and goes through the same pipeline. Give it a try and let me know, there are other ways I can think of to achive a solution. – shenku Jan 28 '14 at 22:59
  • 1
    http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api – shenku Jan 28 '14 at 23:13
  • Assuming I can access siteNum from siteManager, I will see if this (siteManager) is available in RepositoriesInstaller. Controller is too late, because once a particular/the specific Controller has been instantiated by the routing engine, that is the level at which/the place where the site number will have to already be known. – B. Clay Shannon-B. Crow Raven Jan 28 '14 at 23:17
  • 1
    Well there is a few ways you can do this, you could replace SiteManager with SiteRepository directly if you prefer, but it's always good to have an extra business layer between. – shenku Jan 28 '14 at 23:19
  • Where does the SiteManagerModelBinder class need to be, or does it not matter (anywhere in the project)? – B. Clay Shannon-B. Crow Raven Jan 31 '14 at 19:11
  • 1
    It doesn't matter, you just need to register it. It could even be in a different assembly. – shenku Feb 01 '14 at 22:15
1

Here is a different way you could try rather using Castle Windsor which might be better:

Create a repository factory like this:

public interface IRepositoryFactory
{
    Repository CreateRepositoryById(int id);
}

Create a component selector to select the correct repository based on the name like this:

public class RepositoryFactorySelector : DefaultTypedFactoryComponentSelector
{
    protected override string GetComponentName(MethodInfo method, object[] arguments)
    {
        return (method.Name.EndsWith("ById") && arguments.Length >= 1 && arguments[0] is int)
                   ? string.Format("Repository{0}", arguments[0]) 
                   : base.GetComponentName(method, arguments);
    }
}

Register your repositories like this:

public class RepositoryInstaller : IWindsorInstaller
{
    #region IWindsorInstaller Members

    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(Classes.FromThisAssembly().BasedOn<Repository>().Configure(c=>c.Named(c.Implementation.Name)));

        container.Register(
            Component.For<RepositoryFactorySelector, ITypedFactoryComponentSelector>().LifestyleSingleton(),
            Component.For<IRepositoryFactory>().AsFactory(c => c.SelectedWith<RepositoryFactorySelector>()));
    }

    #endregion
}

As long as Windsor is loading your controllers using a constroller factory you can now do this:

public class HomeController : Controller
{
    public IRepositoryFactory RepositoryFactory { get; set; }

    public ActionResult Index(int siteNum)
    {
        var repository = RepositoryFactory.CreateRepositoryById(siteNum);

        // tada!

        return View();
    }

}

For reference my global.asax.cs:

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);

        Container = new WindsorContainer();
        Container.AddFacility<TypedFactoryFacility>(); 

        Container.Install(FromAssembly.This()); 

        var controllerFactory = new WindsorControllerFactory(Container.Kernel);
        ControllerBuilder.Current.SetControllerFactory(controllerFactory);
    }

    public WindsorContainer Container;
}

And my repositories:

public abstract class Repository
{
}

public class Repository1 : Repository
{ 
}

public class Repository2 : Repository
{ 
}
shenku
  • 11,969
  • 12
  • 64
  • 118