What's the difference between RouteCollection.Ignore(url, constraints) and RouteCollection.IgnoreRoute(url, constraints)?
Background
New MVC projects include this IgnoreRoute call in Global.asax RegisterRoutes method to skip routing for requests to .axd locations that are handled elsewhere in the ASP.NET system.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
I wanted to add an additional ignored route to a project and I started to type out the new line. After routes.I, Intellisense pops up with .Ignore and .IgnoreRoute, both sounding about the same.
According to the MSDN docs, you can see that one is an instance method of the System.Web.Routing.RouteCollection class and the other is an extension method on that class from System.Web.Mvc.RouteCollectionExtensions.
RouteCollection.Ignore: "Defines a URL pattern that should not be checked for matches against routes if a request URL meets the specified constraints" (MSDN docs).
RouteCollection.IgnoreRoute: "Ignores the specified URL route for the given list of the available routes and a list of constraints" (MSDN docs).
Both take a route URL pattern and a set of constraints restricting the application of the route on that URL pattern.
Between the source for System.Web.Mvc.RouteCollectionExtensions on CodePlex and running a little ILSpy on my local GAC for System.Web.Routing.RouteCollection, it doesn't appear there is a difference, though they seem to have completely independent code to do the same thing.
RouteCollection.IgnoreRoute (via CodePlex source)
public static void IgnoreRoute(this RouteCollection routes, string url, object constraints) {
if (routes == null) {
throw new ArgumentNullException("routes");
}
if (url == null) {
throw new ArgumentNullException("url");
}
IgnoreRouteInternal route = new IgnoreRouteInternal(url) {
Constraints = new RouteValueDictionary(constraints)
};
routes.Add(route);
}
RouteCollection.Ignore (via ILSpy decompile)
public void Ignore(string url, object constraints) {
if (url == null) {
throw new ArgumentNullException("url");
}
RouteCollection.IgnoreRouteInternal item = new RouteCollection.IgnoreRouteInternal(url) {
Constraints = new RouteValueDictionary(constraints)
};
base.Add(item);
}
Differences
The only real difference is the obvious difference in location, one being an instance method in the RouteCollection class itself and one being an extensions method on that class. After you factor in the code differences that come from instance vs. extension execution (like the vital null check on the extended instance), they appear identical.
At their core, they both use the exact same StopRoutingHandler class. Both have their own versions of a sealed IgnoreRouteInternal class, but those versions are identical in code.
private sealed class IgnoreRouteInternal : Route {
public IgnoreRouteInternal(string url)
: base(url, new StopRoutingHandler()) {
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary routeValues) {
return null;
}
}
Related
I want to customize routes depending on the culture of web application to have a 'friendly' url (to optimize SEO).
For example, for the controller called "FirstItemController", and view called "TheIndexView":
www.myapp.com/fr/mon-premier-element/la-vue-index
www.myapp.com/en/my-first-item/the-index-view
www.myapp.com/es/mi-primer-elemento/la-vista-index
Asp.Net Core 2.2 introduces the concept of Parameter Transformers to routing (https://blogs.msdn.microsoft.com/webdev/2018/10/17/asp-net-core-2-2-0-preview3-now-available/ + https://www.hanselman.com/blog/ASPNETCore22ParameterTransformersForCleanURLGenerationAndSlugsInRazorPagesOrMVC.aspx)
I know, theses articles was wrote for 2.2.0 preview 3.
For the moment, I've create the SlugifyParameterTransformer class and register it in AddMvc options :
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
if (value == null) { return null; }
// Slugify value
return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
}
}
I've tried two approaches in routes options:
app.UseMvc(routes => routes.MapRoute("default", "{culture=fr}/{controller=Home}/{action=Index}/{id?}"));
// works
// AND
app.UseMvc(routes => routes.MapRoute("default", "{culture=fr}/{controller=Home:slugify}/{action=Index:slugify}/{id?}"));
// dosn't works, I don't know where I declare "slugify" for the url template
It work's: MyFirstItemController.TheIndexView url is "my-first-item/the-index-view". Now, how can I localize my urls?
I've tried to add the RouteAttribute and HttpGetAttribute:
[Route("{culture=fr}/mon-premier-element")]
[Route("{culture=en}/my-first-item")]
public class MyFirstItemController : Controller
{
[HttpGet("???????")] // for moment: [controller]/[action]
public IActionResult TheIndexView()
{ /*...*/ }
}
I don't know what can I use in Name value of HttpGetAttribute.
These urls works:
fr/mon-premier-element/ (good)
en/mon-premier-element/ (NOT good)
fr/my-first-item (NOT good)
en/my-first-item (good)
A design goal for a website I'm working on is to keep the URL in the browser in a state where the user can copy it, and the link can be used from another browser/user/machine to return to the spot that the url was copied. (The actual changes will happen via AJAX, but the URL will change to reflect where they are.)
Example: If you were on the customer page looking at customer 123, and had details pulled up on their order #456, and full details on line 6 of this order, your url could simply be /customer/123/456/6
The challenge comes with a second feature: Users can add UI columns (analogous to adding a new tab in a tab view, or a new document in an MDI app) Each column can easily generate a routable url, but I need the url to reflect one or more columns. (E.G. User has both /customer/123/456/6 and /customer/333/55/2 in two side by side columns)
In a perfect world, I'd like the url to be /customer/123/456/6/customer/333/55/2 for the above scenario, but I don't know if MVC routing can handle repetitive patterns, or, if so, how it is done.
Can this be done via routing? If not is there a way to get this type of one-or-more functionality from Url?
You could create a custom route handler (see my previous answer) or derive from a RouteBase like NightOwl888 suggested. Another approach would be to simply use a model binder and a model binder attribute.
public class CustomerInvoiceLineAttribute : CustomModelBinderAttribute
{
public override IModelBinder GetBinder()
{
return new CustomerInvoiceLineModelBinder();
}
}
public class CustomerInvoiceLineModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var path = (string)bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue;
var data = path.Split(new[] { "/customer/" }, StringSplitOptions.RemoveEmptyEntries);
return data.Select(d =>
{
var rawInfo = d.Split('/');
return new CustomerInvoiceLine
{
CustomerId = int.Parse(rawInfo[0]),
InvoiceId = int.Parse(rawInfo[1]),
Line = int.Parse(rawInfo[2])
};
});
}
}
You define your route by specifying a star route data. This mean that the route parameter will contains everything following the action
routes.MapRoute(
name: "CustomerViewer",
url: "customer/{*customerInfo}",
defaults: new { controller = "Customer", action = "Index" });
Then in your controller, you bind your parameter with the same name as the star route parameter using the custom model binder defined above:
public ActionResult Index([CustomerInvoiceLine] IEnumerable<CustomerInvoiceLine> customerInfo)
{
return View();
}
You will need to add validation during the parsing and probably security too, so that a customer cannot read the invoice of other customers.
Also know that URL have a maximum length of 2000 characters.
You can do this with the built-in routing as long as you don't anticipate that any of your patterns will repeat or have optional parameters that don't appear in the same segment of the URL as other optional parameters.
It is possible to use routing with optional parameters by factoring out all of the permutations, but if you ask me it is much simpler to use the query string for this purpose.
NOTE: By definition, a URL must be unique. So you must manually ensure your URLs don't have any collisions. The simplest way to do this is by matching the page with the path (route) and adding this extra information as query string values. That way you don't have to concern yourself with accidentally making routes that are exactly the same.
However, if you insist on using a route for this purpose, you should probably put your URLs in a database in a field with a unique constraint to ensure they are unique.
For the most advanced customization of routing, subclass RouteBase or Route. This allows you to map any URL to a set of route values and map the route values back to the same URL, which lets you use it in an ActionLink or RouteLink to build the URLs for your views and controllers.
public class CustomPageRoute : RouteBase
{
// This matches the incoming URL and translates it into RouteData
// (typically a set of key value pairs in the RouteData.Values dictionary)
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Trim the leading slash
var path = httpContext.Request.Path.Substring(1);
if (/* the path matches your route logic */)
{
result = new RouteData(this, new MvcRouteHandler());
result.Values["controller"] = "MyController";
result.Values["action"] = "MyAction";
// Any other route values to match your action...
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
// This builds the URL for ActionLink and RouteLink
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData result = null;
if (/* all of the expected route values match the request (the values parameter) */)
{
result = new VirtualPathData(this, page.VirtualPath);
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
}
Usage
routes.Add(
name: "CustomPage",
item: new CustomPageRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
I have a very simple test in a test project in a solution using ASP MVC V5 and attribute routing. Attribute routing and the MapMvcAttributeRoutes method are part of ASP MVC 5.
[Test]
public void HasRoutesInTable()
{
var routes = new RouteCollection();
routes.MapMvcAttributeRoutes();
Assert.That(routes.Count, Is.GreaterThan(0));
}
This results in:
System.InvalidOperationException :
This method cannot be called during the applications pre-start initialization phase.
Most of the answers to this error message involve configuring membership providers in the web.config file. This project has neither membership providers or a web.config file so the error seems be be occurring for some other reason. How do I move the code out of this "pre-start" state so that the tests can run?
The equivalent code for attributes on ApiController works fine after HttpConfiguration.EnsureInitialized() is called.
I recently upgraded my project to ASP.NET MVC 5 and experienced the exact same issue. When using dotPeek to investigate it, I discovered that there is an internal MapMvcAttributeRoutes extension method that has a IEnumerable<Type> as a parameter which expects a list of controller types. I created a new extension method that uses reflection and allows me to test my attribute-based routes:
public static class RouteCollectionExtensions
{
public static void MapMvcAttributeRoutesForTesting(this RouteCollection routes)
{
var controllers = (from t in typeof(HomeController).Assembly.GetExportedTypes()
where
t != null &&
t.IsPublic &&
t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
!t.IsAbstract &&
typeof(IController).IsAssignableFrom(t)
select t).ToList();
var mapMvcAttributeRoutesMethod = typeof(RouteCollectionAttributeRoutingExtensions)
.GetMethod(
"MapMvcAttributeRoutes",
BindingFlags.NonPublic | BindingFlags.Static,
null,
new Type[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
null);
mapMvcAttributeRoutesMethod.Invoke(null, new object[] { routes, controllers });
}
}
And here is how I use it:
public class HomeControllerRouteTests
{
[Fact]
public void RequestTo_Root_ShouldMapTo_HomeIndex()
{
// Arrange
var routes = new RouteCollection();
// Act - registers traditional routes and the new attribute-defined routes
RouteConfig.RegisterRoutes(routes);
routes.MapMvcAttributeRoutesForTesting();
// Assert - uses MvcRouteTester to test specific routes
routes.ShouldMap("~/").To<HomeController>(x => x.Index());
}
}
One problem now is that inside RouteConfig.RegisterRoutes(route) I cannot call routes.MapMvcAttributeRoutes() so I moved that call to my Global.asax file instead.
Another concern is that this solution is potentially fragile since the above method in RouteCollectionAttributeRoutingExtensions is internal and could be removed at any time. A proactive approach would be to check to see if the mapMvcAttributeRoutesMethod variable is null and provide an appropriate error/exceptionmessage if it is.
NOTE: This only works with ASP.NET MVC 5.0. There were significant changes to attribute routing in ASP.NET MVC 5.1 and the mapMvcAttributeRoutesMethod method was moved to an internal class.
In ASP.NET MVC 5.1 this functionality was moved into its own class called AttributeRoutingMapper.
(This is why one shouldn't rely on code hacking around in internal classes)
But this is the workaround for 5.1 (and up?):
public static void MapMvcAttributeRoutes(this RouteCollection routeCollection, Assembly controllerAssembly)
{
var controllerTypes = (from type in controllerAssembly.GetExportedTypes()
where
type != null && type.IsPublic
&& type.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)
&& !type.IsAbstract && typeof(IController).IsAssignableFrom(type)
select type).ToList();
var attributeRoutingAssembly = typeof(RouteCollectionAttributeRoutingExtensions).Assembly;
var attributeRoutingMapperType =
attributeRoutingAssembly.GetType("System.Web.Mvc.Routing.AttributeRoutingMapper");
var mapAttributeRoutesMethod = attributeRoutingMapperType.GetMethod(
"MapAttributeRoutes",
BindingFlags.Public | BindingFlags.Static,
null,
new[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
null);
mapAttributeRoutesMethod.Invoke(null, new object[] { routeCollection, controllerTypes });
}
Well, it's really ugly and I'm not sure if it'll be worth the test complexity, but here's how you can do it without modifying your RouteConfig.Register code:
[TestClass]
public class MyTestClass
{
[TestMethod]
public void MyTestMethod()
{
// Move all files needed for this test into a subdirectory named bin.
Directory.CreateDirectory("bin");
foreach (var file in Directory.EnumerateFiles("."))
{
File.Copy(file, "bin\\" + file, overwrite: true);
}
// Create a new ASP.NET host for this directory (with all the binaries under the bin subdirectory); get a Remoting proxy to that app domain.
RouteProxy proxy = (RouteProxy)ApplicationHost.CreateApplicationHost(typeof(RouteProxy), "/", Environment.CurrentDirectory);
// Call into the other app domain to run route registration and get back the route count.
int count = proxy.RegisterRoutesAndGetCount();
Assert.IsTrue(count > 0);
}
private class RouteProxy : MarshalByRefObject
{
public int RegisterRoutesAndGetCount()
{
RouteCollection routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes); // or just call routes.MapMvcAttributeRoutes() if that's what you want, though I'm not sure why you'd re-test the framework code.
return routes.Count;
}
}
}
Mapping attribute routes needs to find all the controllers you're using to get their attributes, which requires accessing the build manager, which only apparently works in app domains created for ASP.NET.
What are you testing here? Looks like you are testing a 3rd party extension method. You shouldn't be using your unit tests to test 3rd party code.
I am trying to use the Unity container to make it easier to unit test my controllers. My controller uses a constructor that accepts an interface to a Repository. In the global.asax file, I instantiate a UnityContainerFactory and register it with the MVC framework and then register the repository and its implementation. I added the [Dependency] attribute to the controller’s CTOR Repository parameter. This all seems to work OK, except that occasionally the factory’s GetControllerInstance(Type controllerType) is called more than once and is passed a null argument as the controllerType.
The first call to the factory is aways correct and the controllerType “ProductsController” is passed-in as an argument. But sometimes, the factory is called a couple more times after the view has been displayed with a null value for the controller and I am not sure why. When the correct value of the controller type is passed that “Call Stack” makes sense to me, but when a null is passed, I am not sure why or who is making the call. Any ideas?
The code and call stacks for the example are shown below.
Call Stack when is works
Test.DLL!Test.UnityHelpers.UnityControllerFactory.GetControllerInstance(System.Type controllerType = {Name = "ProductsController" FullName = "Test.Controllers.ProductsController"}) Line 23 C#
Test.DLL!Test._Default.Page_Load(object sender = {ASP.default_aspx}, System.EventArgs e = {System.EventArgs}) Line 18 + 0x1a bytes C#
Call Stack when NULL is passed at the controllerType
Test.DLL!Test.UnityHelpers.UnityControllerFactory.GetControllerInstance(System.Type controllerType = null) Line 27 C#
First I created a UnityControllerFactory
public class UnityControllerFactory : DefaultControllerFactory
{
UnityContainer container;
public UnityControllerFactory(UnityContainer container)
{
this.container = container;
}
protected override IController GetControllerInstance(Type controllerType)
{
if (controllerType != null)
{
return container.Resolve(controllerType) as IController;
}
else
{
return null; // I never expect to get here, but I do sometimes, the callstack does not show the caller
}
}
}
Next, I added the following code the global.asax file to instantiate the container factory
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
// Create Unity Container if needed
if (_container == null)
{
_container = new UnityContainer();
}
// Instantiate a new factory
IControllerFactory unityControllerFactory = new UnityControllerFactory(_container);
// Register it with the MVC framework
ControllerBuilder.Current.SetControllerFactory(unityControllerFactory);
// Register the SqlProductRepository
_container.RegisterType<IProductsRepository, SqlProductRepository>
(new ContainerControlledLifetimeManager());
}
The app has one controller
public class ProductsController : Controller
{
public IProductsRepository productsRepository;
public ProductsController([Dependency]IProductsRepository productsRepository)
{
this.productsRepository = productsRepository;
}
}
This is likely due to some file type not mapping to a controller in your routes. (images, for example). This will happen more often when you are debugging locally with Cassini in my experience since Cassini allows all requests to route through ASP.NET while in IIS a lot of requests are handled by IIS for you. This would also be why you don't see your code in the stack for this request. If you turn off the "Just My Code" option in Visual Studio, you can sometimes get a better hint about these things.
This is not the only reason this can happen, though, but it's common.
The appropriate thing to do would be to allow the base method handle the request in these situations. It's usually just a simple file request and shouldn't have any impact on you.
Simplest thing to do would be to gate it like this:
if (controllerType != null)
{
return container.Resolve(controllerType) as IController;
}
else
{
return base.GetControllerInstance(requestContext, controllerType);
}
That ought to do it.
To see what the request is for, you might be able to check HttpContext.Current.Request to see what file is not in your route. A lot of times it's not something you care to control, but it'll make you feel better to know what the origin of the request is.
I am trying to come up with an approach to create "dynamic" routing. What I mean, exactly, is that I want to be able to assign the controller and action of a route for each hit rather than having it mapped directly.
For example, a route may look like this "path/{object}" and when that path is hit, a lookup is performed providing the appropriate controller / action to call.
I've tried discovering the mechanisms for creating a custom route handler, but the documentation / discoverability is a bit shady at the moment (I know, its beta - I wouldn't expect any more). Although, I'm not sure if thats even the best approach and perhaps a controller factory or even a default controller/action that performs all of the mappings may be the best route (no pun intended) to go.
Any advice would be appreciated.
You can always use a catch all syntax ( I have no idea if the name is proper).
Route:
routeTable.MapRoute(
"Path",
"{*path}",
new { controller = "Pages", action = "Path" });
Controller action is defined as:
public ActionResult Path(string path)
In the action for controller you will have a path, so just have to spilt it and analyse.
To call another controller you can use a RedirectToAction ( I think this is more proper way). With redirection you can set up a permanent redirectionfor it.
Or use a something like that:
internal class MVCTransferResult : RedirectResult
{
public MVCTransferResult(string url) : base(url)
{
}
public MVCTransferResult(object routeValues)
: base(GetRouteURL(routeValues))
{
}
private static string GetRouteURL(object routeValues)
{
UrlHelper url = new UrlHelper(
new RequestContext(
new HttpContextWrapper(HttpContext.Current),
new RouteData()),
RouteTable.Routes);
return url.RouteUrl(routeValues);
}
public override void ExecuteResult(ControllerContext context)
{
var httpContext = HttpContext.Current;
// ASP.NET MVC 3.0
if (context.Controller.TempData != null &&
context.Controller.TempData.Count() > 0)
{
throw new ApplicationException(
"TempData won't work with Server.TransferRequest!");
}
// change to false to pass query string parameters
// if you have already processed them
httpContext.Server.TransferRequest(Url, true);
// ASP.NET MVC 2.0
//httpContext.RewritePath(Url, false);
//IHttpHandler httpHandler = new MvcHttpHandler();
//httpHandler.ProcessRequest(HttpContext.Current);
}
}
However this method require to run on IIS or a IIS Expres Casinni is not supporting a Server.Transfer method