NOTE: There is no MVC in this code. Pure old Web Forms and .asmx Web Service.
I have inherited a large scale ASP.NET Web Forms & Web Service (.asmx) application at my new company.
Due to some need I am trying to do URL Routing for all Web Forms, which I was successfully able to do.
Now for .asmx, routes.MapPageRoute does not work. Based on the below article, I created an IRouteHandler class. Here's how the code looks:
using System;
using System.Web;
using System.Web.Routing;
using System.Web.Services.Protocols;
using System.Collections.Generic;
public class ServiceRouteHandler : IRouteHandler
{
private readonly string _virtualPath;
private readonly WebServiceHandlerFactory _handlerFactory = new WebServiceHandlerFactory();
public ServiceRouteHandler(string virtualPath)
{
if (virtualPath == null)
throw new ArgumentNullException("virtualPath");
if (!virtualPath.StartsWith("~/"))
throw new ArgumentException("Virtual path must start with ~/", "virtualPath");
_virtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
// Note: can't pass requestContext.HttpContext as the first parameter because that's
// type HttpContextBase, while GetHandler wants HttpContext.
return _handlerFactory.GetHandler(HttpContext.Current, requestContext.HttpContext.Request.HttpMethod, _virtualPath, requestContext.HttpContext.Server.MapPath(_virtualPath));
}
}
http://mikeoncode.blogspot.in/2014/09/aspnet-web-forms-routing-for-web.html
Now when I do routing via Global.asax, it work for the root documentation file but does not work with the Web Methods inside my .asmx files.
routes.Add("myservice", new System.Web.Routing.Route("service/sDxcdfG3SC", new System.Web.Routing.RouteValueDictionary() { { "controller", null }, { "action", null } }, new ServiceRouteHandler("~/service/myoriginal.asmx")));
routes.MapPageRoute("", "service/sDxcdfG3SC", "~/service/myoriginal.asmx");
Goal
I would like to map an .asmx Web Method URL such as www.website.com/service/myservice.asmx/fetchdata to a URL with obscured names in it like www.website.com/service/ldfdsfsdf/dsd3dfd3d using .NET Routing.
How can this be done?
It is slightly more tricky to do this with routing than in the article you posted because you don't want the incoming URL to have a query string parameter and it looks like the WebServiceHandler won't call the method without an ?op=Method parameter.
So, there are a few parts to this:
A custom route (ServiceRoute) to do URL rewriting to add the ?op=Method parameter
An IRouteHandler to wrap the WebServiceHandlerFactory that calls the web service.
A set of extension methods to make registration easy.
ServiceRoute
public class ServiceRoute : Route
{
public ServiceRoute(string url, string virtualPath, RouteValueDictionary defaults, RouteValueDictionary constraints)
: base(url, defaults, constraints, new ServiceRouteHandler(virtualPath))
{
this.VirtualPath = virtualPath;
}
public string VirtualPath { get; private set; }
public override RouteData GetRouteData(HttpContextBase httpContext)
{
// Run a test to see if the URL and constraints don't match
// (will be null) and reject the request if they don't.
if (base.GetRouteData(httpContext) == null)
return null;
// Use URL rewriting to fake the query string for the ASMX
httpContext.RewritePath(this.VirtualPath);
return base.GetRouteData(httpContext);
}
}
ServiceHandler
public class ServiceRouteHandler : IRouteHandler
{
private readonly string virtualPath;
private readonly WebServiceHandlerFactory handlerFactory = new WebServiceHandlerFactory();
public ServiceRouteHandler(string virtualPath)
{
if (virtualPath == null)
throw new ArgumentNullException(nameof(virtualPath));
if (!virtualPath.StartsWith("~/"))
throw new ArgumentException("Virtual path must start with ~/", "virtualPath");
this.virtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
// Strip the query string (if any) off of the file path
string filePath = virtualPath;
int qIndex = filePath.IndexOf('?');
if (qIndex >= 0)
filePath = filePath.Substring(0, qIndex);
// Note: can't pass requestContext.HttpContext as the first
// parameter because that's type HttpContextBase, while
// GetHandler expects HttpContext.
return handlerFactory.GetHandler(
HttpContext.Current,
requestContext.HttpContext.Request.HttpMethod,
virtualPath,
requestContext.HttpContext.Server.MapPath(filePath));
}
}
RouteCollectionExtensions
public static class RouteCollectionExtensions
{
public static void MapServiceRoutes(
this RouteCollection routes,
Dictionary<string, string> urlToVirtualPathMap,
object defaults = null,
object constraints = null)
{
foreach (var kvp in urlToVirtualPathMap)
MapServiceRoute(routes, null, kvp.Key, kvp.Value, defaults, constraints);
}
public static Route MapServiceRoute(
this RouteCollection routes,
string url,
string virtualPath,
object defaults = null,
object constraints = null)
{
return MapServiceRoute(routes, null, url, virtualPath, defaults, constraints);
}
public static Route MapServiceRoute(
this RouteCollection routes,
string routeName,
string url,
string virtualPath,
object defaults = null,
object constraints = null)
{
if (routes == null)
throw new ArgumentNullException("routes");
Route route = new ServiceRoute(
url: url,
virtualPath: virtualPath,
defaults: new RouteValueDictionary(defaults) { { "controller", null }, { "action", null } },
constraints: new RouteValueDictionary(constraints)
);
routes.Add(routeName, route);
return route;
}
}
Usage
You can either use MapServiceRoute to add the routes one at a time (with an optional name):
public static class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
var settings = new FriendlyUrlSettings();
settings.AutoRedirectMode = RedirectMode.Permanent;
routes.EnableFriendlyUrls(settings);
routes.MapServiceRoute("AddRoute", "service/ldfdsfsdf/dsd3dfd3d", "~/service/myoriginal.asmx?op=Add");
routes.MapServiceRoute("SubtractRoute", "service/ldfdsfsdf/dsd3dfd3g", "~/service/myoriginal.asmx?op=Subtract");
routes.MapServiceRoute("MultiplyRoute", "service/ldfdsfsdf/dsd3dfd3k", "~/service/myoriginal.asmx?op=Multiply");
routes.MapServiceRoute("DivideRoute", "service/ldfdsfsdf/dsd3dfd3v", "~/service/myoriginal.asmx?op=Divide");
}
}
Alternatively, you can call MapServiceRoutes to map a batch of your web service routes at once:
public static class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
var settings = new FriendlyUrlSettings();
settings.AutoRedirectMode = RedirectMode.Permanent;
routes.EnableFriendlyUrls(settings);
routes.MapServiceRoutes(new Dictionary<string, string>
{
{ "service/ldfdsfsdf/dsd3dfd3d", "~/service/myoriginal.asmx?op=Add" },
{ "service/ldfdsfsdf/dsd3dfd3g", "~/service/myoriginal.asmx?op=Subtract" },
{ "service/ldfdsfsdf/dsd3dfd3k", "~/service/myoriginal.asmx?op=Multiply" },
{ "service/ldfdsfsdf/dsd3dfd3v", "~/service/myoriginal.asmx?op=Divide" },
});
}
}
NOTE: If you were to have MVC in the application, you should generally register your MVC routes after these routes.
References:
Creating a route for a .asmx Web Service with ASP.NET routing
.NET 4 URL Routing for Web Services (asmx)
Not a direct answer but something worth considering.
You could possibly upgrade your ASMX service to a WCF service with compatible contract so that you don't have to upgrade your clients at all.
With that, you could use a known technique to dynamically route WCF services. Since this known technique involves an arbitrary address for your service, you can bind the WCF service to a .......foo.asmx endpoint address so that your clients not only don't upgrade their client proxies but also they have exactly the same endpoint address.
In other words, to a client, your dynamically routed WCF service looks 1-1 identical as your old ASMX service.
We've succesfully used this technique over couple of last years to upgrade most of all old ASMXes to WCFs, preserving client proxies in many cases.
All technical details are documented in my blog entry
http://www.wiktorzychla.com/2014/08/dynamic-wcf-routing-and-easy-upgrading.html
The article you are referencing is not to provide extensionless routing to asmx WS, is to provide routing from server/whateverYouAre/ws.asmx to server/ws.asmx (the real resource location). This allows JS use local path (current location) to invoque the asmx without worry abot where the browser are.
Anyway, maybe, just maybe, you can use the article as starting point. I never do this so it just a guess:
There are 2 modes to consume your WS. If the client is using SOAP the request URL will be:
/server/service/myoriginal.asmx
with SOAPAction http header and the SOAP XML in the POST body. Your current routing solution should work. BUT if you are consuming the WS though raw HTTP GET/POST (i.e. from a webBrowser) the url of each webMethod is:
/server/service/myoriginal.asmx/webMethod
So I think you could to provide some url routing in the form of:
routes.Add("myservice", new System.Web.Routing.Route("service/sDxcdfG3SC/{webMethod}", new System.Web.Routing.RouteValueDictionary() { { "controller", null }, { "action", null } }, new ServiceRouteHandler("~/service/myoriginal.asmx")));
//Delete routes.MapPageRoute("", "service/sDxcdfG3SC", "~/service/myoriginal.asmx"); from your code, it is wrong even in your actual solution
and modify GetHttpHandler:
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return _handlerFactory.GetHandler(HttpContext.Current, requestContext.HttpContext.Request.HttpMethod, _virtualPath + "\" + requestContext.RouteData.Values["webMethod"], requestContext.HttpContext.Server.MapPath(_virtualPath));
}
to provide the raw URL of the requested resource in the form of /server/service/myoriginal.asmx/webMethod.
My code is write on the fly from the top of my head so just make sure _virtualPath + "/" + requestContext.RouteData.Values["webMethod"] create the right URL before a early rage quit ;-) Modify it if needed.
With some luck; WebServiceHandlerFactory should be able to locate the physical resource and, inspecting the raw URL, execute the webMethod by its name.
If the site is hosted in IIS you could use the IIS URL Rewrite to create a friendly url and redirect it to your internal path as per creating-rewrite-rules-for-the-url-rewrite-module. Each of these rules are stored in the web.config so can be managed within your development environment
The drawback (or benefit depending upon your usage) is that the original path would still be accessible
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 }
);
}
}
I'd like to have a catch all route that matches a general route prefix ("api/myaccount/1") execute if there are no more specific routes defined on other controllers (i.e "api/myaccount/1/feature") however I get the following exception when I do this:
Multiple controller types were found that match the URL. This can
happen if attribute routes on multiple controllers match the requested
URL.
As mentioned here:
Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL it seems this may not be possible.
Wanting a default route to execute when no better one is found sounds pretty common so what am I missing? Do I need to hook lower in the pipeline or something...
FYI: I have the catch all working fine ("api/myaccount/1/{*uri}") it's just the being able to override it that's the problem.
Turns out this is pretty easy, I just needed to create a custom Controller Selector and override the GetControllerName function. That particular override is required because the method you would expect to override:
HttpControllerDescriptor SelectController(HttpRequestMessage request)
does not just return the descriptor (or null if It can't find a match) as you may expect. The method actually handles the request for you and returns a 404 :/ However once you're aware of that it is trivial to work around and I was able to get the behavior I wanted using the code below:
using System.Web.Http;
using System.Web.Http.Dispatcher;
public class CustomControllerSelector : DefaultHttpControllerSelector
{
public override string GetControllerName(HttpRequestMessage request)
{
var name = base.GetControllerName(request);
if(string.IsNullOrEmpty(name))
{
return "MyFeature"; //important not to include "Controller" suffix
}
return name;
}
}
And add it to your configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
...
config.Services.Replace(typeof(IHttpControllerSelector),
new CustomControllerSelector(config));
...
}
}
I use Web API 2 Attribute Routing in my project to provide JSON interface over my data. I am facing weird behaviour of controller selection, not decided yet whether it's a bug or a feature :)
Let me describe my approach.
I would like to simulate OData syntax with help of attribute routing (direct OData usage has been refused due to design principles). For example, to get entity with id=5 I use HTTP GET request to URI http://mydomain.com/api/Entity(5) . I expect to use the same URI with HTTP PUT verb to update the entity. This is where the journey begins...
I would like to have separate controller for getting entities (FirstController in the example provided below) and another one for modifying entities (SecondController). Both controllers handles the same URI (e.g. http://mydomain.com/api/Entity(5)) the only difference is HTTP verb used with the URI - GET should be handled by FirstController, PUT should be handled by SecondController. But the URI is handled by none of them; instead HTTP 404 error is returned.
When I "merge" GET and PUT actions to only one controller (commented out in FirstController), both verbs are handled correctly.
I am using IIS Express and all conventional routes are disabled, only attribute routing is in charge.
It looks like the controller selection process does not work with HTTP verb. In another words, HttpGet and HttpPut attributes just limit action usage but they do not serve as criteria during controller selection. I am not so familiar with MVC / Web API fundamentals, so let me ask you my big question:
Is the behaviour, described herein before, a feature intentionally implemented by MVC / Web API 2 or a bug to be fixed?
If it is considered as a feature, it prevents me to follow design principles. I can live with "merged" controllers but still considering it as a bad practice...
Or am I missing something in my train of thought?
My environment setup:
Windows 7 (virtual machine using Oracle VirtualBox)
Visual Studio 2013
.NET 4.5.1
Web API 2
The following is implementation of FirstController class:
public class FirstController : ApiController
{
[HttpGet]
[Route("api/Entity({id:int})")]
public Output GetEntity(int id)
{
Output output = new Output() { Id = id, Name = "foo" };
return output;
}
//[HttpPut]
//[Route("api/Entity({id:int})")]
//public Output UpdateEntity(int id, UpdateEntity command)
//{
// Output output = new Output() { Id = id, Name = command.Name };
// return output;
//}
}
The following is implementation of SecondController class:
public class SecondController : ApiController
{
[HttpPut]
[Route("api/Entity({id:int})")]
public Output UpdateEntity(int id, UpdateEntity command)
{
Output output = new Output() { Id = id, Name = command.Name };
return output;
}
}
The following is implementation of a console application to test the described behaviour:
class Program
{
static void Main(string[] args)
{
// HTTP client initialization
HttpClient httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("http://localhost:1567");
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// HTTP GET - FirstController.GetEntity
HttpResponseMessage getEntityResponse = httpClient.GetAsync("/api/Entity(5)").Result;
Output getOutput = getEntityResponse.Content.ReadAsAsync<Output>().Result;
// HTTP PUT - SecondController.UpdateEntity
UpdateEntity updateCommand = new UpdateEntity() { Name = "newEntityname" };
HttpResponseMessage updateEntityResponse = httpClient.PutAsJsonAsync("/api/Entity(10)", updateCommand).Result;
Output updateOutput = updateEntityResponse.Content.ReadAsAsync<Output>().Result;
}
}
For completion, the following are used DTOs:
public class UpdateEntity
{
public string Name { get; set; }
}
public class Output
{
public int Id { get; set; }
public string Name { get; set; }
}
Thanks in advance for your responses,
Jan Kacina
This design was intentional as we thought it to be an error case where a user would be having same route template on different controllers which can cause ambiguity in the selection process.
Also if we keep aside attribute routing, how would this work with regular routing? Let's imagine we have 2 regular routes where first one is targeted for FirstController and the second to SecondController. Now if a request url is like api/Entity(5), then Web API would always match the 1st route in the route table which would always hit the FirstController and would never reach SecondController. Remember that once Web API matches a route it tries to go till the action selection process and if the action selection process doesn't result in an action being selected, then an error response is sent to the client. You probably are assuming that if an action is not selected in one controller then Web API would route it to the next one in the route configuration. This is incorrect.
Route probing occurs only once and if it results in a match, then the next steps take place...that is controller and action selection. Hope this helps.
I'm facing an issue with the execution order of the ASP.NET Web API request pipeline.
According to the ASP.NET Web API documentation (available here), global message handlers are supposed to be executed before the routing mechanism.
On this image, MessageHandler1 is a global message handler whereas MessageHandler2 is specific to Route 2.
I created a very simple example to show that there seems to be an issue in the execution order… or I'm really missing something important.
I have this controller
public class FooController : ApiController {
[HttpPut]
public string PutMe() {
return Request.Method.Method;
}
}
It only accepts PUT requests.
The application is configured as such:
protected void Application_Start() {
var configuration = GlobalConfiguration.Configuration;
configuration.MessageHandlers.Add( new SimpleMethodOverrideHandler() );
configuration.Configuration.Routes.MapHttpRoute(
name: "Foo",
routeTemplate: "api/foo",
defaults: new { controller = "foo", action = "putme" },
constraints: new { put = new HttpPutOnlyConstraint() }
);
}
SimpleMethodOverrideHandler is a very simple DelegatingHandler that just changed the request's method according to a "method" parameter in the query string.
public class SimpleMethodOverrideHandler : DelegatingHandler {
protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken ) {
var method = request.RequestUri.ParseQueryString()["method"];
if( !string.IsNullOrEmpty( method ) ) {
request.Method = new HttpMethod( method );
}
return base.SendAsync( request, cancellationToken );
}
}
So basically, requesting /api/foo?method=put in my browser would fire up FooController's PutMe method.
Indeed, as seen earlier, the message handler treats the requests before it gets passed to the HttpRoutingDispatched.
Finally, here's how the the constaint HttpPutOnlyConstraint is defined:
public class HttpPutOnlyConstraint : IHttpRouteConstraint {
public bool Match( HttpRequestMessage request,
IHttpRoute route,
string parameterName,
IDictionary<string, object> values,
HttpRouteDirection routeDirection ) {
return request.Method == HttpMethod.Put;
}
}
Well the problem is that when I request /api/foo?method=put within my browser, the program first enters HttpPutOnlyConstraint's Match method, which is wrong.
If we refer to the previously linked image, the message handler is supposed to be executed first, unfortunately it is not.
So, of course, Match returns false and no controller/action is found for the request, 404 happens.
If I remove the constraint from the route definition, the program enters SimpleMethodOverrideHandler, the request's method gets changed successfully and it is able to match and execute my controller's method.
Am I doing something wrong? Is there a secret configuration parameter to know in order to do such things? :-)
If anyone needs the whole project, it's available here [7KB zip file].
Thank you.
You are confusing routing engine with Web API pipeline. HttpRoutingDispatcher is not a routing engine concept. The route constraints will be processed first because your underlying host needs to build a route table and match the route for your request.
HttpRoutingDispatcher is simply another implementation of HttpMessageHandler and all it does is it examines the IHttpRoute of the route that has been matched, and chooses which message handler to call next. If there is no per-route handler present, it delegates the processing to HttpControllerDispatcher.