Reputation: 31
We are trying to use Nswag and Odata both in our asp.net core API project. We can either Nswag for API documentation or Odata for query simplification. But when we use both of them and try to access API swagger document (https://localhost:5001/swagger/index.html) it's generating this error:
Here's my Startup Files:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.AspNet.OData.Extensions;
using System.Web.Http;
using Microsoft.OpenApi.Models;
using System.Reflection;
using Newtonsoft.Json;
using Microsoft.AspNet.OData.Builder;
using GL.Data.Models.EntityClass;
using Microsoft.Net.Http.Headers;
using Microsoft.AspNet.OData.Formatter;
using Newtonsoft.Json.Serialization;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
using Newtonsoft.Json.Converters;
namespace GL.service
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(options =>
{
// Use camel case properties in the serializer and the spec (optional)
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
// Use string enums in the serializer and the spec (optional)
options.SerializerSettings.Converters.Add(new StringEnumConverter());
});
// registers a Swagger v2.0 document with the name "v1" (default)
services.AddSwaggerDocument(c => {
c.DocumentName = "V1";
c.Title = "GL Controller";
});
services.AddOData();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseCors(b => b.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().AllowCredentials());
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseOpenApi(); // Serves the registered OpenAPI/Swagger documents by default on
app.UseSwaggerUi3(); // Serves the Swagger UI 3 web ui to view the OpenAPI/Swagger
app.UseMvc(routeBuilder =>
{
routeBuilder.EnableDependencyInjection();
routeBuilder.Expand().Select().Filter().Count().OrderBy();
});
}
}
}
Please help to solve this problem.
Upvotes: 2
Views: 2125
Reputation: 159
The root cause of this issue is that OData creates multiple routes with the same name which throws Swagger off. To get around this, you will need to use a custom operation processor. In the code given below, I am using a different route for odata vs api (for example, /odata/country vs /api/country, both of which call the same GET method that has an [EnableQuery] attribute.
In your startup, define your Odata routes:
services.AddControllers(options =>
{
})
.AddOData(opt => opt.AddRouteComponents("Odata", GetEdmModel()).Filter().Select().Expand());
In order to include Odata routes as a part of your Swagger documentation, add a new class in your project (I've called mine "AddOdataOperationProcessor"):
public class AddOdataOperationProcessor : IOperationProcessor
{
private readonly List<string> AddedOdataPaths;
private readonly string OdataPath;
public AddOdataOperationProcessor(string odataPath)
{
AddedOdataPaths = new List<string>();
OdataPath = odataPath.ToLower();
}
public bool Process(OperationProcessorContext context)
{
if (context.OperationDescription.Operation.ExtensionData == null)
context.OperationDescription.Operation.ExtensionData = new Dictionary<string, object>();
if (context.OperationDescription.Path.ToLower().StartsWith(OdataPath))
{
if (!AddedOdataPaths.Contains(context.OperationDescription.Path))
{
AddedOdataPaths.Add(context.OperationDescription.Path);
return true;
}
else
{
context.AllOperationDescriptions.Remove(context.OperationDescription);
return false;
}
}
return true;
}
}
Finally, add a Swagger document with a your custom operation processor. Please note that our custom processor requires an OData path as input. This should match the path you used when added OData in the startup (shown above) and be prepended with a "/":
services.AddSwaggerDocument(config =>
{
config.OperationProcessors.Add(new AddOdataOperationProcessor("/Odata"));
config.PostProcess = document =>
{
document.Info.Version = "v1";
document.Info.Title = "Include Odata";
document.Info.Description = "Includes OData";
document.Info.TermsOfService = "None";
document.Info.Contact = new NSwag.OpenApiContact
{
Name = "Kevat shah",
Email = string.Empty,
Url = "https://marketplace.goldensuncorp.com"
};
document.Info.License = new NSwag.OpenApiLicense
{
//TODO
Name = "Use under LICX",
Url = "https://example.com/license"
};
};
});
How it works: this custom processor is called once for every path that Swagger finds, including the duplicate OData paths. The processor identifies whether or a path is an OData path based on the path entered when you add it to the Swagger Document configuration (in this example, the path is "/Odata"). The variable AddedODataPaths tracks which paths have already been added and ignores paths that have already been added.
EDIT: If you want to ignore all OData routes for the Swagger documentation instead of adding them, use this class:
public class RemoveOdataOperationProcessor : IOperationProcessor
{
private readonly string OdataPath;
public RemoveOdataOperationProcessor(string odataPath)
{
OdataPath = odataPath.ToLower();
}
public bool Process(OperationProcessorContext context)
{
if (context.OperationDescription.Operation.ExtensionData == null)
context.OperationDescription.Operation.ExtensionData = new Dictionary<string, object>();
if (context.OperationDescription.Path.ToLower().StartsWith(OdataPath))
{
context.AllOperationDescriptions.Remove(context.OperationDescription);
return false;
}
return true;
}
}
Upvotes: 2
Reputation: 680
If it still does not work you may have to add additional workaround: .Net 5.0 and Microsoft.AspNetCore.OData 8.0.0-preview3
services.AddSwaggerGen(c =>
{
c.DocInclusionPredicate((docName, apiDesc) =>
{
// Filter out 3rd party controllers
var assemblyName = ((ControllerActionDescriptor)apiDesc.ActionDescriptor).ControllerTypeInfo.Assembly.GetName().Name;
var currentAssemblyName = GetType().Assembly.GetName().Name;
return currentAssemblyName == assemblyName;
});
c.SwaggerDoc("v1", new OpenApiInfo { Title = ApplicationConstant.APP_NAME, Version = "v1" });
});
First workaround:
services.AddOData();
services.AddMvcCore(options =>
{
foreach (var outputFormatter in options.OutputFormatters.OfType<ODataOutputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
}
foreach (var inputFormatter in options.InputFormatters.OfType<ODataInputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
}
});
It is not required to use AddMvcCore, you can also place the code in the AddControllers aswell.
Upvotes: 1
Reputation: 20116
Here is a workaround that you could fix API error(s) when NSwag UI loads.But OData isn't supported in Swashbuckle for AspNetCore and none of the OData endpoints will show in your NSwag UI.
services.AddOData();
services.AddMvcCore(options =>
{
foreach (var outputFormatter in options.OutputFormatters.OfType<ODataOutputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
}
foreach (var inputFormatter in options.InputFormatters.OfType<ODataInputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
}
});
Remember to place services.AddOData();
before the line services.AddMvcCore()
.
Refer to https://github.com/OData/WebApi/issues/1177
Upvotes: 3