0

I want to submit a form including values and an image file to server by jquery ajax and .net api controller. But the server cannot get the data, always showing a null of the input parameter.

I have added config.Formatters.XmlFormatter.SupportedMediaTypes.Add(new System.Net.Http.Headers.MediaTypeHeaderValue("multipart/form-data")); into WebApiConfig.cs file. But it still does not work.

However, the interesting thing is that when I moved my code block into my AdminController.cs, it works.

In the following specific case, if I submit form to /admin/submitnew, it works perfectly. If submit to /api/news, newsModel on server only receive null value.

So my question is, why the data cannot be received/ready under apicontroller, and how to fix that.

NewsEdit.cshtml

@using (Html.BeginForm(null, null, FormMethod.Post, new { id = "editform" }))

{
    @Html.AntiForgeryToken()

<div class="form-horizontal">

    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    <div class="form-group">
        @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control", @id = "title" } })
            @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        <Lable class="control-label col-md-2">Cover Image</Lable>
        <div class="col-md-10">
            <input type="file" name="ImgFile" class="control-label" accept="image/png, image/jpeg" />
            <br /><img src="@Model.ImgPath" style="max-width:300px" />
        </div>
    </div>
</div>

NewsEdit.js

 $("#submit").click(function (e) {
            if ($("#editform").valid()) {
                e.preventDefault();

                $.ajax({
                    url: "/admin/submitnews",
                    type: "POST",
                    data: data,
                    cache: false,
                    contentType: false,
                    processData: false,
                    async: false,
                    success: function () {
                       ****
                    },
                    error: function (e) {
                        ****
                    },
                })
            }

AdminControllers.cs

public class AdminController : Controller{
     [HttpPost]
     [ValidateInput(false)]
     public ActionResult SubmitNews(News newsModel)
     {
      //some code
     }
}

NewsController.cs

 public class NewsController : ApiController{
        [HttpPost]
        [ResponseType(typeof(News))]
        public IHttpActionResult PostNewsModel(News newsModel)
        {
          //some code    
        }
}

2 Answers 2

2

ApiController expects your controllers to receive JSON implicitly, while Controller expects does the same with Form Data. To tell your methods in an apicontroller to expect form data you'll need a [FromForm]

[HttpPost]
[ResponseType(typeof(News))]
public IHttpActionResult PostNewsModel([FromForm] News newsModel)
{
          //some code    
}
Sign up to request clarification or add additional context in comments.

5 Comments

FromForm - is for asp.net core, right? But question tags are about classic asp.net
@vasily.sib Hi, I'm using .net 4.6. No "FromForm". Any ideas for classic .net?
@Lei.L some time ago I was handling this issue, I will try to post an answer shortly
I think this is the best answer for .net core developers, but not for those looking for classic .net solutions.
You can also try to add no attribute at all (neither FromForm nor FromBody) - model binder will attempt to find it in all places, worked for me in .NET Core, didn't test "classic" ASP.NET
0

Some time ago I was handling almost the same issue. The reason why you get this behavior is the fact that there is no "out-of-the-box" formatter for multipart/form-data media-type in ASP.Net WepAPI (meanwhile there is one in ASP.Net MVC, strangely).

I don't remember exact path of SO questions, Microsoft docs, ASP.Net sources and articles I came through, but here is working result:

Create a HttpPostedFileMultipart class to handle posted files:

public class HttpPostedFileMultipart : HttpPostedFileBase
{
    public override string FileName { get; }

    public override string ContentType { get; }

    public override Stream InputStream { get; }

    public override int ContentLength => (int)InputStream.Length;

    public HttpPostedFileMultipart(string fileName, string contentType, byte[] fileContents)
    {
        FileName = fileName;
        ContentType = contentType;
        InputStream = new MemoryStream(fileContents);
    }
}

Then create your MediaTypeFormatter:

public class FormMultipartEncodedMediaTypeFormatter : MediaTypeFormatter
{
    private const string SupportedMediaType = "multipart/form-data";

    public FormMultipartEncodedMediaTypeFormatter()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue(SupportedMediaType));
    }

    // can we deserialize multipart/form-data to specific type
    public override bool CanReadType(Type type)
    {
        if (type == null) throw new ArgumentNullException(nameof(type));
        return true;
    }

    // can we serialize specific type to multipart/form-data
    public override bool CanWriteType(Type type)
    {
        if (type == null) throw new ArgumentNullException(nameof(type));
        return false;
    }

    // deserialization
    public override async Task<object> ReadFromStreamAsync(
        Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
    {
        if (type == null) throw new ArgumentNullException(nameof(type));
        if (readStream == null) throw new ArgumentNullException(nameof(readStream));

        try
        {
            // read content 
            var multipartProvider = await content.ReadAsMultipartAsync();
            // fill out model dictionary
            var modelDictionary = await ToModelDictionaryAsync(multipartProvider);
            // apply dictionary to model instance
            return BindToModel(modelDictionary, type, formatterLogger);
        }
        catch (Exception e)
        {
            if (formatterLogger == null) throw;

            formatterLogger.LogError(string.Empty, e);
            return GetDefaultValueForType(type);
        }
    }

    // fill out model dictionary
    private async Task<IDictionary<string, object>> ToModelDictionaryAsync(MultipartMemoryStreamProvider multipartProvider)
    {
        var dictionary = new Dictionary<string, object>();

        foreach (var element in multipartProvider.Contents)
        {
            // getting element name
            var name = element.Headers.ContentDisposition.Name.Trim('"');

            // if we have a FileName - this is a file
            // if not - pretend this is a string (later binder will transform this strings to objects)
            if (!string.IsNullOrEmpty(element.Headers.ContentDisposition.FileName))
                // create our HttpPostedFileMultipart instance if we have any data
                if (element.Headers.ContentLength.GetValueOrDefault() > 0)
                    dictionary[name] = new HttpPostedFileMultipart(
                        element.Headers.ContentDisposition.FileName.Trim('"'),
                        element.Headers.ContentType.MediaType,
                        await element.ReadAsByteArrayAsync()
                    );
                else
                    dictionary[name] = null;
            else
                dictionary[name] = await element.ReadAsStringAsync();
        }

        return dictionary;
    }

    // apply dictionary to model instance
    private object BindToModel(IDictionary<string, object> data, Type type, IFormatterLogger formatterLogger)
    {
        if (data == null) throw new ArgumentNullException(nameof(data));
        if (type == null) throw new ArgumentNullException(nameof(type));

        using (var config = new HttpConfiguration())
        {
            if (RequiredMemberSelector != null && formatterLogger != null)
                config.Services.Replace(
                    typeof(ModelValidatorProvider),
                    new RequiredMemberModelValidatorProvider(RequiredMemberSelector));

            var actionContext = new HttpActionContext {
                ControllerContext = new HttpControllerContext {
                    Configuration = config,
                    ControllerDescriptor = new HttpControllerDescriptor { Configuration = config }
                }
            };

            // workaround possible locale mismatch
            var cultureBugWorkaround = CultureInfo.CurrentCulture.Clone() as CultureInfo;
            cultureBugWorkaround.NumberFormat = CultureInfo.InvariantCulture.NumberFormat;

            var valueProvider = new NameValuePairsValueProvider(data, cultureBugWorkaround);
            var metadataProvider = actionContext.ControllerContext.Configuration.Services.GetModelMetadataProvider();
            var metadata = metadataProvider.GetMetadataForType(null, type);
            var modelBindingContext = new ModelBindingContext
            {
                ModelName = string.Empty,
                FallbackToEmptyPrefix = false,
                ModelMetadata = metadata,
                ModelState = actionContext.ModelState,
                ValueProvider = valueProvider
            };

            // bind our model
            var modelBinderProvider = new CompositeModelBinderProvider(config.Services.GetModelBinderProviders());
            var binder = modelBinderProvider.GetBinder(config, type);
            var haveResult = binder.BindModel(actionContext, modelBindingContext);

            // store validation errors
            if (formatterLogger != null)
                foreach (var modelStatePair in actionContext.ModelState)
                    foreach (var modelError in modelStatePair.Value.Errors)
                        if (modelError.Exception != null)
                            formatterLogger.LogError(modelStatePair.Key, modelError.Exception);
                        else
                            formatterLogger.LogError(modelStatePair.Key, modelError.ErrorMessage);

            return haveResult ? modelBindingContext.Model : GetDefaultValueForType(type);
        }
    }
}

And finally, register this formatter in your WebApiConfig.Register() method:

    public static void Register(HttpConfiguration config)
    {
        // ...

        // add multipart/form-data formatter
        config.Formatters.Add(new FormMultipartEncodedMediaTypeFormatter());

        // ...
    }

4 Comments

If it has to be so complicated comparing with the solution for .net core, I would rather create a regular controller called apiControllers.cs then do my apis there. Jusi I think....maybe better
@Lei.L I think the reason why WebAPI doesn't have this formatter is the fact, that multipart/form-data media-type is better fits MVC applications. For example, native clients will never post anything to API server in multipart/form-data format. They just post this in JSON. And so can do JS clients.
I agree. And apparently, MS knows this issue and solved only on .net core :(
To be clear: I don't think this an issue. I think this is just a decision (strange one). What about .net Core - it is "solved" by moving whole WepAPI codebase to MVC:)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.