.Net Framework (C#) Web Api file upload button exposed in Swagger - c#

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?

Related

Swagger: File Upload Button not showing

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

How to swagger-document an ASP.NET Core action which reads Request.Body directly

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

Streaming Large ISO files ASP.Net Core

I'm trying to stream large iso files(~4Gb).
Therefore, I followed this article https://dotnetcoretutorials.com/2017/03/12/uploading-files-asp-net-core/ (Streaming Files (Large Files) is a part I was interested in).
My Controller looks like this:
[HttpPost]
[DisableFormValueModelBinding]
[Authorize]
[RequestFormLimits(MultipartBodyLengthLimit = 5368709000)]
[RequestSizeLimit(5368709000)]
[Route("{clusterId}/image_library/upload_image")]
[ProducesResponseType(typeof(AsyncOperationResult), 200)]
[ProducesResponseType(typeof(OperationResult), 400)]
[ProducesResponseType(typeof(string), 401)]
public IActionResult UploadFile()
{
bool result = Request.StreamFile("E:\\").Result; / extension method
if (result)
{
return Ok();
}
return BadRequest();
bool result= Request.StreamFile("E:\\").Result; /extension method
if (result)
{
return Ok();
}
return BadRequest();
}
The DisableFormValueModelBinding Attribute implemented as follows:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var formValueProviderFactory = context.ValueProviderFactories
.OfType<FormValueProviderFactory>()
.FirstOrDefault();
if (formValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(formValueProviderFactory);
}
var jqueryFormValueProviderFactory = context.ValueProviderFactories
.OfType<JQueryFormValueProviderFactory>()
.FirstOrDefault();
if (jqueryFormValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory);
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
And my Kestrel configured this way:
hostBuilder.UseKestrel(options =>
{
options.AddServerHeader = false;
options.Listen(ipAddress, ProviderSettings.API_port);
options.Listen(ipAddress, ProviderSettings.API_SSL_port, listenOptions =>
{
listenOptions.UseHttps(sslSert);
});
options.Limits.MaxRequestBodySize = null;
})
.UseContentRoot(contentRootPath)
.ConfigureLogging(logging =>
{
// Remove default Microsoft Logger
logging.ClearProviders();
});
The problem is that, when I post files on my controller method, they are getting loaded into RAM, but I need them to be streamed on hardrive by butches without devouring my RAM.
Checked my middlewares: none reconfigures Kestrel settings, the same code works fine on another project(I must be missing something).
Any ideas?

API File Upload using HTTP content not exposed in swagger

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)
{
}
}

Custom authorization IActionResults in aspnet-5 mvc-6

In ASP.NET 4 MVC5, I had this class that allowed me to return custom responses for unauthenticated responses to JSON endpoints. Here it is.
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (IsAjax(filterContext))
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new
{
success = false,
error = "You must be signed in."
}
};
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
private bool IsAjax(AuthorizationContext filterContext)
{
return filterContext.ActionDescriptor.GetFilterAttributes(true).OfType<AjaxAttribute>().FirstOrDefault() !=
null;
}
}
However, in MVC6, the new AuthorizeAttribute is no overrides for creating custom IActionResult results. How do I do this in MVC6?
A good point has been made by #blowdart in his comment about whether returning 401/403 should be the expected behaviour. In any case, I have tried a different approach for doing what the OP was asking, modifying the behavior of the default MVC authorization filters so that we return a json when user is unauthorized.
First thing I did was creating a new IAsyncAuthorizationFilter that will format the unauthorized result as a json for ajax request. It will basically:
Wrap an existing filter
Execute the wrapped filter
In case the user is unauthorized by the wrapped filter, return a json for ajax requests
This would be the CustomJsonAuthorizationFilter class:
public class CustomJsonAuthorizationFilter : IAsyncAuthorizationFilter
{
private AuthorizeFilter wrappedFilter;
public CustomJsonAuthorizationFilter(AuthorizeFilter wrappedFilter)
{
this.wrappedFilter = wrappedFilter;
}
public async Task OnAuthorizationAsync(Microsoft.AspNet.Mvc.Filters.AuthorizationContext context)
{
await this.wrappedFilter.OnAuthorizationAsync(context);
if(context.Result != null && IsAjaxRequest(context))
{
context.Result = new JsonResult(new
{
success = false,
error = "You must be signed in."
});
}
return;
}
//This could be an extension method of the HttpContext/HttpRequest
private bool IsAjaxRequest(Microsoft.AspNet.Mvc.Filters.AuthorizationContext filterContext)
{
return filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
}
}
Then I have created an IApplicationModelProvider in order to wrap all existing AuthorizeFilter with the new custom filter. The AuthroizeFilter is added by AuthorizationApplicationModelProvider, but the new provider will be run after the default one since the order of the default provider is -990.
public class CustomFilterApplicationModelProvider : IApplicationModelProvider
{
public int Order
{
get { return 0; }
}
public void OnProvidersExecuted(ApplicationModelProviderContext context)
{
//Do nothing
}
public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
this.ReplaceFilters(context.Result.Filters);
foreach(var controller in context.Result.Controllers)
{
this.ReplaceFilters(controller.Filters);
foreach (var action in controller.Actions)
{
this.ReplaceFilters(action.Filters);
}
}
}
private void ReplaceFilters(IList<IFilterMetadata> filters)
{
var authorizationFilters = filters.OfType<AuthorizeFilter>().ToList();
foreach (var filter in authorizationFilters)
{
filters.Remove(filter);
filters.Add(new CustomJsonAuthorizationFilter(filter));
}
}
}
Finally, update ConfigureServices in startup with the new application model provider:
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApplicationModelProvider, CustomFilterApplicationModelProvider>());
I finally figured it out after looking at the source.
public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
Func<CookieRedirectContext, Task> _old;
public CustomCookieAuthenticationEvents()
{
_old = OnRedirectToLogin;
OnRedirectToLogin = OnCustomRedirectToLogin;
}
public Task OnCustomRedirectToLogin(CookieRedirectContext context)
{
var actionContext = context.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>();
if (actionContext.ActionContext == null)
return _old(context);
if (actionContext.ActionContext.ActionDescriptor.FilterDescriptors.Any(x => x.Filter is AjaxAttribute))
{
// this is an ajax request, return custom JSON telling user that they must be authenticated.
var serializerSettings = context
.HttpContext
.RequestServices
.GetRequiredService<IOptions<MvcJsonOptions>>()
.Value
.SerializerSettings;
context.Response.ContentType = "application/json";
using (var writer = new HttpResponseStreamWriter(context.Response.Body, Encoding.UTF8))
{
using (var jsonWriter = new JsonTextWriter(writer))
{
jsonWriter.CloseOutput = false;
var jsonSerializer = JsonSerializer.Create(serializerSettings);
jsonSerializer.Serialize(jsonWriter, new
{
success = false,
error = "You must be signed in."
});
}
}
return Task.FromResult(0);
}
else
{
// this is a normal request to an endpoint that is secured.
// do what ASP.NET used to do.
return _old(context);
}
}
}
Then, use this event class as follows:
services.Configure<IdentityOptions>(options =>
{
options.Cookies.ApplicationCookie.Events = new CustomCookieAuthenticationEvents();
});
ASP.NET 5 sure made simple things harder to do. Granted though, I can now customize things at a more granular level without effecting other pieces. Also, the source code is amazingly easy to read/understand. I am pretty happy having the confidence that any issue I am having can easily be identified as a bug or resolved by looking at the source.
Cheers to the future!

Categories

Resources