解决微服务网关Ocelot使用AddStoreOcelotConfigurationInConsul后请求404问题 ASP.N

一个小插曲,最近研究 netcore 微服务网关,在使用AddStoreOcelotConfigurationInConsul将配置存到consul后,任何经过网关的请求都出现404,并且没有任何有用的异常信息打印。这里先简单讲讲这个问题是如何发生的,及如何解决。

之前在ASP.NET Core 2 学习笔记(三)中间件提到过大部分扩展的Middleware都会用一个静态方法包装,如:UseMvc()UseRewriter()等,而微服务网关Ocelot 包装了两个静态的异步方法 UseOcelot

OcelotMiddlewareExtensions.cs

public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder)
{
    await builder.UseOcelot(new OcelotPipelineConfiguration());

    return builder;
}

public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration)
{
    var configuration = await CreateConfiguration(builder);
            
    CreateAdministrationArea(builder, configuration);

    if(UsingRafty(builder))
    {
        SetUpRafty(builder);
    }

    if (UsingEurekaServiceDiscoveryProvider(configuration))
    {
        builder.UseDiscoveryClient();
    }

    ConfigureDiagnosticListener(builder);

    var pipelineBuilder = new OcelotPipelineBuilder(builder.ApplicationServices);

    pipelineBuilder.BuildOcelotPipeline(pipelineConfiguration);

    var firstDelegate = pipelineBuilder.Build();

    /*
    inject first delegate into first piece of asp.net middleware..maybe not like this
    then because we are updating the http context in ocelot it comes out correct for
    rest of asp.net..
    */

    builder.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware";

    builder.Use(async (context, task) =>
    {
        var downstreamContext = new DownstreamContext(context);
        await firstDelegate.Invoke(downstreamContext);
    });

    return builder;
}  

为什么会封装成异步的方法,可能是因为从底层一步一步写上来的。但是我个人认为 UseOcelot 是完全可以封装成同步方法,以避免误用。

误用情形,如下:

Startup.cs

public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    await app.UseOcelot();
}

这里将 Startup.Configure() 方法写成了一个不规范的异步方法,其实是不被支持的。但是由于不规范,绕过了检测。

规范写法,如下

Startup.cs

public async Task Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    await app.UseOcelot();
}

检测到Configure返回类型非Void,直接异常:

System.InvalidOperationException:“The 'Configure' method in the type 'OcelotConsul.ApiGateway.Core.Startup' must have a return type of 'Void'.”

这里可以参考aspnet/Hosting关于Configure 异步问题的讨论:

aspnet/Hosting#27

aspnet/Hosting#29

aspnet/Hosting#373

aspnet/Hosting#1088

由于用到了不规范的异步方法,使得线程并没有等待 UseOcelot 内部初始化完成就Host了起来,而造成了一些奇怪的异常,诸如:AddStoreOcelotConfigurationInConsul后请求404等问题。

解决方案就是用 Task.Wait() 方法 阻塞线程,等待 UseOcelot 执行完成,再Host。如下:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseOcelot().Wait();
}

也许 UseOcelot 封装成同步方法会更好。已经给作者提了Issue