4

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}.

4

2 回答 2

3

我目前正在进行的工作,一个在不更改浏览器中的 url 的情况下为所有“未找到”路由提供默认页面的插件,包括处理全局通配符路由所需的大部分内容。使用它来帮助您入门。

要了解这段代码在做什么,有助于了解 ServiceStack 的路由优先级,以及 CatchAllHandlers 如何融入流程。ServiceStack 调用ServiceStackHttpHandlerFactory.GetHandler以获取当前路由的处理程序。

ServiceStackHttpHandlerFactory.GetHandler返回:

  1. 一个匹配的 RawHttpHandler,如果有的话。
  2. 如果是域根,则返回的处理程序( GetCatchAllHandlerIfAny(...),如果有)。
  3. 如果路由与元数据 uri 匹配(我在这里跳过了确切的逻辑,因为它对您的问题并不重要),相关的处理程序(如果有)。
  4. 返回的处理程序(ServiceStackHttpHandlerFactory.GetHandlerForPathInfo如果有)。
  5. 未找到处理程序。

ServiceStackHttpHandlerFactory.GetHandlerForPathInfo返回:

  1. 如果 url 匹配一个有效的 REST 路由,一个新的 RestHandler。
  2. 如果 url 与现有文件或目录匹配,则返回
    • 如果有的话,返回的处理程序 GetCatchAllHandlerIfAny(...),
    • 如果它是受支持的文件类型,StaticFileHandler,
    • 如果它不是受支持的文件类型,则为 ForbiddenHttpHandler。
  3. 返回的处理程序(GetCatchAllHandlerIfAny(...),如果有)。
  4. 无效的。

CatchAllHandlers 数组包含评估 url 并返回处理程序或 null 的函数。数组中的函数按顺序调用,第一个不返回 null 的函数处理路由。让我强调一些关键要素:

首先,插件在注册时将 CatchAllHandler 添加到appHost.CatchAllHandlers数组中。

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

第二,CatchAllHandler。如上所述,可以为域根、现有文件或目录或任何其他不匹配的路由调用该函数。如果满足您的条件,您的方法应该返回一个处理程序,或者返回 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;
    }

对于根域中的通配符,您可能不想劫持可以由另一个 CatchAllHandler 处理的路由。如果是这样,为了避免无限递归,您需要一个自定义GetCatchAllHandlerIfAny方法。

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

这是完整且完全未经测试的插件。它编译。它不保证适用于任何特定目的。

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},\n", 500);
        sb.AppendFormat(" \"Message\": HTML5ModeHandler could not serve file {0}.\n", 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;

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

    //
    // Local copy of private StaticFileHandler.DirectoryExists
    //
    public static bool DirectoryExists(string dirPath, string appFilePath)
    {
        if (dirPath == null) return false;

        try
        {
            if (!ServiceStack.Text.Env.IsMono)
                return Directory.Exists(dirPath);
        }
        catch
        {
            return false;
        }

        if (allDirs == null)
            allDirs = CreateDirIndex(appFilePath);

        var foundDir = allDirs.ContainsKey(dirPath.ToLower());

        //log.DebugFormat("Found dirPath {0} in Mono: ", dirPath, foundDir);

        return foundDir;
    }
    //
    // Local copy of private StaticFileHandler.CreateDirIndex
    //
    static Dictionary<string, string> CreateDirIndex(string appFilePath)
    {
        var indexDirs = new Dictionary<string, string>();

        foreach (var dir in GetDirs(appFilePath))
        {
            indexDirs[dir.ToLower()] = dir;
        }

        return indexDirs;
    }
    //
    // Local copy of private StaticFileHandler.GetDirs
    //
    static List<string> GetDirs(string path)
    {
        var queue = new Queue<string>();
        queue.Enqueue(path);

        var results = new List<string>();

        while (queue.Count > 0)
        {
            path = queue.Dequeue();
            try
            {
                foreach (string subDir in Directory.GetDirectories(path))
                {
                    queue.Enqueue(subDir);
                    results.Add(subDir);
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex);
            }
        }

        return results;
    }
    //
    // 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;
    }

    public override void ProcessRequest(IHttpRequest httpReq, IHttpResponse httpRes, string operationName)
    {
        switch (FileFormat)
        {
            case DefaultFileFormat.Markdown: 
            {
                Markdown.ProcessRequest(httpReq, httpRes, operationName);
                break;
            }
            case DefaultFileFormat.Razor:
            {
                Razor.ProcessRequest(httpReq, httpRes, operationName);
                break;
            }
            case DefaultFileFormat.Static:
            {
                fi.Refresh();
                if (fi.Exists) ProcessStaticPage(httpReq, httpRes, operationName); else  ProcessServerError(httpReq, httpRes, operationName); 
                break;
            }
            default:
            {
                ProcessServerError(httpReq, httpRes, operationName);
                break;
            }
        }            
    }   


    public override object CreateRequest(IHttpRequest request, string operationName)
    {
        return null;
    }

    public override object GetResponse(IHttpRequest httpReq, IHttpResponse httpRes, object request)
    {
        return null;
    }

}       
}
于 2013-07-12T15:48:39.897 回答
3

ServiceStack 现在允许您从根路径添加回退路由来/处理任何不匹配的请求,这些请求不是由 catch-all 处理程序处理或引用现有静态文件。因此,在 v3.9.56 中,您现在可以执行以下操作:

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

另一种选择是注册 aIAppHost.CatchAllHandlers来处理不匹配的路由,但您需要返回自己的IHttpHandler来处理请求,或者返回 aRedirectHttpHandler以重定向到由 ServiceStack 管理的不同路由。

于 2013-07-12T12:23:58.867 回答