Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
503 views
in Technique[技术] by (71.8m points)

asp.net - ServiceStack REST API path variables from root throwing exception

I am trying to write a REST web service using ServiceStack that accepts variable paths off of route. For example:

[Route("/{group}"]
public class Entity : IReturn<SomeType> {}

This throws a NotSupported Exception "RestPath '/{collection}' on type Entity is not supported". However, if I change the path as follows (along with the associated path in AppHost configuration) to:

[Route("/rest/{group}"]

It works just fine. In order to integrate with the system that I am working with, I need to use /{group}.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

My current work in progress, a plugin to serve the default page to all 'not found' routes without changing the url in the browser, includes most of what you'll need to handle a global wildcard route. Use it to get you started.

To understand what this code is doing, it helps to understand ServiceStack's routing priority, and how CatchAllHandlers fit into the process. ServiceStack calls ServiceStackHttpHandlerFactory.GetHandler to get the handler for the current route.

ServiceStackHttpHandlerFactory.GetHandler returns:

  1. A matching RawHttpHandler, if any.
  2. If the domain root, the handler returned by GetCatchAllHandlerIfAny(...), if any.
  3. If the route matches a metadata uri (I'm skipping over the exact logic here, as it's not important for your question), the relevant handler, if any.
  4. The handler returned by ServiceStackHttpHandlerFactory.GetHandlerForPathInfo if any.
  5. NotFoundHandler.

ServiceStackHttpHandlerFactory.GetHandlerForPathInfo returns:

  1. If the url matches a valid REST route, a new RestHandler.
  2. If the url matches an existing file or directory, it returns
    • the handler returned by GetCatchAllHandlerIfAny(...), if any.
    • If it's a supported filetype, a StaticFileHandler,
    • If it's not a supported filetype, the ForbiddenHttpHandler.
  3. The handler returned by GetCatchAllHandlerIfAny(...), if any.
  4. null.

The CatchAllHandlers array contains functions that evaluate the url and either return a handler, or null. The functions in the array are called in sequence and the first one that doesn't return null handles the route. Let me highlight some key elements:

First, the plugin adds a CatchAllHandler to the appHost.CatchAllHandlers array when registered.

    public void Register(IAppHost appHost)
    {
        appHost.CatchAllHandlers.Add((string method, string pathInfo, string filepath) =>
                                        Factory(method, pathInfo, filepath));
    }

Second, the CatchAllHandler. As described above, the function may be called for the domain root, an existing file or directory, or any other unmatched route. Your method should return a handler, if your criteria are met, or return null.

    private static Html5ModeFeature Factory(String method, String pathInfo, String filepath)
    {
        var Html5ModeHandler = Html5ModeFeature.Instance;
        List<string> WebHostRootFileNames = RootFiles();

        // handle domain root
        if (string.IsNullOrEmpty(pathInfo) || pathInfo == "/")
        {
            return Html5ModeHandler;
        }

        // don't handle 'mode' urls
        var mode = EndpointHost.Config.ServiceStackHandlerFactoryPath;
        if (mode != null && pathInfo.EndsWith(mode))
        {
            return null;
        }

        var pathParts = pathInfo.TrimStart('/').Split('/');
        var existingFile = pathParts[0].ToLower();
        var catchAllHandler = new Object();

        if (WebHostRootFileNames.Contains(existingFile))
        {
            var fileExt = Path.GetExtension(filepath);
            var isFileRequest = !string.IsNullOrEmpty(fileExt);

            // don't handle directories or files that have another handler
            catchAllHandler = GetCatchAllHandlerIfAny(method, pathInfo, filepath);
            if (catchAllHandler != null) return null;

            // don't handle existing files under any event
            return isFileRequest ? null : Html5ModeHandler;
        }

        // don't handle non-physical urls that have another handler
        catchAllHandler = GetCatchAllHandlerIfAny(method, pathInfo, filepath);
        if (catchAllHandler != null) return null;

        // handle anything else
        return Html5ModeHandler;
    }

In the case of the wildcard at the root domain, you may not want to hijack routes that can be handled by another CatchAllHandler. If so, to avoid infinite recursion, you'll need a custom GetCatchAllHandlerIfAny method.

    //
    // local copy of ServiceStackHttpHandlerFactory.GetCatchAllHandlerIfAny, prevents infinite recursion
    //
    private static IHttpHandler GetCatchAllHandlerIfAny(string httpMethod, string pathInfo, string filePath)
    {
        if (EndpointHost.CatchAllHandlers != null)
        {
            foreach (var httpHandlerResolver in EndpointHost.CatchAllHandlers)
            {
                if (httpHandlerResolver == Html5ModeFeature.Factory) continue; // avoid infinite recursion

                var httpHandler = httpHandlerResolver(httpMethod, pathInfo, filePath);
                if (httpHandler != null)
                    return httpHandler;
            }
        }

        return null;
    }

Here's the complete, and completely untested, plugin. It compiles. It carries no warranty of fitness for any specific purpose.

using ServiceStack;
using ServiceStack.Common.Web;
using ServiceStack.Razor;
using ServiceStack.ServiceHost;
using ServiceStack.Text;
using ServiceStack.WebHost.Endpoints;
using ServiceStack.WebHost.Endpoints.Formats;
using ServiceStack.WebHost.Endpoints.Support;
using ServiceStack.WebHost.Endpoints.Support.Markdown;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Web;

namespace MyProject.Support
{
public enum DefaultFileFormat
{
    Markdown,
    Razor,
    Static
}

public class Html5ModeFeature : EndpointHandlerBase, IPlugin
{
    private FileInfo fi { get; set; }
    private DefaultFileFormat FileFormat { get; set; }
    private DateTime FileModified { get; set; }
    private byte[] FileContents { get; set; }
    public MarkdownHandler Markdown { get; set; }
    public RazorHandler Razor { get; set; }
    public object Model { get; set; }
    private static Dictionary<string, string> allDirs;

    public string PathInfo { get; set; }

    public void Register(IAppHost appHost)
    {
        appHost.CatchAllHandlers.Add((string method, string pathInfo, string filepath) =>
                                        Factory(method, pathInfo, filepath));
    }

    private Html5ModeFeature()
    {
        foreach (var defaultDoc in EndpointHost.Config.DefaultDocuments)
        {
            if (PathInfo == null) 
            {
                var defaultFileName = Path.Combine(Directory.GetCurrentDirectory(), defaultDoc);
                if (!File.Exists(defaultFileName)) continue;
                PathInfo = (String)defaultDoc; // use first default document found.
            }
        }
        SetFile();
    }

    private static Html5ModeFeature instance;
    public static Html5ModeFeature Instance
    {
        get { return instance ?? (instance = new Html5ModeFeature()); }
    }

    public void SetFile()
    {
        if (PathInfo.EndsWith(MarkdownFormat.MarkdownExt) || PathInfo.EndsWith(MarkdownFormat.TemplateExt))
        {
            Markdown = new MarkdownHandler(PathInfo);
            FileFormat = DefaultFileFormat.Markdown;
            return;
        }
        if (PathInfo.EndsWith(Razor.RazorFormat.RazorFileExtension)) {
            Razor = new RazorHandler(PathInfo);
            FileFormat = DefaultFileFormat.Razor;
            return;
        }
        FileContents = File.ReadAllBytes(PathInfo);
        FileModified = File.GetLastWriteTime(PathInfo);
        FileFormat = DefaultFileFormat.Static;
    }

    //
    // ignore request.PathInfo, return default page, extracted from StaticFileHandler.ProcessResponse
    //
    public void ProcessStaticPage(IHttpRequest request, IHttpResponse response, string operationName)
    {
        response.EndHttpHandlerRequest(skipClose: true, afterBody: r =>
        {

            TimeSpan maxAge;
            if (r.ContentType != null && EndpointHost.Config.AddMaxAgeForStaticMimeTypes.TryGetValue(r.ContentType, out maxAge))
            {
                r.AddHeader(HttpHeaders.CacheControl, "max-age=" + maxAge.TotalSeconds);
            }

            if (request.HasNotModifiedSince(fi.LastWriteTime))
            {
                r.ContentType = MimeTypes.GetMimeType(PathInfo);
                r.StatusCode = 304;
                return;
            }

            try
            {
                r.AddHeaderLastModified(fi.LastWriteTime);
                r.ContentType = MimeTypes.GetMimeType(PathInfo);

                if (fi.LastWriteTime > this.FileModified)
                    SetFile(); //reload

                r.OutputStream.Write(this.FileContents, 0, this.FileContents.Length);
                r.Close();
                return;
            }
            catch (Exception ex)
            {
                throw new HttpException(403, "Forbidden.");
            }
        });
    }

    private void ProcessServerError(IHttpRequest httpReq, IHttpResponse httpRes, string operationName)
    {
        var sb = new StringBuilder();
        sb.AppendLine("{");
        sb.AppendLine(""ResponseStatus":{");
        sb.AppendFormat(" "ErrorCode":{0},
", 500);
        sb.AppendFormat(" "Message": HTML5ModeHandler could not serve file {0}.
", PathInfo.EncodeJson());
        sb.AppendLine("}");
        sb.AppendLine("}");

        httpRes.EndHttpHandlerRequest(skipClose: true, afterBody: r =>
        {
            r.StatusCode = 500;
            r.ContentType = ContentType.Json;
            var sbBytes = sb.ToString().ToUtf8Bytes();
            r.OutputStream.Write(sbBytes, 0, sbBytes.Length);
            r.Close();
        });
        return;
    }

    private static List<string> RootFiles()
    {
        var WebHostPhysicalPath = EndpointHost.Config.WebHostPhysicalPath;
        List<string> WebHostRootFileNames = new List<string>();

        foreach (var filePath in Directory.GetFiles(WebHostPhysicalPath))
        {
            var fileNameLower = Path.GetFileName(filePath).ToLower();
            WebHostRootFileNames.Add(Path.GetFileName(fileNameLower));
        }
        foreach (var dirName in Directory.GetDirectories(WebHostPhysicalPath))
        {
            var dirNameLower = Path.GetFileName(dirName).ToLower();
            WebHostRootFileNames.Add(Path.GetFileName(dirNameLower));
        }
        return WebHostRootFileNames;
    }


    private static Html5ModeFeature Factory(String method, String pathInfo, String filepath)
    {
        var Html5ModeHandler = Html5ModeFeature.Instance;
        List<string> WebHostRootFileNames = RootFiles();

        // handle domain root
        if (string.IsNullOrEmpty(pathInfo) || pathInfo == "/")
        {
            return Html5ModeHandler;
        }

        // don't handle 'mode' urls
        var mode = EndpointHost.Config.ServiceStackHandlerFactoryPath;
        if (mode != null && pathInfo.EndsWith(mode))
        {
            return null;
        }

        var pathParts = pathInfo.TrimStart('/').Split('/');
        var existingFile = pathParts[0].ToLower();
        var catchAllHandler = new Object();

        if (WebHostRootFileNames.Contains(existingFile))
        {
            var fileExt = Path.GetExtension(filepath);
            var isFileRequest = !string.IsNullOrEmpty(fileExt);

            // don't handle directories or files that have another handler
            catchAllHandler = GetCatchAllHandlerIfAny(method, pathInfo, filepath);
            if (catchAllHandler != null) return null;


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...