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 JSON extension methods for HttpClient #33566

Closed
terrajobst opened this issue Mar 13, 2020 · 4 comments
Closed

Add JSON extension methods for HttpClient #33566

terrajobst opened this issue Mar 13, 2020 · 4 comments
Labels
api-approved API was approved in API review, it can be implemented area-System.Text.Json
Milestone

Comments

@terrajobst
Copy link
Member

The full spec is available here.

Usage

Getting data from and to the server is a one-liner

public class RestCustomerRepository : ICustomerRepository
{
    private readonly HttpClient _client;

    public RestCustomerRepository(HttpClient client)
    {
        _client = client;
    }

    public Task<IReadOnlyList<Customer>> GetAllCustomersAsync()
    {
        return _client.GetFromJsonAsync<IReadOnlyList<Customer>>("/customers");
    }

    public Task<Customer?> GetCustomerByIdAsync(int id)
    {
        return _client.GetFromJsonAsync<Customer?>($"/customers/{id}");
    }

    public Task UpdateCustomerAsync(Customer customer)
    {
        return _client.PutAsJsonAsync($"/customers/{customerId}", customer);
    }
}

Dealing with HTTP responses is also still doable

public async Task<Customer> GetCustomerByIdAsync(int id)
{
    var request = new HttpRequestMessage(HttpMethod.Get, $"customers/{id}");
    var response = await _httpClient.SendAsync(request);

    if (response.StatusCode == HttpStatusCode.NotFound)
        return null;

    return await response.Content.ReadFromJsonAsync<Customer>();
}

Constructing HTTP requests is still doable

public async Task CreateCustomerAsync<T>(Customer customer)
{
    var request = new HttpRequestMessage(HttpMethod.Post, "customers/new");
    AddCustomerHeaders(request)

    request.Content = JsonContent.Create(customer);

    var response = await _httpClient.SendAsync(request);

    // ...
}

Proposed APIs

Assembly: System.Net.Http.Json (new)
Dependencies: System.Net.Http, System.Text.Json
NuGet Package: System.Net.Http.Json (new)

#nullable enable

namespace System.Net.Http.Json {
    public static class HttpClientJsonExtensions {
        public static Task<object> GetFromJsonAsync(
            this HttpClient client,
            string requestUri,
            Type type,  
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<object> GetFromJsonAsync(
            this HttpClient client,
            Uri requestUri,
            Type type,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<T> GetFromJsonAsync<T>(
            this HttpClient client,
            string requestUri,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<T> GetFromJsonAsync<T>(
            this HttpClient client,
            Uri requestUri,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PostAsJsonAsync(
            this HttpClient client,
            string requestUri,
            Type type,
            object value,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PostAsJsonAsync(
            this HttpClient client,
            Uri requestUri,
            Type type,
            object? value,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PostAsJsonAsync<T>(
            this HttpClient client,
            string requestUri,
            T value,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PostAsJsonAsync<T>(
            this HttpClient client,
            Uri requestUri,
            T value,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PutAsJsonAsync(
            this HttpClient client,
            string requestUri,
            Type type,
            object? value,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PutAsJsonAsync(
            this HttpClient client,
            Uri requestUri,
            Type type,
            object? value,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PutAsJsonAsync<T>(
            this HttpClient client,
            string requestUri,
            T value,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PutAsJsonAsync<T>(
            this HttpClient client,
            Uri requestUri,
            T value,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        }
    public static class HttpContentJsonExtensions {
        public static Task<object> ReadFromJsonAsync(
            this HttpContent content,
            Type type,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
        public static Task<T> ReadFromJsonAsync<T>(
            this HttpContent content,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default);
    }
    public class JsonContent : HttpContent {
        public static JsonContent Create<T>(  
            T value  
            JsonSerializerOptions options = null);
        public static JsonContent Create<T>(
            T value,
            MediaTypeHeaderValue mediaType,  
            JsonSerializerOptions options = null);
        public static JsonContent Create<T>(  
            T value,  
            string mediaType,  
            JsonSerializerOptions options = null);
        public JsonContent(  
            Type type,  
            object? value,  
            JsonSerializerOptions options = null);
        public JsonContent(  
            Type type,  
            object? value,  
            MediaTypeHeaderValue mediaType,  
            JsonSerializerOptions options = null);
        public JsonContent(  
            Type type,  
            object? value,  
            string mediaType,
            JsonSerializerOptions options = null);
            public Type ObjectType { get; }
            public object? Value { get; }
    }
}
@terrajobst terrajobst added this to the 5.0 milestone Mar 13, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Mar 13, 2020
@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review labels Mar 13, 2020
@terrajobst
Copy link
Member Author

Video

  • HttpClientJsonExtensions
    • JsonSerializerOptions should be nullable
    • Uri and string should be nullable
    • We should add overloads that take CancellationToken without JsonSerializerOptions
    • Should we define the extension methods over HttpMessageInvoker instead (the base type of HttpClient)?
      • We concluded no, because HttpClient is the mainline API and except for low-level code users don't program against HttpMessageInvoker
    • We should remove overloads that take object and Type
      • If the generic overload is instantiated with object it will use the runtime type
      • We should name Type parameter inputType
      • We should swap object value and Type input
    • Rename T to TValue
    • We like the method names as proposed
  • JsonContent
    • We should make the constructors internal and only have the factory methods. This avoids confusion where people never know that there are generic versions.
    • We should expose non-generic factory methods
    • Swap parameters Type type and object value to avoid value changing positions between overloads
    • Rename parameter Type type to Type inputType
    • Don't expose string mediaType and only MediaTypeHeaderValue. If we need the string based one, we can add it later but it should parse the media type instead of just passing it to the constructor of MediaTypeHeaderValue.
#nullable enable

namespace System.Net.Http.Json {
    public static class HttpClientJsonExtensions {
        public static Task<object> GetFromJsonAsync(
            this HttpClient client,
            string? requestUri,
            Type type,  
            CancellationToken cancellationToken);
        public static Task<object> GetFromJsonAsync(
            this HttpClient client,
            string? requestUri,
            Type type,  
            JsonSerializerOptions? options = null,
            CancellationToken cancellationToken = default);
        public static Task<object> GetFromJsonAsync(
            this HttpClient client,
            Uri? requestUri,
            Type type,
            CancellationToken cancellationToken);
        public static Task<object> GetFromJsonAsync(
            this HttpClient client,
            Uri? requestUri,
            Type type,
            JsonSerializerOptions? options = null,
            CancellationToken cancellationToken = default);
        public static Task<TValue> GetFromJsonAsync<TValue>(
            this HttpClient client,
            string? requestUri,
            CancellationToken cancellationToken);
        public static Task<TValue> GetFromJsonAsync<TValue>(
            this HttpClient client,
            string? requestUri,
            JsonSerializerOptions? options = null,
            CancellationToken cancellationToken = default);
        public static Task<TValue> GetFromJsonAsync<TValue>(
            this HttpClient client,
            Uri? requestUri,
            CancellationToken cancellationToken);
        public static Task<TValue> GetFromJsonAsync<TValue>(
            this HttpClient client,
            Uri? requestUri,
            JsonSerializerOptions? options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(
            this HttpClient client,
            string? requestUri,
            TValue value,
            CancellationToken cancellationToken);
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(
            this HttpClient client,
            string? requestUri,
            TValue value,
            JsonSerializerOptions? options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(
            this HttpClient client,
            Uri? requestUri,
            TValue value,
            CancellationToken cancellationToken);
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(
            this HttpClient client,
            Uri? requestUri,
            TValue value,
            JsonSerializerOptions? options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(
            this HttpClient client,
            string? requestUri,
            TValue value,
            CancellationToken cancellationToken);
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(
            this HttpClient client,
            string? requestUri,
            TValue value,
            JsonSerializerOptions? options = null,
            CancellationToken cancellationToken = default);
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(
            this HttpClient client,
            Uri? requestUri,
            TValue value,
            CancellationToken cancellationToken);
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(
            this HttpClient client,
            Uri? requestUri,
            TValue value,
            JsonSerializerOptions? options = null,
            CancellationToken cancellationToken = default);
        }
    public static class HttpContentJsonExtensions {
        public static Task<object> ReadFromJsonAsync(
            this HttpContent content,
            Type type,
            JsonSerializerOptions? options = null,
            CancellationToken cancellationToken = default);
        public static Task<T> ReadFromJsonAsync<T>(
            this HttpContent content,
            JsonSerializerOptions? options = null,
            CancellationToken cancellationToken = default);
    }
    public class JsonContent : HttpContent {
        public static JsonContent Create<T>(
            T value,
            MediaTypeHeaderValue mediaType,
            JsonSerializerOptions? options = null);
        public static JsonContent Create(
            object? inputValue,
            Type type,
            MediaTypeHeaderValue mediaType,
            JsonSerializerOptions? options = null);
        public Type ObjectType { get; }
        public object? Value { get; }
    }
}

@terrajobst terrajobst removed the untriaged New issue has not been triaged by the area owner label Mar 13, 2020
@dersia
Copy link

dersia commented Mar 13, 2020

I was recently working with some REST-APIs and what I came across a lot was that next to Get, Post and Put api creators often also use Patch and Delete with json content (i.e. apple app store connect api or ms app center api). So I think it makes sense to also include those to methods.

Another thing that I came across often, is that there are Types used for success and failure responses (Type with nullable error objects), so I am wondering if it makes sense to have an overload on GetFromJsonAsync that also deserializes even if the HttpStatusCode is not a success code.
Maybe something like `GetFromJsonAsync(..., bool continueOnFailure = false)

As for JsonContent, what will happen, if someone casts the Content object of the HttpResonseMessage to JsonContent, will that work? Or should there also be an extension Method on HttpContent that is AsJsonContent<TValue>() or AsJsonContent(Type outputType)

@terrajobst
Copy link
Member Author

Duplicate of #32937

@terrajobst terrajobst marked this as a duplicate of #32937 Mar 13, 2020
@terrajobst
Copy link
Member Author

terrajobst commented Mar 13, 2020

@dersia

I was recently working with some REST-APIs and what I came across a lot was that next to Get, Post and Put api creators often also use Patch and Delete with json content (i.e. apple app store connect api or ms app center api). So I think it makes sense to also include those to methods.

We're on a really tight deadline to hit Blazor release in May. I don't think we have the time to add these overloads. FWIW, we considered them but that they seemed rare, but if folks ask, we can add them. So they are still on the table for .NET 5.

Another thing that I came across often, is that there are Types used for success and failure responses (Type with nullable error objects), so I am wondering if it makes sense to have an overload on GetFromJsonAsync that also deserializes even if the HttpStatusCode is not a success code. Maybe something like GetFromJsonAsync(..., bool continueOnFailure = false)

We could, but my hunch is that this is going into the long tail of possible tweaks. If you need this, you should call the SendAsync API and use the extension method against HttpContent.

As for JsonContent, what will happen, if someone casts the Content object of the HttpResonseMessage to JsonContent, will that work? Or should there also be an extension Method on HttpContent that is AsJsonContent<TValue>() or AsJsonContent(Type outputType)

JsonContent is for sending; the user controls the type. When receiving, you won't be able to cast. That's why we have extension methods defined for HttpContent as well.

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
Projects
None yet
Development

No branches or pull requests

3 participants