Reputation: 583
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
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
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