Ryan Wilson
Ryan Wilson

Reputation: 10765

How to handle validation error(s) Server Side when a file is uploaded

I have a form which is validated server side and if the form has invalid items, the model is returned to the View and JavaScript then highlights fields which are invalid. This is a multi-part form which contains an input type file for a file upload. I am using Visual Studio 2017, ASP.Net Core 2.1 and ASP.Net MVC design pattern.

Problem: User submits the form and has a file attached but the form is invalid due to one or many input fields. When passing the Model back to the View with validation errors, the file is no longer attached as you can't populate an input type file from the server (at least not that I know of, think this is a security feature).

Desired Behavior: The user doesn't have to re-select the file in the input for upload a second time.

Current Solution: I serialize and save the uploaded file in Session on the server and then set a property on the Model which tells the View that a file is saved in session and remove the input type file for upload, then on resubmit, if the form is valid, deserialize the file out of Session, do what I need to do with the file, and remove it from Session.

Question: Is there a better way of handling this? If so, please offer example documentation or advice.

Upvotes: 0

Views: 2236

Answers (2)

Racil Hilan
Racil Hilan

Reputation: 25351

As you guessed, for security reasons, no major browsers gives you access to the actual path to the file selected by the user. Your server code will not receive the real path, and even JavaScript is not allowed access to it. Instead, the browser generates a fake path, and the only real value in it is the file name.

For that reason, the only way to keep the file in your current design (i.e. standard form) after the form is submitted is the way you did it as you described it in your question. Obviously, this is not a good way especially if the file is too big, then each round trip to the server will take a long time.

To solve it differently, you'll need to change the design. You need to make the browser retain the real path value. The only way to do this is by not submitting the form. So you need to submit the form values without submitting the form. The only way to do this I can think of is by submitting the form using Ajax. This means that the result of the server-side validation will have to come back as an Ajax response (usually JSON, but can be serialized in other ways if you like) and then JavaScript will take that response and display the errors on the form as required. In this way, the file is still re-submitted every time to the server, but at least the server doesn't need to send it back to the client like the way described in the question. That saves half of the bandwidth, but will require more programming. It is still not recommended if the file can be big. In that case, it is probably better to let the user reselect the file again rather than having to wait a long time for the file to be uploaded again.

To add the Ajax helper to .Net Core, you can simply download the JavaScript file from the project GitHub and link to it in the header of your HTML. However, I prefer to only link to it on the pages where I need it, because most pages don't need it. What I do is, first create a partial view for it:

<environment names="Development">
    <script src="~/lib/jquery-ajax-unobtrusive/dist/jquery.unobtrusive-ajax.js"></script>
</environment>
<environment exclude="Development">
    <script src="~/lib/jquery-ajax-unobtrusive/dist/jquery.unobtrusive-ajax.min.js"></script>
</environment>

You can use CDN if you like. I cannot find a CDN other than cdn.jsdelivr.net, and I cannot find a good fallback test so I left asp-fallback-test empty:

<environment names="Development">
    <script src="~/lib/jquery-ajax-unobtrusive/dist/jquery.unobtrusive-ajax.js"></script>
</environment>
<environment exclude="Development">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.unobtrusive-ajax.min.js"
        asp-fallback-src="~/lib/jquery-ajax-unobtrusive/dist/jquery.unobtrusive-ajax.min.js"
        asp-fallback-test=""
        crossorigin="anonymous"
        integrity="sha256-PAC000yuHt78nszJ2RO0OiDMu/uLzPLRlYTk8J3AO10="></script>
</environment>

Either way, call this file anything you like, let's say _AjaxScriptsPartial.cshtml. Now in the view where you need it, add it like this:

@section Scripts {
    @{ await Html.RenderPartialAsync("_AjaxScriptsPartial"); }
}

Here is a good explanation on how to use it: Using Unobtrusive Ajax In Razor Pages.

Alternatively, you can keep your current design, but instead of serializing the file and send it back to the browser, save it on the server and send the URL to the browser. Now, in the JavaScript, replace the file input field with a simple message like "File uploaded successfully" or any other way to indicate to the user that the file doesn't need to be re-selected. When the form is re-submitted and all server validation succeed, get the physical file on the server. Your code will have to know whether to upload the file or get it from the server (by checking if the file input field has a file). It also needs to know where to get it on the server. This way will work fine for all file sizes, because the files is only uploaded once, but it requires some work.

Upvotes: 1

Jimenemex
Jimenemex

Reputation: 3166

I'm not sure of the validation you are doing, but can it be moved to the front-end? Typically validation in the front-end is more for user experience. (Eg. Incorrect Formats, fields left blank, incorrect data type in field). If that's the case I'd move it to the front-end, if not you can leave it in the back-end.

You could validate the fields in the form using AJAX when they leave focus. This can guarantee that when they submit the form, the fields in the form will always be correct. I find a healthy mix of front-end and server side validation works best and not have everything in the back-end.

The crude example below sends off an ajax request to the controller to validate the input field when the onblur method fires. It will set the border to either green or red if it passed validation and display a message if there is one to display. Just make sure your model matches up with what you would be sending in your request:

JS:

<script>
    function validate() {
        var input = { input: $('#one').val() };

        $.ajax({
            url: 'ValidateTextBox/',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(input),
            success: function (data) {
                if (JSON.parse(data.Result)) {
                    $('#one').css('border', '5px solid green');
                } else {
                    $('#one').css('border', '5px solid red');
                }
                $('#result').text(data.Message);
            }
        })
    }
</script>
<input id="one" type="text" onblur="validate()"/>
<label id="result"></label>

Controller.cs

[HttpPost]
public ActionResult ValidateTextBox(Dummy data)
{
    int result = 0;

    if (!int.TryParse(data.input, out result)) {
        return Json(new { Result = false, Message = "Please enter in numbers" });
    }
    return Json(new { Result = true, Message = "Success" });
}

Model.cs

public class Dummy
{
    public string input { get; set; }
}

Upvotes: 1

Related Questions