From 27d6fb48655d02d78ac8f425a70bf6cefe063069 Mon Sep 17 00:00:00 2001 From: Steve Ward Date: Tue, 16 Oct 2018 15:43:48 +0100 Subject: [PATCH 1/6] Isolated Content Serialization - Added IContentSerializer, JsonContentSerializer, XmlContentSerializer - RefitSettings allows the IContentSerializer to be specified - Refit defaults to JsonContentSerializer for back-compat - Tests modified to exercise Json and Xml Serialization - .NET Standard 1.4 has package dep on System.Xml.XmlSerializer --- Refit.Tests/MultipartTests.cs | 41 ++++--- Refit.Tests/RestService.cs | 29 +++++ Refit.Tests/XmlContentSerializerTests.cs | 104 +++++++++++++++++ Refit/ApiException.cs | 6 +- Refit/JsonContentSerializer.cs | 47 ++++++++ Refit/Refit.csproj | 1 + Refit/RefitSettings.cs | 25 +++- Refit/RequestBuilderImplementation.cs | 54 ++++----- Refit/XmlContentSerializer.cs | 140 +++++++++++++++++++++++ 9 files changed, 396 insertions(+), 51 deletions(-) create mode 100644 Refit.Tests/XmlContentSerializerTests.cs create mode 100644 Refit/JsonContentSerializer.cs create mode 100644 Refit/XmlContentSerializer.cs diff --git a/Refit.Tests/MultipartTests.cs b/Refit.Tests/MultipartTests.cs index 130127886..73230ffa4 100644 --- a/Refit.Tests/MultipartTests.cs +++ b/Refit.Tests/MultipartTests.cs @@ -402,9 +402,16 @@ public async Task MultipartUploadShouldWorkWithFileInfoPart() } } - [Fact] - public async Task MultipartUploadShouldWorkWithAnObject() + [Theory] + [InlineData(typeof(JsonContentSerializer), "application/json")] + [InlineData(typeof(XmlContentSerializer), "application/xml")] + public async Task MultipartUploadShouldWorkWithAnObject(Type contentSerializerType, string mediaType) { + if (!(Activator.CreateInstance(contentSerializerType) is IContentSerializer serializer)) + { + throw new ArithmeticException($"{contentSerializerType.FullName} does not implement {nameof(IContentSerializer)}"); + } + var model1 = new ModelObject { Property1 = "M1.prop1", @@ -421,8 +428,8 @@ public async Task MultipartUploadShouldWorkWithAnObject() Assert.Equal("theObject", parts[0].Headers.ContentDisposition.Name); Assert.Null(parts[0].Headers.ContentDisposition.FileName); - Assert.Equal("application/json", parts[0].Headers.ContentType.MediaType); - var result0 = JsonConvert.DeserializeObject(await parts[0].ReadAsStringAsync()); + Assert.Equal(mediaType, parts[0].Headers.ContentType.MediaType); + var result0 = serializer.Deserialize(await parts[0].ReadAsStringAsync()); Assert.Equal(model1.Property1, result0.Property1); Assert.Equal(model1.Property2, result0.Property2); } @@ -430,16 +437,24 @@ public async Task MultipartUploadShouldWorkWithAnObject() var settings = new RefitSettings() { - HttpMessageHandlerFactory = () => handler + HttpMessageHandlerFactory = () => handler, + ContentSerializer = serializer }; var fixture = RestService.For(BaseAddress, settings); var result = await fixture.UploadJsonObject(model1); } - [Fact] - public async Task MultipartUploadShouldWorkWithObjects() + [Theory] + [InlineData(typeof(JsonContentSerializer), "application/json")] + [InlineData(typeof(XmlContentSerializer), "application/xml")] + public async Task MultipartUploadShouldWorkWithObjects(Type contentSerializerType, string mediaType) { + if (!(Activator.CreateInstance(contentSerializerType) is IContentSerializer serializer)) + { + throw new ArithmeticException($"{contentSerializerType.FullName} does not implement {nameof(IContentSerializer)}"); + } + var model1 = new ModelObject { Property1 = "M1.prop1", @@ -451,7 +466,6 @@ public async Task MultipartUploadShouldWorkWithObjects() Property1 = "M2.prop1" }; - var handler = new MockHttpMessageHandler { Asserts = async content => @@ -462,16 +476,16 @@ public async Task MultipartUploadShouldWorkWithObjects() Assert.Equal("theObjects", parts[0].Headers.ContentDisposition.Name); Assert.Null(parts[0].Headers.ContentDisposition.FileName); - Assert.Equal("application/json", parts[0].Headers.ContentType.MediaType); - var result0 = JsonConvert.DeserializeObject(await parts[0].ReadAsStringAsync()); + Assert.Equal(mediaType, parts[0].Headers.ContentType.MediaType); + var result0 = serializer.Deserialize(await parts[0].ReadAsStringAsync()); Assert.Equal(model1.Property1, result0.Property1); Assert.Equal(model1.Property2, result0.Property2); Assert.Equal("theObjects", parts[1].Headers.ContentDisposition.Name); Assert.Null(parts[1].Headers.ContentDisposition.FileName); - Assert.Equal("application/json", parts[1].Headers.ContentType.MediaType); - var result1 = JsonConvert.DeserializeObject(await parts[1].ReadAsStringAsync()); + Assert.Equal(mediaType, parts[1].Headers.ContentType.MediaType); + var result1 = serializer.Deserialize(await parts[1].ReadAsStringAsync()); Assert.Equal(model2.Property1, result1.Property1); Assert.Equal(model2.Property2, result1.Property2); } @@ -479,7 +493,8 @@ public async Task MultipartUploadShouldWorkWithObjects() var settings = new RefitSettings() { - HttpMessageHandlerFactory = () => handler + HttpMessageHandlerFactory = () => handler, + ContentSerializer = serializer }; var fixture = RestService.For(BaseAddress, settings); diff --git a/Refit.Tests/RestService.cs b/Refit.Tests/RestService.cs index 7fe72ca60..35076145e 100644 --- a/Refit.Tests/RestService.cs +++ b/Refit.Tests/RestService.cs @@ -11,6 +11,7 @@ using Refit; // InterfaceStubGenerator looks for this using RichardSzalay.MockHttp; using System.IO; +using System.Text; namespace Refit.Tests { @@ -1107,5 +1108,33 @@ public async Task ServiceOutsideNamespacePostRequest() mockHttp.VerifyNoOutstandingExpectation(); } + + [Fact] + public async Task CanSerializeContentAsXml() + { + var mockHttp = new MockHttpMessageHandler(); + var contentSerializer = new XmlContentSerializer(); + var settings = new RefitSettings + { + HttpMessageHandlerFactory = () => mockHttp, + ContentSerializer = contentSerializer + }; + + mockHttp + .Expect(HttpMethod.Post, "/users") + .WithHeaders("Content-Type:application/xml; charset=utf-8") + .Respond(req => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("Created", Encoding.UTF8, "application/xml") + }); + + var fixture = RestService.For("https://api.github.com", settings); + + var result = await fixture.CreateUser(new User()).ConfigureAwait(false); + + Assert.Equal("Created", result.Name); + + mockHttp.VerifyNoOutstandingExpectation(); + } } } diff --git a/Refit.Tests/XmlContentSerializerTests.cs b/Refit.Tests/XmlContentSerializerTests.cs new file mode 100644 index 000000000..d48b93f91 --- /dev/null +++ b/Refit.Tests/XmlContentSerializerTests.cs @@ -0,0 +1,104 @@ +using System; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; +using Xunit; + +namespace Refit.Tests +{ + public class XmlContentSerializerTests + { + public class Dto + { + public DateTime CreatedOn { get; set; } + + public string Identifier { get; set; } + + [XmlElement(Namespace = "https://google.com")] + public string Name { get; set; } + } + + [Fact] + public void MediaTypeShouldBeApplicationXml() + { + var dto = BuildDto(); + var sut = new XmlContentSerializer(); + + var content = sut.Serialize(dto); + + Assert.Equal("application/xml", content.Headers.ContentType.MediaType); + } + + [Fact] + public async Task ShouldSerializeToXml() + { + var dto = BuildDto(); + var sut = new XmlContentSerializer(); + + var content = sut.Serialize(dto); + var document = new XmlDocument(); + document.LoadXml(await content.ReadAsStringAsync()); + + var root = document[nameof(Dto)] ?? throw new NullReferenceException("Root element was not found"); + Assert.Equal(dto.CreatedOn, XmlConvert.ToDateTime(root[nameof(Dto.CreatedOn)].InnerText, XmlDateTimeSerializationMode.Utc)); + Assert.Equal(dto.Identifier, root[nameof(Dto.Identifier)].InnerText); + Assert.Equal(dto.Name, root[nameof(Dto.Name)].InnerText); + } + + [Fact] + public async Task ShouldSerializeToXmlUsingAttributeOverrides() + { + const string overridenRootElementName = "dto-ex"; + + var dto = BuildDto(); + var serializerSettings = new XmlContentSerializerSettings(); + var attributes = new XmlAttributes { XmlRoot = new XmlRootAttribute(overridenRootElementName) }; + serializerSettings.XmlAttributeOverrides.Add(dto.GetType(), attributes); + var sut = new XmlContentSerializer(serializerSettings); + + var content = sut.Serialize(dto); + var document = new XmlDocument(); + document.LoadXml(await content.ReadAsStringAsync()); + + Assert.Equal(overridenRootElementName, document.DocumentElement?.Name); + } + + [Fact] + public async Task ShouldSerializeToXmlUsingNamespaceOverrides() + { + const string prefix = "google"; + + var dto = BuildDto(); + var serializerSettings = new XmlContentSerializerSettings { XmlNamespaces = new XmlSerializerNamespaces() }; + serializerSettings.XmlNamespaces.Add(prefix, "https://google.com"); + var sut = new XmlContentSerializer(serializerSettings); + + var content = sut.Serialize(dto); + var document = new XmlDocument(); + document.LoadXml(await content.ReadAsStringAsync()); + + Assert.Equal(prefix, document["Dto"]?["Name", "https://google.com"]?.Prefix); + } + + [Fact] + public void ShouldDeserializeFromXml() + { + var serializerSettings = new XmlContentSerializerSettings { XmlNamespaces = new XmlSerializerNamespaces() }; + var sut = new XmlContentSerializer(serializerSettings); + + var dto = sut.Deserialize("123"); + + Assert.Equal("123", dto.Identifier); + } + + private static Dto BuildDto() + { + var dto = new Dto { + CreatedOn = DateTime.UtcNow, + Identifier = Guid.NewGuid().ToString(), + Name = "Test Dto Object" + }; + return dto; + } + } +} diff --git a/Refit/ApiException.cs b/Refit/ApiException.cs index 541707ea6..4dca6e924 100644 --- a/Refit/ApiException.cs +++ b/Refit/ApiException.cs @@ -2,6 +2,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; @@ -17,11 +18,8 @@ public class ApiException : Exception public HttpMethod HttpMethod { get; } public Uri Uri => RequestMessage.RequestUri; public HttpRequestMessage RequestMessage { get; } - public HttpContentHeaders ContentHeaders { get; private set; } - public string Content { get; private set; } - public bool HasContent => !string.IsNullOrWhiteSpace(Content); public RefitSettings RefitSettings { get; set; } @@ -37,7 +35,7 @@ protected ApiException(HttpRequestMessage message, HttpMethod httpMethod, HttpSt } public T GetContentAs() => HasContent ? - JsonConvert.DeserializeObject(Content, RefitSettings.JsonSerializerSettings) : + RefitSettings.ContentSerializer.Deserialize(Content) : default; #pragma warning disable VSTHRD200 // Use "Async" suffix for async methods diff --git a/Refit/JsonContentSerializer.cs b/Refit/JsonContentSerializer.cs new file mode 100644 index 000000000..af840f7cf --- /dev/null +++ b/Refit/JsonContentSerializer.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Refit { + + public class JsonContentSerializer : IContentSerializer + { + private readonly JsonSerializerSettings jsonSerializerSettings; + + public JsonContentSerializer() : this(new JsonSerializerSettings()) + { + } + + public JsonContentSerializer(JsonSerializerSettings jsonSerializerSettings) + { + this.jsonSerializerSettings = jsonSerializerSettings ?? throw new ArgumentNullException(nameof(jsonSerializerSettings)); + } + + public HttpContent Serialize(object item) + { + return new StringContent(JsonConvert.SerializeObject(item, jsonSerializerSettings), Encoding.UTF8, "application/json"); + } + + public async Task DeserializeAsync(HttpContent content, Type objectType) + { + var serializer = JsonSerializer.Create(jsonSerializerSettings); + + using (var stream = await content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var reader = new StreamReader(stream)) + using (var jsonTextReader = new JsonTextReader(reader)) + return serializer.Deserialize(jsonTextReader, objectType); + } + + public T Deserialize(string content) + { + var serializer = JsonSerializer.Create(jsonSerializerSettings); + + using (var reader = new StringReader(content)) + using (var jsonTextReader = new JsonTextReader(reader)) + return serializer.Deserialize(jsonTextReader); + } + } +} diff --git a/Refit/Refit.csproj b/Refit/Refit.csproj index 9fb8be1b3..38e8ff56b 100644 --- a/Refit/Refit.csproj +++ b/Refit/Refit.csproj @@ -17,6 +17,7 @@ + diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index 5c17fbe55..ffcde0377 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Collections.Concurrent; using System.Globalization; using System.Linq; @@ -13,21 +12,43 @@ namespace Refit { public class RefitSettings { + JsonSerializerSettings jsonSerializerSettings; + public RefitSettings() { UrlParameterFormatter = new DefaultUrlParameterFormatter(); FormUrlEncodedParameterFormatter = new DefaultFormUrlEncodedParameterFormatter(); + ContentSerializer = new JsonContentSerializer(); } public Func> AuthorizationHeaderValueGetter { get; set; } public Func HttpMessageHandlerFactory { get; set; } - public JsonSerializerSettings JsonSerializerSettings { get; set; } + public JsonSerializerSettings JsonSerializerSettings + { + get => jsonSerializerSettings; + set + { + jsonSerializerSettings = value; + ContentSerializer = new JsonContentSerializer(value); + } + } + + public IContentSerializer ContentSerializer { get; set; } public IUrlParameterFormatter UrlParameterFormatter { get; set; } public IFormUrlEncodedParameterFormatter FormUrlEncodedParameterFormatter { get; set; } public bool Buffered { get; set; } = true; } + public interface IContentSerializer + { + HttpContent Serialize(object item); + + Task DeserializeAsync(HttpContent content, Type objectType); + + T Deserialize(string content); + } + public interface IUrlParameterFormatter { string Format(object value, ParameterInfo parameterInfo); diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index 6833e55ab..eb6a47268 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -10,7 +10,6 @@ using System.Text.RegularExpressions; using System.Threading; using System.Web; -using Newtonsoft.Json; using System.Collections.Concurrent; using System.Net.Http.Headers; @@ -25,7 +24,7 @@ partial class RequestBuilderImplementation : IRequestBuilder }; readonly Dictionary> interfaceHttpMethods; readonly ConcurrentDictionary interfaceGenericHttpMethods; - readonly JsonSerializer serializer; + readonly IContentSerializer serializer; readonly RefitSettings settings; public Type TargetType { get; } @@ -34,7 +33,7 @@ public RequestBuilderImplementation(RefitSettings refitSettings = null) Type targetInterface = typeof(TApi); settings = refitSettings ?? new RefitSettings(); - serializer = JsonSerializer.Create(settings.JsonSerializerSettings); + serializer = settings.ContentSerializer; interfaceGenericHttpMethods = new ConcurrentDictionary(); if (targetInterface == null || !targetInterface.GetTypeInfo().IsInterface) @@ -201,8 +200,7 @@ void AddMultipartItem(MultipartFormDataContent multiPartContent, string fileName Exception e = null; try { - var stringContent = new StringContent(JsonConvert.SerializeObject(itemValue, settings.JsonSerializerSettings), Encoding.UTF8, "application/json"); - multiPartContent.Add(stringContent, parameterName); + multiPartContent.Add(settings.ContentSerializer.Serialize(itemValue), parameterName); return; } catch(Exception ex) @@ -277,25 +275,26 @@ Func> BuildCancellableTaskFuncF return (T)stream; } - using (var stream = await content.ReadAsStreamAsync().ConfigureAwait(false)) - using (var reader = new StreamReader(stream)) + if (serializedReturnType == typeof(string)) { - if (serializedReturnType == typeof(string)) + using(var stream = await content.ReadAsStreamAsync().ConfigureAwait(false)) + using(var reader = new StreamReader(stream)) { - var str = (object)await reader.ReadToEndAsync().ConfigureAwait(false); - if (isApiResponse) - return ApiResponse.Create(resp, str); - return (T)str; - } + if (serializedReturnType == typeof(string)) + { + var str = (object)await reader.ReadToEndAsync().ConfigureAwait(false); + if (isApiResponse) + return ApiResponse.Create(resp, str); + return (T)str; + } - using (var jsonReader = new JsonTextReader(reader)) - { - var json = serializer.Deserialize(jsonReader, serializedReturnType); - if (isApiResponse) - return ApiResponse.Create(resp, json); - return (T)json; } } + + var json = await serializer.DeserializeAsync(content, serializedReturnType); + if (isApiResponse) + return ApiResponse.Create(resp, json); + return (T)json; } finally { @@ -467,24 +466,15 @@ Func BuildRequestFactoryForMethod(RestMethodInfo r break; case BodySerializationMethod.Default: case BodySerializationMethod.Json: - var param = paramList[i]; + var content = serializer.Serialize(paramList[i]); switch (restMethod.BodyParameterInfo.Item2) { case false: - ret.Content = new PushStreamContent((stream, _, __) => - { - using (var writer = new JsonTextWriter(new StreamWriter(stream))) - { - serializer.Serialize(writer, param); - } - }, - new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" }); + ret.Content = new PushStreamContent( + async (stream, _, __) => { await content.CopyToAsync(stream).ConfigureAwait(false); }, content.Headers.ContentType); break; case true: - ret.Content = new StringContent( - JsonConvert.SerializeObject(paramList[i], settings.JsonSerializerSettings), - Encoding.UTF8, - "application/json"); + ret.Content = content; break; } diff --git a/Refit/XmlContentSerializer.cs b/Refit/XmlContentSerializer.cs new file mode 100644 index 000000000..ecf92b148 --- /dev/null +++ b/Refit/XmlContentSerializer.cs @@ -0,0 +1,140 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; + +namespace Refit { + + public class XmlContentSerializer : IContentSerializer + { + private readonly XmlContentSerializerSettings settings; + + public XmlContentSerializer() : this(new XmlContentSerializerSettings()) + { + } + + public XmlContentSerializer(XmlContentSerializerSettings settings) + { + this.settings = settings ?? throw new ArgumentNullException(nameof(settings)); + } + + public HttpContent Serialize(object item) + { + var xmlSerializer = new XmlSerializer(item.GetType(), settings.XmlAttributeOverrides); + + using(var output = new StringWriter()) + { + using(var writer = XmlWriter.Create(output, settings.XmlReaderWriterSettings.WriterSettings)) + { + xmlSerializer.Serialize(writer, item, settings.XmlNamespaces); + return new StringContent(output.ToString(), Encoding.UTF8, "application/xml"); + } + } + } + + public async Task DeserializeAsync(HttpContent content, Type objectType) + { + var xmlSerializer = new XmlSerializer(objectType, settings.XmlAttributeOverrides); + + using (var input = new StringReader(await content.ReadAsStringAsync().ConfigureAwait(false))) + { + using (var reader = XmlReader.Create(input, settings.XmlReaderWriterSettings.ReaderSettings)) + { + return xmlSerializer.Deserialize(reader); + } + } + } + + public T Deserialize(string content) + { + var xmlSerializer = new XmlSerializer(typeof(T), settings.XmlAttributeOverrides); + + using (var input = new StringReader(content)) + { + using (var reader = XmlReader.Create(input, settings.XmlReaderWriterSettings.ReaderSettings)) + { + return (T)(object)xmlSerializer.Deserialize(reader); + } + } + } + } + + public class XmlReaderWriterSettings + { + private XmlReaderSettings readerSettings; + private XmlWriterSettings writerSettings; + + public XmlReaderWriterSettings() : this(new XmlReaderSettings(), new XmlWriterSettings()) + { + } + + public XmlReaderWriterSettings(XmlReaderSettings readerSettings) : this(readerSettings, new XmlWriterSettings()) + { + } + + public XmlReaderWriterSettings(XmlWriterSettings writerSettings) : this(new XmlReaderSettings(), writerSettings) + { + } + + public XmlReaderWriterSettings(XmlReaderSettings readerSettings, XmlWriterSettings writerSettings) + { + ReaderSettings = readerSettings; + WriterSettings = writerSettings; + } + + public XmlReaderSettings ReaderSettings + { + get + { + ApplyOverrideSettings(); + return readerSettings; + } + set => readerSettings = value ?? throw new ArgumentNullException(nameof(value)); + } + + public XmlWriterSettings WriterSettings + { + get + { + ApplyOverrideSettings(); + return writerSettings; + } + set => writerSettings = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// The writer and reader settings are set by the caller, but certain properties + /// should remain set to meet the demands of the XmlContentSerializer. Those properties + /// are always set here. + /// + private void ApplyOverrideSettings() + { + writerSettings.Async = true; + readerSettings.Async = true; + } + } + + public class XmlContentSerializerSettings + { + public XmlContentSerializerSettings() + { + XmlReaderWriterSettings = new XmlReaderWriterSettings(); + XmlNamespaces = new XmlSerializerNamespaces( + new[] + { + new XmlQualifiedName(string.Empty, string.Empty), + }); + + XmlAttributeOverrides = new XmlAttributeOverrides(); + } + + public XmlReaderWriterSettings XmlReaderWriterSettings { get; set; } + + public XmlSerializerNamespaces XmlNamespaces { get; set; } + + public XmlAttributeOverrides XmlAttributeOverrides { get; set; } + } +} From 741b7b6b61c3be66ac28c442bd637bc1e3dacbcc Mon Sep 17 00:00:00 2001 From: stevewgh Date: Wed, 14 Nov 2018 23:47:29 +0000 Subject: [PATCH 2/6] Isolated Content Serialization continued - Added Obsolete attributes - Added Serialized enum as a generic replacement of JSON enum - Updated tests to use ContentSerializer property - Updated README with XML example --- README.md | 59 +++++++++++++++++++++++---- Refit.Tests/RestService.cs | 32 +++++++-------- Refit/Attributes.cs | 10 ++++- Refit/JsonContentSerializer.cs | 29 ++++++++----- Refit/RefitSettings.cs | 1 + Refit/RequestBuilderImplementation.cs | 3 ++ Refit/RestMethodInfo.cs | 2 +- 7 files changed, 100 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 05a779a5e..0945a7196 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,8 @@ type of the parameter: * If the type is `string`, the string will be used directly as the content * If the parameter has the attribute `[Body(BodySerializationMethod.UrlEncoded)]`, the content will be URL-encoded (see [form posts](#form-posts) below) -* For all other types, the object will be serialized as JSON. +* For all other types, the object will be serialized using the content serializer specified in +RefitSettings (JSON is the default). #### Buffering and the `Content-Length` header @@ -198,17 +199,19 @@ APIs: ```csharp var gitHubApi = RestService.For("https://api.github.com", new RefitSettings { - JsonSerializerSettings = new JsonSerializerSettings { - ContractResolver = new SnakeCasePropertyNamesContractResolver() + ContentSerializer = new JsonContentSerializer( + new JsonSerializerSettings { + ContractResolver = new SnakeCasePropertyNamesContractResolver() } - }); + )}); var otherApi = RestService.For("https://api.example.com", new RefitSettings { - JsonSerializerSettings = new JsonSerializerSettings { - ContractResolver = new CamelCasePropertyNamesContractResolver() + ContentSerializer = new JsonContentSerializer( + new JsonSerializerSettings { + ContractResolver = new CamelCasePropertyNamesContractResolver() } - }); + )}); ``` Property serialization/deserialization can be customised using Json.NET's @@ -223,6 +226,48 @@ public class Foo } ``` +#### XML Content + +XML requests and responses are serialized/deserialized using _System.Xml.Serialization.XmlSerializer_. +By default, Refit will use JSON content serialization, to use XML content configure the ContentSerializer to use the `XmlContentSerializer`: + +```csharp +var gitHubApi = RestService.For("https://www.w3.org/XML", + new RefitSettings { + ContentSerializer = new XmlContentSerializer() + }); +``` + +Property serialization/deserialization can be customised using attributes found in the _System.Xml.Serialization_ namespace: + +```csharp + public class Foo + { + [XmlElement(Namespace = "https://www.w3.org/XML")] + public string Bar { get; set; } + } +``` + +The _System.Xml.Serialization.XmlSerializer_ provides many options for serializing, those options can be set by providing an `XmlContentSerializerSettings` to the `XmlContentSerializer` constructor: + +```csharp +var gitHubApi = RestService.For("https://www.w3.org/XML", + new RefitSettings { + ContentSerializer = new XmlContentSerializer( + new XmlContentSerializerSettings + { + XmlReaderWriterSettings = new XmlReaderWriterSettings() + { + ReaderSettings = new XmlReaderSettings + { + IgnoreWhitespace = true + } + } + } + ) + }); +``` + #### Form posts For APIs that take form posts (i.e. serialized as `application/x-www-form-urlencoded`), diff --git a/Refit.Tests/RestService.cs b/Refit.Tests/RestService.cs index 35076145e..cd402a42a 100644 --- a/Refit.Tests/RestService.cs +++ b/Refit.Tests/RestService.cs @@ -40,7 +40,7 @@ public interface IRequestBin Task PostRawStringDefault([Body] string str); [Post("/foo")] - Task PostRawStringJson([Body(BodySerializationMethod.Json)] string str); + Task PostRawStringJson([Body(BodySerializationMethod.Serialized)] string str); [Post("/foo")] Task PostRawStringUrlEncoded([Body(BodySerializationMethod.UrlEncoded)] string str); @@ -244,7 +244,7 @@ public async Task HitTheGitHubUserApiAsApiResponse() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; var responseMessage = new HttpResponseMessage() @@ -280,7 +280,7 @@ public async Task HitTheNonExistentApiAsApiResponse() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.Expect(HttpMethod.Get, "https://api.github.com/give-me-some-404-action") @@ -309,7 +309,7 @@ public async Task HitTheNonExistentApi() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.Expect(HttpMethod.Get, "https://api.github.com/give-me-some-404-action") @@ -337,7 +337,7 @@ public async Task HitTheGitHubUserApiAsObservableApiResponse() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; var responseMessage = new HttpResponseMessage() @@ -373,7 +373,7 @@ public async Task HitTheGitHubUserApi() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.Expect(HttpMethod.Get, "https://api.github.com/users/octocat") @@ -397,7 +397,7 @@ public async Task HitWithCamelCaseParameter() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.Expect(HttpMethod.Get, "https://api.github.com/users/octocat") @@ -420,7 +420,7 @@ public async Task HitTheGitHubOrgMembersApi() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.Expect(HttpMethod.Get, "https://api.github.com/orgs/github/members") @@ -444,7 +444,7 @@ public async Task HitTheGitHubUserSearchApi() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.Expect(HttpMethod.Get, "https://api.github.com/search/users") @@ -467,7 +467,7 @@ public async Task HitTheGitHubUserApiAsObservable() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.Expect(HttpMethod.Get, "https://api.github.com/users/octocat") @@ -492,7 +492,7 @@ public async Task HitTheGitHubUserApiAsObservableAndSubscribeAfterTheFact() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.When(HttpMethod.Get, "https://api.github.com/users/octocat") @@ -547,7 +547,7 @@ public async Task ShouldRetHttpResponseMessage() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.When(HttpMethod.Get, "https://api.github.com/") @@ -568,7 +568,7 @@ public async Task ShouldRetHttpResponseMessageWithNestedInterface() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.When(HttpMethod.Get, "https://api.github.com/") @@ -729,7 +729,7 @@ public async Task CanGetDataOutOfErrorResponses() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.When(HttpMethod.Get, "https://api.github.com/give-me-some-404-action") @@ -758,7 +758,7 @@ public async Task ErrorsFromApiReturnErrorContent() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.Expect(HttpMethod.Post, "https://api.github.com/users") @@ -788,7 +788,7 @@ public async Task ErrorsFromApiReturnErrorContentWhenApiResponse() var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp, - JsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() } + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) }; mockHttp.Expect(HttpMethod.Post, "https://api.github.com/users") diff --git a/Refit/Attributes.cs b/Refit/Attributes.cs index 542b2b0cc..fd5f1236c 100644 --- a/Refit/Attributes.cs +++ b/Refit/Attributes.cs @@ -96,19 +96,25 @@ public class MultipartAttribute : Attribute { } public enum BodySerializationMethod { /// - /// JSON encodes data except for strings. Strings are set as-is + /// Encodes everything using the ContentSerializer in RefitSettings except for strings. Strings are set as-is /// Default, /// /// Json encodes everything, including strings /// + [Obsolete("Use BodySerializationMethod.Serialized instead", false)] Json, /// /// Form-UrlEncode's the values /// - UrlEncoded + UrlEncoded, + + /// + /// Encodes everything using the ContentSerializer in RefitSettings + /// + Serialized } [AttributeUsage(AttributeTargets.Parameter)] diff --git a/Refit/JsonContentSerializer.cs b/Refit/JsonContentSerializer.cs index af840f7cf..a9b9680f5 100644 --- a/Refit/JsonContentSerializer.cs +++ b/Refit/JsonContentSerializer.cs @@ -5,29 +5,38 @@ using System.Threading.Tasks; using Newtonsoft.Json; -namespace Refit { - +namespace Refit +{ public class JsonContentSerializer : IContentSerializer { - private readonly JsonSerializerSettings jsonSerializerSettings; + private readonly Lazy jsonSerializerSettings; - public JsonContentSerializer() : this(new JsonSerializerSettings()) - { - } + public JsonContentSerializer() : this(null) { } public JsonContentSerializer(JsonSerializerSettings jsonSerializerSettings) { - this.jsonSerializerSettings = jsonSerializerSettings ?? throw new ArgumentNullException(nameof(jsonSerializerSettings)); + this.jsonSerializerSettings = new Lazy(() => + { + if (jsonSerializerSettings == null) + { + if(JsonConvert.DefaultSettings == null) + { + return new JsonSerializerSettings(); + } + return JsonConvert.DefaultSettings(); + } + return jsonSerializerSettings; + }); } public HttpContent Serialize(object item) { - return new StringContent(JsonConvert.SerializeObject(item, jsonSerializerSettings), Encoding.UTF8, "application/json"); + return new StringContent(JsonConvert.SerializeObject(item, jsonSerializerSettings.Value), Encoding.UTF8, "application/json"); } public async Task DeserializeAsync(HttpContent content, Type objectType) { - var serializer = JsonSerializer.Create(jsonSerializerSettings); + var serializer = JsonSerializer.Create(jsonSerializerSettings.Value); using (var stream = await content.ReadAsStreamAsync().ConfigureAwait(false)) using (var reader = new StreamReader(stream)) @@ -37,7 +46,7 @@ public async Task DeserializeAsync(HttpContent content, Type objectType) public T Deserialize(string content) { - var serializer = JsonSerializer.Create(jsonSerializerSettings); + var serializer = JsonSerializer.Create(jsonSerializerSettings.Value); using (var reader = new StringReader(content)) using (var jsonTextReader = new JsonTextReader(reader)) diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index ffcde0377..4521fe415 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -24,6 +24,7 @@ public RefitSettings() public Func> AuthorizationHeaderValueGetter { get; set; } public Func HttpMessageHandlerFactory { get; set; } + [Obsolete("Set RefitSettings.ContentSerializer = new JsonContentSerializer(JsonSerializerSettings) instead.", false)] public JsonSerializerSettings JsonSerializerSettings { get => jsonSerializerSettings; diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index eb6a47268..9ea943224 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -465,7 +465,10 @@ Func BuildRequestFactoryForMethod(RestMethodInfo r ret.Content = paramList[i] is string str ? (HttpContent)new StringContent(Uri.EscapeDataString(str), Encoding.UTF8, "application/x-www-form-urlencoded") : new FormUrlEncodedContent(new FormValueDictionary(paramList[i], settings)); break; case BodySerializationMethod.Default: +#pragma warning disable CS0618 // Type or member is obsolete case BodySerializationMethod.Json: +#pragma warning restore CS0618 // Type or member is obsolete + case BodySerializationMethod.Serialized: var content = serializer.Serialize(paramList[i]); switch (restMethod.BodyParameterInfo.Item2) { diff --git a/Refit/RestMethodInfo.cs b/Refit/RestMethodInfo.cs index d360406ff..e21fbf550 100644 --- a/Refit/RestMethodInfo.cs +++ b/Refit/RestMethodInfo.cs @@ -210,7 +210,7 @@ Tuple FindBodyParameter(IList } if (refParams.Count == 1) { - return Tuple.Create(BodySerializationMethod.Json, false, parameterList.IndexOf(refParams[0])); + return Tuple.Create(BodySerializationMethod.Serialized, false, parameterList.IndexOf(refParams[0])); } return null; From 7a387ed26dc21222b122eb86db5278e255981270 Mon Sep 17 00:00:00 2001 From: Steve Ward Date: Sun, 25 Nov 2018 20:06:22 +0000 Subject: [PATCH 3/6] Changes made in response to PR feedback IContentSeriaizer - all methods made async - removed 2nd deserialize method - type safe operation ApiException - Added GetContentAsAsync() method - Marked GetContentAs() as obsolete --- Refit.Tests/MultipartTests.cs | 6 +- Refit.Tests/RequestBuilder.cs | 23 ++++++ Refit.Tests/RestService.cs | 38 ++++++++-- Refit.Tests/XmlContentSerializerTests.cs | 15 ++-- Refit/ApiException.cs | 7 +- Refit/ApiResponse.cs | 2 - Refit/JsonContentSerializer.cs | 16 ++--- Refit/RefitSettings.cs | 6 +- Refit/RequestBuilderImplementation.cs | 89 ++++++++++++------------ Refit/RestMethodInfo.cs | 4 ++ Refit/ValidationApiException.cs | 3 +- Refit/XmlContentSerializer.cs | 30 ++------ 12 files changed, 136 insertions(+), 103 deletions(-) diff --git a/Refit.Tests/MultipartTests.cs b/Refit.Tests/MultipartTests.cs index 73230ffa4..b3364dc4d 100644 --- a/Refit.Tests/MultipartTests.cs +++ b/Refit.Tests/MultipartTests.cs @@ -429,7 +429,7 @@ public async Task MultipartUploadShouldWorkWithAnObject(Type contentSerializerTy Assert.Equal("theObject", parts[0].Headers.ContentDisposition.Name); Assert.Null(parts[0].Headers.ContentDisposition.FileName); Assert.Equal(mediaType, parts[0].Headers.ContentType.MediaType); - var result0 = serializer.Deserialize(await parts[0].ReadAsStringAsync()); + var result0 = await serializer.DeserializeAsync(parts[0]); Assert.Equal(model1.Property1, result0.Property1); Assert.Equal(model1.Property2, result0.Property2); } @@ -477,7 +477,7 @@ public async Task MultipartUploadShouldWorkWithObjects(Type contentSerializerTyp Assert.Equal("theObjects", parts[0].Headers.ContentDisposition.Name); Assert.Null(parts[0].Headers.ContentDisposition.FileName); Assert.Equal(mediaType, parts[0].Headers.ContentType.MediaType); - var result0 = serializer.Deserialize(await parts[0].ReadAsStringAsync()); + var result0 = await serializer.DeserializeAsync( parts[0]); Assert.Equal(model1.Property1, result0.Property1); Assert.Equal(model1.Property2, result0.Property2); @@ -485,7 +485,7 @@ public async Task MultipartUploadShouldWorkWithObjects(Type contentSerializerTyp Assert.Equal("theObjects", parts[1].Headers.ContentDisposition.Name); Assert.Null(parts[1].Headers.ContentDisposition.FileName); Assert.Equal(mediaType, parts[1].Headers.ContentType.MediaType); - var result1 = serializer.Deserialize(await parts[1].ReadAsStringAsync()); + var result1 = await serializer.DeserializeAsync(parts[1]); Assert.Equal(model2.Property1, result1.Property1); Assert.Equal(model2.Property2, result1.Property2); } diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index 258ef4df2..66d3e55c0 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -68,6 +68,11 @@ public interface IRestMethodInfoTests [Patch("/foo/{id}")] IObservable PatchSomething(int id, [Body] string someAttribute); + [Post("/foo/{id}")] + Task> PostReturnsApiResponse(int id); + + [Post("/foo/{id}")] + Task PostReturnsNonApiResponse(int id); [Post("/foo")] Task PostWithBodyDetected(Dictionary theData); @@ -377,6 +382,24 @@ public void UsingThePatchAttributeSetsTheCorrectMethod() Assert.Equal("PATCH", fixture.HttpMethod.Method); } + + [Fact] + public void ApiResponseShouldBeSet() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PostReturnsApiResponse))); + + Assert.True(fixture.IsApiResponse); + } + + [Fact] + public void ApiResponseShouldNotBeSet() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PostReturnsNonApiResponse))); + + Assert.False(fixture.IsApiResponse); + } } [Headers("User-Agent: RefitTestClient", "Api-Version: 1")] diff --git a/Refit.Tests/RestService.cs b/Refit.Tests/RestService.cs index cd402a42a..9b8de8a8b 100644 --- a/Refit.Tests/RestService.cs +++ b/Refit.Tests/RestService.cs @@ -735,14 +735,13 @@ public async Task CanGetDataOutOfErrorResponses() mockHttp.When(HttpMethod.Get, "https://api.github.com/give-me-some-404-action") .Respond(HttpStatusCode.NotFound, "application/json", "{'message': 'Not Found', 'documentation_url': 'http://foo/bar'}"); - var fixture = RestService.For("https://api.github.com", settings); try { await fixture.NothingToSeeHere(); Assert.True(false); } catch (ApiException exception) { Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); - var content = exception.GetContentAs>(); + var content = await exception.GetContentAsAsync>(); Assert.Equal("Not Found", content["message"]); Assert.NotNull(content["documentation_url"]); @@ -771,7 +770,7 @@ public async Task ErrorsFromApiReturnErrorContent() var result = await Assert.ThrowsAsync(async () => await fixture.CreateUser(new User{Name = "foo"})); - var errors = result.GetContentAs(); + var errors = await result.GetContentAsAsync(); Assert.Contains("error1", errors.Errors); Assert.Contains("message", errors.Errors); @@ -803,7 +802,7 @@ public async Task ErrorsFromApiReturnErrorContentWhenApiResponse() Assert.False(response.IsSuccessStatusCode); Assert.NotNull(response.Error); - var errors = response.Error.GetContentAs(); + var errors = await response.Error.GetContentAsAsync(); Assert.Contains("error1", errors.Errors); Assert.Contains("message", errors.Errors); @@ -813,6 +812,37 @@ public async Task ErrorsFromApiReturnErrorContentWhenApiResponse() } } + [Fact] + public async Task ErrorsFromApiReturnErrorContentNonAsync() + { + var mockHttp = new MockHttpMessageHandler(); + + var settings = new RefitSettings + { + HttpMessageHandlerFactory = () => mockHttp, + ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings() { ContractResolver = new SnakeCasePropertyNamesContractResolver() }) + }; + + mockHttp.Expect(HttpMethod.Post, "https://api.github.com/users") + .Respond(HttpStatusCode.BadRequest, "application/json", "{ 'errors': [ 'error1', 'message' ]}"); + + + var fixture = RestService.For("https://api.github.com", settings); + + + var result = await Assert.ThrowsAsync(async () => await fixture.CreateUser(new User { Name = "foo" })); + + +#pragma warning disable CS0618 // Ensure that this code continues to be tested until it is removed + var errors = result.GetContentAs(); +#pragma warning restore CS0618 + + Assert.Contains("error1", errors.Errors); + Assert.Contains("message", errors.Errors); + + mockHttp.VerifyNoOutstandingExpectation(); + } + [Fact] public void NonRefitInterfacesThrowMeaningfulExceptions() { diff --git a/Refit.Tests/XmlContentSerializerTests.cs b/Refit.Tests/XmlContentSerializerTests.cs index d48b93f91..6dd65cadc 100644 --- a/Refit.Tests/XmlContentSerializerTests.cs +++ b/Refit.Tests/XmlContentSerializerTests.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; @@ -19,12 +20,12 @@ public class Dto } [Fact] - public void MediaTypeShouldBeApplicationXml() + public async Task MediaTypeShouldBeApplicationXmlAsync() { var dto = BuildDto(); var sut = new XmlContentSerializer(); - var content = sut.Serialize(dto); + var content = await sut.SerializeAsync(dto); Assert.Equal("application/xml", content.Headers.ContentType.MediaType); } @@ -35,7 +36,7 @@ public async Task ShouldSerializeToXml() var dto = BuildDto(); var sut = new XmlContentSerializer(); - var content = sut.Serialize(dto); + var content = await sut.SerializeAsync(dto); var document = new XmlDocument(); document.LoadXml(await content.ReadAsStringAsync()); @@ -56,7 +57,7 @@ public async Task ShouldSerializeToXmlUsingAttributeOverrides() serializerSettings.XmlAttributeOverrides.Add(dto.GetType(), attributes); var sut = new XmlContentSerializer(serializerSettings); - var content = sut.Serialize(dto); + var content = await sut.SerializeAsync(dto); var document = new XmlDocument(); document.LoadXml(await content.ReadAsStringAsync()); @@ -73,7 +74,7 @@ public async Task ShouldSerializeToXmlUsingNamespaceOverrides() serializerSettings.XmlNamespaces.Add(prefix, "https://google.com"); var sut = new XmlContentSerializer(serializerSettings); - var content = sut.Serialize(dto); + var content = await sut.SerializeAsync(dto); var document = new XmlDocument(); document.LoadXml(await content.ReadAsStringAsync()); @@ -81,12 +82,12 @@ public async Task ShouldSerializeToXmlUsingNamespaceOverrides() } [Fact] - public void ShouldDeserializeFromXml() + public async Task ShouldDeserializeFromXmlAsync() { var serializerSettings = new XmlContentSerializerSettings { XmlNamespaces = new XmlSerializerNamespaces() }; var sut = new XmlContentSerializer(serializerSettings); - var dto = sut.Deserialize("123"); + var dto = await sut.DeserializeAsync(new StringContent("123")); Assert.Equal("123", dto.Identifier); } diff --git a/Refit/ApiException.cs b/Refit/ApiException.cs index 4dca6e924..e02be4ecd 100644 --- a/Refit/ApiException.cs +++ b/Refit/ApiException.cs @@ -34,8 +34,11 @@ protected ApiException(HttpRequestMessage message, HttpMethod httpMethod, HttpSt RefitSettings = refitSettings; } - public T GetContentAs() => HasContent ? - RefitSettings.ContentSerializer.Deserialize(Content) : + [Obsolete("Use GetContentAsAsync() instead", false)] + public T GetContentAs() => GetContentAsAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + + public async Task GetContentAsAsync() => HasContent ? + await RefitSettings.ContentSerializer.DeserializeAsync(new StringContent(Content)).ConfigureAwait(false) : default; #pragma warning disable VSTHRD200 // Use "Async" suffix for async methods diff --git a/Refit/ApiResponse.cs b/Refit/ApiResponse.cs index 27920c125..5c7df954b 100644 --- a/Refit/ApiResponse.cs +++ b/Refit/ApiResponse.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; diff --git a/Refit/JsonContentSerializer.cs b/Refit/JsonContentSerializer.cs index a9b9680f5..2ffa1aadb 100644 --- a/Refit/JsonContentSerializer.cs +++ b/Refit/JsonContentSerializer.cs @@ -29,26 +29,18 @@ public JsonContentSerializer(JsonSerializerSettings jsonSerializerSettings) }); } - public HttpContent Serialize(object item) + public Task SerializeAsync(object item) { - return new StringContent(JsonConvert.SerializeObject(item, jsonSerializerSettings.Value), Encoding.UTF8, "application/json"); + var content = new StringContent(JsonConvert.SerializeObject(item, jsonSerializerSettings.Value), Encoding.UTF8, "application/json"); + return Task.FromResult((HttpContent)content); } - public async Task DeserializeAsync(HttpContent content, Type objectType) + public async Task DeserializeAsync(HttpContent content) { var serializer = JsonSerializer.Create(jsonSerializerSettings.Value); using (var stream = await content.ReadAsStreamAsync().ConfigureAwait(false)) using (var reader = new StreamReader(stream)) - using (var jsonTextReader = new JsonTextReader(reader)) - return serializer.Deserialize(jsonTextReader, objectType); - } - - public T Deserialize(string content) - { - var serializer = JsonSerializer.Create(jsonSerializerSettings.Value); - - using (var reader = new StringReader(content)) using (var jsonTextReader = new JsonTextReader(reader)) return serializer.Deserialize(jsonTextReader); } diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index 4521fe415..7f060e415 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -43,11 +43,9 @@ public JsonSerializerSettings JsonSerializerSettings public interface IContentSerializer { - HttpContent Serialize(object item); + Task SerializeAsync(object item); - Task DeserializeAsync(HttpContent content, Type objectType); - - T Deserialize(string content); + Task DeserializeAsync(HttpContent content); } public interface IUrlParameterFormatter diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index 9ea943224..b0d608105 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -141,21 +141,23 @@ public Func BuildRestResultFuncForMethod(string me // if you need to AOT everything, so we need to reflectively // invoke buildTaskFuncForMethod. var taskFuncMi = GetType().GetMethod(nameof(BuildTaskFuncForMethod), BindingFlags.NonPublic | BindingFlags.Instance); - var taskFunc = (MulticastDelegate)taskFuncMi.MakeGenericMethod(restMethod.SerializedReturnType) - .Invoke(this, new[] { restMethod }); + var taskFunc = (MulticastDelegate)(restMethod.IsApiResponse ? + taskFuncMi.MakeGenericMethod(restMethod.SerializedReturnType, restMethod.SerializedGenericArgument) : + taskFuncMi.MakeGenericMethod(restMethod.SerializedReturnType, restMethod.SerializedReturnType)).Invoke(this, new[] { restMethod }); return (client, args) => taskFunc.DynamicInvoke(client, args); } // Same deal var rxFuncMi = GetType().GetMethod(nameof(BuildRxFuncForMethod), BindingFlags.NonPublic | BindingFlags.Instance); - var rxFunc = (MulticastDelegate)rxFuncMi.MakeGenericMethod(restMethod.SerializedReturnType) - .Invoke(this, new[] { restMethod }); + var rxFunc = (MulticastDelegate)(restMethod.IsApiResponse ? + rxFuncMi.MakeGenericMethod(restMethod.SerializedReturnType, restMethod.SerializedGenericArgument) : + rxFuncMi.MakeGenericMethod(restMethod.SerializedReturnType, restMethod.SerializedReturnType)).Invoke(this, new[] { restMethod }); - return (client, args) => rxFunc.DynamicInvoke(client, args); + return (client, args) => rxFunc.DynamicInvoke(client, args); } - void AddMultipartItem(MultipartFormDataContent multiPartContent, string fileName, string parameterName, object itemValue) + async Task AddMultipartItemAsync(MultipartFormDataContent multiPartContent, string fileName, string parameterName, object itemValue) { if (itemValue is HttpContent content) @@ -196,11 +198,11 @@ void AddMultipartItem(MultipartFormDataContent multiPartContent, string fileName return; } - // Fallback to Json + // Fallback to serializer Exception e = null; try { - multiPartContent.Add(settings.ContentSerializer.Serialize(itemValue), parameterName); + multiPartContent.Add(await settings.ContentSerializer.SerializeAsync(itemValue).ConfigureAwait(false), parameterName); return; } catch(Exception ex) @@ -212,7 +214,7 @@ void AddMultipartItem(MultipartFormDataContent multiPartContent, string fileName throw new ArgumentException($"Unexpected parameter type in a Multipart request. Parameter {fileName} is of type {itemValue.GetType().Name}, whereas allowed types are String, Stream, FileInfo, Byte array and anything that's JSON serializable", nameof(itemValue), e); } - Func> BuildCancellableTaskFuncForMethod(RestMethodInfo restMethod) + Func> BuildCancellableTaskFuncForMethod(RestMethodInfo restMethod) { return async (client, ct, paramList) => { @@ -220,7 +222,7 @@ Func> BuildCancellableTaskFuncF throw new InvalidOperationException("BaseAddress must be set on the HttpClient instance"); var factory = BuildRequestFactoryForMethod(restMethod, client.BaseAddress.AbsolutePath, restMethod.CancellationToken != null); - var rq = factory(paramList); + var rq = await factory(paramList); HttpResponseMessage resp = null; HttpContent content = null; var disposeResponse = true; @@ -228,7 +230,7 @@ Func> BuildCancellableTaskFuncF { resp = await client.SendAsync(rq, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false); content = resp.Content ?? new StringContent(string.Empty); - + if (restMethod.SerializedReturnType == typeof(HttpResponseMessage)) { disposeResponse = false; // caller has to dispose @@ -239,16 +241,13 @@ Func> BuildCancellableTaskFuncF return (T)(object)resp; } - var isApiResponse = restMethod.SerializedReturnType.GetTypeInfo().IsGenericType && - restMethod.SerializedReturnType.GetGenericTypeDefinition() == typeof(ApiResponse<>); - if (!resp.IsSuccessStatusCode) { disposeResponse = false; - + var exception = await ApiException.Create(rq, restMethod.HttpMethod, resp, restMethod.RefitSettings).ConfigureAwait(false); - if (isApiResponse) + if (restMethod.IsApiResponse) { return ApiResponse.Create(resp, default(T), exception); } @@ -256,12 +255,12 @@ Func> BuildCancellableTaskFuncF throw exception; } - var serializedReturnType = (isApiResponse) ? restMethod.SerializedGenericArgument : restMethod.SerializedReturnType; + var serializedReturnType = restMethod.IsApiResponse ? restMethod.SerializedGenericArgument : restMethod.SerializedReturnType; if (serializedReturnType == typeof(HttpContent)) { disposeResponse = false; // caller has to clean up the content - if (isApiResponse) + if (restMethod.IsApiResponse) return ApiResponse.Create(resp, content); return (T)(object)content; } @@ -270,31 +269,33 @@ Func> BuildCancellableTaskFuncF { disposeResponse = false; // caller has to dispose var stream = (object)await content.ReadAsStreamAsync().ConfigureAwait(false); - if (isApiResponse) + if (restMethod.IsApiResponse) return ApiResponse.Create(resp, stream); return (T)stream; } if (serializedReturnType == typeof(string)) { - using(var stream = await content.ReadAsStreamAsync().ConfigureAwait(false)) - using(var reader = new StreamReader(stream)) + using (var stream = await content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var reader = new StreamReader(stream)) { - if (serializedReturnType == typeof(string)) - { - var str = (object)await reader.ReadToEndAsync().ConfigureAwait(false); - if (isApiResponse) - return ApiResponse.Create(resp, str); - return (T)str; - } - + var str = (object)await reader.ReadToEndAsync().ConfigureAwait(false); + if (restMethod.IsApiResponse) + return ApiResponse.Create(resp, str); + return (T)str; } } - var json = await serializer.DeserializeAsync(content, serializedReturnType); - if (isApiResponse) - return ApiResponse.Create(resp, json); - return (T)json; + var body = await serializer.DeserializeAsync(content); + if (restMethod.IsApiResponse) + { + return ApiResponse.Create(resp, body); + } + + // Unfortunate side-effect of having no 'class' or 'T : TBody' constraints. + // However, we know that T must be the same as TBody because IsApiResponse != true so + // this code is safe at runtime. + return (T)(object)body; } finally { @@ -302,7 +303,7 @@ Func> BuildCancellableTaskFuncF // Especially important if it has open files/streams rq.Dispose(); if (disposeResponse) - { + { resp?.Dispose(); content?.Dispose(); } @@ -398,10 +399,10 @@ List> BuildQueryMap(IDictionary dictionary, string return kvps; } - Func BuildRequestFactoryForMethod(RestMethodInfo restMethod, string basePath, bool paramsContainsCancellationToken) + Func> BuildRequestFactoryForMethod(RestMethodInfo restMethod, string basePath, bool paramsContainsCancellationToken) { - return paramList => + return async paramList => { // make sure we strip out any cancelation tokens if (paramsContainsCancellationToken) @@ -469,7 +470,7 @@ Func BuildRequestFactoryForMethod(RestMethodInfo r case BodySerializationMethod.Json: #pragma warning restore CS0618 // Type or member is obsolete case BodySerializationMethod.Serialized: - var content = serializer.Serialize(paramList[i]); + var content = await serializer.SerializeAsync(paramList[i]).ConfigureAwait(false); switch (restMethod.BodyParameterInfo.Item2) { case false: @@ -574,12 +575,12 @@ Func BuildRequestFactoryForMethod(RestMethodInfo r { foreach (var item in enumerable) { - AddMultipartItem(multiPartContent, itemName, parameterName, item); + await AddMultipartItemAsync(multiPartContent, itemName, parameterName, item); } } else { - AddMultipartItem(multiPartContent, itemName, parameterName, itemValue); + await AddMultipartItemAsync(multiPartContent, itemName, parameterName, itemValue); } } @@ -625,9 +626,9 @@ Func BuildRequestFactoryForMethod(RestMethodInfo r }; } - Func> BuildRxFuncForMethod(RestMethodInfo restMethod) + Func> BuildRxFuncForMethod(RestMethodInfo restMethod) { - var taskFunc = BuildCancellableTaskFuncForMethod(restMethod); + var taskFunc = BuildCancellableTaskFuncForMethod(restMethod); return (client, paramList) => { @@ -647,9 +648,9 @@ Func> BuildRxFuncForMethod(RestMethodInf }; } - Func> BuildTaskFuncForMethod(RestMethodInfo restMethod) + Func> BuildTaskFuncForMethod(RestMethodInfo restMethod) { - var ret = BuildCancellableTaskFuncForMethod(restMethod); + var ret = BuildCancellableTaskFuncForMethod(restMethod); return (client, paramList) => { @@ -670,7 +671,7 @@ Func BuildVoidTaskFuncForMethod(RestMethodInfo restM throw new InvalidOperationException("BaseAddress must be set on the HttpClient instance"); var factory = BuildRequestFactoryForMethod(restMethod, client.BaseAddress.AbsolutePath, restMethod.CancellationToken != null); - var rq = factory(paramList); + var rq = await factory(paramList); var ct = CancellationToken.None; diff --git a/Refit/RestMethodInfo.cs b/Refit/RestMethodInfo.cs index e21fbf550..ee7a988dc 100644 --- a/Refit/RestMethodInfo.cs +++ b/Refit/RestMethodInfo.cs @@ -31,6 +31,7 @@ public class RestMethodInfo public Type SerializedReturnType { get; set; } public RefitSettings RefitSettings { get; set; } public Type SerializedGenericArgument { get; set; } + public bool IsApiResponse { get; } static readonly Regex parameterRegex = new Regex(@"{(.*?)}"); static readonly HttpMethod patchMethod = new HttpMethod("PATCH"); @@ -98,6 +99,9 @@ public RestMethodInfo(Type targetInterface, MethodInfo methodInfo, RefitSettings } CancellationToken = ctParams.FirstOrDefault(); + + IsApiResponse = SerializedReturnType.GetTypeInfo().IsGenericType && + SerializedReturnType.GetGenericTypeDefinition() == typeof(ApiResponse<>); } void VerifyUrlPathIsSane(string relativePath) diff --git a/Refit/ValidationApiException.cs b/Refit/ValidationApiException.cs index f1c81dcc4..bdd96b04a 100644 --- a/Refit/ValidationApiException.cs +++ b/Refit/ValidationApiException.cs @@ -27,7 +27,6 @@ public static ValidationApiException Create(ApiException exception) /// /// The problem details of the RFC 7807 validation exception. /// - public new ProblemDetails Content => GetContentAs(); - + public new ProblemDetails Content => GetContentAsAsync().ConfigureAwait(false).GetAwaiter().GetResult(); } } diff --git a/Refit/XmlContentSerializer.cs b/Refit/XmlContentSerializer.cs index ecf92b148..e99083ad6 100644 --- a/Refit/XmlContentSerializer.cs +++ b/Refit/XmlContentSerializer.cs @@ -21,7 +21,7 @@ public XmlContentSerializer(XmlContentSerializerSettings settings) this.settings = settings ?? throw new ArgumentNullException(nameof(settings)); } - public HttpContent Serialize(object item) + public Task SerializeAsync(object item) { var xmlSerializer = new XmlSerializer(item.GetType(), settings.XmlAttributeOverrides); @@ -30,35 +30,19 @@ public HttpContent Serialize(object item) using(var writer = XmlWriter.Create(output, settings.XmlReaderWriterSettings.WriterSettings)) { xmlSerializer.Serialize(writer, item, settings.XmlNamespaces); - return new StringContent(output.ToString(), Encoding.UTF8, "application/xml"); + var content = new StringContent(output.ToString(), Encoding.UTF8, "application/xml"); + return Task.FromResult((HttpContent)content); } } } - public async Task DeserializeAsync(HttpContent content, Type objectType) - { - var xmlSerializer = new XmlSerializer(objectType, settings.XmlAttributeOverrides); - - using (var input = new StringReader(await content.ReadAsStringAsync().ConfigureAwait(false))) - { - using (var reader = XmlReader.Create(input, settings.XmlReaderWriterSettings.ReaderSettings)) - { - return xmlSerializer.Deserialize(reader); - } - } - } - - public T Deserialize(string content) + public async Task DeserializeAsync(HttpContent content) { var xmlSerializer = new XmlSerializer(typeof(T), settings.XmlAttributeOverrides); - using (var input = new StringReader(content)) - { - using (var reader = XmlReader.Create(input, settings.XmlReaderWriterSettings.ReaderSettings)) - { - return (T)(object)xmlSerializer.Deserialize(reader); - } - } + using (var input = new StringReader(await content.ReadAsStringAsync().ConfigureAwait(false))) + using (var reader = XmlReader.Create(input, settings.XmlReaderWriterSettings.ReaderSettings)) + return (T)xmlSerializer.Deserialize(reader); } } From 6d3a5f474267aadbee05ce226e9b85150d5401a4 Mon Sep 17 00:00:00 2001 From: Steve Ward Date: Mon, 26 Nov 2018 22:01:42 +0000 Subject: [PATCH 4/6] Added missing ConfigureAwait(false) & fixed deadlock on ValidationApiException.Content --- Refit.Tests/MultipartTests.cs | 18 +++++++++--------- Refit/ApiException.cs | 8 +++++--- Refit/RequestBuilderImplementation.cs | 8 ++++---- Refit/ValidationApiException.cs | 12 +++++++++--- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/Refit.Tests/MultipartTests.cs b/Refit.Tests/MultipartTests.cs index b3364dc4d..5e865d22b 100644 --- a/Refit.Tests/MultipartTests.cs +++ b/Refit.Tests/MultipartTests.cs @@ -429,7 +429,7 @@ public async Task MultipartUploadShouldWorkWithAnObject(Type contentSerializerTy Assert.Equal("theObject", parts[0].Headers.ContentDisposition.Name); Assert.Null(parts[0].Headers.ContentDisposition.FileName); Assert.Equal(mediaType, parts[0].Headers.ContentType.MediaType); - var result0 = await serializer.DeserializeAsync(parts[0]); + var result0 = await serializer.DeserializeAsync(parts[0]).ConfigureAwait(false); Assert.Equal(model1.Property1, result0.Property1); Assert.Equal(model1.Property2, result0.Property2); } @@ -477,7 +477,7 @@ public async Task MultipartUploadShouldWorkWithObjects(Type contentSerializerTyp Assert.Equal("theObjects", parts[0].Headers.ContentDisposition.Name); Assert.Null(parts[0].Headers.ContentDisposition.FileName); Assert.Equal(mediaType, parts[0].Headers.ContentType.MediaType); - var result0 = await serializer.DeserializeAsync( parts[0]); + var result0 = await serializer.DeserializeAsync( parts[0]).ConfigureAwait(false); Assert.Equal(model1.Property1, result0.Property1); Assert.Equal(model1.Property2, result0.Property2); @@ -485,7 +485,7 @@ public async Task MultipartUploadShouldWorkWithObjects(Type contentSerializerTyp Assert.Equal("theObjects", parts[1].Headers.ContentDisposition.Name); Assert.Null(parts[1].Headers.ContentDisposition.FileName); Assert.Equal(mediaType, parts[1].Headers.ContentType.MediaType); - var result1 = await serializer.DeserializeAsync(parts[1]); + var result1 = await serializer.DeserializeAsync(parts[1]).ConfigureAwait(false); Assert.Equal(model2.Property1, result1.Property1); Assert.Equal(model2.Property2, result1.Property2); } @@ -534,7 +534,7 @@ public async Task MultipartUploadShouldWorkWithMixedTypes() Assert.Equal("theObjects", parts[0].Headers.ContentDisposition.Name); Assert.Null(parts[0].Headers.ContentDisposition.FileName); Assert.Equal("application/json", parts[0].Headers.ContentType.MediaType); - var result0 = JsonConvert.DeserializeObject(await parts[0].ReadAsStringAsync()); + var result0 = JsonConvert.DeserializeObject(await parts[0].ReadAsStringAsync().ConfigureAwait(false)); Assert.Equal(model1.Property1, result0.Property1); Assert.Equal(model1.Property2, result0.Property2); @@ -542,14 +542,14 @@ public async Task MultipartUploadShouldWorkWithMixedTypes() Assert.Equal("theObjects", parts[1].Headers.ContentDisposition.Name); Assert.Null(parts[1].Headers.ContentDisposition.FileName); Assert.Equal("application/json", parts[1].Headers.ContentType.MediaType); - var result1 = JsonConvert.DeserializeObject(await parts[1].ReadAsStringAsync()); + var result1 = JsonConvert.DeserializeObject(await parts[1].ReadAsStringAsync().ConfigureAwait(false)); Assert.Equal(model2.Property1, result1.Property1); Assert.Equal(model2.Property2, result1.Property2); Assert.Equal("anotherModel", parts[2].Headers.ContentDisposition.Name); Assert.Null(parts[2].Headers.ContentDisposition.FileName); Assert.Equal("application/json", parts[2].Headers.ContentType.MediaType); - var result2 = JsonConvert.DeserializeObject(await parts[2].ReadAsStringAsync()); + var result2 = JsonConvert.DeserializeObject(await parts[2].ReadAsStringAsync().ConfigureAwait(false)); Assert.Equal(2, result2.Foos.Length); Assert.Equal("bar1", result2.Foos[0]); Assert.Equal("bar2", result2.Foos[1]); @@ -567,7 +567,7 @@ public async Task MultipartUploadShouldWorkWithMixedTypes() Assert.Equal("anEnum", parts[4].Headers.ContentDisposition.Name); Assert.Null(parts[4].Headers.ContentDisposition.FileName); Assert.Equal("application/json", parts[4].Headers.ContentType.MediaType); - var result4 = JsonConvert.DeserializeObject(await parts[4].ReadAsStringAsync()); + var result4 = JsonConvert.DeserializeObject(await parts[4].ReadAsStringAsync().ConfigureAwait(false)); Assert.Equal(AnEnum.Val2, result4); Assert.Equal("aString", parts[5].Headers.ContentDisposition.Name); @@ -579,7 +579,7 @@ public async Task MultipartUploadShouldWorkWithMixedTypes() Assert.Equal("anInt", parts[6].Headers.ContentDisposition.Name); Assert.Null(parts[6].Headers.ContentDisposition.FileName); Assert.Equal("application/json", parts[6].Headers.ContentType.MediaType); - var result6 = JsonConvert.DeserializeObject(await parts[6].ReadAsStringAsync()); + var result6 = JsonConvert.DeserializeObject(await parts[6].ReadAsStringAsync().ConfigureAwait(false)); Assert.Equal(42, result6); } @@ -630,7 +630,7 @@ public async Task MultipartUploadShouldWorkWithHttpContent() Assert.Equal("myName", parts[0].Headers.ContentDisposition.Name); Assert.Equal("myFileName", parts[0].Headers.ContentDisposition.FileName); Assert.Equal("application/custom", parts[0].Headers.ContentType.MediaType); - var result0 = await parts[0].ReadAsStringAsync(); + var result0 = await parts[0].ReadAsStringAsync().ConfigureAwait(false); Assert.Equal("some text", result0); } }; diff --git a/Refit/ApiException.cs b/Refit/ApiException.cs index e02be4ecd..cfb759052 100644 --- a/Refit/ApiException.cs +++ b/Refit/ApiException.cs @@ -54,12 +54,14 @@ public static async Task Create(HttpRequestMessage message, HttpMe try { + exception.ContentHeaders = response.Content.Headers; + exception.Content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (response.Content.Headers.ContentType.MediaType.Equals("application/problem+json")) { - exception = ValidationApiException.Create(exception); + exception = await ValidationApiException.Create(exception).ConfigureAwait(false); } - exception.ContentHeaders = response.Content.Headers; - exception.Content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + response.Content.Dispose(); } catch diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index b0d608105..7f6b301d9 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -222,7 +222,7 @@ Func> BuildCancellableTaskFuncF throw new InvalidOperationException("BaseAddress must be set on the HttpClient instance"); var factory = BuildRequestFactoryForMethod(restMethod, client.BaseAddress.AbsolutePath, restMethod.CancellationToken != null); - var rq = await factory(paramList); + var rq = await factory(paramList).ConfigureAwait(false); HttpResponseMessage resp = null; HttpContent content = null; var disposeResponse = true; @@ -575,12 +575,12 @@ Func> BuildRequestFactoryForMethod(RestMethod { foreach (var item in enumerable) { - await AddMultipartItemAsync(multiPartContent, itemName, parameterName, item); + await AddMultipartItemAsync(multiPartContent, itemName, parameterName, item).ConfigureAwait(false); } } else { - await AddMultipartItemAsync(multiPartContent, itemName, parameterName, itemValue); + await AddMultipartItemAsync(multiPartContent, itemName, parameterName, itemValue).ConfigureAwait(false); } } @@ -671,7 +671,7 @@ Func BuildVoidTaskFuncForMethod(RestMethodInfo restM throw new InvalidOperationException("BaseAddress must be set on the HttpClient instance"); var factory = BuildRequestFactoryForMethod(restMethod, client.BaseAddress.AbsolutePath, restMethod.CancellationToken != null); - var rq = await factory(paramList); + var rq = await factory(paramList).ConfigureAwait(false); var ct = CancellationToken.None; diff --git a/Refit/ValidationApiException.cs b/Refit/ValidationApiException.cs index bdd96b04a..7a7d6005f 100644 --- a/Refit/ValidationApiException.cs +++ b/Refit/ValidationApiException.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace Refit { @@ -14,19 +15,24 @@ public class ValidationApiException : ApiException { } +#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods /// /// Creates a new instance of a ValidationException from an existing ApiException. /// /// An instance of an ApiException to use to build a ValidationException. /// ValidationApiException - public static ValidationApiException Create(ApiException exception) + public static async Task Create(ApiException exception) +#pragma warning restore VSTHRD200 { - return new ValidationApiException(exception); + return new ValidationApiException(exception) + { + Content = await exception.GetContentAsAsync() + }; } /// /// The problem details of the RFC 7807 validation exception. /// - public new ProblemDetails Content => GetContentAsAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + public new ProblemDetails Content { get; private set; } } } From babaed0dd77d226dd404f6902db2a0d5734baf3e Mon Sep 17 00:00:00 2001 From: Steve Ward Date: Tue, 27 Nov 2018 08:31:37 +0000 Subject: [PATCH 5/6] Added missing ConfigureAwait(false) --- Refit/ValidationApiException.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Refit/ValidationApiException.cs b/Refit/ValidationApiException.cs index 7a7d6005f..d74c35ba4 100644 --- a/Refit/ValidationApiException.cs +++ b/Refit/ValidationApiException.cs @@ -26,7 +26,7 @@ public static async Task Create(ApiException exception) { return new ValidationApiException(exception) { - Content = await exception.GetContentAsAsync() + Content = await exception.GetContentAsAsync().ConfigureAwait(false) }; } From cc05c3a9feea80200309f40beed9cbe4b304883f Mon Sep 17 00:00:00 2001 From: Steve Ward Date: Thu, 29 Nov 2018 08:44:09 +0000 Subject: [PATCH 6/6] Made IContentSerializer.SerializeAsync generic --- Refit/JsonContentSerializer.cs | 2 +- Refit/RefitSettings.cs | 2 +- Refit/XmlContentSerializer.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Refit/JsonContentSerializer.cs b/Refit/JsonContentSerializer.cs index 2ffa1aadb..7240a085b 100644 --- a/Refit/JsonContentSerializer.cs +++ b/Refit/JsonContentSerializer.cs @@ -29,7 +29,7 @@ public JsonContentSerializer(JsonSerializerSettings jsonSerializerSettings) }); } - public Task SerializeAsync(object item) + public Task SerializeAsync(T item) { var content = new StringContent(JsonConvert.SerializeObject(item, jsonSerializerSettings.Value), Encoding.UTF8, "application/json"); return Task.FromResult((HttpContent)content); diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index 7f060e415..8df29817e 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -43,7 +43,7 @@ public JsonSerializerSettings JsonSerializerSettings public interface IContentSerializer { - Task SerializeAsync(object item); + Task SerializeAsync(T item); Task DeserializeAsync(HttpContent content); } diff --git a/Refit/XmlContentSerializer.cs b/Refit/XmlContentSerializer.cs index e99083ad6..c82350900 100644 --- a/Refit/XmlContentSerializer.cs +++ b/Refit/XmlContentSerializer.cs @@ -21,7 +21,7 @@ public XmlContentSerializer(XmlContentSerializerSettings settings) this.settings = settings ?? throw new ArgumentNullException(nameof(settings)); } - public Task SerializeAsync(object item) + public Task SerializeAsync(T item) { var xmlSerializer = new XmlSerializer(item.GetType(), settings.XmlAttributeOverrides);