How do I get Web API / Castle Windsor to recognize

2020-07-24 03:41发布

问题:

I found out why the placeholder "Home Controller" was getting called here

I commented out the reference to HomeController in RouteConfig.cs and added a Controller that I want it to call instead:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        //If get 404 error re: favicon, see http://docs.castleproject.org/(X(1)S(vv4c5o450lhlzb45p5wzrq45))/Windsor.Windsor-tutorial-part-two-plugging-Windsor-in.ashx or just uncomment the line below:
        //routes.IgnoreRoute("{*favicon}", new { favicon = @"(.*/)?favicon.ico(/.*)?" });

        //routes.MapRoute(
        //    name: "Default",
        //    url: "{controller}/{action}/{id}",
        //    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        //);

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "DepartmentsController", action = "GetCountOfDepartmentRecords", id = UrlParameter.Optional }
        );
    }
}

...but that gives me a null here in WindsorControllerFactory:

protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
    if (controllerType == null)
    {
        throw new HttpException(404, string.Format("The controller for path '{0}' could not be found.", requestContext.HttpContext.Request.Path));
    }
    return (IController)_kernel.Resolve(controllerType);
}

...even though GetCountOfDepartmentRecords() does exist in DepartmentsController:

public int GetCountOfDepartmentRecords()
{
    return _deptsRepository.Get();
}

So what am I missing as far as setup or configuration or ... ???

Note: I also tried the RouteConfig entry sans the "Controller" verbiage:

defaults: new { controller = "Departments", action = "GetCountOfDepartmentRecords", id = UrlParameter.Optional }

...but it makes no difference.

UPDATE

This is what I have in WindsorDependencyResolver:

public class ApiControllersInstaller : IWindsorInstaller
{
    public void Install(Castle.Windsor.IWindsorContainer container, Castle.MicroKernel.SubSystems.Configuration.IConfigurationStore store)
    {
        container.Register(Classes.FromThisAssembly()
         .BasedOn<ApiController>()
         .LifestylePerWebRequest());
    }
}

...am I doing something wrong?

UPDATE 2

Perhaps I have too many classes that implement IWindsorInstaller in my project?

&& ||, relatedly, I'm calling container.Register() too many times/in too many places?

I've got calls to container.Register() in three spots:

0) WindsorDependencyResolver.cs:

public class ApiControllersInstaller : IWindsorInstaller
{
    public void Install(Castle.Windsor.IWindsorContainer container, Castle.MicroKernel.SubSystems.Configuration.IConfigurationStore store)
    {
        container.Register(Classes.FromThisAssembly()
         .BasedOn<ApiController>()
         .LifestylePerWebRequest());
    }
}

1) SomethingProviderInstaller.cs:

public class SomethingProviderInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(Classes.FromThisAssembly()
                               .BasedOn(typeof(ISomethingProvider))
                               .WithServiceAllInterfaces());
        container.AddFacility<TypedFactoryFacility>();
        container.Register(Component.For<IMyFirstFactory>().AsFactory()); 
    }
}

2) RepositoriesInstaller

public class RepositoriesInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(
                           Types.FromThisAssembly()
                                   .Where(type => type.Name.EndsWith("Repository"))
                                   .WithService.DefaultInterfaces()
                                   .Configure(c => c.LifestylePerWebRequest()));
    }
}

Is this "too much of a good thing"?

UPDATE 3

My take on it is that this:

public class ApiControllersInstaller : IWindsorInstaller
{
    public void Install(Castle.Windsor.IWindsorContainer container, Castle.MicroKernel.SubSystems.Configuration.IConfigurationStore store)
    {
        container.Register(Classes.FromThisAssembly()
         .BasedOn<ApiController>()
         .LifestylePerWebRequest());
    }
}

...should register everything that implements the ApiController interface; so, DepartmentsController should be installed.

public class DepartmentsController : ApiController

(as should all of the other Controllers, as they all implement the ApiController interface).

While this one:

public void Install(IWindsorContainer container, IConfigurationStore store)
{
    container.Register(Classes.FromThisAssembly()
                           .BasedOn(typeof(ISomethingProvider))
                           .WithServiceAllInterfaces());
    container.AddFacility<TypedFactoryFacility>();
    container.Register(Component.For<IMyFirstFactory>().AsFactory()); 
}

...would only register the single class that implements ISomethingProvider.

And finally this one:

container.Register( Types.FromThisAssembly() .Where(type => type.Name.EndsWith("Controllers")) .WithService.DefaultInterfaces() .Configure(c => c.LifestylePerWebRequest()));

...would register everything named Controller.

Note: I replaced:

.Where(type => type.Name.EndsWith("Repository"))

...with what it is above (Repository).

I put breakpoints in them all, and they were all hit following were hit, in reverse order from how they are shown above. So I don't know if only one call to container.Register() is allowed, or why I'm getting a null Controller...

UPDATE 4

Based on the example here by Summit Dishpanhands, I commented out the code I had in RepositoriesInstaller and replaced it with this:

container.Register(
  Component.For<IDepartmentRepository>().ImplementedBy<DepartmentRepository>()
);

...still no joy in Mudville, though...

UPDATE 5

Using Summit's code here [Dependency Injection in WebAPI with Castle Windsor, I changed the code in Global.asax.cs from:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    GlobalConfiguration.Configure(WebApiConfig.Register);
    BootstrapContainer();
}

private static void BootstrapContainer()
{
    _container = new WindsorContainer().Install(FromAssembly.This());
    var controllerFactory = new WindsorControllerFactory(_container.Kernel);

    ControllerBuilder.Current.SetControllerFactory(controllerFactory);

    GlobalConfiguration.Configuration.Services.Replace(
        typeof(IHttpControllerActivator), new WindsorCompositionRoot(_container));
}

...to this:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

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

    ConfigureWindsor(GlobalConfiguration.Configuration);
}

public static void ConfigureWindsor(HttpConfiguration configuration)
{
    _container = new WindsorContainer();
    _container.Install(FromAssembly.This());
    _container.Kernel.Resolver.AddSubResolver(new CollectionResolver(_container.Kernel, true));
    var dependencyResolver = new WindsorDependencyResolver(_container);
    configuration.DependencyResolver = dependencyResolver;
}   

...and I seem to have gotten further. Now, instead of a runtime exception, I get a browsetime exception, namely HTTP Error 403.14 - Forbidden:

Do I really need to mess with ISS, or is there something else that would solve this conundrum?

UPDATE 6

In response to Kiran Challa:

When I change it to MapHttpRoute like so:

routes.MapHttpRoute(
    name: "Departments",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Departments", action = "GetCountOfDepartmentRecords", id = UrlParameter.Optional }
);

...I get the compile-time error "'System.Web.Routing.RouteCollection' does not contain a definition for 'MapHttpRoute' and no extension method 'MapHttpRoute' accepting a first argument of type 'System.Web.Routing.RouteCollection' could be found..."

UPDATE 7

Thanks for your help, Kiran.

I do have a WebApiConfig.cs, and it actually already has all this:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApiWithParameters",
            routeTemplate: "api/{controller}/{ID}/{CountToFetch}"
            //defaults: new { ID = RouteParameter.Optional, CountToFetch = RouteParameter.Optional }
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApiWith3Parameters",
            routeTemplate: "api/{controller}/{ID}/{packSize}/{CountToFetch}"
            //defaults: new { ID = RouteParameter.Optional, packSize = RouteParameter.Optional, CountToFetch = RouteParameter.Optional }
        );

    }
}

...so I guess I should just comment out the stuff in RouteConfig.cs; it seems I've got too many files; it's as if my closet is chockfull of flammables.

UPDATE 8

For Adam Connelly:

"I'm assuming you mean that controllerType is null."

Yes.

"Where you currently have something like the following:

var controllerFactory = new WindsorControllerFactory(container.Kernel); ControllerBuilder.Current.SetControllerFactory(controllerFactory); You'll need to do something like this as well (or instead if your project only uses WebApi):

GlobalConfiguration.Configuration.Services.Replace( typeof (IHttpControllerActivator), new WindsorControllerActivator(container));"

Actually, I did have both of them:

private static void BootstrapContainer()
{
    _container = new WindsorContainer().Install(FromAssembly.This());
    var controllerFactory = new WindsorControllerFactory(_container.Kernel);

    ControllerBuilder.Current.SetControllerFactory(controllerFactory);

    GlobalConfiguration.Configuration.Services.Replace(
        typeof(IHttpControllerActivator), new WindsorCompositionRoot(_container));
}

...but am currently not calling BootstrapContainer(), so neither is called. What replaced that is:

private static IWindsorContainer _container;
. . .
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

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

    ConfigureWindsor(GlobalConfiguration.Configuration);
}

public static void ConfigureWindsor(HttpConfiguration configuration)
{
    _container = new WindsorContainer();
    _container.Install(FromAssembly.This());
    _container.Kernel.Resolver.AddSubResolver(new CollectionResolver(_container.Kernel, true));
    var dependencyResolver = new WindsorDependencyResolver(_container);
    configuration.DependencyResolver = dependencyResolver;
}   

UPDATE 9

Serdar, this seems very helpful, especially the ServiceInstaller : IWindsorInstaller method.

Now in that, I see that IRepository has a different lifestyle (LifestyleTransient) than the rest (LifestylePerWebRequest).

Is IRepository something I should have a reference to (I don't, it's unresolvable), or is it a custom interface of yours, such as all the rest appear to be (IUserService, IAppService, etc.)?

回答1:

Ok, so let's break this down. You're saying that you're getting a null in WindsorControllerFactory.GetControllerInstance. I'm assuming you mean that controllerType is null.

What this means is that your problem is nothing to do with Windsor, so you can save yourself a lot of confusion by ignoring your Windsor configuration for now. The problem is that your WebApi routing isn't setup correctly, so WebApi isn't able to find the appropriate controller based on the route you've supplied.

Like Kiran said, it doesn't look like you've actually mapped any routes for the WebApi, which is probably what your problem is.

Additionally, WebApi doesn't use the WindsorControllerFactory that MVC uses, you need a separate class that implements IHttpControllerActivator (see Serdar's post above for a link with an example).

Where you currently have something like the following:

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

You'll need to do something like this as well (or instead if your project only uses WebApi):

GlobalConfiguration.Configuration.Services.Replace(
    typeof (IHttpControllerActivator),
    new WindsorControllerActivator(container));

So here's what you need to do:

  • Setup your WebApi routes so that WebApi rather than MVC handles calls to your API.
  • Implement IHttpControllerActivator, and register your new implementation with WebApi.

Update

I've grabbed your source and fixed a few problems with it. I created a pull request at https://github.com/bclayshannon/WebAPICastleWindsorExtravaganza/pull/1 with the fixes in it. Take a look at that for the details of what I changed. Here's a quick summary of what I changed:

  • There was a duplicate Windsor registration of IDepartmentsRepository which was causing an exception to be thrown during startup.
  • I had to replace the WebApiConfig.Register call in Global.asax.cs with a call to GlobalConfiguration.Configure. Previously I was getting a 404 (but that could just be a difference between my machine and yours).
  • I removed some of the MapRoutes calls that would have caused the MVC framework to handle the WebApi routes.

Additionally, I moved all the API controllers into an api folder to make things easier to understand, and I used attribute routing on the DepartmentsController. An example call is GET /api/Departments/Count. Using attribute routing isn't necessary, but it's normally easier to create a decent API with.

I've verified that I can now make the following requests and get into the DepartmentsController:

  • GET /api/Departments/Count
  • GET /api/Departments?ID=123&CountToFetch=100


回答2:

MapRoute extension is for MVC controller whereas DepartmentsController is a Web API controller implementing IHttpController interface(example: ApiController implements this interface). You should use MapHttpRoute extension instead and also you can use WebApiConfig.cs if you have.