John Graham
John Graham

Reputation: 583

Web API Controller not found when class is generic

I'm trying to use a Generic Controller in my Web API. My goal, which I am currently failing at, is to pass in an object from my front end that will have say a typeId. Based on this typeId I was going to use a factory to inject the correct class implementation of a generic interface. I believe my Factory, Interface and Service is correct, but for some reason when I add a Generic to the API I am getting a 404. It works without a generic and just a test method. I am using autofac for my IoC registration.

API Controller:

public class ListItemsController<T> : ApiControllerBase
{
    private readonly IListItemsService<T> _service;

    public ListItemsController(int listItemTypeId)
    {
        _service = ListItemsFactory<T>.InitializeService(listItemTypeId);
    }

    [HttpGet]
    [Route("{listItemTypeId: int}")]
    public IEnumerable<T> GetAll()
    {
        return _service.GetAll();
    }

    [HttpGet]
    [Route("test")]
    public IHttpActionResult Test()
    {
        return Ok();
    }
}

Factory:

public class ListItemsFactory<T>
{
    public ListItemsFactory(IPrimaryContext context) : base()
    {
    }

    public static IListItemsService<T> InitializeService(int listItemType)
    {
        switch (listItemType)
        {
            case 1: return (IListItemsService<T>)
                new FloorTypeService(new PrimaryContext());
            default: return null;
        }
    }
}

Interface:

public interface IListItemsService<T>
{
    IEnumerable<T> GetAll();
    void Save(T obj);
    T GetById(int id);
    void Delete(int id);
}

Error:

No HTTP resource was found that matches the request URI 'http://localhost:9000/api/v1/listitems/test'. No type was found that matches the controller named 'listitems'.

I'm not sure what piece I'm missing here. I'm using routing attributes but here is my API config:

private static void SetupRoutes(HttpConfiguration config)
{
    config.MapHttpAttributeRoutes(new CustomDirectRouteProvider());
    config.Routes.MapHttpRoute("DefaultApi", "api/v{version}/{controller}/{id}",
        new { id = RouteParameter.Optional });
}

Upvotes: 2

Views: 1988

Answers (2)

Nikolaus
Nikolaus

Reputation: 1869

Instead of resolving the type and trying to map to the right Controller, you also can create a Controller for each Type, which inherits from your GenericController. Then you don't have to copy the Code, but have a Controller for each Type, where you can route to by RouteAttribute.:

public class ListItemsController<T> : ApiControllerBase
{
    //Properties/Fields should be protected to can be accessed from InstanceController.
    protected readonly IListItemsService<T> _service;
    // I think listItemTypeId is not necessary, if generic-type T is used?
    public ListItemsController()
    {
        _service = ListItemsFactory<T>.InitializeService();
    }

    [HttpGet] // No need for RouteAttribute, because it will be in InstanceController.
    public IEnumerable<T> GetAll()
    {
        return _service.GetAll();
    }

    [HttpGet]
    [Route("test")] // This can rest here, because you want to use it.
    public IHttpActionResult Test()
    {
        return Ok();
    }



}

The implemented InstanceController can look like this:

[RoutePrefix("api/{controller}")]
public class FloorItemsController  ListItemsController<Floor> 
{
    // delegate the Constructor-call to base()
    public ListItemsController()
        :base()
    {

    }

     // No need to reimplement Methods.
}

The RouteConfiguration should be set back to default, because RouteAttributes are set for this.

Upvotes: 2

andyroschy
andyroschy

Reputation: 509

Basically, what you'll need to do is to replace the controller activator, with a custom implementation.

First, createa class that implements the IHttpControllerSelector interface. Take a look at this link for some of the thing you should be aware before creating a custom activator. At the bottom there's a link to some code example of a custom implmentation.

Now, this depends on what your rules will actually be, but for perfomance reasons,you should try to build a solution that always map the same controller name to the same closed type of your generic controller type. A simple implementation for your case would look something like this:

public HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
    IHttpRouteData routeData = request.GetRouteData();
    if (routeData == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    //get the generyc type of your controller
    var genericControllerType = typeof(ListItemsController<>);

    // Get the route value from which you'll get the type argument from your controller.
    string typeParameterArgument = GetRouteVariable<string>(routeData, 'SomeKeyUsedToDecideTheClosedType');
    Type typeArgument = //Somehow infer the generic type argument,  form your route value based on your needs
    Type[] typeArgs = { typeof(typeArgument) };
    //obtain the closed generyc type
    var t = genericControllerType.MakeGenericType(typeArgs);            

    //configuration must be an instance of HttpConfiguration, most likeley you would inject this on the activator constructor on the config phase
    new HttpControllerDescriptor(_configuration, t.Name, t); 

}

Finally, on your ApiConfig class you'll need to add this line:

GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), 
        new MyOwnActivatior());

I can't test this code right now, so it might need some tweaking, but hopefully this will guide you on the right direction. Do take notice of the link i pasted above, since there are important considerations you'll need to take into account before implementing a custom activator. Also, check the code example linked on that post to see how to implement the GetControllerMapping method

Upvotes: 2

Related Questions