Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JsonContent.Create overloads that accept JsonTypeInfo. #51544

Closed
layomia opened this issue Apr 20, 2021 · 19 comments · Fixed by #89614
Closed

Add JsonContent.Create overloads that accept JsonTypeInfo. #51544

layomia opened this issue Apr 20, 2021 · 19 comments · Fixed by #89614
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Text.Json Cost:S Work that requires one engineer up to 1 week source-generator Indicates an issue with a source generator feature
Milestone

Comments

@layomia
Copy link
Contributor

layomia commented Apr 20, 2021

EDIT New proposal can be found here: #51544 (comment)

Details

Background

The existing JsonContent type in System.Net.Http.Json uses existing JsonSerializerOptions-based JsonSerializer.Serialize(Async) overloads which root reflection code and all built-in STJ converters (even when not needed in an app). We should add a new type that uses new JsonSerializerContext-based overloads, which are AOT-friendly and allow for a smaller app. Also the existing type has been marked as "requires unreferenced code", so it is beneficial to provide an alternative which is trim-friendly.

API Proposal

public sealed partial class JsonContent<TValue> : HttpContent
{
  internal JsonContent() { }
  public TValue? Value { get { throw null; } }
}

We should also expose static Create methods for these types to the existing JsonContent :

public sealed partial class JsonContent : HttpContent
{
  // Existing
  // internal JsonContent() { }
  // public Type ObjectType { get { throw null; } }
  // public object? Value { get { throw null; } }
  // public static JsonContent Create(object? inputValue, Type inputType, MediaTypeHeaderValue? mediaType = null, JsonSerializerOptions? options = null) { throw null; }
  // public static JsonContent Create<T>(T inputValue, MediaTypeHeaderValue? mediaType = null, JsonSerializerOptions? options = null) { throw null; }

  // New create methods for JsonContent<TValue>
  public static JsonContent<object?> Create(object? inputValue, Type inputType, JsonSerializerContext context, MediaTypeHeaderValue? mediaType = null) { throw null; }
  public static JsonContent<TValue> Create(TValue? inputValue, JsonTypeInfo<TValue> jsonTypeInfo, MediaTypeHeaderValue? mediaType = null) { throw null; }
}

The new create methods should go on JsonContent - if we put them on JsonContent<TValue>, then it will be possible for users to write awkward code like this:

JsonContent<object> content = JsonContent<int>.Create(myVal, type, context);

Usage

Just like the existing JsonContent type, JsonContent<TValue> can be used when building a HttpResponseMessage based on JSON. The difference is that now the JSON will be serialized in a trim-safe way which is also optimized for app size:

public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default)
{
  if (client == null)
  {
      throw new ArgumentNullException(nameof(client));
  }

  JsonContent<TValue> content = JsonContent.Create(value, jsonTypeInfo);
  return client.PutAsync(requestUri, content, cancellationToken);
}

Q/A

Can JsonContent<TValue> derive from JsonContent<T>?

It's better not to do this, because the ILLinker won't be able to trim away reflection-based code and extra JsonConverters used by the existing JsonContent type. The underlying infrastructure for serializing the JSON content is based on overriding virtual methods. Making the new JsonContent<TValue> type derive from the existing type is bad because the linker will preserve the virtual and override methods of called methods in the hierarchy, because it doesn't know which one will be called at run-time. This is discussed in more detail here.

@layomia layomia added this to the 6.0.0 milestone Apr 20, 2021
@layomia layomia self-assigned this Apr 20, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Apr 20, 2021
@ghost
Copy link

ghost commented Apr 20, 2021

Tagging subscribers to this area: @eiriktsarpalis, @layomia
See info in area-owners.md if you want to be subscribed.

Issue Details

Following up from #51528, implement the following APIs approved in #45448. The corresponding HttpClientJsonExtensions.GetFromJsonAsync methods have already been implemented.

namespace System.Net.Http.Json
{
  public static partial class HttpClientJsonExtensions
  {
    public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
    public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
    public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
    public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
  }

  public sealed partial class JsonContent : HttpContent
  {
    public static JsonContent Create(object? inputValue, Type inputType, JsonSerializerContext context, MediaTypeHeaderValue mediaType = null) { throw null; }
    public static JsonContent Create<T>(T inputValue, JsonTypeInfo<T> jsonValueInfo, MediaTypeHeaderValue mediaType = null) { throw null; }
  }
}

The challenge here is making these methods linker/size friendly, since the underlying HttpContent.SerializeToStreamAsync abstraction on JsonContent is already reserved for the JsonSerializerOptions-based implementation, which roots all built-in converters (size), and reflection-based logic to create type metadata (linker unfriendly).

The solution could be to add new (internal) PostAsync methods directly on JsonContent which can accept and use type metadata to forward to JsonSerializer.

Currently targeting preview 5 to get this work in. cc @eerhardt, @CoffeeFlux, @jozkee.

Author: layomia
Assignees: layomia
Labels:

area-System.Text.Json

Milestone: 6.0.0

@layomia layomia removed the untriaged New issue has not been triaged by the area owner label Apr 20, 2021
@layomia layomia changed the title Implement HttpClientJsonExtensions Post/Put methods and JsonContent Create methods that take JSON serialization metadata Provide JsonContent<T> type that uses pre-generated serialization metadata Apr 30, 2021
@layomia layomia added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Apr 30, 2021
@terrajobst terrajobst added api-ready-for-review API is ready for review, it is NOT ready for implementation blocking Marks issues that we want to fast track in order to unblock other important work and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels May 13, 2021
@bartonjs
Copy link
Member

bartonjs commented May 21, 2021

Video

  • Because the JsonContent type is currently sealed with a non-public constructor there's more flexibility and we can add the JsonContent<T> : JsonContent relationship.
  • The JsonContent.Create that takes a TValue parameter should be declared generic (typo).
namespace System.Net.Http.Json
{
    public sealed partial class JsonContent<TValue> : JsonContent
    {
        internal JsonContent() { }
        public TValue? Value { get { throw null; } }
    }
    partial class JsonContent
    {
        public static JsonContent<object?> Create(object? inputValue, Type inputType, JsonSerializerContext context, MediaTypeHeaderValue? mediaType = null) { throw null; }
        public static JsonContent<TValue> Create<TValue>(TValue? inputValue, JsonTypeInfo<TValue> jsonTypeInfo, MediaTypeHeaderValue? mediaType = null) { throw null; }
    }
}
namespace System.Net.Http.Json
{
-    public sealed partial class JsonContent
+    public abstract partial class JsonContent
     {
        internal JsonContent();
     }
+   internal sealed partial class ReflectionBasedJsonContent  : JsonContent { ... }
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels May 21, 2021
@layomia layomia removed the blocking Marks issues that we want to fast track in order to unblock other important work label May 21, 2021
@layomia
Copy link
Contributor Author

layomia commented Jul 23, 2021

Triage: we can do this in .NET 7.

@layomia layomia modified the milestones: 6.0.0, 7.0.0 Jul 23, 2021
@eiriktsarpalis eiriktsarpalis added the source-generator Indicates an issue with a source generator feature label Oct 14, 2021
@eiriktsarpalis
Copy link
Member

Question: what necessitates needing to make JsonContent<T> public? It's my impression that the T parameter can be encapsulated in the implementation and hidden away from the user. E.g.

    partial class JsonContent
    {
        public static JsonContent Create(object? inputValue, Type inputType, JsonSerializerContext context, MediaTypeHeaderValue? mediaType = null) { throw null; }
        public static JsonContent Create<TValue>(TValue? inputValue, JsonTypeInfo<TValue> jsonTypeInfo, MediaTypeHeaderValue? mediaType = null) { throw null; }
    }

ErisApps added a commit to ErisApps/CatCore that referenced this issue Oct 16, 2021
ErisApps added a commit to ErisApps/CatCore that referenced this issue Oct 16, 2021
@layomia
Copy link
Contributor Author

layomia commented Nov 22, 2021

@eiriktsarpalis IL trimming: we need a separate content type which does not derive from JsonContent so that we can provide a fresh implementation of HttpContent.SerializeToStreamAsync that does not root the JsonSerializer methods that use reflection. Some discussion here: #51609 (comment).

@martincostello
Copy link
Member

I was just following this blog post which still lists the new JsonContent.Create() overloads as part of .NET 6, but couldn't find them which lead me to this issue.

Might be worth updating the blog post to remove them. I'd have left a comment on the blog, but comments are closed.

ErisApps added a commit to ErisApps/CatCore that referenced this issue Nov 29, 2021
@layomia
Copy link
Contributor Author

layomia commented Dec 2, 2021

I've updated the blog post - we should add these APIs in .NET 7.

@tipa
Copy link

tipa commented Apr 2, 2023

This issue seems to have been postponed a few times. Is this planned for .NET8 or should I be looking for workarounds (e.g. using StringContent) instead?

@eiriktsarpalis
Copy link
Member

It's currently in the Future milestone so we're not planning on delivering this for .NET 8.

@tipa
Copy link

tipa commented Apr 3, 2023

Ok, too bad. Is there a recommended workaround? Like using JsonSerializer.Serialize and hand over the string to a StringContent object? Or is there something else that I might have overlooked? Thanks!

@eiriktsarpalis
Copy link
Member

You could use the existing overloads accepting JsonSerializerOptions, if you pass in the Options property associated with a source generated JsonSerializerContext.

@lateapexearlyspeed
Copy link
Contributor

Hi, when think about implementation, I have question about the new class JsonContent<T> shape (I know it had been approved though)
I understand we should use new class rather than existing JsonContent for IL trimming, but just confirm, is using a generic version JsonContent to accept both compiler-timed type and run-timed type argument proper ? Note for run-time type, return type of the Create() method overload is JsonContent<object?>
For implementation, consider constructor of JsonContent<T> is internal, so during 2 public Create() methods, we can just unify both Create()'s implementations to generate info like:
(object inputValue and JsonTypeInfo jsonTypeInfo) and then pass them to new JsonContent class which just uses these info to use source generate.
The only thing I can find is because JsonSerializer.Serialize(Stream utf8Json, object? value, JsonTypeInfo jsonTypeInfo) not existing in old STJ package ?
Please correct me, thanks !

@brantburnett
Copy link
Contributor

It's currently in the Future milestone so we're not planning on delivering this for .NET 8.

Is this postponed simply due to team bandwidth and you'd be interested in accepting a community contribution that meets the accepted API surface on this issue? Or are there other considerations? I ask because this would be very helpful for https://github.com/CenterEdge/Yardarm so I'd be happy to do the work to get it included.

@eiriktsarpalis
Copy link
Member

It's also lack of bandwidth, but it's also the fact that the proposal was approved two years ago and is probably no longer fit for purpose given the recent additions in contract customisation:

  1. We should not expose the generic JsonContent<T>. The reason originally cited does not apply since .NET 7 and the inclusion of the EnableReflectionByDefault feature switch in .NET 8 means that even with the current code unchanged we don't risk rooting reflection components unless done intentionally.
  2. We should not expose a JsonSerializerContext overload. Most scenaria nowadays (including the aspnetcore defaults in .NET 8) involve combining multiple contexts so in most cases JsonSerializerOptions is the predominant unit of serialization configuration.

We should instead consider exposing overloads that do not force linker warnings at all:

public sealed partial class JsonContent : HttpContent
{
  public static JsonContent Create(TValue? inputValue, JsonTypeInfo<TValue> jsonTypeInfo, MediaTypeHeaderValue? mediaType = null);
  public static JsonContent Create(object? inputValue, JsonTypeInfo typeInfo, MediaTypeHeaderValue? mediaType = null);
}

@eiriktsarpalis eiriktsarpalis added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-approved API was approved in API review, it can be implemented help wanted [up-for-grabs] Good issue for external contributors labels Apr 9, 2023
@tipa
Copy link

tipa commented Apr 20, 2023

You could use the existing overloads accepting JsonSerializerOptions, if you pass in the Options property associated with a source generated JsonSerializerContext.

I tried it like you recommended:

requestMessage.Content = JsonContent.Create(new MyPostObject()
{
    ....
}, options: new JsonSerializerOptions() { TypeInfoResolver = MyContext.Default });

However the same build warning persists, because the same method is still being called, just with an options object instead of null. I don't see another overload I could use instead.

warning IL2026: Using member 'System.Net.Http.Json.JsonContent.Create<T>(T, MediaTypeHeaderValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.

@eiriktsarpalis
Copy link
Member

@tipa it's unfortunate, but all serialization overloads accepting a JsonSerializerOptions parameter will produce this linker warning because of their default behavior. Assuming you're passing it an options that belongs to a JsonSerializerContext then that warning should remain a false positive that does not translate to any run time linker-related problems.

@eiriktsarpalis eiriktsarpalis changed the title Provide JsonContent<T> type that uses pre-generated serialization metadata Add JsonContent.Create overloads that accept JsonTypeInfo. Jun 15, 2023
@eiriktsarpalis eiriktsarpalis removed the Priority:1 Work that is critical for the release, but we could probably ship without label Jun 15, 2023
@bartonjs
Copy link
Member

bartonjs commented Jul 27, 2023

Video

Looks good as proposed (modulo typos)

namespace System.Net.Http.Json
{
    public partial class JsonContent
    {
        public static JsonContent Create<TValue>(TValue? inputValue, JsonTypeInfo<TValue> jsonTypeInfo, MediaTypeHeaderValue? mediaType = null);
        public static JsonContent Create(object? inputValue, JsonTypeInfo jsonTypeInfo, MediaTypeHeaderValue? mediaType = null);
    }
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jul 27, 2023
@brantburnett
Copy link
Contributor

Great to see this API approved! I'd still love to take this on if that helps, just let me know. I don't want to cross streams if someone is planning to take this internally.

@eiriktsarpalis eiriktsarpalis modified the milestones: Future, 9.0.0 Jul 27, 2023
@eiriktsarpalis
Copy link
Member

Feel free to pick it up @brantburnett

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jul 28, 2023
@eiriktsarpalis eiriktsarpalis modified the milestones: 9.0.0, 8.0.0 Jul 31, 2023
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jul 31, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Aug 30, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Text.Json Cost:S Work that requires one engineer up to 1 week source-generator Indicates an issue with a source generator feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants