0

I want to optimize response returning from ASP.NET Core application. In order to do that I'm serializing the object to JSON UTF-8 bytes and inserting it to SQL as varbinary(max). I initially though that I'll get a stream from the column value using ADO.NET and just return it from Controller. This worked perfectly fine at the beginning as I've simply returned the Stream from the controller and it worked:

public async Task<Stream> GetSingleObject()
{
    Response.ContentType = "application/json; charset=UTF-8";

    var streamWithUtf8JsonBytesFromDb = await _repository.GetSingleObject();
    return streamWithUtf8JsonBytesFromDb;
}

Unfortunately I have to attach additional properties to the response object:

{
    "metadata": null,
    "data": (here I would like to copy bytes from SQL stream directly to the response without any transformation)
    "links": [
        "self": "some url"
    ]
}

How can I use System.Text.Json to achieve this result in performant and usable fashion?

I've came up with two approaches so far, but I think there has to be a better way:

  1. First solution:
//using Microsoft.AspNetCore.Http.Extensions;
//using Microsoft.AspNetCore.Http;
public async Task GetSingleObject()
{
    Response.ContentType = "application/json; charset=UTF-8";
    
    await Response.WriteAsync("{\"meta\":null,\"data\":", System.Text.Encoding.UTF8)
    
    var streamWithUtf8JsonBytesFromDb = await _repository.GetSingleObject();
    await streamWithUtf8JsonBytesFromDb.CopyToAsync(Response.Body)
    
    await Response.WriteAsync($",\"links\":[{{\"self\":{Request.GetEncodedUrl()}}}]}}", System.Text.Encoding.UTF8);
}

Constructing JSON parts from string and concatenating it in the stream is obviously painful

  1. Second solution:
public class Meta
{ /* Not relevant */ }

public class ObjectResponse
{
    public Meta? Meta { get; set; }
    public JsonDocument Data { get; set; }
    public Dictionary<string, string> Links { get; set; }
}

//using Microsoft.AspNetCore.Http.Extensions;
//using Microsoft.AspNetCore.Http;
public async Task<ObjectResponse> GetSingleObject()
{
    var streamWithUtf8JsonBytesFromDb = await _repository.GetSingleObject();

    return new ObjectResponse {
        Meta = null,
        Data = await JsonDocument.ParseAsync(streamWithUtf8JsonBytesFromDb),
        Links = new Dictionary<string, string> {
            { "self", Request.GetEncodedUrl()}
        };
    }
}

This is better than the first approach, but it adds unnecessary parsing of the stream which I'd like to omit.

What I'd think is the best idea is to use the following:

public class Meta
{ /* Not relevant */ }

public class ObjectResponse
{
    public Meta? Meta { get; set; }
    public Stream Data { get; set; }
    public Dictionary<string, string> Links { get; set; }
}

//using Microsoft.AspNetCore.Http.Extensions;
//using Microsoft.AspNetCore.Http;
public async Task<ObjectResponse> GetSingleObject()
{
    return new ObjectResponse {
        Meta = null,
        Data = await _repository.GetSingleObject(),
        Links = new Dictionary<string, string> {
            { "self", Request.GetEncodedUrl()}
        };
    }
}

But it doesn't work, because System.Text.Json tries to serialize this stream instead of copying it directly to the response body (which makes sense of course). Am I missing something? Is there a custom converter I could use to achieve the result I'd like?

roten
  • 196
  • 13
  • Do you need to read from `streamWithUtf8JsonBytesFromDb` asynchronously? – dbc Jan 27 '22 at 16:26
  • Short answer: yes. Long answer: this stream comes from the DB, so I don't want to synchronously wait for IO/Network operation to happen – roten Jan 27 '22 at 16:29
  • 1
    Then that may be very difficult to do purely with System.Text.Json because 1) `JsonConverter.Write()` does not support calling `async` methods, and 2) `Utf8JsonWriter` supports asynchrony only through a custom mechanism that uses internal methods and classes. (Notice that `Utf8JsonWriter` has no `WriteAsync()` methods.) You can look at [`JsonSerializer.WriteStreamAsync()`](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs#L306) to get some idea of what is involved. – dbc Jan 27 '22 at 16:45
  • @dbc Do you know the way to make this work with buffering DB stream parts in MemoryStream (using Microsoft.IO.RecyclableMemoryStream to avoid constant byte[] allocation) or reading it through PipeReader (from System.IO.Pipelines). This would enable me to use sync scenarios as I'll consume the DB stream asynchronously, but then can transfrom the data synchronously. – roten Jan 27 '22 at 17:04
  • 1
    Sorry, I don't know how to do that, maybe [Panagiotis Kanavos](https://stackoverflow.com/users/134204/panagiotis-kanavos) might. – dbc Jan 27 '22 at 18:17
  • 1
    All I can suggest is manually writing bits and pieces of JSON to `Response.Body` (or maybe `Response.BodyWriter.AsStream(true)`?) e.g. as shown in this demo fiddle: https://dotnetfiddle.net/nSu6Hk. It works in a demo fiddle but I haven't tested it in a server environment. To wire it up, see maybe [ASP.NET Core Disable Response Buffering](https://stackoverflow.com/q/60607912/3744182) and [Using the BodyReader and BodyWriter in ASP.NET Core 3.0](https://www.stevejgordon.co.uk/using-the-bodyreader-and-bodywriter-in-asp-net-core-3-0). – dbc Jan 27 '22 at 18:18
  • Does that answer your question? – dbc Jan 27 '22 at 21:36
  • It's definitely helpful. Thank you @dbc – roten Jan 28 '22 at 08:15

0 Answers0