NSwag: Generate C# Client from multiple Versions of an API - c#

We are versioning our API and generating the Swagger specification using Swashbuckle in ASP.NET Core 1.1. We can generate two API docs based on those JSON specification files:
<!-- language: c# -->
services.AddSwaggerGen(setupAction =>
{
setupAction.SwaggerDoc("0.1", new Info { Title = "Api", Version = "0.1", Description = "API v0.1" });
setupAction.SwaggerDoc("0.2", new Info { Title = "Api", Version = "0.2", Description = "API v0.2" });
// more configuration omitted
}
We are including all actions in both spec files, unless it is mapped to a specific version using the [MapToApiVersion] and ApiExplorerSettings(GroupName ="<version>")] attributes. Methods belonging to an older version only are also decorated with the [Obsolete] attribute:
<!-- language: c# -->
[MapToApiVersion("0.1")]
[ApiExplorerSettings(GroupName = "0.1")]
[Obsolete]
However, we want to have only one C# Client generated from the Union of both spec files, where all methods are included in the Client, 0.1 as well as 0.2, but all obsolete methods marked, in fact, as obsolete.
I have looked into both NSwag (which we are using for quite some time now) as well as AutoRest. AutoRest seems to support a merging scenario, but I could not get it to work because of schema validation errors (and I am more than unsure whether our specific scenario would be actually supported).
My last idea as of now to get this sorted is to somehow JSON-merge the specs into one and then feed it to NSwag.
Do we miss anything here? Is this somehow possible to realize with NSwag?

I wrote an article about similar problem https://medium.com/dev-genius/nswag-charp-client-from-multiple-api-versions-7c79a3de4622
First of all, create a schema. As I see, there are two approaches:
one schema where multiple versions are living
own schema for each version
Next, create clients for each supported version and wrap them under the wrapper client:
public class AppApiClient
{
public IV1Client V1 { get; }
public IV2Client V2 { get; }
public AppApiClient(HttpClient httpClient)
{
V1 = new V1Client(httpClient);
V2 = new V2Client(httpClient);
}
}

Here is my idea, expanding from the comments:
With swashbuckle you can generate as many SwaggerDoc as you like, the idea on this case is to generate 3 keep the same 2 versions that you have and add one more that will have everything.
c.MultipleApiVersions(
(apiDesc, targetApiVersion) =>
targetApiVersion.Equals("default") || // Include everything by default
apiDesc.Route.RouteTemplate.StartsWith(targetApiVersion), // Only include matching routes for other versions
(vc) =>
{
vc.Version("default", "Swagger_Test");
vc.Version("v1_0", "Swagger_Test V1_0");
vc.Version("v2_0", "Swagger_Test V2_0");
});
Here is a working sample:
http://swagger-net-test-multiapiversions.azurewebsites.net/swagger/ui/index?filter=Api
And the entire code for that project is on GitHub:
https://github.com/heldersepu/Swagger-Net-Test/tree/MultiApiVersions

Packages:
Install-Package Swashbuckle.AspNetCore
Install-Package Microsoft.AspNetCore.Mvc.Versioning
ValueV1Controller.cs
[ApiVersion("1")]
[Route("api/v{version:apiVersion}/Values")]
public class ValuesV1Controller : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
ValueV2Controller.cs
[ApiVersion("2")]
[Route("api/v{version:apiVersion}/Values")]
public class ValuesV2Controller : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1.2", "value2.2" };
}
}
Startup.cs
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();
services.AddApiVersioning();
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API - V1", Version = "v1" });
c.SwaggerDoc("v2", new Info { Title = "My API - V2", Version = "v2" });
c.DocInclusionPredicate((docName, apiDesc) =>
{
var versions = apiDesc.ControllerAttributes()
.OfType<ApiVersionAttribute>()
.SelectMany(attr => attr.Versions);
return versions.Any(v => $"v{v.ToString()}" == docName);
});
c.OperationFilter<RemoveVersionParameters>();
c.DocumentFilter<SetVersionInPaths>();
});
}
// 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();
}
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v2/swagger.json", "My API V2");
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseMvc();
}
}
public class RemoveVersionParameters : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
var versionParameter = operation.Parameters?.SingleOrDefault(p => p.Name == "version");
if (versionParameter != null)
operation.Parameters.Remove(versionParameter);
}
}
public class SetVersionInPaths : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.Paths = swaggerDoc.Paths
.ToDictionary(
path => path.Key.Replace("v{version}", swaggerDoc.Info.Version),
path => path.Value
);
}
}

Related

Serve multiple Swagger UIs on different URLs with single C# ASP.NET Core application

I need to serve multiple swagger UIs on single C# ASP.NET Core application. This is needed because application API consists of internal "private" API for UI and other stuff and "public" API that can be accessed by other applications and users.
Each Swagger endpoint should be on it's own swagger UI page and have a different URL address. I am able to divide existing API specification to two different json files and the json files in different routes using this code on Startup.cs Configure method:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseSwagger()
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("v0.1_public/swagger.json", "Public API v0.1");
c.SwaggerEndpoint("v0.1_private/swagger.json", "Private API v0.1");
});
...
}
I divide the specifications by filtering and adding two Swagger generators in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSwaggerGen(c =>
{
c.DocumentFilter<PublicAPISwaggerFilter>();
c.SwaggerDoc("v0.1_public", new OpenApiInfo
{
Title = "Public API",
Version = "v0.1"
});
});
services.AddSwaggerGen(c =>
{
c.DocumentFilter<PrivateApiSwaggerFilter>();
c.SwaggerDoc("v0.1_private", new OpenApiInfo
{
Title = "Private API",
Version = "v0.1"
});
});
...
}
Swagger UI is then served on https://localhost:port/swagger and both endpoints are listed on dropdown menu.
How can I create two swagger UIs when other is on route https://locahost:port/private/swagger and other one is https://locahost:port/public/swagger and each one them is displaying only one of the endpoints described above?
you can place your private and public controllers in separate directories (i.e Controllers/Private and Controllers/Public) and use controller model convention
Create controller conventions
public sealed class SwaggerConventions : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
// private controllers will contain 'private' word in namespace
bool isPrivate = controller.ControllerType.Namespace != null && controller.ControllerType.Namespace.Contains("Private");
controller.ApiExplorer.GroupName = isPrivate ? "private" : "public;
}
}
Use conventions in your startup/program.cs
builder.AddControllers(c =>
{
c.Conventions.Add(new SwaggerConentions());
})
Configure swagger endpoints
services.AddSwaggerGen(swagger =>
{
swagger.SwaggerDoc("public", new OpenApiInfo { Title = "public API", Version = "v1" });
swagger.SwaggerDoc("private", new OpenApiInfo { Title = "private API", Version = "v1" });
}
app
.UseSwagger()
.UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"/swagger/public/swagger.json", "Public API V1");
c.SwaggerEndpoint($"/swagger/private/swagger.json", "Private API");
}
You will need to split out your private and public controllers into separate applications. Doing so is also beneficial as you don't want to mix publicly accessibly API's with internal ones. It will be easier to maintain this way.
If its the specific URL convention you are after this can be handled by IIS or other tools you are using to host the applications.

ASP.Net Core specifying multiple content-types with response compression

What is the correct way to specify that your API can return compressed responses via Swagger/SwashbuckleUI in ASP.NET Core 3.x?
The goal is to support both non-compressed and compressed responses, giving the calling user the flexibility to choose what they want via their Accept-Encoding header. This is a hard requirement as one consuming team is limited in what libraries they can use to make the API calls. The code itself is doing what is required, but I can't get Swagger/SwashbuckleUI to reflect this feature in the documentation.
Given the following example:
Startup.cs
public void ConfigureServices(IServiceConnection services)
{
services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = System.IO.Compression.CompressionLevel.Optimal;
});
services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider();
options.EnableForHttps = true;
}
}
public void Configure(IApplicationBuilder app, IWebHostEnvrironment env)
{
app.UseResponseCompression();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
MyController.cs
[ApiController]
[Route("api/dev/mycontroller")]
[Produces("application/json")]
[ApiExplorerSettings(GroupName = "My Swagger Name")]
public class MyController : Controller
{
[HttpGet("")]
public async Task<ActionResult<ResponseDTO>> GetAll()
{
var dbResults = _repository.Model.GetAll();
if (dbResults != null)
{
return Ok(dbResults.ToResponseDTOs());
}
else
{
return BadRequest();
}
}
}
I would expect that I need to add another string to the ProducesAttribute:
[Produces("application/json", "br")]
So that Swagger/SwashbuckleUI would display that, similar to examples I have seen for dual json/xml support. However, when I try to build the project after adding the "br" string to the ProducesAttribute I get:
System.FormatException "The header contains invalid values at index 0: 'br'"
from the MapControllers() call in the Configure method on startup.

Getting OData Count in ASP.NET Core WebAPI

Using the sample code from Hassan Habib's Supercharging ASP.NET Core API with OData blog post, I am able to get the record count using an OData query of $count=true:
What needs to be configured to get the response object to be wrapped in an OData context so that the #odata.count property will show?
In my own ASP.NET Core web API project, I cannot get the simple $count parameter to work and I have no idea why.
With Hassan's sample code, the response JSON is wrapped in an OData context and the payload (an IEnumerable<Student> object) is in the value property of the JSON response. In my project, the OData context wrapper does not exist; my code never returns OData context, it only returns the payload object of type IEnumerable<T>:
I've also noticed that the Content-Type in the response header is application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8 in the sample project, where as it is simply application/json; charset=utf-8 in my project. I don't see any setting that controls this in either project, so I'm assuming the Microsoft.AspNetCore.Odata NuGet package is magically changing the response when it's configured properly.
My project is also using .NET Core 2.2 (Upgraded from 2.1), all the same versions of NuGet packages as Hassan's sample projects, and all the same settings in the StartUp.cs class... although my StartUp.cs is way more complicated (hence the reason I'm not posting it's content here.)
I could reproduce your issue when i use [Route("api/[controller]")]and [ApiController] with the startup.cs like below:
app.UseMvc(routeBuilder =>
{
routeBuilder.Expand().Select().Count().OrderBy().Filter();
routeBuilder.EnableDependencyInjection();
});
To fix it,be sure you have built a private method to do a handshake between your existing data models (OData model in this case) and EDM.
Here is a simple demo:
1.Controller(comment on Route attribute and ApiController attribute):
//[Route("api/[controller]")]
//[ApiController]
public class StudentsController : ControllerBase
{
private readonly WSDbContext _context;
public StudentsController(WSDbContext context)
{
_context = context;
}
// GET: api/Students
[HttpGet]
[EnableQuery()]
public IEnumerable<Student> Get()
{
return _context.Students;
}
}
//[Route("api/[controller]")]
//[ApiController]
public class SchoolsController : ControllerBase
{
private readonly WSDbContext _context;
public SchoolsController(WSDbContext context)
{
_context = context;
}
// GET: api/Schools
[HttpGet]
[EnableQuery()]
public IEnumerable<School> Get()
{
return _context.Schools;
}
2.Startup.cs():
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.AddMvcCore(action => action.EnableEndpointRouting = false);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
var connection = #"Server=(localdb)\mssqllocaldb;Database=WSDB;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<WSDbContext>(options => options.UseSqlServer(connection));
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.UseHttpsRedirection();
app.UseMvc(routeBuilder =>
{
routeBuilder.Expand().Select().Count().OrderBy().Filter();
routeBuilder.MapODataServiceRoute("api", "api", GetEdmModel());
});
}
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Student>("Students");
builder.EntitySet<Student>("Schools");
return builder.GetEdmModel();
}
}
Just been battling this.
I found that if I request my controller at /api/Things that most of the OData options work but $count doesn't.
However, $count does work if I request the same method via /odata/Things.
In my case I wanted to extend existing Api methods with [EnableQuery] but have it include the count metadata.
I ended up extending the EnableQuery attribute to return a different reponse, it worked perfectly.
public class EnableQueryWithMetadataAttribute : EnableQueryAttribute
{
public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
if (actionExecutedContext.Result is ObjectResult obj && obj.Value is IQueryable qry)
{
obj.Value = new ODataResponse
{
Count = actionExecutedContext.HttpContext.Request.ODataFeature().TotalCount,
Value = qry
};
}
}
public class ODataResponse
{
[JsonPropertyName("#odata.count")]
public long? Count { get; set; }
[JsonPropertyName("value")]
public IQueryable Value { get; set; }
}
}
You can just set an empty preffix route when you map OData, and you will receive OData with your request your endpoint.
routeBuilder.MapODataServiceRoute("ODataEdmModel", "", GetEdmModel());
In my case I've created a special action named $count that users the OData Filter query my collection (EF Database) and return the Count;
[HttpGet("$count")]
public async Task<int> Count(ODataQueryOptions<MyBookEntity> odataQueryOptions)
{
var queryable = this.dataContext.MyBooks;
return await odataQueryOptions.Filter
.ApplyTo(queryable, new ODataQuerySettings())
.Cast<MyBookEntity>()
.CountAsync();
}

Is it possible to use Swagger with AspNetCore Odata?

Yesterday I searched solution how to use swagger on Core Odata, I tried few libraries but with no success, it seams that currently it's not fully supported.
May be this info could be useful for somebody. Actually It's possible to use NSwag and create documentation for Odata Core from the box. There is workaround.
Just add swagger and Odata settings to Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(options =>
{
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
options.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.DefaultContractResolver();
});
services.AddOData();
//etc
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
var builder = new ODataConventionModelBuilder(app.ApplicationServices));
builder.EntitySet<Test>(nameof(Test));
app.UseMvc(routebuilder =>
{
routebuilder.MapODataServiceRoute("odata", "odata", builder.GetEdmModel());
});
app.UseSwaggerUi(typeof(Startup).GetTypeInfo().Assembly,
settings =>
{
settings.GeneratorSettings.DefaultPropertyNameHandling = PropertyNameHandling.CamelCase;
});
app.UseMvc();
//etc
}
Next mark Controller with route attribute as it would WebApi. Note: route should be different from odata.
Add [EnableQuery] to your IQueryable Action. Note2: you can't use [FromODataUri] for swagger docs Action with it should be marked as [SwaggerIgnore]
[Produces("application/json")]
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
[EnableQuery]
public IQueryable<Test> Get()
{
return _testService.Query();
}
//etc
}
Get swagger run!

`SwaggerRequestExample` is being ignored

As I was adding swagger to my API, I wanted to get default values and response examples. I added the NuGet packages and tried to follow this tutorial. The SwaggerResponseExample attribute works properly but the SwaggerRequestExample seems to be simply ignored.
With my action defined as follow
[SwaggerRequestExample(typeof(int), typeof(PersonExamples.Request))]
[SwaggerResponseExample(200, typeof(PersonExamples.Response))]
/* more attribute & stuff */
public IActionResult Get(int id) { /* blabla */ }
The PersonExamples class being defined as follow (non-revelant code removed)
public class PersonExamples
{
public class Request : IExamplesProvider
{
public object GetExamples() { return _persons.List().First().Id; }
}
public class Response : IExamplesProvider
{
public object GetExamples() { return _persons.List().First(); }
}
}
Here is also the relevant part of the Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(conf =>
{
conf.SwaggerDoc(_documentationPrefix, new Info
{
Title = "Global",
Version = "v0",
});
conf.OperationFilter<ExamplesOperationFilter>();
var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "Global.xml");
conf.IncludeXmlComments(filePath);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSwagger(opt =>
{
opt.RouteTemplate = "{documentName}/swagger.json";
});
if (env.IsDevelopment())
{
app.UseSwaggerUI(conf =>
{
conf.SwaggerEndpoint($"/{_documentationPrefix}/swagger.json", "DataService API");
conf.RoutePrefix = "doc/swagger";
});
}
}
When I run my project and go to the swagger.json page I notice that the response example is properly written, but the request example is nowhere to be found. After further debugging, I notice that a breakpoint placed in PersonExamples.Response.GetExamples will be hit when the page is called, but one placed in the PersonExamples.Request.GetExamples method won't. So i believe that the SwaggerRequestExample attribute never calls the method and may not even be called itself.
Did I improperly used the tag ? Why is it never called ?
I know this question is quite old, but Swagger Examples don't support GET request parameters (query parameters). It only works when the parameters are in the request body (eg: POST requests)

Categories

Resources