ASP.NET Core 6-7 Web API, Disable multipart/form-data model binding entirely (how to disable IFormFile bind?) - asp.net-core

Im trying to create dotnet 7 webapi endpoint with streaming file upload but I always get an error that I cant overcome. I'm Trying to use a library called UploadStream, Github page: https://github.com/ma1f/uploadstream
Error:
System.IO.IOException: Unexpected end of Stream, the content may have already been read by another component.
at Microsoft.AspNetCore.WebUtilities.MultipartReaderStream.ReadAsync(Memory1 buffer, CancellationToken cancellationToken) at Microsoft.AspNetCore.WebUtilities.StreamHelperExtensions.DrainAsync(Stream stream, ArrayPool1 bytePool, Nullable1 limit, CancellationToken cancellationToken) at Microsoft.AspNetCore.WebUtilities.MultipartReader.ReadNextSectionAsync(CancellationToken cancellationToken) at Microsoft.AspNetCore.Http.Features.FormFeature.InnerReadFormAsync(CancellationToken cancellationToken) at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.FormFileModelBinder.GetFormFilesAsync(String modelName, ModelBindingContext bindingContext, ICollection1 postedFiles)
at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.FormFileModelBinder.BindModelAsync(ModelBindingContext bindingContext)
at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexObjectModelBinder.BindPropertyAsync(ModelBindingContext bindingContext, ModelMetadata property, IModelBinder propertyBinder, String fieldName, String modelName)
at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexObjectModelBinder.BindPropertiesAsync(ModelBindingContext bindingContext, Int32 propertyData, IReadOnlyList`1 boundProperties)
at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexObjectModelBinder.BindModelCoreAsync(ModelBindingContext bindingContext, Int32 propertyData)
It happens on line 31 in ControllerExtensions.cs with the method controller.TryUpdateModelAsync<T>(model, prefix: "", valueProvider : form);
Controller Code:
[HttpPost]
[DisableFormValueModelBinding]
public async Task<IActionResult> UploadPresentationV1()
{
byte[] buffer = new byte[1024 * 100];
List<IFormFile> files = new List<IFormFile>();
var model = await this.StreamFiles<UploadPresentationCommand>(async x =>
{
using (var stream = x.OpenReadStream())
while (await stream.ReadAsync(buffer, 0, buffer.Length) > 0) ;
files.Add(x);
});
return Ok("");
}
Model UploadPresentationCommand:
public class UploadPresentationCommand
{
[Required]
public string Name { get; set; }
[Required]
public string PresentationContent { get; set; }
public List<IFormFile> Images { get; set; }
}
I tried to use a disable form model binding attribute what I found in the microsoft documentation:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
What I found out, somehow even with this attribute the List<IFormFile> Images gets binded from the request to the model (For example if I specify it in the endpoint parameter like that [FromForm] UploadPresentationCommand uploadPresentationCommand, The files are binded, but the other two properties are not, so I assume that this is the root of the problem and I dont know how to disable this type of binding entirely, also im not sure if it could help.

Related

Best way to insert high trafic into CosmosDb with Entity Framework Core

we have ASP.NET Core 6.0 website and want to write logging data into CosmosdDB.
Therefor i have a static CosmosDB Manager, which has the CosmosDBContext as static property, and the add method uses this context to AddLog() and SaveContext().
I had to put the Context in a lock, as multiple calls to the AddLog() method caused an error.
Is this the right way to have (concurrent) Writes to the CosmosDB?
I had previously separate Context for each request, but this opened to many threads...
Any thoughts on this Implementation?
public static class LogManager {
private static CosmosDBContext Context { get; set; }
public static void AddLog(string data) {
var item = new LogItem {
Data = data
};
if (Context == null) Context = new();
lock (Context) {
Context.Add(item);
Context.SaveChanges();
}
}
}
public class LogItem {
public string Data { get; set; }
}

Custom model binding through body in ASP.Net Core

I would like to bind an object in a controller through the body of a HTTP Post.
It works like this
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException("No context found");
string modelName = bindingContext.ModelName;
if (String.IsNullOrEmpty(modelName)) {
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
string value = bindingContext.ValueProvider.GetValue(modelName).FirstValue;
...
The modelName is viewModel (honestly, I don't know why, but it works...)
My controller looks like this
[HttpPost]
[Route("my/route")]
public IActionResult CalcAc([ModelBinder(BinderType = typeof(MyModelBinder))]IViewModel viewModel)
{
....
i.e. it works, when I make this HTTP-Post request
url/my/route?viewModel=URLparsedJSON
I would like however to pass it through the body of the request, i.e.
public IActionResult Calc([FromBody][ModelBinder(BinderType = typeof(MyModelBinder))]IViewModel viewModel)
In my Modelbinder then, the modelName is "" and the ValueProvider yields null... What am I doing wrong?
UPDATE
Example; Assume you have an interface IGeometry and many implementations of different 2D shapes, like Circle: IGeometry or Rectangle: IGeometry or Polygon: IGeometry. IGeometry itself has the method decimal getArea(). Now, my URL shall calculate the area for any shape that implements IGeometry, that would look like this
[HttpPost]
[Route("geometry/calcArea")]
public IActionResult CalcArea([FromBody]IGeometry geometricObject)
{
return Ok(geometricObject.getArea());
// or for sake of completness
// return Ok(service.getArea(geometricObject));
}
the problem is, you cannot bind to an interface, that yields an error, you need a class! That's where the custom model binder is used. Assume your IGeometryalso has the following property string Type {get; set;}
the in the custom model binding you would simply search for that Type in the passed json and bind it to the correct implementation. Something like
if (bodyContent is Rectangle) // that doesn't work ofc, but you get the point
var boundObject = Newtonsoft.Json.JsonConvert.DeserializeObject<Rectangle>(jsonString);
ASP.Net EF
In ASP.Net EF the custom model binding looks like this
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
here you get the body of the HTTPPost request like this
string json = actionContext.Request.Content.ReadAsStringAsync().Result;
in ASP.Net Core you don't have the actionContext, only the bindingContext where I can't find the body of the HTTP Post.
UPDATE 2
Ok, I found the body, see accepted answer. Now inside the controller method I really have an object from type IGeometry (an interface) that is instantiated inside the custom model binder! My controller method looks like this:
[HttpPost]
[Route("geometry/calcArea")]
public IActionResult CalcArea([FromBody]IGeometry geometricObject)
{
return Ok(service.getArea(geometricObject));
}
And my injected service like this
public decimal getArea(IGeometry viewModel)
{
return viewModel.calcArea();
}
IGeometry on the other hand looks like this
public interface IGeometry
{
string Type { get; set; } // I use this to correctly bind to each implementation
decimal calcArea();
...
Each class then simply calculates the area accordingly, so
public class Rectangle : IGeometry
{
public string Type {get; set; }
public decimal b0 { get; set; }
public decimal h0 { get; set; }
public decimal calcArea()
{
return b0 * h0;
}
or
public class Circle : IGeometry
{
public string Type {get; set; }
public decimal radius { get; set; }
public decimal calcArea()
{
return radius*radius*Math.Pi;
}
I found a solution. The body of a HTTP Post request using ASP.NET Core can be obtained in a custom model binder using this lines of code
string json;
using (var reader = new StreamReader(bindingContext.ActionContext.HttpContext.Request.Body, Encoding.UTF8))
json = reader.ReadToEnd();
I found the solution after looking at older EF projects. There the body is inside the ActionContext which is passed separately as an argument in the BindModel method. I found that the same ActionContext is part of the ModelBindingContext in ASP.Net Core, where you get an IO.Stream instead of a string (easy to convert :-))

Model Binding for multipart/form-data (File + JSON) post in ASP.NET Core 1.1

I'm attempting to build an ASP.NET Core 1.1 Controller method to handle an HTTP Request that looks like the following:
POST https://localhost/api/data/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--------------------------625450203542273177701444
Host: localhost
Content-Length: 474
----------------------------625450203542273177701444
Content-Disposition: form-data; name="file"; filename="myfile.txt"
Content-Type: text/plain
<< Contents of my file >>
----------------------------625450203542273177701444
Content-Disposition: form-data; name="text"
Content-Type: application/json
{"md5":"595f44fec1e92a71d3e9e77456ba80d0","sessionIds":["123","abc"]}
----------------------------625450203542273177701444--
It's a multipart/form-data request with one part being a (small) file and the other part a json blob that is based on a provided specification.
Ideally, I'd love my controller method to look like:
[HttpPost]
public async Task Post(UploadPayload payload)
{
// TODO
}
public class UploadPayload
{
public IFormFile File { get; set; }
[Required]
[StringLength(32)]
public string Md5 { get; set; }
public List<string> SessionIds { get; set; }
}
But alas, that doesn't Just Work {TM}. When I have it like this, the IFormFile does get populated, but the json string doesn't get deserialized to the other properties.
I've also tried adding a Text property to UploadPayload that has all the properties other than the IFormFile and that also doesn't receive the data. E.g.
public class UploadPayload
{
public IFormFile File { get; set; }
public UploadPayloadMetadata Text { get; set; }
}
public class UploadPayloadMetadata
{
[Required]
[StringLength(32)]
public string Md5 { get; set; }
public List<string> SessionIds { get; set; }
}
A workaround that I have is to avoid model binding and use MultipartReader along the lines of:
[HttpPost]
public async Task Post()
{
...
var reader = new MultipartReader(Request.GetMultipartBoundary(), HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
var filePart = section.AsFileSection();
// Do stuff & things with the file
section = await reader.ReadNextSectionAsync();
var jsonPart = section.AsFormDataSection();
var jsonString = await jsonPart.GetValueAsync();
// Use $JsonLibrary to manually deserailize into the model
// Do stuff & things with the metadata
...
}
Doing the above bypasses model validation features, etc. Also, I thought maybe I could take that jsonString and then somehow get it into a state that I could then call await TryUpdateModelAsync(payloadModel, ...) but couldn't figure out how to get there either - and that didn't seem all that clean either.
Is it possible to get to my desired state of "transparent" model binding like my first attempt? If so, how would one get to that?
The first problem here is that the data needs to be sent from the client in a slightly different format. Each property in your UploadPayload class needs to be sent in its own form part:
const formData = new FormData();
formData.append(`file`, file);
formData.append('md5', JSON.stringify(md5));
formData.append('sessionIds', JSON.stringify(sessionIds));
Once you do this, you can add the [FromForm] attribute to the MD5 property to bind it, since it is a simple string value. This will not work for the SessionIds property though since it is a complex object.
Binding complex JSON from the form data can be accomplished using a custom model binder:
public class FormDataJsonBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if(bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));
// Fetch the value of the argument by name and set it to the model state
string fieldName = bindingContext.FieldName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(fieldName);
if(valueProviderResult == ValueProviderResult.None) return Task.CompletedTask;
else bindingContext.ModelState.SetModelValue(fieldName, valueProviderResult);
// Do nothing if the value is null or empty
string value = valueProviderResult.FirstValue;
if(string.IsNullOrEmpty(value)) return Task.CompletedTask;
try
{
// Deserialize the provided value and set the binding result
object result = JsonConvert.DeserializeObject(value, bindingContext.ModelType);
bindingContext.Result = ModelBindingResult.Success(result);
}
catch(JsonException)
{
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
}
You can then use the ModelBinder attribute in your DTO class to indicate that this binder should be used to bind the MyJson property:
public class UploadPayload
{
public IFormFile File { get; set; }
[Required]
[StringLength(32)]
[FromForm]
public string Md5 { get; set; }
[ModelBinder(BinderType = typeof(FormDataJsonBinder))]
public List<string> SessionIds { get; set; }
}
You can read more about custom model binding in the ASP.NET Core documentation: https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding
I'm not 100% clear on how this would work for ASP.NET Core but for Web API (so I assume a similar path exists here) you'd want to go down the road of a Media Formatter. Here's an example (fairly similar to your question) Github Sample with blog post
Custom formatters might be the ticket? https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-formatters

The 0.3 WebJobs SDK broke my parameter bindings

I have the following method definition:
public static void ProcessPackageRequestMessage(
[QueueTrigger(queues.PACKAGE)] PackageRequestMessage message,
[Blob(blobs.PACKAGE + "/{RequestId}_{BlobFile}")] ICloudBlob blob,
[Table(tables.PACKAGE)] CloudTable table,
[Queue(queues.EMAIL)] out PackageEmailMessage packageEmailMessage)
The class PackageRequestMessage is defined as follows:
public class PackageRequestMessage
{
public Guid RequestId { get; set; }
public Guid FactoryId { get; set; }
public string BlobFile { get; set; }
public string SKU { get; set; }
public string EmailAddress { get; set; }
}
In version 0.2 of the SDK, when a JSON message of PackageRequestMessage was posted to the queue, this method was called, and the appropriate Blob was found, based on the parameters in the PackageRequestMessage (RequestId, and BlobFile), and all worked well.
Now, in version 0.3 of the SDK, I get the following error:
System.InvalidOperationException: System.InvalidOperationException: Exception binding parameter 'blob' ---> System.InvalidOperationException: No value for name parameter 'RequestId'
at Microsoft.Azure.Jobs.RouteParser.ApplyNamesWorker(String pattern, IDictionary2 names, Boolean allowUnbound)
at Microsoft.Azure.Jobs.RouteParser.ApplyBindingData(String pattern, IReadOnlyDictionary2 bindingData)
at Microsoft.Azure.Jobs.Host.Blobs.Bindings.BlobBinding.Bind(BindingContext context)
at Microsoft.Azure.Jobs.Host.Runners.TriggerParametersProvider1.Bind()
--- End of inner exception stack trace ---
at Microsoft.Azure.Jobs.Host.Runners.DelayedException.Throw()
at Microsoft.Azure.Jobs.Host.Runners.WebSitesExecuteFunction.ExecuteWithSelfWatch(MethodInfo method, ParameterInfo[] parameterInfos, IReadOnlyDictionary2 parameters, TextWriter consoleOutput)
at Microsoft.Azure.Jobs.Host.Runners.WebSitesExecuteFunction.ExecuteWithOutputLogs(FunctionInvokeRequest request, IReadOnlyDictionary2 parameters, TextWriter consoleOutput, CloudBlobDescriptor parameterLogger, IDictionary2 parameterLogCollector)
at Microsoft.Azure.Jobs.Host.Runners.WebSitesExecuteFunction.ExecuteWithLogMessage(FunctionInvokeRequest request, RuntimeBindingProviderContext context, FunctionStartedMessage message, IDictionary`2 parameterLogCollector)
at Microsoft.Azure.Jobs.Host.Runners.WebSitesExecuteFunction.Execute(FunctionInvokeRequest request, RuntimeBindingProviderContext context)
In the dashboard, the message itself is shown with a valid RequestId present in the JSON, so I'm not sure why it's reported missing.
pianomanjh, I was able to reproduce the issue you are describing and I filed a bug. It seems that this failure only occurs in the blob name pattern, parameter binding is not affected.
The workaround for now is to use string instead of Guid for the property types.
Just found a solution to the blob issue i 0.3.0. Compared to version 0.2.0 you have to define the Blob to FileAccess.Write to make it work. It fixed the issue I described above to be able to stream to a blob

Custom Attribute Filter throwing System.NullReferenceException

I am implementing a dynamic File Download from a WebAPI using this http://johnculviner.com/jquery-file-download-plugin-for-ajax-like-feature-rich-file-downloads/
The key here is it sets a cookie with the request in a custom Attribute Filter which tells the browser when the file has finished generating, the loading message is then removed and the file streamed to the browser.
This all works fine when I test locally but when it is deployed to a server I get a System.NullReferenceException. The error points to the attribute filter - the link above has an example of how to do this in ASP.NET MVC 3 but I am using 4 so converted to this -
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class FileDownloadAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public string CookieName { get; set; }
public string CookiePath { get; set; }
public FileDownloadAttribute(string cookieName = "fileDownload", string cookiePath = "/")
{
CookieName = cookieName;
CookiePath = cookiePath;
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
CheckAndHandleFileResult(actionExecutedContext);
base.OnActionExecuted(actionExecutedContext);
}
/// <summary>
/// Write a cookie to inform jquery.fileDownload that a successful file download has occured
/// </summary>
/// <param name="filterContext"></param>
private void CheckAndHandleFileResult(HttpActionExecutedContext filterContext)
{
List<CookieHeaderValue> cookies = new List<CookieHeaderValue>();
CookieHeaderValue cookie = new CookieHeaderValue(CookieName, "true"){ Path = CookiePath };
cookies.Add(cookie);
filterContext.Response.Headers.AddCookies(cookies);
}
}
This is the error I get
System.NullReferenceException at API.Attributes.FileDownloadAttribute.OnActionExecuted(HttpActionExecutedContext actionExecutedContext) in c:\Dev\Pensions\Main\API\Attributes\FileDownloadAttribute.cs:line 28 at System.Web.Http.Filters.ActionFilterAttribute.CallOnActionExecuted(HttpActionContext actionContext, HttpResponseMessage response, Exception exception) at System.Web.Http.Filters.ActionFilterAttribute.<>c_DisplayClass2.b_1(CatchInfo1 info) at System.Threading.Tasks.TaskHelpersExtensions.<>c__DisplayClass41.b__3() at System.Threading.Tasks.TaskHelpersExtensions.CatchImpl[TResult](Task task, Func`1 continuation, CancellationToken cancellationToken)
Anyone have any ideas? I don't understand why it is fine running locally (through IIS) but not when deployed to a server.
EDIT -
Line 28 in the error refers to this one
CheckAndHandleFileResult(actionExecutedContext);
EDIT 2- Updated with full Attribute Filter class