I'm having a frustrating problem when I try to make a call to my Web API controller method with $.getJSON : it always ends up with the following message being displayed in the console : "Failed to load resource: the server responded with a status of 405 (Method Not Allowed)"
Here's my controller :
using MyProject.Domain;
using MyProject.WebApp.Session;
using System;
using System.Linq;
using System.Web.Mvc;
namespace MyProject.WebApp.ApiControllers.Favorites
{
public class FavoriteArticlesController : BaseController<FavoriteArticle, Guid>
{
[HttpGet]
public object SetFavorite(Guid articleId, bool isFavorite)
{
try
{
if (isFavorite)
{
var favorite = new FavoriteArticle
{
UserId = UserInfo.GetUserId(),
ArticleId = articleId
};
_repo.Upsert(favorite);
}
else
{
var favorite = _repo.GetAll()
.First(fa => fa.ArticleId.CompareTo(articleId) == 0);
_repo.Delete(favorite.Id);
}
_repo.Commit();
return new { Success = true, Error = (string)null };
}
catch (Exception ex)
{
return new { Success = false, Error = ex.Message };
}
}
}
}
In case that is relevant in any way, BaseController naturally derives from ApiController. Here's the code if needed :
using MyProject.Data.Repository;
using MyProject.Data.Services;
using MyProject.Domain;
using System.Web.Http;
namespace MyProject.WebApp.ApiControllers
{
public class BaseController<TEntity, TKey> : ApiController
where TEntity : class, IEntity<TKey>, new()
{
protected UnitOfWork _unitOfWork;
protected Repository<TEntity, TKey> _repo;
protected BaseController()
{
_unitOfWork = new UnitOfWork();
_repo = _unitOfWork.GetRepository<TEntity, TKey>();
}
}
}
And here's one of the functions that make the call :
$.fn.bindFavoriteArticle = function () {
this.click(function () {
var link = $(this);
$.getJSON('/api/FavoriteArticles/SetFavorite', { ajax: true, articleId: link.attr('data-target-id'), isFavorite: true }, function (response) {
if (response.Success === true) {
link.children('i').removeClass('fa-heart-o')
.addClass('fa-heart');
link.attr('data-toggle', 'unfavoriteArticle')
.unbind('click')
.bindUnfavoriteArticle();
} else {
// TODO : use bootstrap alert messages
alert(response.Error);
}
});
});
};
I saw here and there that the route configuration could be the source of the problem, so here's the content of RouteConfig.cs :
using System.Web.Mvc;
using System.Web.Routing;
namespace MyProject.WebApp
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "ApiDefault",
url: "api/{controller}/{action}/{id}",
defaults: new { controller = "SubscriptionsController", action = "GetSelectList", id = UrlParameter.Optional }
);
}
}
}
Any idea of what's going on ? I feel like there's a lot I'm missing concerning how Web APIs work...
As mentioned in one of the other answers, the code configured the wrong routes. For web api configure the WebApiConfig
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
// Web API routes
//Enable Attribute routing is they are being used.
config.MapHttpAttributeRoutes();
//Convention based routes.
//Matches GET /api/FavoriteArticles/SetFavorite
config.Routes.MapHttpRoute(
name: "DefaultActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//Matches GET /api/FavoriteArticles
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Make sure that it is configured before MVC routes.
protected void Application_Start() {
// Pass a delegate to the Configure method.
GlobalConfiguration.Configure(WebApiConfig.Register);
//...other configurations
}
Some advice about refactoring the controller to be a little more restful. Try to return IHttpActionResult
from actions. simplifies the framework and give you more control of how the response is returned.
[RoutePrefix("api/favoritearticles"]
public class FavoriteArticlesController : BaseController<FavoriteArticle, Guid> {
[HttpPost]
[Route("{articleId:guid}")] //Matches POST api/favoritearticles/{articleId:guid}
public IHttpActionResult SetFavorite(Guid articleId) {
var favorite = new FavoriteArticle
{
UserId = UserInfo.GetUserId(),
ArticleId = articleId
};
_repo.Upsert(favorite);
_repo.Commit();
return Ok(new { Success = true, Error = (string)null });
}
[HttpDelete]
[Route("{articleId:guid}")] //Matches DELETE api/favoritearticles/{articleId:guid}
public IHttpActionResult RemoveFavorite(Guid articleId) {
var favorite = _repo.GetAll()
.First(fa => fa.ArticleId == articleId);
if(favorite == null) return NotFound();
_repo.Delete(favorite.Id);
_repo.Commit();
return Ok(new { Success = true, Error = (string)null });
}
}
Controllers should be as lean as possible so even the above should be slimmed down even more via an injected service into the controller.
Error handling is a cross-cutting concern and should also be extracted and handled via the framework's extensibility points.