How to set the same route template in different controller - c#

I got a code from another team, this is a .net core 2.2 web api with a controller : CustomerDemandController and I have to create another one (ManagerDemandController) with pretty much the same methods in it. In both of the controllers, I have a "Get by Id" method. which looks like this :
[ApiVersion(Constants.LatestVersion)]
[Route("api/{version:apiVersion}/[controller]/")]
[ControllerName("customerdemands")]
[Produces("application/json")]
[EnableCors("SiteCorsPolicy")]
public class CustomerDemandController : ControllerBase
{
private const string GetByIdOperation = "GetById";
[Get("{id}", Name = GetByIdOperation)]
public async Task<ActionResult<CustomerDemandResponse>> GetAsync([FromRoute] string id)
=> await this.GetAsync(() => Service.GetByIdAsync(id),
ConversionHelper.Convert);
...
(same method in the other controller with ManagerDemandResponse as response).
Now that I have added the new controller, I want to test if the old one still works and this is not the case anymore due to the same route name "GetById" in both controllers.
System.InvalidOperationException : The following errors occurred with attribute routing information:
Error 1: Attribute routes with the same name 'GetById' must have the
same template: Action:
'DemandManagement.Api.Controllers.CustomerDemandController.GetAsync
(DemandManagement.Api)' - Template:
'api/{version:apiVersion}/customerdemands/{id}' Action:
'DemandManagement.Api.Controllers.ManagerDemandController.GetAsync
(DemandManagement.Api)' - Template:
'api/{version:apiVersion}/managerdemands/{id}'
How can I have the same templates as the controller name is different ?

The issue here is the route name and not necessarily the template. Change the route Name. Route Names should be unique to avoid route conflicts.
//...
public class CustomerDemandController : ControllerBase
{
private const string GetByIdOperation = "GetCustomerDemandById"; //<-- Unique
[Get("{id}", Name = GetByIdOperation)]
public async Task<ActionResult<CustomerDemandResponse>> GetAsync([FromRoute] string id)
=> await this.GetAsync(() => Service.GetByIdAsync(id),
ConversionHelper.Convert);
//...
Route names can be used to generate a URL based on a specific route. Route names have no impact on the URL matching behavior of routing and are only used for URL generation. Route names must be unique application-wide.
emphasis mine
Reference Routing to controller actions in ASP.NET Core : Route Name

Related

Controller inheritance and choosing which controller has prevalence

I have a baseproject and different inheriting projects. The base project has controllers I may want to occasionally inherit and override (partially).
Base project:
public virtual ActionResult Index(string filter = "", int page = 1)
Sub project:
public override ActionResult Index(string filter = "", int page = 1)
Now I changed the routeConfig, so the routing is mapped to the logic from the correct namespace.
context.MapRoute(
"Routename",
"AreaName/{controller}/{action}/{id}",
new { controller = "ControllerName", action = "Index", id = UrlParameter.Optional },
new string[] { "ProjectName.Areas.AreaName.SpecificControllers"}
);
However, I want new added routes to be taken from the specific project should they exist there. The ones which are not existant should be taken from the base project's controller. (The specific controller basically starts out empty and will only contains methods for when overriding is desirable). To try and implement this functionality, I added the other project to the routing here:
context.MapRoute(
"Routename",
"AreaName/{controller}/{action}/{id}",
new { controller = "ControllerName", action = "Index", id = UrlParameter.Optional },
new string[] { "ProjectName.Areas.AreaName.SpecificControllers", "ProjectName.Areas.AreaName.GenericControllers"}
);
However, this obviously leads to the following error:
Multiple types were found that match the controller named 'MethodName'. This can happen if the route that services this request ('CRM/{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
The request for 'MethodName' has found the following matching controllers:
ProjectName.Areas.AreaName.SpecificControllers.ControllerName
ProjectName.Areas.AreaName.GenericControllers.ControllerName
Is there a way to implement this so that my routing will always look at the specific controller first and only at the generic controller if it cannot find the method in the specific controller?
Generally routing choose the base controller method as far as i know.
There is no direct support to resolve the issue you mentioned in this question.
There are couple of workarounds to resolve this.
Option 1 (My Favourite): Admin on base and Route on inherited controller.
To Use [Area] on the base controller and [Route] on the inherited controllers.
I personally like this approach because it keeps the code inside controller clean.
[Area("Admin")]
AdminBaseController: Controller { }
[Route("Users"))
UserAdminController : AdminBaseController { }
Url would be /Admin/Users/Action
Option 2: Use Specific Route Prefix in derived controller actions
[Route("Admin")]
AdminBaseController: Controller { }
public static string UserAdminControllerPrefix = "/Users";
UserAdminController : AdminBaseController {
[Route(UserAdminControllerPrefix + "/ActionName")]
public void ActionName() { }
}
Formed URL would be /Admin/Users/ActionName
you can choose whichever option which fits your style.
Hope this helps.
Both the approaches mentioned in this answer : ASP.NET Core MVC Attribute Routing Inheritance

default route with id does not working in asp.net core

This is my controller:
public class HomeController : Controller
{
public IActionResult Index()
{
var route = Request.Path.Value;
return View("index" as object);
}
[HttpGet("{id}")]
public IActionResult Index(int id)
{
return View("index id" as object);
}
}
This is my route:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
url : /1 -> return index with id
url : /Home/Index/1 -> return index without id
I don't understand why?
You're using mixed routing - you've got conventional routing for the first action and attribute routing for the second.
When you navigate to /1, you hit the second action with the id parameter, because it's been set to use attribute routing for a path of /{id} (by using [HttpGet("{id}")]): Attribute routing overrides conventional routing.
When you navigate to /Home/Index/1, you hit your first action without the id parameter, simply because the other action you have is no longer a match given that it's been set to use attribute routing (/{id}), so it no longer matches /Home/Index/1 at all. With your conventional routing template from UseMvc, you've said that id is optional and so the match is still valid.
In order to achieve what you're looking for, you can use attribute routing exclusively for this controller. Here's what that would look like:
[Route("/")]
[Route("[controller]/[action]")]
public class HomeController : Controller
{
public IActionResult Index()
{
...
}
[HttpGet("{id}")]
public IActionResult Index(int id)
{
...
}
}
The addition of the two [Route(...)] attributes here adds support for the following two routes:
/ and /{id}.
/Home/Index and /Home/Index/{id}.
[controller] and [action] are placeholders that represent the name of the controller and action respectively - You could also just use the literal values Home and Index if you'd prefer something more fixed.
You don't necessarily need both [Route(...)] attributes, but the / version ensures the root of the site also matches the same controller/action pair.

MVC IgnoreRoute /?_escaped_fragment_= to continue Reverse Proxy with IIS ARR

Technical Information
AngularJS single page app
Umbraco 7.3.0 website, extended to register routes via Umbraco.Core.ApplicationEventHandler in a separate class library
Scenario
I have an AngularJS single page app (SPA) that I'm trying to pre-render via an external PhantomJS service.
I want MVC's route handler to ignore the route /?_escaped_fragment_={fragment}, so the request can be handled directly by ASP.NET and thus passed on to IIS to proxy the request.
In Theory
Umbraco is built on ASP.NET MVC.
Routes are configurable via System.Web.Routing.RouteCollection class.
When extending Umbraco with custom routes, any routes configured via the System.Web.Routing.RouteTable will take precedence over Umbraco routes, thus never being handled by Umbraco**
Possible methods for my scenario
public void Ignore(string url) or
public void Ignore(string url, object constraints)
**I could be wrong. As far as I'm aware, custom routing takes precedence as it's done before the Umbraco routes are registered. However I'm unsure whether telling MVC to ignore a route would also prevent Umbraco from handling that route.
In Practise
I have attempted to ignore the routes with the following:
Attempt one:
routes.Ignore("?_escaped_fragment_={*pathInfo}");
This throws an error: The route URL cannot start with a '/' or '~' character and it cannot contain a '?' character.
Attempt two:
routes.Ignore("{*escapedfragment}", new { escapedfragment = #".*\?_escaped_fragment_=\/(.*)" });
This didn't result in an error, however Umbraco still picked up the request and handed me back my root page. Regex validation on Regexr.
Questions
Can MVC actually ignore a route based on its query string?
Is my knowledge of Umbraco's routing correct?
Is my regex correct?
Or am I missing something?
The built-in routing behavior doesn't take the query string into consideration. However, routing is extensible and can be based on query string if needed.
The simplest solution is to make a custom RouteBase subclass that can detect your query string, and then use the StopRoutingHandler to ensure the route doesn't function.
public class IgnoreQueryStringKeyRoute : RouteBase
{
private readonly string queryStringKey;
public IgnoreQueryStringKeyRoute(string queryStringKey)
{
if (string.IsNullOrWhiteSpace(queryStringKey))
throw new ArgumentNullException("queryStringKey is required");
this.queryStringKey = queryStringKey;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
if (httpContext.Request.QueryString.AllKeys.Any(x => x == queryStringKey))
{
return new RouteData(this, new StopRoutingHandler());
}
// Tell MVC this route did not match
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
// Tell MVC this route did not match
return null;
}
}
Usage
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// This route should go first
routes.Add(
name: "IgnoreQuery",
item: new IgnoreQueryStringKeyRoute("_escaped_fragment_"));
// Any other routes should be registered after...
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}

Restrict route to controller namespace in ASP.NET Core

I'm trying to restrict the controllers of my ASP.NET Core routes to a certain namespace.
In previous versions of ASP.NET MVC there was an overload that provided a string[] namespaces parameter when adding routes. This is missing in ASP.NET MVC 6. So after some googling, I tried playing around with something like
app.UseMvc(routes => {
var dataTokens = new RouteValueDictionary {
{
"Namespaces", new[] {"ProjectA.SomeNamespace.Controllers"}
}
};
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}",
defaults: null,
constraints: null,
dataTokens: dataTokens
);
});
but it doesn't seem to do what I want. Is there a way to restrict the routing engine to a certain namespace?
Update
I just realized it may have to do something with the fact that I'm using attribute routing on each individual controller? Does attribute routing funk up the routes defined by app.UseMvc()?
Update 2
More details:
I've two completely independent Web API projects. Incidentally, a few of the routes are identical in both (ie. ~/api/ping). These projects are independent in Production, one is an endpoint for users, one is an endpoint for administrators.
I also have unit tests, using Microsoft.AspNet.TestHost. A few of these unit tests require functionality of both of these Web API projects (ie. need "admin" endpoint to fully setup a test case for "user"). But when I reference both API projects, the TestHost gets confused because of the identical routes and it complains about "multiple matching routes":
Microsoft.AspNet.Diagnostics.DeveloperExceptionPageMiddleware: Error: An unhandled exception has occurred while executing the request
Microsoft.AspNet.Mvc.Infrastructure.AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied:
ProjectA.SomeNamespace.Controllers.PingController.Ping
ProjectB.SomeNamespace.Controllers.PingController.Ping
at Microsoft.AspNet.Mvc.Infrastructure.DefaultActionSelector.SelectAsync(RouteContext context)
at Microsoft.AspNet.Mvc.Infrastructure.MvcRouteHandler.<RouteAsync>d__6.MoveNext()
Update:
I've found solution through using ActionConstraint. You have to add custom Action Constraint attribute about duplicate actions.
Example with duplicate Index methods.
First HomeController
namespace WebApplication.Controllers
{
public class HomeController : Controller
{
[NamespaceConstraint]
public IActionResult Index()
{
return View();
}
}
}
Second HomeController
namespace WebApplication
{
public class HomeController : Controller
{
[NamespaceConstraint]
public IActionResult Index()
{
return View();
}
}
}
Configure routing
app.UseMvc(cR =>
cR.MapRoute("default", "{controller}/{action}", null, null,
new { Namespace = "WebApplication.Controllers.HomeController" }));
Action constraint
namespace WebApplication
{
public class NamespaceConstraint : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
var dataTokenNamespace = (string)routeContext.RouteData.DataTokens.FirstOrDefault(dt => dt.Key == "Namespace").Value;
var actionNamespace = ((ControllerActionDescriptor)action).MethodInfo.DeclaringType.FullName;
return dataTokenNamespace == actionNamespace;
}
}
}
First answer:
Does attribute routing funk up the routes defined by app.UseMvc()?
Attribute routing and Convention-based routing (routes.MapRoute(...) work independently. And attribute routes have advantage over convention routes.
but it doesn't seem to do what I want. Is there a way to restrict the routing engine to a certain namespace?
Answer from developers:
Instead of using a list of namespaces to group your controllers we recommend using Areas. You can attribute your controllers (regardless of which assembly they are in) with a specific Area and then create a route for that Area.
You can see a test website that shows an example of using Areas in MVC 6 here: https://github.com/aspnet/Mvc/tree/dev/test/WebSites/RoutingWebSite.
Example using Area with convention-based routing
Controller:
//Reached through /admin/users
//have to be located into: project_root/Areas/Admin/
[Area("Admin")]
public class UsersController : Controller
{
}
Configure convention-based routing:
app.UseMvc(routes =>
{
routes.MapRoute(
"areaRoute",
"{area:exists}/{controller}/{action}",
new { controller = "Home", action = "Index" });
}
Example using Area with attribute-based routing
//Reached through /admin/users
//have to be located into: project_root/Areas/Admin/
[Area("Admin")]
[Route("[area]/[controller]/[action]", Name = "[area]_[controller]_[action]")]
public class UsersController : Controller
{
}

ASP.NET WebApi Change Feed Route Name

I'm transitioning out of using WCF Data Services (since they're apparently dead), and attempting to build OData feeds using the newer Microsoft ASP.NET WebApi 2.1. I'm looking for a way to rename my feeds to be different than the class name.
I built out all my controllers, and now I'm trying to rename them just slightly to preserve the idea of set vs. single entities. (For example, the feed should be named WorkCategories, but the class name should be WorkCategory). Is this possible? I want to do something like this:
public static void Register(HttpConfiguration config)
{
builder.EntitySet<EmailSequenceItem>("EmailSequenceItems");
builder.EntitySet<EmailSequence>("EmailSequences");
builder.EntitySet<WorkCategory>("WorkCategories");
...
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
config.MapHttpAttributeRoutes();
}
My controller looks like this (built from templates):
public class WorkCategoryController: ODataController
{
private dcMaintContext db = new dcMaintContext();
// GET odata/WorkCategory
[Queryable]
public IQueryable<WorkCategory> GetWorkCategory()
{
return db.WorkCategories;
}
...
}
But what I get when I rename any of the feeds is a 404 when navigating to http://localhost/odata/WorkCategories:
HTTP/1.1 404 Not Found
Message: No HTTP resource was found that matches the request URI 'http://localhost/odata/WorkCategories'."
MessageDetail: No type was found that matches the controller named 'WorkCategories'
The name of the controller should be same with the EntitySet name by default, which is WorkCategories.
So other controller name except for WorkCategoriesController won't work unless you create your own IODataRoutingConvention.
For the method name, webapi has its default routing rule.
For get entityset, GetWorkCategories() and Get() will work.
For get entity, GetWorkCategory(int key) and Get(int key) will work.
If you want to customize the method name, you can use AttributeRouting in webapi 2.2.
http://blogs.msdn.com/b/webdev/archive/2014/03/13/getting-started-with-asp-net-web-api-2-2-for-odata-v4-0.aspx
[ODataRoute("WorkCategories/WhateverName")]
public IQueryable WhateverName() {...}
Aha! I found this just after I posted it. I just need to rename my controller class to WorkCategoriesController, and the 2 Queryable methods to GetWorkCategories instead of WorkCategory
public class WorkCategoriesController : ODataController
{
private dcMaintContext db = new dcMaintContext();
// GET odata/WorkCategory
[Queryable]
public IQueryable<WorkCategory> GetWorkCategories()
{
return db.WorkCategories;
}
// GET odata/WorkCategory(5)
[Queryable]
public SingleResult<WorkCategory> GetWorkCategories([FromODataUri] int key)
{
return SingleResult.Create(db.WorkCategories.Where(workcategory => workcategory.ID == key));
}
...
}

Categories

Resources