Override host of webapi odata links

后端 未结 6 1744
Happy的楠姐
Happy的楠姐 2020-12-06 19:55

I\'m using WebAPI 2.2 and Microsoft.AspNet.OData 5.7.0 to create an OData service that supports paging.

When hosted in the production environment, the WebAPI lives o

相关标签:
6条回答
  • 2020-12-06 20:33

    The answer by lencharest is promising, but I found an improvement on his method. Rather than using the UrlHelper, I created a class derived from System.Net.Http.DelegatingHandler. This class is inserted (first) into the message handling pipeline and thus has a crack at altering the incoming HttpRequestMessage. It's an improvement over the above solution because in addition to altering the controller-specific URLs (as the UrlHelper does, e,g, https://data.contoso.com/odata/MyController), it also alters the url that appears as the xml:base in the OData service document (e.g., https://data.contoso.com/odata).

    My particular application was to host an OData service behind a proxy server, and I wanted all the URLs presented by the server to be the externally-visible URLs, not the internally-visible ones. And, I didn't want to have to rely on annotations for this; I wanted it to be fully automatic.

    The message handler looks like this:

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

    You wire the handler up in WebApiConfig.cs:

        config.Routes.MapODataServiceRoute(
            routeName: "odata",
            routePrefix: "odata",
            model: builder.GetEdmModel(),
            pathHandler: new DefaultODataPathHandler(),
            routingConventions: ODataRoutingConventions.CreateDefault()
        );
        config.MessageHandlers.Insert(0, new BehindProxyMessageHandler());
    
    0 讨论(0)
  • 2020-12-06 20:38

    There is another solution, but it overrides url for the entire context. What I'd like to suggest is:

    1. Create owin middleware and override Host and Scheme properties inside
    2. Register the middleware as the first one

    Here is an example of middleware

    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 is the host you want to have. Example: test.com

    ProxyScheme is the scheme you want: Example: https

    Example of middleware registration

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.Use(typeof(RewriteUrlMiddleware));
            var config = new HttpConfiguration();
            WebApiConfig.Register(config);
            app.UseWebApi(config);
        }
    }
    
    0 讨论(0)
  • 2020-12-06 20:40

    A couple of years later, using ASP.NET Core, I figured that the easiest way to apply it in my service was to just create a filter that masquerades the host name. (AppConfig is a custom configuration class that contains the host name, among other things.)

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

    Apply the filter to the controller base class.

    [MasqueradeHostFilter]
    public class AppODataController : ODataController
    {
    }
    

    The result is a nicely formatted output:

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

    Just my two cents.

    0 讨论(0)
  • 2020-12-06 20:40

    Your question boils down to controlling the service root URI from within the service itself. My first thought was to look for a hook on the media type formatters used to serialize responses. ODataMediaTypeFormatter.MessageWriterSettings.PayloadBaseUri and ODataMediaTypeFormatter.MessageWriterSettings.ODataUri.ServiceRoot are both settable properties that suggest a solution. Unfortunately, ODataMediaTypeFormatter resets these properties on every call to WriteToStreamAsync.

    The work-around is not obvious, but if you dig through the source code you'll eventually reach a call to IODataPathHandler.Link. A path handler is an OData extension point, so you can create a custom path handler that always returns an absolute URI which begins with the service root you desire.

    public class CustomPathHandler : DefaultODataPathHandler
    {
        private const string ServiceRoot = "http://example.com/";
    
        public override string Link(ODataPath path)
        {
            return ServiceRoot + base.Link(path);
        }
    }
    

    And then register that path handler during service configuration.

    // config is an instance of HttpConfiguration
    config.MapODataServiceRoute(
        routeName: "ODataRoute",
        routePrefix: null,
        model: builder.GetEdmModel(),
        pathHandler: new CustomPathHandler(),
        routingConventions: ODataRoutingConventions.CreateDefault()
    );
    
    0 讨论(0)
  • 2020-12-06 20:51

    Using system.web.odata 6.0.0.0.

    Setting the NextLink property too soon is problematic. Every reply will then have a nextLink in it. The last page should of course be free of such decorations.

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

    URLs present in a payload (whether request or response) MAY be represented as relative URLs.

    One way that I hope will work is to override 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() is where the "overflow" is detected. It basically asks for pagesize+1 rows and will set NextLink if the result set contains more than pagesize rows.

    At this point it is relatively easy to rewrite NextLink to a relative URL.

    The downside is that every odata method must now be adorned with the new myEnableQuery attribute:

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

    and other URLs embedded elsewhere remains problematic. odata.context remains a problem. I want to avoid playing with the request URL, because I fail to see how that is maintainable over time.

    0 讨论(0)
  • 2020-12-06 20:54

    Rewriting the RequestUri is sufficient to affect @odata.nextLink values because the code that computes the next link depends on the RequestUri directly. The other @odata.xxx links are computed via a UrlHelper, which is somehow referencing the path from the original request URI. (Hence the AccountName you see in your @odata.context link. I've seen this behavior in my code, but I haven't been able to track down the source of the cached URI path.)

    Rather than rewrite the RequestUri, we can solve the problem by creating a CustomUrlHelper class to rewrite OData links on the fly. The new GetNextPageLink method will handle @odata.nextLink rewrites, and the Link method override will handle all other rewrites.

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

    Wire-up the CustomUrlHelper in the Initialize method of a base controller class.

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

    Note in the above that the page size will be the same for all actions in a given controller class. You can work around this limitation by moving the assignment of ODataProperties().NextLink to the body of a specific action method as follows:

    var helper = this.RequestContext.Url as CustomUrlHelper;
    this.Request.ODataProperties().NextLink = helper.GetNextPageLink(otherPageSize);
    
    0 讨论(0)
提交回复
热议问题