6

我正在使用 WebAPI 2.2 和 Microsoft.AspNet.OData 5.7.0 创建支持分页的 OData 服务。

当托管在生产环境中时,WebAPI 存在于未暴露在外部的服务器上,因此 OData 响应中返回的各种链接,例如@odata.context@odata.nextLink指向内部 IP 地址http://192.168.X.X/<AccountName>/api/...等。

我已经能够Request.ODataProperties().NextLink通过在每个 ODataController 方法中实现一些逻辑来用外部 URL 替换内部 URL来修改https://account-name.domain.com/api/...,但这非常不方便,它只修复了 NextLinks。

有没有办法在配置 OData 服务时设置外部主机名?我看到了一个属性Request.ODataProperties().Path,想知道是否可以在config.MapODataServiceRoute("odata", "odata", GetModel());调用时设置基本路径,或者在GetModel()实现中使用例如ODataConventionModelBuilder


更新:到目前为止,我想出的最佳解决方案是创建一个BaseODataController覆盖该Initialize方法并检查是否Request.RequestUri.Host.StartsWith("beginning-of-known-internal-IP-address")然后像这样进行 RequestUri 重写:

var externalAddress = ConfigClient.Get().ExternalAddress;  // e.g. https://account-name.domain.com
var account = ConfigClient.Get().Id;  // e.g. AccountName
var uriToReplace = new Uri(new Uri("http://" + Request.RequestUri.Host), account);
string originalUri = Request.RequestUri.AbsoluteUri;
Request.RequestUri = new Uri(Request.RequestUri.AbsoluteUri.Replace(uriToReplace.AbsoluteUri, externalAddress));
string newUri = Request.RequestUri.AbsoluteUri;
this.GetLogger().Info($"Request URI was rewritten from {originalUri} to {newUri}");

这完美地修复了@odata.nextLink所有控制器的 URL,但由于某种原因,@odata.contextURL 仍然得到AccountName部分(例如https://account-name.domain.com/AccountName/api/odata/$metadata#ControllerName)所以它们仍然没有工作。

4

6 回答 6

7

重写RequestUri足以影响@odata.nextLink值,因为计算下一个链接的代码直接依赖于RequestUri。其他@odata.xxx链接是通过 a 计算的UrlHelper,它以某种方式引用来自原始请求 URI 的路径。(因此AccountName您在@odata.context链接中看到。我在我的代码中看到了这种行为,但我无法追踪缓存的 URI 路径的来源。)

我们可以通过创建一个类来动态重写 OData 链接RequestUri来解决问题,而不是重写. CustomUrlHelperGetNextPageLink方法将处理@odata.nextLink重写,Link方法覆盖将处理所有其他重写。

public class CustomUrlHelper : System.Web.Http.Routing.UrlHelper
{
    public CustomUrlHelper(HttpRequestMessage request) : base(request)
    { }

    // Change these strings to suit your specific needs.
    private static readonly string ODataRouteName = "ODataRoute"; // Must be the same as used in api config
    private static readonly string TargetPrefix = "http://localhost:8080/somePathPrefix"; 
    private static readonly int TargetPrefixLength = TargetPrefix.Length;
    private static readonly string ReplacementPrefix = "http://www.contoso.com"; // Do not end with slash

    // Helper method.
    protected string ReplaceTargetPrefix(string link)
    {
        if (link.StartsWith(TargetPrefix))
        {
            if (link.Length == TargetPrefixLength)
            {
                link = ReplacementPrefix;
            }
            else if (link[TargetPrefixLength] == '/')
            {
                link = ReplacementPrefix + link.Substring(TargetPrefixLength);
            }
        }

        return link;
    }

    public override string Link(string routeName, IDictionary<string, object> routeValues)
    {
        var link = base.Link(routeName, routeValues);

        if (routeName == ODataRouteName)
        {
            link = this.ReplaceTargetPrefix(link);
        }

        return link;
    }

    public Uri GetNextPageLink(int pageSize)
    {
        return new Uri(this.ReplaceTargetPrefix(this.Request.GetNextPageLink(pageSize).ToString()));
    }
}

连接基本控制器类CustomUrlHelperInitialize方法。

public abstract class BaseODataController : ODataController
{
    protected abstract int DefaultPageSize { get; }

    protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext)
    {
        base.Initialize(controllerContext);

        var helper = new CustomUrlHelper(controllerContext.Request);
        controllerContext.RequestContext.Url = helper;
        controllerContext.Request.ODataProperties().NextLink = helper.GetNextPageLink(this.DefaultPageSize);
    }

请注意上面的页面大小对于给定控制器类中的所有操作都是相同的。您可以通过将分配移动到特定操作方法的主体来解决此限制,ODataProperties().NextLink如下所示:

var helper = this.RequestContext.Url as CustomUrlHelper;
this.Request.ODataProperties().NextLink = helper.GetNextPageLink(otherPageSize);
于 2016-03-03T21:01:30.277 回答
6

lencharest 的回答很有希望,但我发现他的方法有所改进。我没有使用 UrlHelper,而是创建了一个派生自 System.Net.Http.DelegatingHandler 的类。此类(首先)插入到消息处理管道中,因此在更改传入的 HttpRequestMessage 方面存在漏洞。这是对上述解决方案的改进,因为除了更改特定于控制器的 URL(如 UrlHelper 所做的那样,例如https://data.contoso.com/odata/MyController),它还更改显示为的 url OData 服务文档中的 xml:base(例如https://data.contoso.com/odata)。

我的特定应用程序是在代理服务器后面托管 OData 服务,我希望服务器提供的所有 URL 都是外部可见的 URL,而不是内部可见的 URL。而且,我不想为此依赖注释;我希望它是全自动的。

消息处理程序如下所示:

    public class BehindProxyMessageHandler : DelegatingHandler
    {
        protected async override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var builder = new UriBuilder(request.RequestUri);
            var visibleHost = builder.Host;
            var visibleScheme = builder.Scheme;
            var visiblePort = builder.Port;

            if (request.Headers.Contains("X-Forwarded-Host"))
            {
                string[] forwardedHosts = request.Headers.GetValues("X-Forwarded-Host").First().Split(new char[] { ',' });
                visibleHost = forwardedHosts[0].Trim();
            }

            if (request.Headers.Contains("X-Forwarded-Proto"))
            {
                visibleScheme = request.Headers.GetValues("X-Forwarded-Proto").First();
            }

            if (request.Headers.Contains("X-Forwarded-Port"))
            {
                try
                {
                    visiblePort = int.Parse(request.Headers.GetValues("X-Forwarded-Port").First());
                }
                catch (Exception)
                { }
            }

            builder.Host = visibleHost;
            builder.Scheme = visibleScheme;
            builder.Port = visiblePort;

            request.RequestUri = builder.Uri;
            var response = await base.SendAsync(request, cancellationToken);
            return response;
        }
    }

在 WebApiConfig.cs 中连接处理程序:

    config.Routes.MapODataServiceRoute(
        routeName: "odata",
        routePrefix: "odata",
        model: builder.GetEdmModel(),
        pathHandler: new DefaultODataPathHandler(),
        routingConventions: ODataRoutingConventions.CreateDefault()
    );
    config.MessageHandlers.Insert(0, new BehindProxyMessageHandler());
于 2019-05-21T03:56:06.450 回答
2

还有另一种解决方案,但它会覆盖整个上下文的 url。我想建议的是:

  1. 创建owin中间件并覆盖里面的Host和Scheme属性
  2. 将中间件注册为第一个

这是一个中间件的例子

public class RewriteUrlMiddleware : OwinMiddleware
{
    public RewriteUrlMiddleware(OwinMiddleware next)
        : base(next)
    {
    }

    public override async Task Invoke(IOwinContext context)
    {
        context.Request.Host = new HostString(Settings.Default.ProxyHost);
        context.Request.Scheme = Settings.Default.ProxyScheme;
        await Next.Invoke(context);
    }
}

ProxyHost 是您想要拥有的主机。示例:test.com

ProxyScheme 是您想要的方案:示例:https

中间件注册示例

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Use(typeof(RewriteUrlMiddleware));
        var config = new HttpConfiguration();
        WebApiConfig.Register(config);
        app.UseWebApi(config);
    }
}
于 2016-12-14T16:30:44.763 回答
2

几年后,使用 ASP.NET Core,我发现在我的服务中应用它的最简单方法就是创建一个伪装主机名的过滤器。(AppConfig是一个自定义配置类,其中包含主机名等。)

public class MasqueradeHostFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var appConfig = context.HttpContext.RequestServices.GetService<AppConfig>();
        if (!string.IsNullOrEmpty(appConfig?.MasqueradeHost))
            context.HttpContext.Request.Host = new HostString(appConfig.MasqueradeHost);
    }
}

将过滤器应用于控制器基类。

[MasqueradeHostFilter]
public class AppODataController : ODataController
{
}

结果是格式良好的输出:

{ "@odata.context":"https://app.example.com/odata/$metadata" }

只是我的两分钱。

于 2020-08-26T01:12:39.863 回答
0

使用 system.web.odata 6.0.0.0。

过早设置 NextLink 属性是有问题的。然后,每个回复都会有一个 nextLink。最后一页当然应该没有这样的装饰。

http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793048说:

有效载荷(无论是请求还是响应)中的 URL 可以表示为相对 URL。

我希望可行的一种方法是覆盖 EnableQueryAttribute:

public class myEnableQueryAttribute : EnableQueryAttribute
{
    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
        var result = base.ApplyQuery(queryable, queryOptions);
        var nextlink = queryOptions.Request.ODataProperties().NextLink;
        if (nextlink != null)
            queryOptions.Request.ODataProperties().NextLink = queryOptions.Request.RequestUri.MakeRelativeUri(nextlink);
        return result;
    }
}

ApplyQuery()是检测到“溢出”的地方。它基本上要求行,如果结果集包含多pagesize+1行,它将设置。NextLinkpagesize

此时,重写NextLink为相对 URL 相对容易。

缺点是现在必须用新的 myEnableQuery 属性装饰每个 odata 方法:

[myEnableQuery]
public async Task<IHttpActionResult> Get(ODataQueryOptions<TElement> options)
{
  ...
}

其他嵌入的 URL 仍然存在问题。odata.context 仍然是一个问题。我想避免使用请求 URL,因为我看不到随着时间的推移它是如何维护的。

于 2016-12-13T20:41:36.907 回答
-1

您的问题归结为从服务本身内部控制服务根 URI。我的第一个想法是在用于序列化响应的媒体类型格式化程序上寻找一个挂钩。ODataMediaTypeFormatter.MessageWriterSettings.PayloadBaseUri并且ODataMediaTypeFormatter.MessageWriterSettings.ODataUri.ServiceRoot都是建议解决方案的可设置属性。不幸的是,ODataMediaTypeFormatter 每次调用WriteToStreamAsync.

解决方法并不明显,但如果您深入研究源代码,您最终会调用IODataPathHandler.Link. 路径处理程序是一个 OData 扩展点,因此您可以创建一个自定义路径处理程序,该处理程序始终返回一个绝对 URI,该绝对 URI 以您想要的服务根目录开头。

public class CustomPathHandler : DefaultODataPathHandler
{
    private const string ServiceRoot = "http://example.com/";

    public override string Link(ODataPath path)
    {
        return ServiceRoot + base.Link(path);
    }
}

然后在服务配置期间注册该路径处理程序。

// config is an instance of HttpConfiguration
config.MapODataServiceRoute(
    routeName: "ODataRoute",
    routePrefix: null,
    model: builder.GetEdmModel(),
    pathHandler: new CustomPathHandler(),
    routingConventions: ODataRoutingConventions.CreateDefault()
);
于 2016-02-26T18:13:24.807 回答