How to download the model as a JSON file?

Viewed 105

My model is held in a JavaScript object on the client side, where the user can edit its properties via the UI controls. I want to offer the user an option to download a JSON file representing the model they're editing. I'm using MVC core with .net 6.

What I've tried

Action method (using Newtonsoft.Json to serialize the model to JSON):

public IActionResult Download([FromForm]SomeModel someModel)
{
    var json = JsonConvert.SerializeObject(someModel);
    var characters = json.ToCharArray();
    var bytes = new byte[characters.Length];
    for (var i = 0; i < characters.Length; i++)
    {
        bytes[i] = (byte)characters[i];
    }

    var stream = new MemoryStream();
    stream.Write(bytes);
    stream.Position = 0;
    return this.File(stream, "APPLICATION/octet-stream", "someFile.json");
}

Code in the view to call this method:

<button class="btn btn-primary" onclick="download()">Download</button>

And the event handler for this button (using jQuery's ajax magic):

function download() {
    $.ajax({
        url: 'https://hostname/ControllerName/Download',
        method: 'POST',
        data: { someModel: someModel },
        success: function (data) {
            console.log('downloading', data);
        },
    });
}

What happened

The browser console shows that my model has been posted to the server, serialized to JSON and the JSON has been returned to the browser. However no file is downloaded.

Something else I tried

I also tried a link like this to call the action method:

@Html.ActionLink("Download", "Download", "ControllerName")

What happened

This time a file was downloaded, however, because ActionLink can only make GET requests, which have no request body, the user's model isn't passed to the server and instead the file which is downloaded represents a default instance of SomeModel.

The ask

So I know I can post my model to the server, serialize it to JSON and return that JSON to the client, and I know I can get the browser to download a JSON-serialized version of a model, but how can I do both in the same request?

Edit: What I've done with the answer

I've accepted Xinran Shen's answer, because it works as-is, but because I believe that just copying code from Stack Overflow without understanding what it does or why isn't good practice, I did a bit of digging and my version of the saveData function now looks like this:

function saveData(data, fileName) {
    // Convert the data to a JSON string and store it in a blob, a file-like
    // object which can be downloaded without it existing on the server.
    // See https://developer.mozilla.org/en-US/docs/Web/API/Blob
    var json = JSON.stringify(data);
    var blob = new Blob([json], { type: "octet/stream" });

    // Create a URL from which the blob can be downloaded - see
    // https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
    var url = window.URL.createObjectURL(blob);

    // Add a hidden hyperlink to the page, which will download the file when clicked
    var a = document.createElement("a");
    a.style = "display: none";
    a.href = url;
    a.download = fileName;
    document.body.appendChild(a);

    // Trigger the click event on the hyperlink to download the file
    a.click();

    // Release the blob's URL.
    // Browsers do this when the page is unloaded, but it's good practice to
    // do so as soon as it's no longer needed.
    window.URL.revokeObjectURL(url);

    // Remove the hidden hyperlink from the page
    a.remove();
}

Hope someone finds this useful

2 Answers

First, Your code is right, You can try to access this method without ajax, You will find it can download file successfully,But You can't use ajax to achieve this, because JavaScript cannot interact with disk, you need to use Blob to save the file. change your javascript like this:

function download() {
            
            $.ajax({
                url: 'https://hostname/ControllerName/Download',
                method: 'Post',
                data: { someModel: someModel },,
                
                success: function (data) {
                            fileName = "my-download.json";
                            saveData(data,fileName)
                        },
                        
            });
        }
        
        var saveData = (function () {
            var a = document.createElement("a");
            document.body.appendChild(a);
            a.style = "display: none";
            return function (data, fileName) {
                var json = JSON.stringify(data),
                    blob = new Blob([json], {type: "octet/stream"}),
                    url = window.URL.createObjectURL(blob);
                a.href = url;
                a.download = fileName;
                a.click();
                window.URL.revokeObjectURL(url);
            };
        }());

I think you may need FileStreamResult, also you need to set the MIME type to text file or json file.

// instead of this
return this.File(stream, "APPLICATION/octet-stream", "someFile.json");

// try this
return new FileStreamResult(stream, new MediaTypeHeaderValue("text/plain"))  
{  
    FileDownloadName = "someFile.txt"  
};

// or   
return new FileStreamResult(stream, new MediaTypeHeaderValue("application/json"))  
{  
    FileDownloadName = "someFile.json"  
};

Reference: https://www.c-sharpcorner.com/article/fileresult-in-asp-net-core-mvc2/

Related