如何通过 Swashbuckle 使 SwaggerUI 在 traefik 代理后面的 docker 容器中工作?

How to make SwaggerUI via Swashbuckle work in a docker container behind a traefik proxy?

我有一个简单的 .NET 核心/.NET 5 API returns 一些数据通过 OData 和一些数据通过“传统”端点,尽管所有数据都在继承 ODataController 的同一控制器中。

当我从 Visual Studio 2019 年开始使用 IIS Express 并点击 http:///dev-machine/swagger 时,我得到了 API 的预期 SwaggerUI 输出。

如果我将它部署到 docker 容器,我仍然可以到达我的 API 端点和 /swagger/v1/swagger.json 以及位于 /swagger 的 SwaggerUI .

一旦我把那个东西放在 traefik 路由器后面,我就无法再访问 /swagger,但是 /swagger/v1/swagger。json 仍然有效。

我在这里不知所措,不知道从哪里开始 - 所以:如果有人能指出正确的方向,我将不胜感激!

我的docker-compose.yaml项目:

    version: '3'
    services:
      traefik_msstockentries:
        restart: always
        image: "traefik"
        container_name: "traefik_msstockentries"
        command:
          - "--api.insecure=true"
          - "--providers.docker=true"
          - "--providers.docker.exposedbydefault=false"
          - "--entrypoints.web.address=:80"
        ports:
          - "9016:80"
          - "8016:8080"
        volumes:
          - "/var/run/docker.sock:/var/run/docker.sock:ro"
        networks:
          default:
            ipv4_address: 192.168.239.254
      msstockentries:
        restart: always
        container_name: "msstockentries"
        image: "msstockentries:dev"
        environment:
          - 'ASPNETCORE_ENVIRONMENT=Development'
          - 'TZ=Europe/Berlin'
        labels:
         - "traefik.enable=true"
         - "traefik.http.routers.msstockentries.rule=Host(`msstockentries`)"
         - "traefik.http.routers.msstockentries.entrypoints=web"
        networks:
          - default
        depends_on:
          - traefik_msstockentries
    networks:
      default:
        driver: bridge
        ipam:
          config:
            - subnet: 192.168.239.0/24

我的 Dockerfile(非常简单...)

WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM base AS final
WORKDIR .
COPY /publish/MS.StockEntries .
ENTRYPOINT ["dotnet", "MS.StockEntries.dll"]

我的Startup.cs

public class Startup
{
    public IConfiguration Configuration { get; }

    public static readonly LoggerFactory loggerFactory = new LoggerFactory(new[]
    {
        new DebugLoggerProvider()
    });

    private readonly ILogger _logger = loggerFactory.CreateLogger("StartupLogger");

    public Startup(IWebHostEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();

        this.Configuration = builder.Build();
    }

    public void ConfigureServices(IServiceCollection services)
    {

        services.Configure<MSStockEntriesConfig>(Configuration.GetSection("MSStockEntriesConfig"));
        MSStockEntriesConfig config = new MSStockEntriesConfig();
        Configuration.GetSection("MSStockEntriesConfig").Bind(config);

        // EdgeBlood Datenbank-Context
        services.AddDbContext<EdgeBloodDbContext>(optionsBuilder =>
        {
            optionsBuilder
            //.UseLoggerFactory(loggerFactory)
            .UseOracle(config.xxx, oracleOptions =>
            {
                oracleOptions.UseOracleSQLCompatibility("11");
            })
            //.EnableSensitiveDataLogging()
            ;
        });

        services.AddControllers();

        services.AddOData();

        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo
            {
                Title = "MS.StockEntries",
                Version = "v1",
                Description = "Get items on stock",
                Contact = new OpenApiContact
                {
                    Name = "Team xxx",
                    Email = "xxx@yyy.com"
                }
            });

            // Set the comments path for the Swagger JSON and UI.
            var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
            var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
            c.IncludeXmlComments(xmlPath);

            // Use method name as operationId
            c.CustomOperationIds(apiDesc =>
            {
                return apiDesc.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null;
            });
        });

        SetOutputFormatters(services);
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        app.UseAuthorization();
        app.UseAuthentication();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();               endpoints.Expand().Count().MaxTop(null).SkipToken().OrderBy().Filter();
            // Set OData-Route
            endpoints.MapODataRoute("OData", "OData", GetEdmModel());
        });
        
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Activate Swagger
        app.UseSwagger();
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "MS.StockEntries v1");
        });

        // Http-Redirection
        app.UseHttpsRedirection();
    }

    /// <summary>
    /// Create EdmModel for OData
    /// </summary>
    /// <returns>EdmModel für OData</returns>
    private IEdmModel GetEdmModel()
    {
        var odataBuilder = new ODataConventionModelBuilder();
        odataBuilder.EntitySet<StockEntry>("StockEntries");
        return odataBuilder.GetEdmModel();
    }

    /// <summary>
    /// Use swagger with ODataControllers
    /// </summary>
    /// <param name="services"></param>
    private static void SetOutputFormatters(IServiceCollection services)
    {
        services.AddMvcCore(options =>
        {
            IEnumerable<ODataOutputFormatter> outputFormatters =
                options.OutputFormatters.OfType<ODataOutputFormatter>()
                    .Where(formatter => formatter.SupportedMediaTypes.Count == 0);

            IEnumerable<ODataInputFormatter> inputFormatters =
                options.InputFormatters.OfType<ODataInputFormatter>()
                    .Where(formatter => formatter.SupportedMediaTypes.Count == 0);

            foreach (var outputFormatter in outputFormatters)
            {
                outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
                outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
            }

            foreach (var inputFormatter in inputFormatters)
            {
                inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
                inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));

            }

        });
    }
}

我的 traefik 配置:

http:
  routers:
    router-msstockentriesping:
      rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/ping`)"
      service: service-msstockentries
      middlewares:
        - "msstockentriesping-stripprefix"
        - "msstockentries-header"
        - "msstockentriesping-addendpoint"
    router-msstockentriesswagger:
      rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/index.html`)"
      service: service-msstockentries
      middlewares:
        - "msstockentriesswagger-stripprefix"
        - "msstockentries-header"
        - "msstockentriesswagger-addendpoint"
    router-msstockentriesswaggerdesc:
      rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/v1/swagger.json`)"
      service: service-msstockentries
      middlewares:
        - "msstockentriesswaggerdesc-stripprefix"
        - "msstockentries-header"
        - "msstockentriesswaggerdesc-addendpoint"
    router-msstockentries:
      rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries`)"
      service: service-msstockentries
      middlewares:
        - "msstockentries-stripprefix"
        - "msstockentries-header"
        - "msstockentries-addendpoint"
  middlewares:
    msstockentries-stripprefix:
      stripPrefix:
        prefixes:
          - "/msstockentries"
    msstockentriesping-stripprefix:
      stripPrefix:
        prefixes:
          - "/msstockentries/ping"
    msstockentriesswagger-stripprefix:
      stripPrefix:
        prefixes:
          - "/msstockentries/swagger/index.html"
    msstockentriesswaggerdesc-stripprefix:
      stripPrefix:
        prefixes:
          - "/msstockentries/swagger/v1/swagger.json"
    msstockentries-header:
      headers:
        customRequestHeaders:
          Host: "msstockentries"
    msstockentriesping-addendpoint:
      addPrefix:
        prefix: "/StockEntries/Ping"
    msstockentries-addendpoint:
      addPrefix:
        prefix: "/OData/StockEntries"
    msstockentriesswagger-addendpoint:
      addPrefix:
        prefix: "/swagger/index.html"
    msstockentriesswaggerdesc-addendpoint:
      addPrefix:
        prefix: "/swagger/v1/swagger.json"
  services:
    service-msstockentries:
      loadBalancer:
        passHostHeader: true
        servers:
          - url: 'http://zzz:9016'

终于找到我的错误了...

我最终为 SwaggerUI 所需的文件定义了明确的 Traefik 路由器和相关规则,并详细说明了 SwaggerEndpoint(在 app.UseSwaggerUI())。现在一切正常。

traefik配置的相关部分:

    http:
      routers:
        router-msstockentriesswagger:
          rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger`)"
          service: service-msstockentries
          middlewares:
            - "msstockentriesswagger-addendpoint"
        router-msstockentriesswaggercss:
          rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui.css`)"
          service: service-msstockentries
          middlewares:
            - "msstockentriesswaggercss-addendpoint"
        router-msstockentriesswaggerbundle:
          rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui-bundle.js`)"
          service: service-msstockentries
          middlewares:
            - "msstockentriesswaggerbundle-addendpoint"
        router-msstockentriesswaggerpreset:
          rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui-standalone-preset.js`)"
          service: service-msstockentries
          middlewares:
            - "msstockentriesswaggerpreset-addendpoint"
        router-msstockentriesswaggerdesc:
          rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/v1`)"
          service: service-msstockentries
          middlewares:
            - "msstockentriesswaggerv1-addendpoint"
      middlewares:
        msstockentriesswagger-addendpoint:
          addPrefix:
            prefix: "/swagger/index.html"
        msstockentriesswaggercss-addendpoint:
          addPrefix:
            prefix: "/swagger/swagger-ui.css"
        msstockentriesswaggerbundle-addendpoint:
          addPrefix:
            prefix: "/swagger/swagger-ui-bundle.js"
        msstockentriesswaggerpreset-addendpoint:
          addPrefix:
            prefix: "/swagger/swagger-ui-standalone-preset.js"
        msstockentriesswaggerv1-addendpoint:
          addPrefix:
            prefix: "/swagger/v1/swagger.json"

Startup.cs的相关部分:

    app.UseSwaggerUI(c =>
    {
        c.RoutePrefix = "swagger";
        c.SwaggerEndpoint($"{misApiServiceConfig.SwaggerUiEndpoint}", $"{misApiServiceConfig.SwaggerUiServicename} {misApiServiceConfig.SwaggerUIServiceversion}");
     });

要使用的 SwaggerUIEndpoit 可以在 appsettings.json 中设置。