Open OculiViridi opened 6 years ago
I just tried with the latest version (v18.19.0) and it is set to json by default (it only has json):
Can you provide a sample and/or provide the controller code?
Can you also show your NSwag middleware registration in startup.cs?
@RSuter I've already updated to v11.19.0.
Here it is my Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(config =>
{
// HTTP 406 when not supported format is requested by client
config.ReturnHttpNotAcceptable = true;
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
// Add FluentValidation
.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<MachineCreateModelValidator>());
// Add API versioning
services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ReportApiVersions = true;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseHsts();
// Add Exception handling
app.UseGlobalExceptionHandler();
}
// Add Swagger
app.UseSwagger();
app.UseMvc();
}
}
UseSwagger
extension method
public static class ServiceExtensions
{
public static void UseSwagger(this IApplicationBuilder app)
{
string title = "My Control API";
string description = "My API for Core functionalities.";
app.UseSwaggerWithApiExplorer(config =>
{
config.GeneratorSettings.OperationProcessors.TryGet<ApiVersionProcessor>().IncludedVersions = new[] { "1.0" };
config.SwaggerRoute = "v1.0.json";
config.GeneratorSettings.Title = title;
config.GeneratorSettings.Description = description;
});
app.UseSwaggerWithApiExplorer(config =>
{
config.GeneratorSettings.OperationProcessors.TryGet<ApiVersionProcessor>().IncludedVersions = new[] { "2.0" };
config.SwaggerRoute = "v2.0.json";
config.GeneratorSettings.Title = title;
config.GeneratorSettings.Description = description;
});
app.UseSwaggerUi3(config =>
{
config.SwaggerRoutes.Add(new SwaggerUi3Route("v1.0", "/v1.0.json"));
config.SwaggerRoutes.Add(new SwaggerUi3Route("v2.0", "/v2.0.json"));
});
}
}
And this is one of the controllers (Machines
)
[ApiVersion("1.0")]
[SwaggerTag("Machines", Description = "Core operations on machines.")]
public class MachinesController : BaseController
{
[HttpGet("{id:long}")]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<ActionResult<Machine>> Get(long id)
{
return await Mediator.Send(new GetMachineByIdQuery() { Id = id });
}
[HttpGet]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<ActionResult<List<Machine>>> List()
{
return await Mediator.Send(new GetMachineListQuery());
}
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.Created)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> Create(MachineCreateModel machine)
{
if (machine == null)
return BadRequest();
long newId = await Core.CreateMachine(machine);
return CreatedAtAction(nameof(Get), new { id = newId }, null);
}
[HttpPut]
[ProducesResponseType((int)HttpStatusCode.Accepted)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> Update(long id, Machine machine)
{
if (machine == null || machine.Id != id)
{
return BadRequest();
}
await Mediator.Send(new MachineUpdateCommand { Machine = machine });
return AcceptedAtAction(nameof(Get), new { id = id }, machine);
}
[HttpPost]
public async Task<IActionResult> NormalizeConfiguration(int machineId)
{
await Core.MachineNormalizeConfiguration(machineId);
return Ok();
}
[HttpPost]
public async Task<IActionResult> Start(int machineId)
{
await Core.MachineStart(machineId);
return Ok();
}
[HttpPost]
public async Task<IActionResult> Stop(int machineId)
{
await Core.MachineStop(machineId);
return Ok();
}
}
I'd say UseSwagger() is Swashbuckle and not NSwag, so you need to check in the Swashbuckle docs why this happens, right?
@RSuter Sorry! My fault! I was missing the code of MY UseSwagger()
extension method!!! 😛
I've updated the previous post. Please take a look now... Thanks!
@RSuter I've just updated to latest version (11.19.2) but the problem still remains. What am I doing wrong?
I've noticed just now, that the default content-type selected on the dropdowns is different for GET and POST/PUT. So for GETs there's "text/plain"
and for POSTs/PUTs "application/json"
instead.
@RSuter Hi! I'm now on v11.20.1, but the problem is still there. As described in my last comment
the default content-type selected on the dropdowns is different for GET and POST/PUT. So for GETs there's "text/plain" and for POSTs/PUTs "application/json" instead
I think it can be a matter of configuration... can we investigate further?
Did you check the generated swagger.json? There you will see text/plain somewhere.. can you post that?
Here it is my JSON.
There's "text/plain" as first value of produces
list in the /api/Machines/Get
method.
{
"x-generator": "NSwag v11.20.1.0 (NJsonSchema v9.11.0.0 (Newtonsoft.Json v11.0.0.0))",
"swagger": "2.0",
"info": {
"title": "Control API",
"description": "API for Core functionalities.",
"version": "1.0.0"
},
"host": "localhost:5001",
"schemes": [
"http"
],
"consumes": [
"application/json-patch+json",
"application/json",
"text/json",
"application/*+json"
],
"paths": {
"/api/v1.0/Machines/Get/{id}": {
"get": {
"tags": [
"Machines"
],
"operationId": "Machines_Get",
"produces": [
"text/plain",
"application/json",
"text/json"
],
"parameters": [
{
"type": "integer",
"name": "id",
"in": "path",
"required": true,
"format": "int64",
"x-nullable": false
}
],
"responses": {
"200": {
"x-nullable": true,
"description": "",
"schema": {
"$ref": "#/definitions/Machine"
}
}
}
}
},
"/api/v1.0/Machines/Create": {
"post": {
"tags": [
"Machines"
],
"operationId": "Machines_Create",
"consumes": [
"application/json-patch+json",
"application/json",
"text/json",
"application/*+json"
],
"parameters": [
{
"name": "machine",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/MachineCreateModel"
},
"x-nullable": false
}
],
"responses": {
"201": {
"description": ""
},
"400": {
"description": ""
}
}
}
},
"/api/v1.0/Machines/Update": {
"put": {
"tags": [
"Machines"
],
"operationId": "Machines_Update",
"consumes": [
"application/json-patch+json",
"application/json",
"text/json",
"application/*+json"
],
"parameters": [
{
"type": "integer",
"name": "id",
"in": "query",
"format": "int64",
"x-nullable": false
},
{
"name": "machine",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Machine"
},
"x-nullable": false
}
],
"responses": {
"202": {
"description": ""
},
"400": {
"description": ""
}
}
}
}
},
"definitions": {
"Machine": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"status",
"machineDriverId"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"mnmConfiguration": {
"type": "string"
},
"status": {
"$ref": "#/definitions/MachineBootStatus"
},
"machineDriverId": {
"type": "integer",
"format": "int64"
},
"machineDriver": {
"$ref": "#/definitions/MachineDriver"
},
"driverConfiguration": {
"type": "string"
},
"driverStatus": {
"type": "string"
}
}
},
"MachineBootStatus": {
"type": "string",
"description": "",
"x-enumNames": [
"Disabled",
"Stopped",
"Started",
"DriverNotFound",
"InvalidDriverConfig",
"InvalidMnmConfig",
"NoRoutes",
"Failed"
],
"enum": [
"Disabled",
"Stopped",
"Started",
"DriverNotFound",
"InvalidDriverConfig",
"InvalidMnmConfig",
"NoRoutes",
"Failed"
]
},
"MachineDriver": {
"type": "object",
"additionalProperties": false,
"required": [
"id"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"model": {
"type": "string"
},
"signature": {
"type": "string"
}
}
},
"MachineCreateModel": {
"type": "object",
"additionalProperties": false,
"required": [
"machineDriverId"
],
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"mnmConfiguration": {
"type": "string"
},
"phaseName": {
"type": "string"
},
"machineDriverId": {
"type": "integer",
"format": "int64"
}
}
}
},
"tags": [
{
"name": "Machines",
"description": "Core operations on machines."
}
]
}
I temporary solved the problem by simply putting the Produces
attribute on the top of all of my controllers, like this:
[Produces("application/json")]
[ApiVersion("1.0")]
[SwaggerTag("Machines", Description = "Core operations on machines.")]
public class MachinesController : ControllerBase
{ }
I forgot to mention that if you are using a common base class for all your Controllers, you can just put the Produces
attribute only on the base class.
[ApiController]
[Produces("application/json")]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
public abstract class BaseController : ControllerBase
{
// TODO: Add needed common methods/properties for all Controllers
}
[Authorize]
[ApiVersion("1.0")]
[OpenApiTag("Machines", Description = "Operations on machines")]
public class MachinesController : BaseController
{
// TODO: Add controller methods
}
[Authorize]
[ApiVersion("1.0")]
[OpenApiTag("Machines", Description = "Operations on machines")]
public class DriversController : BaseController
{
// TODO: Add controller methods
}
@RSuter But I don't know if it is really necessary, since you said that should come automatically. Any news?
Also There are one more temporary resolving way.
Just override Produces
on AddSwaggerDocument
when add swagger document on startup.cs
services.AddSwaggerDocument(settings =>
{
settings.PostProcess = document => document.Produces = new List<string>
{
"application/json",
"text/json"
};
});
Also There are one more temporary resolving way.
Just override
Produces
onAddSwaggerDocument
when add swagger document on startup.csservices.AddSwaggerDocument(settings => { settings.PostProcess = document => document.Produces = new List<string> { "application/json", "text/json" }; });
@js-lee0624
Just a note for users reading the suggestion, from NSwag v13, there was a refactoring, so the method is called AddOpenApiDocument
instead of AddSwaggerDocument
.
@OculiViridi
you can just put the Produces attribute only on the base class.
I don't think that would be a good idea in many cases as Produces changes the behavior of the application, basically it turns off content negotiation. It might work if you always produce JSON output, but it is definitely not RESTful to ignore the Accept HTTP request header.
Also There are one more temporary resolving way.
Just override
Produces
onAddSwaggerDocument
when add swagger document on startup.csservices.AddSwaggerDocument(settings => { settings.PostProcess = document => document.Produces = new List<string> { "application/json", "text/json" }; });
Just found this thread, but I unable to handle this issue with the solution provided above. We customize several NSwag related things, so I also tried on an NSwag sample project for .Net Core 3.1 with AddOpenApiDocument extension, but no luck.
I'm also having the same problem. For example, if I try to use something like this to define the output content type per status code
[ProducesResponseType(typeof(MyObj), StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest, MediaTypeNames.Text.Plain)]
The content type just gets ignored.
While debugging, I could see that the information was there but it was not used (OperationResponseProcessor.cs)
The value comes from a hard-coded logic in the code (OpenApiResponse.cs)
As a workaround, I did an Operation Processor to change the content type.
public class ForceResponseContentTypeAttribute(int statusCode, string contentType)
: OpenApiOperationProcessorAttribute(typeof(ForceResponseContentTypeOperationProcessor), statusCode, contentType)
{
private class ForceResponseContentTypeOperationProcessor(int statusCode, string contentType) : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
var statusCodeString = statusCode.ToString();
foreach (var response in context.OperationDescription.Operation.Responses)
{
if (response.Key == statusCodeString)
{
if (response.Value.Content is { Count: >= 1 })
{
var existentContent = response.Value.Content.First();
response.Value.Content.Remove(existentContent.Key);
response.Value.Content.Add(contentType, existentContent.Value);
}
}
}
return true;
}
}
}
And use it like this
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest, MediaTypeNames.Text.Plain)] // this prefills the needed data
[ForceResponseContentType(StatusCodes.Status400BadRequest, MediaTypeNames.Text.Plain)]
It won't work for all use cases but is enough for me.
Since latest 2-3 releases (I don't know exactly which one) I notice that the default content-type selected on the swagger HTML dropdown menu for the method reponse, is not
"application/json"
but"text/plain"
.If I don't change it everytime before clicking on Try button, I get an error because no content-type negotiation for responses is allowed in my application. I think that
"application/json"
should be the right default value, but anyway is it possible to set the default content-type for response in Swagger configuration to avoid changing it everytime?This is my actual configuration
The HTML result