Stream large content from ASP.NET Core to avoid OutOfMemoryException

I had to implement an endpoint in ASP.NET Core, fetching a large amount of data and returning it to the client. Just to end up in an System.OutOfMemoryException on the server. With just a few changed code lines and the built-in support for streaming content in ASP.NET Core, I was able to fix the issue. Stay tuned to learn how.

Ever needed to return a large response from a web server? Depending on your hardware, you can quickly end up in a situation like mine, where you use all of the server's memory to build the return type to the client, before actually returning it.

Let me demonstrate with a simple example. I'll start with a simple view, implemented directly in the /Home/Index.cshtml file:

<div class="text-center">
    <script>
        function Fetch() {
            $.get("/home/values", function (data) {
                console.log(data);
            });
        }
    </script>
    <button class="btn btn-primary btn-lg" onclick="Fetch()">Fetch</button><br />
</div>

Good old HTML with inline JavaScript as it would have looked 20 years ago. The client isn't important here, other than to provide us with a button to call the endpoint implemented in the HomeController:

public IEnumerable<string> Values()
{
    var random = new Random();
    var result = new List<string>();
    for (var i = 0; i < 100000000; i++)
    {
        result.Add(random.Next(100000, 999999).ToString());
    }
    return result;
}

The endpoint builds a list of 100,000,000 random strings and returns it to the client. As you probably figured out already, running the code causes the following exception to happen:

We are trying to build a very large data structure in memory, before returning it to the client. To be able to compare with the solution presented in a moment, here's how the call looks in Developer Tools on the client:

The response is pending and finally fails with a status code 500 (caused by the OutOfMemoryException.

The solution is to use the yield keyword built into C#. Let's refactor the endpoint to use yield:

public IEnumerable<string> Values()
{
    var random = new Random();
    for (var i = 0; i < 100000000; i++)
    {
        yield return random.Next(100000, 999999).ToString();
    }
}

The code is pretty similar to the previous version. But now we no longer build a list in memory, but rather yield return each random string to the client. Streaming content like this is visible if we inspect the request in Developer Tools:

As shown in the video, the request returns a 200 response as soon as I click the Fetch button and the server starts streaming a lot of MBs to the client. Simple as that 👍