I have a simple .NET core/.NET 5 API that returns some data via OData and some Data via "traditional" endpoints though all in the same controller inheriting ODataController.
When I'm starting the project with IIS Express from Visual Studio 2019 and hit http://dev-machine/swagger, I get the expected SwaggerUI-output for my API.
If I deploy it to a docker container, I can still get to my API endpoints and to /swagger/v1/swagger.json and to the SwaggerUI at /swagger.
As soon as I put that thing behind a traefik router, I'm no longer able to get to /swagger, but /swagger/v1/swagger.json still works.
I'm at a loss here and don't know where to start - so: if anyone could point me in the right direction, that'd be greatly appreciated!
My docker-compose.yaml for the project:
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
My Dockerfile (pretty simple...)
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM base AS final
WORKDIR .
COPY /publish/MS.StockEntries .
ENTRYPOINT ["dotnet", "MS.StockEntries.dll"]
My 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 = "[email protected]"
}
});
// 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"));
}
});
}
}
My traefik config:
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'
Finally found my mistake(s)...
I ended up defining explicit Traefik routers and associated rules for the files needed by SwaggerUI as well as detailing the SwaggerEndpoint (at app.UseSwaggerUI()). Now everything is working as expected.
Relevant parts of traefik config:
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"
Relevant parts of Startup.cs:
app.UseSwaggerUI(c =>
{
c.RoutePrefix = "swagger";
c.SwaggerEndpoint($"{misApiServiceConfig.SwaggerUiEndpoint}", $"{misApiServiceConfig.SwaggerUiServicename} {misApiServiceConfig.SwaggerUIServiceversion}");
});
The SwaggerUIEndpoit to use can be set in appsettings.json.