Getting an ApiController to work with areas?
Use the WebAPI 2 attributes, since you are using MVC 5, and you can get rid of a lot of that boilerplate code by declaring the routes for your API along with it's implementation (you can also specify verbs for HTTP actions, and even use attributes to auto-convert to XML/JSON/serialization-of-the-month).
Unless you are using areas for some other reason, you really don't need them to implement a Web API.
In particular, what you want is the RoutePrefix attribute.
If two or more areas has apicontroller with same name, then in order to invoke controller in specific area, the area name must be included in URL.
So http://example.com/api/communication/someAction wont work.
In this case, it can be
http://example.com/supporters/api/communication/someAction and http://example.com/chatters/api/communication/someAction
The custom httpcontrollerselector given in http://blogs.infosupport.com/asp-net-mvc-4-rc-getting-webapi-and-areas-to-play-nicely works fine with mvc5 too.
Remove following lines in webapiconfig
config.Routes.MapHttpRoute("SupportersApi", "api/supporters/{controller}/{id}", new {id = RouteParameter.Optional, area = "Supporters"}
);
config.Routes.MapHttpRoute("ChatterApi", "api/chatter/{controller}/{id}", new { id = RouteParameter.Optional, area = "Chatter" }
);
Here are the steps, which works fine
1. Add following extension method to the project.
public static class AreaRegistrationContextExtensions
{
public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate)
{
return context.MapHttpRoute(name, routeTemplate, null, null);
}
public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate, object defaults)
{
return context.MapHttpRoute(name, routeTemplate, defaults, null);
}
public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate, object defaults, object constraints)
{
var route = context.Routes.MapHttpRoute(name, routeTemplate, defaults, constraints);
if (route.DataTokens == null)
{
route.DataTokens = new RouteValueDictionary();
}
route.DataTokens.Add("area", context.AreaName);
return route;
}
}
2. In each AreaRegistration file, add route which includes area name in routeTemplate
To SupportAreaRegistration, add
context.MapHttpRoute(
name: "Supporters_DefaultApi",
routeTemplate: "supporters/api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
To ChatterAreaRegistration, add
context.MapHttpRoute(
name: "Chatters_DefaultApi",
routeTemplate: "chatters/api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Its context.MapHttpRoute, not context.Routes
3. Add custom HttpControllerSelector
public class AreaHttpControllerSelector : DefaultHttpControllerSelector
{
private const string AreaRouteVariableName = "area";
private readonly HttpConfiguration _configuration;
private readonly Lazy<ConcurrentDictionary<string, Type>> _apiControllerTypes;
public AreaHttpControllerSelector(HttpConfiguration configuration)
: base(configuration)
{
_configuration = configuration;
_apiControllerTypes = new Lazy<ConcurrentDictionary<string, Type>>(GetControllerTypes);
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
return this.GetApiController(request);
}
private static string GetAreaName(HttpRequestMessage request)
{
var data = request.GetRouteData();
if (data.Route.DataTokens == null)
{
return null;
}
else
{
object areaName;
return data.Route.DataTokens.TryGetValue(AreaRouteVariableName, out areaName) ? areaName.ToString() : null;
}
}
private static ConcurrentDictionary<string, Type> GetControllerTypes()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var types = assemblies
.SelectMany(a => a
.GetTypes().Where(t =>
!t.IsAbstract &&
t.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase) &&
typeof(IHttpController).IsAssignableFrom(t)))
.ToDictionary(t => t.FullName, t => t);
return new ConcurrentDictionary<string, Type>(types);
}
private HttpControllerDescriptor GetApiController(HttpRequestMessage request)
{
var areaName = GetAreaName(request);
var controllerName = GetControllerName(request);
var type = GetControllerType(areaName, controllerName);
return new HttpControllerDescriptor(_configuration, controllerName, type);
}
private Type GetControllerType(string areaName, string controllerName)
{
var query = _apiControllerTypes.Value.AsEnumerable();
if (string.IsNullOrEmpty(areaName))
{
query = query.WithoutAreaName();
}
else
{
query = query.ByAreaName(areaName);
}
return query
.ByControllerName(controllerName)
.Select(x => x.Value)
.Single();
}
}
public static class ControllerTypeSpecifications
{
public static IEnumerable<KeyValuePair<string, Type>> ByAreaName(this IEnumerable<KeyValuePair<string, Type>> query, string areaName)
{
var areaNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}.", areaName);
return query.Where(x => x.Key.IndexOf(areaNameToFind, StringComparison.OrdinalIgnoreCase) != -1);
}
public static IEnumerable<KeyValuePair<string, Type>> WithoutAreaName(this IEnumerable<KeyValuePair<string, Type>> query)
{
return query.Where(x => x.Key.IndexOf(".areas.", StringComparison.OrdinalIgnoreCase) == -1);
}
public static IEnumerable<KeyValuePair<string, Type>> ByControllerName(this IEnumerable<KeyValuePair<string, Type>> query, string controllerName)
{
var controllerNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}{1}", controllerName, AreaHttpControllerSelector.ControllerSuffix);
return query.Where(x => x.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase));
}
}
4. Make change in Application_Start method in Global.Asax file, in order to use AreaHttpControllerSelector instead of DefaultHttpControllerSelector
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new AreaHttpControllerSelector(GlobalConfiguration.Configuration));
Try the below configuration. The trick here is to register the namespace to search for the API Controllers when the route matches.
config.Routes.MapHttpRoute(
name: "chatterApi",
routeTemplate: "api/chatter/{controller}/{action}",
defaults: new { action = "", controller = "", namespaces = new string[] { "WebApplication.chatter.api" } }
);
config.Routes.MapHttpRoute(
name: "supportersApi",
routeTemplate: "api/supporters/{controller}/{action}",
defaults: new { action = "", controller = "", namespaces = new string[] { "WebApplication.supporters.api" } }
);