dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
34.51k stars 9.75k forks source link

Introduce generic `JsonResult<T>` #55535

Open alex-jitbit opened 1 week ago

alex-jitbit commented 1 week ago

Background and Motivation

Currently in System.Text.Json we have JsonSerializer.Serialize<T>(...) that serializes an object "as T". Which means, if we cast an object to some base class, or to an interface - the serializer will use that exact casted type's properties etc..

HOWEVER the AspNetCore JsonResult works differently. When it serializes an object, it always takes the object's hard type. See the source here: https://source.dot.net/#Microsoft.AspNetCore.Mvc.Core/Infrastructure/SystemTextJsonResultExecutor.cs,61 which means that even if we cast the object to something - nah, it does not care, it just uses the actual type.

As a workaround, people either use JsonSerializer.Serialize<T> to serialize to intermediate string and then return content (not optimal since it buffers everything into a huge string before sending to the client). Or - people write their own JsonResult<T> that handles this (tat's what I did)

Proposed API

Disclaimer: this is just the code I use as a workaround, it's very simple, and misses stuff, but just to give the idea...

/// <summary>
/// Serializes to output stream
/// Almost same as .NET built-in json result but allows specifying a type
/// </summary>
public class JsonResult<T> : IActionResult
{
    private readonly T _value;
    private readonly JsonSerializerOptions _options;

    public JsonResult(T value, JsonSerializerOptions options)
    {
        _value = value;
        _options = options;
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        var response = context.HttpContext.Response;
        response.ContentType = "application/json";

        await JsonSerializer.SerializeAsync<T>(response.Body, _value, _options); //serialize to response stream
        await response.Body.FlushAsync();
    }
}

Usage Examples

public IActionResult Get()
{
    return new JsonResult<T>(someData, _someSerializerOptionsField);
}

Alternative Designs

Another option would e to somehow pass the type to existing JsonResult

martincostello commented 1 week ago

Given that MVC supports IResult now (#40639), would using the existing TypedResults.Json<T>() method solve this use case without introducing any new APIs?

alex-jitbit commented 1 week ago

@martincostello wow, at first glance this looks exactly like a solution. Thanks. This is planned for .NET 9 correct?

martincostello commented 1 week ago

It was added in .NET 7.

alex-jitbit commented 1 week ago

@martincostello unfortunately TypedResults.Json suffers from the same problem - it does not care about casting. It just uses GetType(). See here https://source.dot.net/#Microsoft.AspNetCore.Http.Results/HttpResultsHelper.cs,41

For example if I pass myCollection.Cast<ILightWeightInterfaceThatHidesSomeProperties> to it - it just takes the underlying type, ignoreing the Cast