ASP.NET 5/MVC 6 pipeline branching and controller namespace constraint - c#

I would like to use pipeline branching in ASP.NET 5/MVC 6 in such a way that only controllers in namespace xxx.yyy.Pipe1.Controllers are "available" to a branch mapped like this in 'Startup.cs' app.Map("/pipe1", ConfigurePipe1);
What would be the preferred, and/or correct, way to do that?
It is not so much about controller discovery as about restricting the set of controllers that can be resolved during request processing. My reason for doing this is the need to use different authentication schemes per pipeline, and thereby per set of controllers.
Thanks!

If I understood you correctly you want to map applications by the corresponding controller's namespace?
I think this is not possible. There is MapWhen method. I tried to resolve the controller when it invoked but I had no luck (I knew it was hopeless).
app.MapWhen(context => {
var shouldWeMap = ... // here I tried many things but it was impossible to resolve the controller.
return shouldWeMap;
}, ConfigurePipe1);
If you want to learn which controller will be hit, you have to let asp.net map this request to a mvc configuration. But after doing this, you miss your chance to map that request to an app as already having done it :(

I found a workaround. It uses a custom IActionFilter to check which configuration is used in every request.
Just map a new configuration as you suggested before:
app.Map("/pipe1", ConfigurePipe1);
And then in ConfigurePipe1 make MVC to MapRoute with a different signature (unique name?). With this way you can implement your own global IActionFilter and force it to check which RouteData has been used.
And there you can do whatever you want. Check controller's namespace and so on...
So ConfigurePipe1 may be like:
public void ConfigurePipe1(IApplicationBuilder app)
{
app.UseMvc(routes =>
{
routes.MapRoute(
name: "CustomPipeRoute",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" }
);
});
}
And in custom IActionFilter we can check them like:
public class GlobalActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
var controllerNamespace = context.Controller.GetType().Namespace;
var pipeRoute = context.RouteData.Routers.OfType<TemplateRoute>().FirstOrDefault(x => x.Name == "CustomPipeRoute");
if (pipeRoute != null)
{
// We are using /pipe1
}
if (.....)
{
// You can redirect to somewhere else if you want.
var controller = (Controller)context.Controller;
context.Result = controller.RedirectToAction("Index", "Controller");
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
Also don't forget to register the custom filter as:
services.AddMvc(config =>
{
config.Filters.Add(typeof(GlobalActionFilter));
});

Related

NET Core 5 How to override an action on a controller

Let's imagine I have a "platform" with some basic features. It has its own set of controllers with actions delivered as a class library. For example, let's name it as CartController with Order action, as a result, API URL will be Cart/Order.
For some reason, I want to extend logic in Order process, I added MyCartController with Order action in my project where I reference the platform. Now I need to set up the app to use my custom action instead of the original one.
In ASP MVC 5 (based on .NET Framework) I can do it as follows
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
var route = new Route("Cart/Order", new RouteValueDictionary(new Dictionary<string, object>
{
{ "action", nameof(MyCartController.Order) },
{ "controller", nameof(MyCartController).Replace("Controller", "") }
}), new MvcRouteHandler());
routes.Insert(RouteTable.Routes.Count - 2, route);
}
}
and then call it from Startup as
public override void Configuration(IAppBuilder builder)
{
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
and it worked well.
But after migration to .Net Core 5, it stopped working.
In Startup, I added
public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("MyCartOrder", "Cart/Order", new { controller = nameof(MyCartController).Replace("Controller", ""), action = nameof(MyCartController.Order) });
});
}
with no luck. It looks like they broke something helpful here https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Routing/ControllerActionEndpointDataSource.cs#L100
I tried to decorate my controller with Route("Cart") attribute and HttpPost("Order") with AmbiguousMatchException: The request matched multiple endpoints exception.
I have no idea of how to override it then.
So my question is: How to override the route to use my action on my controller in .Net Core 5? What is the equivalent of routes.Add from .Net 4.8 which works for URL matching but not only for links generation?
Any thoughts and ideas will be appreciated.
You can mark the needed action in the "override" controller with Http{Method}Attribute and set Order property to some value (by default -1 should do the trick):
[HttpPost("Order", Order = -1)]
HttpMethodAttribute.Order Property:
Gets the route order. The order determines the order of route execution. Routes with a lower order value are tried first. When a route doesn't specify a value, it gets the value of the Order or a default value of 0 if the RouteAttribute doesn't define a value on the controller.
Or set the property with the same name of RouteAttribute on the controler:
[Route("Cart", Order = -1)]

Asp.net core mvc optional parameter for Index

im currently trying to create a Controller in ASP.net core mvc with an optional parameter "id".
Im fairly new to asp.net, I've tried to check other posts but nothing solved my problem.
public class TestController : Controller
{
private readonly ITestRepository _TestRepository;
public TestController(ITestRepository TestRepository)
{
_TestRepository = TestRepository;
}
public async Task<IActionResult> Index(string id)
{
if (string.IsNullOrEmpty(id))
{
return View("Search");
}
var lieferscheinInfo = await _TestRepository.GetTestdata(Convert.ToInt64(id));
if (lieferscheinInfo == null)
{
// return err view
throw new Exception("error todo");
}
return View(lieferscheinInfo);
}
}
I want to open the site like this "localhost:6002/Test" or "localhost:6002/Test/123750349" e.g, the parameter can be an int as well, i've tried both(string and int) but it doesnt work.
Either the site returns 404 (for both cases, with and without an parameter) or the parameter gets ignored and is always null.
I've tried to add [Route("{id?}")] on the Index but it did not change anything.
Greetings
your code should work just fine. string parameters accepts null by default so you dont need to specify can you check how the routing is set up in your startup.cs file.
You can add routing through attribute for example the following :
[Route("Test")]
[Route("Test/Index")]
[Route("Test/Index/{id?}")]
Will allow you to access the Action using :
/test/?id=sasasa
test/Index/?id=sasasa
test/Index
check Routing Documentation at Microsoft site :
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-3.1
In your project's Startup class make sure you are using the right MapControllerRoute
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Test}/{action=Index}/{id?}");
});

ASP.NET Web API versioning with URL Query gives "duplicate route" error

First, a little disclaimer: I have already created a GitHub issue for this at the aspnet-api-versioning repo. The content of this question is basically the same as the content in that github issue.
I am using ASP.NET Web API on .NET 4.5.2.
My example API looks like this:
namespace App.Backend.Controllers.Version1
{
[ApiVersion("1.0")]
public class SomeController: ApiController
{
[HttpGet]
[Route("api/entity/{id:int:min(1)}")]
public async Task<IHttpActionResult> ApiAction(int id) //Already running in production
{
//Accessible by using ?api-version=1.0 OR by omitting that since this is the default version
return Ok();
}
}
}
namespace App.Backend.Controllers.Version2
{
[ApiVersion("2.0")]
public class SomeController : ApiController
{
[HttpGet]
[Route("api/entity/{id:int:min(1)}")]
public async Task<IHttpActionResult> ApiAction(int id)
{
//Accessible by using ?api-version=2.0 OR by omitting that since this is the default version
return Ok();
}
}
}
The config is as follows:
// Add versioning
config.AddApiVersioning(o =>
{
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
});
When I send a request, though, the following happens:
System.InvalidOperationException: A route named 'RegisterHours' is already in the route collection. Route names must be unique.
Duplicates:
api/some/ApiAction
api/some/ApiAction
This is weird to me because in the wiki there is an example exactly like my situation
I'd like to use the ?api-version={version} option but it looks I have no choice to use the URL path version now (api/v1.0/some/apiAction and api/v2.0/some/apiAction. If that's true, I guess I have to add another Route to every existing action which will be like api/v{version:apiVersion}/controller/action to allow them to use v1.0 so it will be uniform in the entire application?
What do you guys advise? I could just use /v2.0/ in the URL of version 2 of the API I guess, but I'd prefer the query string version.
It doesn't show it in the example, but the error message:
System.InvalidOperationException: A route named 'RegisterHours' is already in the route collection. Route names must be unique.
means that there are multiple entries with the same name in the route table. If I had to guess, the attribute routes are actually being defined as:
[Route("api/entity/{id:int:min(1)}", Name = "RegisterHours")]
...or something like that.
Unfortunately, the route table is not API version-aware. The names in the route table must be unique. When you specify the name in the RouteAttribute, it causes this issue. The only real way around it is to use unique route names; for example, Name = "RegisterHoursV1" and Name = "RegisterHoursV2".
Aside: you don't need:
var constraintResolver = new DefaultInlineConstraintResolver()
{
ConstraintMap = { ["apiVersion"] = typeof( ApiVersionRouteConstraint ) }
};
configuration.MapHttpAttributeRoutes( constraintResolver );
unless you are versioning by URL segment.
After you have fixed the duplicate "RegisterHours" (as per the git hub issues page responses) you should also ensure you have the constraintResolver setup in your startup.cs
public void Configuration( IAppBuilder builder )
{
// we only need to change the default constraint resolver for services that want urls with versioning like: ~/v{version}/{controller}
var constraintResolver = new DefaultInlineConstraintResolver() { ConstraintMap = { ["apiVersion"] = typeof( ApiVersionRouteConstraint ) } };
var configuration = new HttpConfiguration();
var httpServer = new HttpServer( configuration );
// reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
configuration.AddApiVersioning( o => o.ReportApiVersions = true );
configuration.MapHttpAttributeRoutes( constraintResolver );
builder.UseWebApi( httpServer );
}
Otherwise your attributes to change the route (api/entity) won't work because the route doesn't match the controller name "Some" and so won't match the default routing ie. ~\api\controllername\
public class ***Some***Controller : ApiController
{
[HttpGet]
[Route( "api/***entity***/{id:int:min(1)}" )] <--- won't work without the constraint resolver
public async Task<IHttpActionResult> ApiAction( int id )
{
//Accessible by using ?api-version=2.0 OR by omitting that since this is the default version
return Ok();
}

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.

Is it possible to set a global RoutePrefix for an entire MVC application?

I have an MVC site that we are using just to host our new checkout process. There is a rewrite rule in our ARR server in place so that any requests for https://www.oursite.com/checkout go to this new application instead of the legacy site.
This means that every request to the new application starts with /checkout which leaves us with the slightly unsatisfactory situation of having every controller in the site decorated with [RoutePrefix("checkout")].
Is there a way that I can set a global route prefix that automatically applies to all controllers in the application? We don't have a universal base controller that we own to put an attribute on. The only options I could think of are to go back to the old fashioned routing:
routes.MapRoute(
"Default",
"checkout/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
I'd much prefer to use Attribute Routing, so I want to call something like routes.MapMvcAttributeRoutes(globalPrefix: "checkout");
How can I accomplish this?
I found out a way to do it, not sure if it's best practice but it seems to work just fine. I noticed that the MapMvcAttributeRoutes method can take an IDirectRouteProvider as argument.
It took a bit of guesswork but I was able to write a class that derives from the framework's DefaultDirectRouteProvider and overrides the GetRoutePrefix method:
public class CheckoutPrefixRouteProvider : DefaultDirectRouteProvider
{
protected override string GetRoutePrefix(ControllerDescriptor controllerDescriptor)
{
return "checkout/" + base.GetRoutePrefix(controllerDescriptor);
}
}
I can then use this new class as follows:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes(new CheckoutPrefixRouteProvider());
}
}

Categories