1. 程式人生 > >讓 Ocelot 與 asp.net core “共存”

讓 Ocelot 與 asp.net core “共存”

讓 Ocelot 與 asp.net core “共存”

Intro

我們的 API 之前是一個單體應用,各個模組的服務是通過 Assembly 整合在一起,最後部署在一個 web server 下的。

我們已經在拆分服務並且在 Ocelot 的基礎上封裝了我們自己的閘道器,但是服務還沒有完全拆分,於是有這麼一個需求,對於 Ocelot 配置的路由去交給 Ocelot 去轉發到真正的服務地址,而那些 Ocelot 沒有定義的路由則讓交給 AspNetCore 去處理。

實現原理

實現原理是讓 Ocelot 作為一個動態分支路由,只有當 Ocelot 配置了對應路由的下游地址才走 Ocelot 的分支,才把請求交給 Ocelot 處理。

我們可以使用 MapWhen 來處理,接下來就需要知道怎麼樣判斷 Ocelot 是否配置了某一個路由,Ocelot 內部的處理管道,在向下遊請求之前是要找到對應匹配的下游路由,所以我們去看一看 Ocelot 的原始碼,看看 Ocelot 內部是怎麼找下游路由的,Ocelot 找下游路由中介軟體原始碼

        public async Task Invoke(DownstreamContext context)
        {
            var upstreamUrlPath = context.HttpContext.Request.Path.ToString();

            var upstreamQueryString = context.HttpContext.Request.QueryString.ToString();

            var upstreamHost = context.HttpContext.Request.Headers["Host"];

            Logger.LogDebug($"Upstream url path is {upstreamUrlPath}");

            var provider = _factory.Get(context.Configuration);

            // 獲取下游路由
            var downstreamRoute = provider.Get(upstreamUrlPath, upstreamQueryString, context.HttpContext.Request.Method, context.Configuration, upstreamHost);

            if (downstreamRoute.IsError)
            {
                Logger.LogWarning($"{MiddlewareName} setting pipeline errors. IDownstreamRouteFinder returned {downstreamRoute.Errors.ToErrorString()}");

                SetPipelineError(context, downstreamRoute.Errors);
                return;
            }            
            
            var downstreamPathTemplates = string.Join(", ", downstreamRoute.Data.ReRoute.DownstreamReRoute.Select(r => r.DownstreamPathTemplate.Value));
            
            Logger.LogDebug($"downstream templates are {downstreamPathTemplates}");

            context.TemplatePlaceholderNameAndValues = downstreamRoute.Data.TemplatePlaceholderNameAndValues;

            await _multiplexer.Multiplex(context, downstreamRoute.Data.ReRoute, _next);
        }

通過上面的原始碼,我們就可以判斷 Ocelot 是否有與請求相匹配的下游路由資訊

實現

既然找到了 Ocelot 如何找下游路由,就先給 Ocelot 加一個擴充套件吧,實現程式碼如下,Ocelot 擴充套件完整程式碼

        public static IApplicationBuilder UseOcelotWhenRouteMatch(this IApplicationBuilder app,
            Action<IOcelotPipelineBuilder, OcelotPipelineConfiguration> builderAction)
            => UseOcelotWhenRouteMatch(app, builderAction, new OcelotPipelineConfiguration());

        public static IApplicationBuilder UseOcelotWhenRouteMatch(this IApplicationBuilder app,
            Action<OcelotPipelineConfiguration> pipelineConfigurationAction,
            Action<IOcelotPipelineBuilder, OcelotPipelineConfiguration> builderAction)
        {
            var pipelineConfiguration = new OcelotPipelineConfiguration();
            pipelineConfigurationAction?.Invoke(pipelineConfiguration);
            return UseOcelotWhenRouteMatch(app, builderAction, pipelineConfiguration);
        }

        public static IApplicationBuilder UseOcelotWhenRouteMatch(this IApplicationBuilder app, Action<IOcelotPipelineBuilder, OcelotPipelineConfiguration> builderAction, OcelotPipelineConfiguration configuration)
        {
            app.MapWhen(context =>
            {
                // 獲取 OcelotConfiguration
                var internalConfigurationResponse =
                    context.RequestServices.GetRequiredService<IInternalConfigurationRepository>().Get();
                if (internalConfigurationResponse.IsError || internalConfigurationResponse.Data.ReRoutes.Count == 0)
                {
                    // 如果沒有配置路由資訊,不符合分支路由的條件,直接退出
                    return false;
                }

                var internalConfiguration = internalConfigurationResponse.Data;
                var downstreamRouteFinder = context.RequestServices
                    .GetRequiredService<IDownstreamRouteProviderFactory>()
                    .Get(internalConfiguration);
                // 根據請求以及上面獲取的Ocelot配置獲取下游路由
                var response = downstreamRouteFinder.Get(context.Request.Path, context.Request.QueryString.ToString(),
                    context.Request.Method, internalConfiguration, context.Request.Host.ToString());
                // 如果有匹配路由則滿足該分支路由的條件,交給 Ocelot 處理
                return !response.IsError
                       && !string.IsNullOrEmpty(response.Data?.ReRoute?.DownstreamReRoute?.FirstOrDefault()
                           ?.DownstreamScheme);
            }, appBuilder => appBuilder.UseOcelot(builderAction, configuration).Wait());

            return app;
        }

使用

在 Startup 裡

ConfigurationServices 配置 mvc 和 Ocelot

Configure 方法裡配置 ocelot 和 mvc


app.UseOcelotWhenRouteMatch((ocelotBuilder, pipelineConfiguration) =>
                            {
                                // This is registered to catch any global exceptions that are not handled
                                // It also sets the Request Id if anything is set globally
                                ocelotBuilder.UseExceptionHandlerMiddleware();
                                // This is registered first so it can catch any errors and issue an appropriate response
                                ocelotBuilder.UseResponderMiddleware();
                                ocelotBuilder.UseDownstreamRouteFinderMiddleware();
                                ocelotBuilder.UseDownstreamRequestInitialiser();
                                ocelotBuilder.UseRequestIdMiddleware();
                                ocelotBuilder.UseMiddleware<ClaimsToHeadersMiddleware>();
                                ocelotBuilder.UseLoadBalancingMiddleware();
                                ocelotBuilder.UseDownstreamUrlCreatorMiddleware();
                                ocelotBuilder.UseOutputCacheMiddleware();
                                ocelotBuilder.UseMiddleware<HttpRequesterMiddleware>();
                                // cors headers
                                ocelotBuilder.UseMiddleware<CorsMiddleware>();
                            });

app.UseMvc();

新建一個 TestController

    [Route("/api/[controller]")]
    public class TestController : ControllerBase
    {
        public IActionResult Get()
        {
            return Ok(new
            {
                Tick = DateTime.UtcNow.Ticks,
                Msg = "Hello Ocelot",
            });
        }
    }

具體程式碼可以參考這個 閘道器示例專案

示例專案的 Ocelot 配置是存在 Redis 裡面的,配置的 ReRoutes 如下:

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api.php?key=free&appid=0&msg={everything}",
      "UpstreamPathTemplate": "/api/chat/{everything}",
      "UpstreamHttpMethod": [
        "Get",
        "POST",
        "PUT",
        "PATCH",
        "DELETE",
        "OPTIONS"
      ],
      "AddHeadersToRequest": {
      },
      "RequestIdKey": "RequestId",
      "ReRouteIsCaseSensitive": false,
      "ServiceName": "",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "api.qingyunke.com",
          "Port": 80
        }
      ],
      "DangerousAcceptAnyServerCertificateValidator": false
    }
  ],
  "GlobalConfiguration": {
      "HttpHandlerOptions": {
        "AllowAutoRedirect": false,
        "UseCookieContainer": false,
        "UseTracing": false
      }
  }
}

執行專案進行測試:

訪問 Ocelot 定義的路由 http://localhost:65125/api/chat/hello ,返回資訊如圖所示:

訪問 Mvc 定義的路由 http://localhost:65125/api/test,返回資訊如圖所示:

上面正常的返回就表示我們的 Ocelot 和 Mvc 同時工作了~

Reference

  • https://github.com/ThreeMammals/Ocelot
  • https://github.com/WeihanLi/AspNetCorePlayground/tree/master/TestGateway