I'm trying to create a POST endpoint that will allow me to upload a single File in an API. I'm using swagger.
The problem is: no matter what I do, the "upload button" is not shown in my Swagger page.
This is my controller API post call:
[HttpPost("{groupId:int:min(1)}/validate")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status502BadGateway)]
public async Task<IActionResult> UploadDocument(IFormFile file)
{
// TODO: handle file upload
return await Task.FromResult(Ok());
}
I have a FileUploadFilter that written like this:
public class FileUploadFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var formParameters = context.ApiDescription.ParameterDescriptions
.Where(paramDesc => paramDesc.IsFromForm());
if (formParameters.Any())
{
// already taken care by swashbuckle. no need to add explicitly.
return;
}
if (operation.RequestBody != null)
{
// NOT required for form type
return;
}
if (context.ApiDescription.HttpMethod == HttpMethod.Post.Method)
{
var uploadFileMediaType = new OpenApiMediaType() {
Schema = new OpenApiSchema() {
Type = "object",
Properties =
{
["files"] = new OpenApiSchema()
{
Type = "array",
Items = new OpenApiSchema()
{
Type = "string",
Format = "binary"
}
}
},
Required = new HashSet<string>() { "files" }
}
};
operation.RequestBody = new OpenApiRequestBody {
Content = { ["multipart/form-data"] = uploadFileMediaType }
};
}
}
}
public static class Helper
{
internal static bool IsFromForm(this ApiParameterDescription apiParameter)
{
var source = apiParameter.Source;
var elementType = apiParameter.ModelMetadata?.ElementType;
return (source == BindingSource.Form || source == BindingSource.FormFile)
|| (elementType != null && typeof(IFormFile).IsAssignableFrom(elementType));
}
}
and I'm injecting it into the Swagger Definition like this:
c.OperationFilter<FileUploadFilter>();
No matter what I do, the produces swagger page presents me with the following:
A "path" parameter named "groupId"
A request body that does not have an "upload button" and just says: file string($binary)
How can I display an "upload button" in my swagger home page? Any help would be greatly appreciated.
Nevermind. I was not "hitting" the "try it out" button. My bad. It works like a charm
Related
I'm working on a .Net Framework (NOT .Net Core) Web Api and need to implement a file upload endpoint in a controller, but not being able to do it exposing a file upload button in Swagger.
I'm using Swagger to test my API and I have a Swagger-Net 8.3 package installed, that's why I'd like to continue testing with Swagger and not with Postman (if possible).
I've been trying with many different methods implementations to no avail (never see an upload file button in Swagger interface). One example could be the next:
[HttpPost]
public async Task<HttpResponseMessage> PostFormData(IFormFile fileUpload)
{
// Check if the request contains multipart/form-data.
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
string root = HttpContext.Current.Server.MapPath("~/App_Data");
var provider = new MultipartFormDataStreamProvider(root);
try
{
// Read the form data.
await Request.Content.ReadAsMultipartAsync(provider);
// This illustrates how to get the file names.
foreach (MultipartFileData file in provider.FileData)
{
Trace.WriteLine(file.Headers.ContentDisposition.FileName);
Trace.WriteLine("Server file path: " + file.LocalFileName);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
catch (System.Exception e)
{
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
}
}
And another example could be:
[ValidateMimeMultipartContentFilter]
[HttpPost, Route("softwarepackage")]
public Task<GenericEntity> UploadSingleFile(IFormFile fileUpload)
{
var streamProvider = new MultipartFormDataStreamProvider("ServerUploadFolder");
var task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith<GenericEntity>(t =>
{
var firstFile = streamProvider.FileData.FirstOrDefault();
if (firstFile != null)
{
// Do something with firstFile.LocalFileName
}
return new GenericEntity
{
};
});
return task;
}
public class FileOperationFilter : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
if (operation.operationId.ToLower() == "softwarepackage_uploadsinglefile")
{
if (operation.parameters == null)
operation.parameters = new List<Parameter>(1);
else
operation.parameters.Clear();
operation.parameters.Add(new Parameter
{
name = "File",
#in = "formData",
description = "Upload software package",
required = true,
type = "file"
});
operation.consumes.Add("application/form-data");
}
}
}
public class ValidateMimeMultipartContentFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
}
}
But none of them displays an "Upload File" button in Swagger UI.
I've seen many implementations for a .Net Core Web Api, but any ideas on how to do it with a NON Core Web Api?
I have a Controller action method which reads the Request.Body directly (instead of using File) for streaming and other purposes. The problem is there is no model binding and therefore Swagger doesn't document the contract. For example:
[HttpPost("upload")]
[DisableFormValueModelBinding]
public async Task<IActionResult> UploadAsync()
{
// Read from Request.Body manually, expecting content type to be multipart/*
return Ok();
}
When loading Swagger UI, there is no way to upload a file, etc.
Is there any way to support this with attributes in ASP.NET Core?
The API:
[HttpPost]
public async Task<IActionResult> Post(
[FromForm(Name = "myFile")]IFormFile myFile)
{
using (var fileContentStream = new MemoryStream())
{
await myFile.CopyToAsync(fileContentStream);
await System.IO.File.WriteAllBytesAsync(Path.Combine(folderPath, myFile.FileName), fileContentStream.ToArray());
}
return CreatedAtRoute(routeName: "myFile", routeValues: new { filename = myFile.FileName }, value: null); ;
}
Operation filter
public class SwaggerFileOperationFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
if (operation.OperationId == "Post")
{
operation.Parameters = new List<IParameter>
{
new NonBodyParameter
{
Name = "myFile",
Required = true,
Type = "file",
In = "formData"
}
};
}
}
}
Startup- ConfigureServices
services.AddSwaggerGen(
options =>
{
options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info { Title = "My API", Version = "v1" });
options.OperationFilter<SwaggerFileOperationFilter>();
});
The result in swagger UI:
The source is:enter link description here
I have a very simple controller action that takes in a viewmodel. I simply want to check the model in code and if it's not valid, dump the modelstate back as a BadRequest.
[HttpPost]
[Route("SaveBraceStep1")]
[SwaggerOperation(OperationId = "SaveBraceStep1")]
[ProducesResponseType(200, Type = typeof(VM.ProjectBraceDataModelStep1))]
public async Task<IActionResult> SaveBraceStep1(VM.ProjectBraceDataModelStep1 model)
{
if (!ModelState.IsValid)
{
return new BadRequestObjectResult(ModelState.Errors());
}
var project = await bracingDataService.SaveBraceStep1(model);
return Ok(project);
}
When the result comes back to Chrome, it's as expected.
I have an http interceptor as follows:
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, } from '#angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
export class HttpErrorInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request)
.pipe(catchError(err => {
// how can I read the results of the ModelState here
console.log(err);
return throwError('An error has been thrown');
}));
}
}
Here is what is dumped to the console
I've scoured thru tons of google searches and user blobs, but I can't seem to find an accepted concise way to do this.
I'm really blown away since I would expect this to be pretty much standardized by now.
Any Thoughts? Thank you in advance. Happy Coding!
~Mike
OK, so I figured this out with the help from https://www.strathweb.com/2018/07/centralized-exception-handling-and-request-validation-in-asp-net-core/
You basically have to register a new Action Type, then insert it into the pipeline.
public Task ExecuteResultAsync(ActionContext context)
{
var modelStateEntries = context.ModelState.Where(e => e.Value.Errors.Count > 0).ToArray();
var errors = new List<ValidationError>();
var details = "See ValidationErrors for details";
if (modelStateEntries.Any())
{
if (modelStateEntries.Length == 1 && modelStateEntries[0].Value.Errors.Count == 1 && modelStateEntries[0].Key == string.Empty)
{
details = modelStateEntries[0].Value.Errors[0].ErrorMessage;
}
else
{
foreach (var modelStateEntry in modelStateEntries)
{
foreach (var modelStateError in modelStateEntry.Value.Errors)
{
var error = new ValidationError
{
Name = modelStateEntry.Key,
Description = modelStateError.ErrorMessage
};
errors.Add(error);
}
}
}
}
var problemDetails = new ValidationProblemDetails
{
Status = 400,
Title = "Request Validation Error",
Instance = $"urn:myorganization:badrequest:{Guid.NewGuid()}",
Detail = details,
ValidationErrors = errors
};
context.HttpContext.Response.StatusCode = 400;
var json = JsonConvert.SerializeObject(problemDetails);
context.HttpContext.Response.WriteAsync(json);
return Task.CompletedTask;
}
}
public class ValidationProblemDetails : ProblemDetails
{
public ICollection<ValidationError> ValidationErrors { get; set; }
}
public class ValidationError
{
public string Name { get; set; }
public string Description { get; set; }
}
Then in startup.cs register it.
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = ctx => new ValidationProblemDetailsResult();
});
Then wired up in the interceptor
#Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
constructor(private uiService: Services.UIService, private injector: Injector) {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request)
.pipe(catchError((err: HttpErrorResponse) => {
const reader = new FileReader();
reader.addEventListener('loadend', (e) => {
const result = this.buildResponse(JSON.parse(e.srcElement['result']));
this.uiService.HttpError(result);
});
reader.readAsText(err.error);
return throwError(err);
}));
}
buildResponse(e: any): ApplicationHttpErrorResponse {
const model: ApplicationHttpErrorResponse = { ValidationErrors: [] };
if (e.ValidationErrors && e.ValidationErrors.length > 0) {
for (let i = 0; i < e.ValidationErrors.length; i++) {
const validator: ApplicationHttpError = {
Name: e.ValidationErrors[i].Name,
Description: e.ValidationErrors[i].Description
};
model.ValidationErrors.push(validator);
}
}
return model;
}
}
It can then be used in the UI service to display issues to the end user.
I am using .NetCore 3 and Swagger 5.0.0-rc4. I am trying to upload file(image) using Swagger but it does not work because the apply method in the IOperationFilter or even Swashbuckle.AspNetCore.Swagger are missing some attributes. For instance NonBodyParameter and Consumes do not exit in Swagger 5.0
Do anyone use face the same problem or tried to solve it?
public class FileOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (operation.OperationId.ToLower() == "apivaluesuploadpost")
{
operation.Parameters.Clear();
operation.Parameters.Add(new **NonBodyParameter**
{
Name = "uploadedFile",
In = "formData",
Description = "Upload File",
Required = true,
Type = "file"
});
operation.**Consumes**.Add("multipart/form-data");
}
}
}
AS for the missing Parameters Now these are changed to OpenApiParameter and OpenApiOperation.
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (operation.OperationId == "MyOperation")
{
operation.Parameters.Clear();
operation.Parameters.Add(new OpenApiParameter
{
Name = "formFile",
In = ParameterLocation.Header,
Description = "Upload File",
Required = true,
Schema= new OpenApiSchema
{
Type="file",
Format="binary"
}
});
var uploadFileMediaType = new OpenApiMediaType()
{
Schema = new OpenApiSchema()
{
Type = "object",
Properties =
{
["uploadedFile"] = new OpenApiSchema()
{
Description = "Upload File",
Type = "file",
Format = "binary"
}
},
Required = new HashSet<string>()
{
"uploadedFile"
}
}
};
operation.RequestBody = new OpenApiRequestBody
{
Content =
{
["multipart/form-data"] = uploadFileMediaType
}
};
}
}
I managed to solve this in Swashbuckle.AspNetCore 6.1.5
Swagger recognizes automatically IFormFile as a multipart/form-data media type.
You just have to delete the filter class and remove the [FromForm] or [FromBody] attribute from your parameter in the controller.
void Post([FromForm] IFileForm file) <= old
void Post(IFileForm file) <= new
I am implementing a swagger interface into an existing web API. The current API controller exposes an async upload function which uses the Request.Content to transport an image asynchronously. The code that has been used is explained in this article.
My api controller:
[HttpPost]
[Route("foo/bar/upload")]
public async Task<HttpResponseMessage> Upload()
{
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
var provider = await Request.Content.ReadAsMultipartAsync(new InMemoryMultipartFormDataStreamProvider());
NameValueCollection formData = provider.FormData;
HttpResponseMessage response;
//access files
IList<HttpContent> files = provider.Files;
if (files.Count > 0)
{
HttpContent file1 = files[0];
using (Stream input = await file1.ReadAsStreamAsync())
{
object responseObj = ExternalProcessInputStream(input)
response = Request.CreateResponse(HttpStatusCode.OK, responseObj);
}
}
else
{
response = Request.CreateResponse(HttpStatusCode.BadRequest);
}
return response;
}
This works dandy, but when i expose this through swagger i have a parameterless function, which returns an error when used.
My question is how can supply a proper value to test this method with?
You'll need to add a custom IOperationFilter to handle this.
Given you have a controller like so:
[ValidateMimeMultipartContentFilter]
[HttpPost, Route("softwarepackage")]
public Task<SoftwarePackageModel> UploadSingleFile()
{
var streamProvider = new MultipartFormDataStreamProvider(ServerUploadFolder);
var task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith<SoftwarePackageModel>(t =>
{
var firstFile = streamProvider.FileData.FirstOrDefault();
if (firstFile != null)
{
// Do something with firstFile.LocalFileName
}
return new SoftwarePackageModel
{
};
});
return task;
}
You then need to create an Swashbuckle.Swagger.IOperationFilter to add a file upload parameter to your function like:
public class FileOperationFilter : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
if (operation.operationId.ToLower() == "softwarepackage_uploadsinglefile")
{
if (operation.parameters == null)
operation.parameters = new List<Parameter>(1);
else
operation.parameters.Clear();
operation.parameters.Add(new Parameter
{
name = "File",
#in = "formData",
description = "Upload software package",
required = true,
type = "file"
});
operation.consumes.Add("application/form-data");
}
}
}
And in your Swagger config you'll need to register the filter:
config.EnableSwagger(c => {... c.OperationFilter<FileOperationFilter>(); ... });
To top this up, I also added a FilterAttribute to filter out Multipart content:
public class ValidateMimeMultipartContentFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
}
}