简体   繁体   中英

Mapping to an ASMX service using routing in ASP.NET MVC

I wonder if there's any way to map a URL to ASMX service much like it is done with pages (using routes.MapPageRoute() method).

When I tried to do it simply by pointing the MapPageRoute to my service I get the error

Type 'MvcApplication1.Services.EchoService' does not inherit from 'System.Web.UI.Page'.

Matthias.

I stumbled upon this question trying to find the answer myself, and since I did figure out a way to do it, I figured I'd answer it.

The reason I needed this is because I'm converting an old ASP.NET website to ASP.NET MVC, and for compatibility purposes I need a web service available at a specific URL. However, the path of that URL is now handled by a Controller in the new site, so I cannot have a physical directory with the same name (since that will prevent the controller from being invoked for other URLs with that path other than the web service).

The PageRouteHandler , which is used by RouteCollection.MapPageRoute , indeed requires that the handler for the target path derives from System.Web.Page , which isn't the case for web services. So instead, it is necessary to create a custom page handler:

using System;
using System.Web;
using System.Web.Routing;
using System.Web.Services.Protocols;

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));
    }
}

This route handler will create an appropriate handler for the web service based on the request and mapped virtual path of the service.

You can add a route for a web service now as follows:

routes.Add("RouteName", new Route("path/to/your/service", new RouteValueDictionary() { { "controller", null }, { "action", null } }, new ServiceRouteHandler("~/actualservice.asmx")));

Note: you must specify the controller and action values in the route value dictionary (even though they're set to null), otherwise the Html.ActionLink helper will always use this route for every single link (unless a match was found in the list before this route). Since you probably want to add this route before the default MVC route, it's important that it doesn't get matched that way.

Of course, you can create your own extension method to alleviate this task:

public static Route MapServiceRoute(this RouteCollection routes, string routeName, string url, string virtualPath)
{
    if( routes == null )
        throw new ArgumentNullException("routes");
    Route route = new Route(url, new RouteValueDictionary() { { "controller", null }, { "action", null } }, new ServiceRouteHandler(virtualPath));
    routes.Add(routeName, route);
    return route;
}

After which you can simply do:

routes.MapServiceRoute("RouteName", "path/to/your/service", "~/actualservice.asmx");

I hope this helps someone, despite the age of this question. :)

Now that we waited two years with an anwer, how about using Web API instead? :)

EDIT: Kidding aside if that doesn't work for you and you still need an answer, leave a comment and I will see if I can't come up with a better one.

I attempted the original post's solution ( also posted here ), but I encountered a serious problem. I couldn't target the web method within the web service. When attempting to do so I got an exception stating the file didn't exist.

If you truly want to map an MVC route to a .ASMX web service the solution is explained here .

I believe that solution to be a hack by abusing the built-in types, because it uses reflection to bypass the intentional restrictive access members on the built-in .NET types.

Here is the method I'm taking which I believe to be much more straightforward.

  1. First off, you should design your web services in the .ASMX file so that all the web service does is act as a published interface. At that point we don't need to target the .ASMX web service's methods directly. The important code has been made re-useable in core classes that are agnostic to the application's entry-point. We need this anyway so we can run automated tests!

  2. Replace the MVC's web service method with a new route that has a custom route handler and http handler.

Old Route:

        routes.MapRoute(
            "Lead",
            "lead/{action}.mvc",
            new { controller = "Lead" });

New Route:

        var dict = new RouteValueDictionary
                   {
                       { "controller", null },
                       { "action", null }
                   };
        var handler = new LeadRouteHandler();
        var route = new Route("lead/MVC_General.mvc", dict, handler);
        routes.Add("Lead", route);

Note that the new route has the action hard-coded "MVC_General". This is because I wish to improve upon the giant controller class and create a handler for each action that way I can have small class with a single responsibility for each web method.

  1. Implement the route's handlers.

IRouteHandler:

public class LeadRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new LeadHttpHandler();
    }
}

IHttpHandler:

public class LeadHttpHandler : IHttpHandler
{

    public bool IsReusable
    {
        get { return false; }
    }

    public void ProcessRequest(HttpContext context)
    {
        // Just enough code to preserve the route's json interface for tests
        var typedResult = new PsaLeadSubmissionResult();
        typedResult.Registered = false;
        typedResult.Message = new List<string>
                              {
                                  "Not Implemented"
                              };

        var jsonResult = JsonConvert.SerializeObject(typedResult);

        context.Response.Write(jsonResult);
    }
}

From within IHttpHandler's ProcessRequest method we gain full control over that route's action. With a well designed web service architecture all we need to do is call the class's that support the .ASMX web method you are trying to map the route to.

The result is a very clean Global.asax file. We could have done all of this without the URL routing just by manually inspecting the URL, but that's too important of a file to bloat.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM