UNIT TESTING: ROUTE CONSTRAINTS

Một phần của tài liệu Giáo trình lập trình ASP.NET Apress pro ASP NET MVC3 framework pre release (Trang 323 - 329)

When testing constrained routes it is important to test for both the URLs that will match and the URLs you are trying to exclude – we can do this using the helper methods that we introduced at the start of the chapter. As an example, here is the test method that we used to test the route defined in Listing 11-20:

[TestMethod]

public void TestIncomingRoutes() { TestRouteMatch("~/", "Home", "Index");

TestRouteMatch("~/Home", "Home", "Index");

TestRouteMatch("~/Home/Index", "Home", "Index");

TestRouteMatch("~/Home/About", "Home", "About");

TestRouteMatch("~/Home/About/MyId", "Home", "About", new { id = "MyId" });

TestRouteMatch("~/Home/About/MyId/More/Segments", "Home", "About", new {

id = "MyId",

catchall = "More/Segments"

});

TestRouteFail("~/Home/OtherAction");

TestRouteFail("~/Account/Index");

TestRouteFail("~/Account/About");

}

Our helper methods also include support for testing HTTP method constraints – we just need to pass the HTTP method we want to test as a parameter to the TestRouteMatch and TestRouteFail methods, as shown in this test method, which tests the route defined in Listing 11- 21:

[TestMethod]

public void RegisterRoutesTest() {

TestRouteMatch("~/", "Home", "Index", null, "GET");

TestRouteFail("~/", "POST");

}

If you don’t specify an HTTP method, then the helper methods default to the GET method.

Defining a Custom Constraint

If the standard constrains are not sufficient to your needs, you can define your own custom constraints by implementing the IRouteConstraint interface. Listing 11-23 demonstrates a custom constraint that operates on the user-agent information provided by a browser as part of a request.

Listing 11-23. Creating a custom route constraint using System.Web;

using System.Web.Routing;

namespace URLsAndRoutes.Infrastructure {

public class UserAgentConstraint : IRouteConstraint { private string requiredUserAgent;

public UserAgentConstraint(string agentParam) { requiredUserAgent = agentParam;

}

public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {

return httpContext.Request.UserAgent != null &&

httpContext.Request.UserAgent.Contains(requiredUserAgent);

} } }

The IRouteConstraint interface defines the Match method, which an implementation can use to indicate to the routing system if its constraint has been satisfied. The parameters provided to the Match method provide access to the request from the client, the route that is being evaluated, the parameter name of the constraint, the segment variables extracted from the URL and details of whether the request is to check an incoming or outgoing URL. For our example, we check the value of the UserAgent property of the client request to see if it contains a value that was passed to our constructor. Listing 11-24 shows our custom constraint used in a route.

Listing 11-24. Applying a custom constraint in a route public static void RegisterRoutes(RouteCollection routes) {

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new {

controller = "^H.*", action = "Index|About",

httpMethod = new HttpMethodConstraint("GET", "POST"), customConstraint = new UserAgentConstraint("IE") },

new[] { "URLsAndRoutes.Controllers" });

}

In the listing, we have constrained the route so that it will only match requests made from browsers whose user-agent string contains IE, which includes requests from Microsoft browsers.

Note To be clear, because this is the kind of thing we get letters about, we are not suggesting that you restrict your application so that it supports only one kind of browser. We have used user-agent strings solely to demonstrate custom route constraints and believe in equal opportunities for all browsers. We really hate web sites that try to force their preference for browsers on users.

Routing Requests for Disk Files

Not all of the requests for an MVC application are for controllers and actions – we still need a way to serve content such as images, static HTML files, JavaScript libraries and so on. As a demonstration, we have created a file in the Content folder of our example MVC application called StaticContent.html, the contents of which are shown in Listing 11-25.

Listing 11- 25. The StaticContent.html file

<html>

<head><title>Static HTML Content</title></head>

<body>This is the static html file (~/Content/StaticContent.html)</body>

</html>

The routing system provides integrated support for serving such content - if we start the application and request the URL /Content/StaticContent.html, then we will see the contents of this simple HTML file displayed in the browser, as shown by Figure 11-6.

Figure 11-6. Requesting the static content file

By default, the routing system checks to see if a URL matches a disk file before

evaluating the application’s routes. If there is a match, then the disk file is served and the routes are never used. We can reverse this behavior so that our routes are evaluated before disk files are checked – we do this by setting the RouteExistingFiles property of the RouteCollection to true, as shown in Listing 11-26.

Listing 11-26. Enabling route evaluation before file checking public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true;

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new {

controller = "^H.*", action = "Index|About",

httpMethod = new HttpMethodConstraint("GET", "POST"), customConstraint = new UserAgentConstraint("IE") },

new[] { "URLsAndRoutes.Controllers" });

}

The convention is to place this statement close to the top of the RegisterRoutes method – although it will take effect even if you set it after you have defined your routes. Once the property have been set to true, we can define routes that match URLs that correspond to disk files, such as the one shown in Listing 11-27.

Listing 11-27. A route whose URL pattern corresponds to a disk file public static void RegisterRoutes(RouteCollection routes) {

routes.RouteExistingFiles = true;

routes.MapRoute("DiskFile", "Content/StaticContent.html", new {

controller = "Account", action = "LogOn", },

new {

customConstraint = new UserAgentConstraint("IE") });

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new {

controller = "^H.*", action = "Index|About",

httpMethod = new HttpMethodConstraint("GET", "POST"), customConstraint = new UserAgentConstraint("IE") },

new[] { "URLsAndRoutes.Controllers" });

}

This route maps requests for the URL Content/StaticContent.html to the LogOn action of the Account controller. We have added a constraint to the route which means that it will only match requests that are made from browsers whose user-agent string contains Chrome. This is a contrived example to demonstrate combining these features – we are not suggesting you do things like this in a real application.

When the RouteExistingFiles property is enabled, disk files will be delivered to clients only if there is no matching route for the request – for our example route, this means that IE users will get the response from the Account controller while all other users will see the static content. You can see the URL mapping at work in Figure 11-7.

Figure 11-7. Intercepting a request for a disk file using a route

Routing requests intended for disk files requires careful thought – not least because URL patterns will match these kinds of URL as eagerly as any other. For example, a request for /Content/StaticContent.html will be matches by a URL pattern such as {controller}/{action}. Unless you are very careful, you can end up with some exceptionally strange results and reduced performance - enabling this option is very much a last resort. To demonstrate this, we have created a second HTML file in the Content directory, called OtherStaticContent.html and added a new route to the RegisterRoutes method, as shown in Listing 11-28.

Listing 11-28. Adding a route to the RegisterRoutes method public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true;

routes.MapRoute("DiskFile", "Content/StaticContent.html", new {

controller = "Account", action = "LogOn", },

new {

customConstraint = new UserAgentConstraint("IE") });

routes.MapRoute("MyNewRoute", "{controller}/{action}");

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new {

controller = "^H.*", action = "Index|About",

httpMethod = new HttpMethodConstraint("GET", "POST"), customConstraint = new UserAgentConstraint("IE") },

new[] { "URLsAndRoutes.Controllers" });

}

When we request the URL Content/OtherStaticContent.html, our request is matches against the URL we added in Listing 11-28, such that the target controller is Content and the action method is OtherStaticContent.html – this will happen with any request for a file whose URL has two segments. Of course, there is no such controller or action method and the user will be sent a 404 – Not Found error.

Bypassing the Routing System

Setting the RouteExistingFiles property, which we demonstrated in the previous section, makes the routing system more inclusive – i.e. requests that would normally bypass the routing system are now evaluated against the routes we have defined.

The counterpart to this feature is the ability to make the routing system less inclusive and prevent URLs from being evaluated against our routes. We do this using the IgnoreRoute method of the RouteCollection class, as shown in Listing 11-29.

Listing 11-29. Using the IgnoreRoute method

public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true;

routes.MapRoute("DiskFile", "Content/StaticContent.html", new {

controller = "Account", action = "LogOn", },

new {

customConstraint = new UserAgentConstraint("IE") });

routes.IgnoreRoute("Content/{filename}.html");

routes.MapRoute("", "{controller}/{action}");

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new {

controller = "^H.*", action = "Index|About",

httpMethod = new HttpMethodConstraint("GET", "POST"), customConstraint = new UserAgentConstraint("IE") },

new[] { "URLsAndRoutes.Controllers" });

}

We can use segment variables like {filename} to match a range of URLs – in this case, the URL pattern will match any two segment URL where the first segment is Content and the second content has the .html extension.

The IgnoreRoute method creates an entry in the RouteCollection where the route handler is an instance of the StopRoutingHandler class, rather than MvcRouteHandler. The routing system is hard-coded to recognize this handler – if the URL pattern passed to the IgnoreRoute method matches, then no subsequent routes will be evaluated, just as when a regular route is matched. It follows, therefore, that where we place the call to the IgnoreRoute method is significant – in Listing 11-27 we have used this feature to minimize the impact of the RouteExistingFiles property by not routing any requests for HTML files.

Generating Outgoing URLs

Handling incoming URLs is only part of the story. We also need to be able use our URL schema to generate outgoing URLs we can embed in our views, so that the user can click on links and submit forms back to our application in a way that will target the correct controller and action. In this section, we’ll show you different techniques for generating outgoing URLs.

Một phần của tài liệu Giáo trình lập trình ASP.NET Apress pro ASP NET MVC3 framework pre release (Trang 323 - 329)

Tải bản đầy đủ (PDF)

(603 trang)