Attribute routing not hitting the action methods inside controllers - c#

I'm using attribute routing in my MVC5 application and it is working fine. When I tried to create an area and placed attribute routing on the controller inside it, it returns 404.
I know, to enable attribute routing inside Area, I have to use [RouteArea("Area Name Here")] and also have to add routes.MapMvcAttributeRoutes(); inside my RouteConfig class. I did all this and designed my controller like this:
[RouteArea("Client")]
[RoutePrefix("Client")]
public class ClientController : Controller
{
#region Properties
private readonly string apiUrl = ConfigurationManager.AppSettings["apiUrl"];
#endregion
#region Constructor
public ClientController()
{
}
#endregion
#region Action Methods
[HttpGet, Route("create")]
public ActionResult Index()
{
--logic here
}
#endregion
}
When i run the application using the route: http://localhost:26189/Client/create, I'm able to hit the constructor but not the Index method. Interestingly, if i remove the attribute [HttpGet, Route("create")] and try this route http://localhost:26189/Client/Index it will hit the Index method.
I'm going through these links but not found the exact fix:
https://blogs.msdn.microsoft.com/webdev/2013/10/17/attribute-routing-in-asp-net-mvc-5/#route-areas

You likely have ordering problems in your startup path. Make sure things are called in this order:
AreaRegistration.RegisterAllAreas();
routes.MapMvcAttributeRoutes();
Any convention-based routes

Related

MVC WebApi advanced custom routing

I'm struggling with this for some time now. I've searched all over the internet, didn't find any solution.
I'd like to create a webapi project with somewhat custom routing. Using VS 2019, project is of type ASP.NET WebApi on .NET Core 2.2. Routing should be like this:
Basic application must reside on url similar to "https://my.server.com/myapi". URLs which will be called are in form "https://my.server.com/myapi/{InstanceName}/{CommandName}?{customParams}"
I have one controller defined in my project and I would like to redirect all requests to that controller, where instanceName could be parameter of all the methods contained in a controller, so I would get a value for that parameter. CommandName is basicly the same as "action" RouteData by MVC principles. As you can see there is no controller specified, since all is handled by one controller.
So far I've tried setup routing like this:
Startup.cs
app.UseMvc(routes =>
{
routes.MapRoute(
name: "MyRoute",
template: "{instance}/{action}",
defaults: new { controller = "MyController" });
});
}
MyController.cs
[Route("/")]
[ApiController]
public class MyController : ControllerBase
{
[HttpGet("{instance}/info")]
public JsonResult Info(string instance, InfoCommand model)
{
// Just return serialized model for now.
var result = new JsonResult(model);
return result;
}
}
But this does not work. I get 415 response from (I think) web server when I call for example
https://my.server.com/myapi/MYINSTANCE/info?param1=value1&param2=value2
While debugging from VS this URL looks like this:
https://localhost:12345/MYINSTANCE/info?param1=value1&param2=value2
but I think it shouldn't matter for routing.
In best case scenario (putting [Route("{instance}")] above controller and [HttpGet("info")] above Info method) I get 404 response, which is also what I do not want.
I've even tried creating my own ControllerFactory, but that didn't work either (changing controller inside ControllerFactory's create method and adding another parameter to RouteData).
How to setup routing like that? Is it even possible? I would still like to use all other MVC features (model binding, proper routing, auth features, etc.), it's just this routing I cannot figure it out.
Your attempt resulting a in 415 Unsupported Media Type error was your best one.
You were only missing the FromQuery as shown below.
The error indicates that the complex type InfoCommand could not be resolved.
You must specify that it must be parsed from the querystring.
Note that the route defined via MapRoute doesn't have effect, since you are using attribute-based routing; it's only one or the other.
[Route("/")]
[ApiController]
public class MyController : ControllerBase
{
[HttpGet("{instance}/info")]
public JsonResult Info(string instance, [FromQuery] InfoCommand model)
{
var result = new JsonResult(model);
return result;
}
}
public class InfoCommand
{
public InfoCommand()
{}
public string Param1 { get; set; }
public string Param2 { get; set; }
}

Getting RouteData for actions with RouteAttribute

I have a custom IRouter implementation, which looks in its basic form like this, for simplicity's sake I hardcoded some values:
public class MyRouter : IRouter
{
private readonly IRouter router;
public MyRouter (IRouter router)
{
this.router = router;
}
public async Task RouteAsync(RouteContext context)
{
context.RouteData.Values["controller"] = "Home";
context.RouteData.Values["action"] = "Index";
context.RouteData.Values["area"] = "";
await router.RouteAsync(context);
}
}
This works for a simple controller without a [Route] attribute defined:
public class HomeControlller
{
public IActionResult Index()
{
return View();
}
}
Again, this works correctly. Going to / will show the page.
However, as soon as I add [Route] attributes, I get a 404:
[Route("foo")]
public class HomeControlller
{
[Route("bar")]
public IActionResult Index()
{
return View();
}
}
Now, if I go to /foo/bar, I will see the page. However, if I go to /, I get a 404.
How can I fix this? If I look at the RouteData values when going to /foo/bar, I still see the values Home and Index as values for controller and action, respectively.
This is by design.
Actions are either conventionally routed or attribute routed. Placing a route on the controller or the action makes it attribute routed. Actions that define attribute routes cannot be reached through the conventional routes and vice-versa. Any route attribute on the controller makes all actions in the controller attribute routed.
Reference: Mixed routing: Attribute routing vs conventional routing.
For Conventional router, it is using MvcRouteHandler, and Attribute route will use MvcAttributeRouteHandler. When Controller or Action used with Route[], it will not go to Converntional router when you request the specific method.

Mixing WebAPI and MVC AttributeRouting with Route Prefix?

I'm pretty new to setting up routes and routing in MVC. At my last job we used attribute routing for our WebAPI stuff, so I'm pretty familiar with that (RoutePrefix, Route, and HttpGet/HttpPost attributes, etc). And I can get my current project to work just fine with attributes.
So now what I want to do is "prefix" all of the webApi routes with "api". So instead of going to mysite/test/hello, I want to go to mysite/api/test/hello.
This is what I have, using only attribute routing, and it works just fine:
[RoutePrefix("Test")]
public class TestController : ApiController
{ ....
[HttpPost]
[Route("{message}")]
public HttpResponse EchoBack(string message)
{
// return message ... in this case, "hello"
}
}
Now, I know I can change the RoutePrefix to "api/Test" (which works 100%), but I don't want to change all my controllers, I would rather be able to perform this by changing the values passed in to config.Routes.MapHttpRoute in WebApiConfig.
Is this possible?
What you describe can be done in attribute routing by using what is referred to as a global route prefix.
Referencing this article Global route prefixes with attribute routing in ASP.NET Web API
Create a DirectRouteProvider
public class CentralizedPrefixProvider : DefaultDirectRouteProvider {
private readonly string _centralizedPrefix;
public CentralizedPrefixProvider(string centralizedPrefix) {
_centralizedPrefix = centralizedPrefix;
}
protected override string GetRoutePrefix(HttpControllerDescriptor controllerDescriptor) {
var existingPrefix = base.GetRoutePrefix(controllerDescriptor);
if (existingPrefix == null) return _centralizedPrefix;
return string.Format("{0}/{1}", _centralizedPrefix, existingPrefix);
}
}
To quote article.
The CentralizedPrefixProvider shown above, takes a prefix that is
globally prepended to every route. If a particular controller has it’s
own route prefix (obtained via the base.GetRoutePrefix method
invocation), then the centralized prefix is simply prepended to that
one too.
Configure it in the WebAPiConfig like this
config.MapHttpAttributeRoutes(new CentralizedPrefixProvider("api"));
So now for example if you have a controller like this
[RoutePrefix("Test")]
public class TestController : ApiController {
[HttpPost]
[Route("{message}")]
public IHttpActionResult EchoBack(string message) { ... }
}
The above action will be accessed via
<<host>>/api/Test/{message}
where api will be prepended to the controller actions route.

Using Both of Attribute and Convention Routing

Is there a way to use Convention and Attribute Routing together?
I want to call an action method with the real name of method and controller when I defined the attribute routing.
The mapping method is calling at startup:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();///
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Here is the controller:
[RoutePrefix("d")]
[Route("{action=index}")]
public class DefaultController : Controller
{
[Route]
public ActionResult Index()
{
return View();
}
[Route("f")]
public ActionResult Foo()
{
return View();
}
}
I can reach to Foo action method with /d/f url. But when I try this url: /Default/Foo, the 404 error occurs. Actually it throws the action not found exception which it says like A public action method 'Foo' was not found on controller 'Namespace...DefaultController'.
I checked the source code of asp.net mvc and I saw these lines:
if (controllerContext.RouteData.HasDirectRouteMatch())
{
////////
}
else
{
ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);
return actionDescriptor;
}
It checks if there is a direct route or not, which the /Default/Foo route is not a direct route so it should act as convention routing which is registered at startup as {controller}/{action}/{id}. But it doesn't find the action with controllerDescriptor.FindAction method and it throws the exception.
Is this a bug or cant I use both routing methods together? Or are there any workaround to use both?
Edit
I debugged into mvc source code, and I saw these lines:
namespace System.Web.Mvc
{
// Common base class for Async and Sync action selectors
internal abstract class ActionMethodSelectorBase
{
private StandardRouteActionMethodCache _standardRouteCache;
protected void Initialize(Type controllerType)
{
ControllerType = controllerType;
var allMethods = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public);
ActionMethods = Array.FindAll(allMethods, IsValidActionMethod);
// The attribute routing mapper will remove methods from this set as they are mapped.
// The lookup tables are initialized lazily to ensure that direct routing's changes are respected.
StandardRouteMethods = new HashSet<MethodInfo>(ActionMethods);
}
The last comments about attribute routing explains why this problem happens. The Attribute routing removes StandardRouteMethods when you call MapMvcAttributeRoutes.
I'm still seeking a workaround.
I want to call an action method with the real name of method and controller when I defined the attribute routing.
It seems like you are going pretty far in the wrong direction if all you want is to call the application knowing the controller and action names. If you have the name of the controller and action (area and other route values) you can use them to get the URL to use quite easily.
var url = Url.Action("Index", "Default");
// returns /d/f
If this works for your usage, then you don't have to have a duplicate set of route mappings at all.
NOTE: Creating 2 URLs to the same page is not SEO friendly (unless you use the canonical tag). In fact, many question here on SO are about removing the duplicate routes from the route table. It seems like they did us all a favor by removing the duplicate routes so we don't have to ignore them all manually.
Alternatives
One possible workaround is to add 2 route attributes to the action method (I don't believe it can be done without removing the RoutePrefix from the controller).
[Route("d/f")]
[Route("Default/Foo")]
public ActionResult Foo()
{
return View();
}
Another possible workaround - don't use attribute routing for all or part of your application. Attribute routing only supports a subset of convention-based routing features, but is useful in some specific scenarios. This does not appear to be one of those scenarios.

Specify [HttpGet], [HttpPost] or [AcceptVerbs] for the whole controller

Many action filters can be specified for the whole Controller in ASP.NET MVC, resulting in it being applied to all of the actions in the controller. For example:
[Authorize]
public class MyController : Controller
{
// ....
}
means that the [Authorize] applies to all of the actions in the controller, which is very convenient.
But when I try to put [HttpGet], [HttpPost] or [AcceptVerbs(...)] on the controller, the compiler complains that the attribute is only usable for methods (which is right, since they are defined with [AttributeUsage] pointing only toward methods).
What if I want to limit all actions in a controller to POST verb only?
My question is:
Is there any way in the framework to achieve this, without writing my own attribute?
Why are these attributes implemented this way, not allowing them on controllers?
HttpGet and HttpPost inherits from ActionMethodSelectorAttribute Class, and are valids only for methods. I think that you need create your own attribute.
Custom attribute to solve this:
public sealed class AllowedMethodsAttribute : ActionFilterAttribute
{
private readonly string[] methods;
public AllowedMethodsAttribute(params string[] methods) => this.methods = methods.Select(r => r.Trim().ToUpperInvariant()).ToArray();
public override void OnActionExecuting(ActionExecutingContext actionContext)
{
if (!methods.Contains(actionContext.HttpContext.Request.Method))
{
actionContext.Result = new StatusCodeResult(405);
}
}
}
Usage:
[AllowedMethods("GET", "PATCH")]

Categories