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
This commit is contained in:
Steve Ward 2018-10-16 15:43:48 +01:00 коммит произвёл stevewgh
Родитель dc69773a31
Коммит 27d6fb4865
9 изменённых файлов: 396 добавлений и 51 удалений

Просмотреть файл

@ -402,9 +402,16 @@ namespace Refit.Tests
}
}
[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 @@ namespace Refit.Tests
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<ModelObject>(await parts[0].ReadAsStringAsync());
Assert.Equal(mediaType, parts[0].Headers.ContentType.MediaType);
var result0 = serializer.Deserialize<ModelObject>(await parts[0].ReadAsStringAsync());
Assert.Equal(model1.Property1, result0.Property1);
Assert.Equal(model1.Property2, result0.Property2);
}
@ -430,16 +437,24 @@ namespace Refit.Tests
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler
HttpMessageHandlerFactory = () => handler,
ContentSerializer = serializer
};
var fixture = RestService.For<IRunscopeApi>(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 @@ namespace Refit.Tests
Property1 = "M2.prop1"
};
var handler = new MockHttpMessageHandler
{
Asserts = async content =>
@ -462,16 +476,16 @@ namespace Refit.Tests
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<ModelObject>(await parts[0].ReadAsStringAsync());
Assert.Equal(mediaType, parts[0].Headers.ContentType.MediaType);
var result0 = serializer.Deserialize<ModelObject>(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<ModelObject>(await parts[1].ReadAsStringAsync());
Assert.Equal(mediaType, parts[1].Headers.ContentType.MediaType);
var result1 = serializer.Deserialize<ModelObject>(await parts[1].ReadAsStringAsync());
Assert.Equal(model2.Property1, result1.Property1);
Assert.Equal(model2.Property2, result1.Property2);
}
@ -479,7 +493,8 @@ namespace Refit.Tests
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler
HttpMessageHandlerFactory = () => handler,
ContentSerializer = serializer
};
var fixture = RestService.For<IRunscopeApi>(BaseAddress, settings);

Просмотреть файл

@ -11,6 +11,7 @@ using Xunit;
using Refit; // InterfaceStubGenerator looks for this
using RichardSzalay.MockHttp;
using System.IO;
using System.Text;
namespace Refit.Tests
{
@ -1107,5 +1108,33 @@ namespace Refit.Tests
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("<User><Name>Created</Name></User>", Encoding.UTF8, "application/xml")
});
var fixture = RestService.For<IGitHubApi>("https://api.github.com", settings);
var result = await fixture.CreateUser(new User()).ConfigureAwait(false);
Assert.Equal("Created", result.Name);
mockHttp.VerifyNoOutstandingExpectation();
}
}
}

Просмотреть файл

@ -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<Dto>("<Dto><Identifier>123</Identifier></Dto>");
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;
}
}
}

Просмотреть файл

@ -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 @@ namespace Refit
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 @@ namespace Refit
}
public T GetContentAs<T>() => HasContent ?
JsonConvert.DeserializeObject<T>(Content, RefitSettings.JsonSerializerSettings) :
RefitSettings.ContentSerializer.Deserialize<T>(Content) :
default;
#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods

Просмотреть файл

@ -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<object> 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<T>(string content)
{
var serializer = JsonSerializer.Create(jsonSerializerSettings);
using (var reader = new StringReader(content))
using (var jsonTextReader = new JsonTextReader(reader))
return serializer.Deserialize<T>(jsonTextReader);
}
}
}

Просмотреть файл

@ -17,6 +17,7 @@
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard1.4' ">
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="1.1.2" />
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.5.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0 " />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net461'">

Просмотреть файл

@ -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<Task<string>> AuthorizationHeaderValueGetter { get; set; }
public Func<HttpMessageHandler> 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<object> DeserializeAsync(HttpContent content, Type objectType);
T Deserialize<T>(string content);
}
public interface IUrlParameterFormatter
{
string Format(object value, ParameterInfo parameterInfo);

Просмотреть файл

@ -10,7 +10,6 @@ using System.Text;
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 @@ namespace Refit
};
readonly Dictionary<string, List<RestMethodInfo>> interfaceHttpMethods;
readonly ConcurrentDictionary<CloseGenericMethodKey, RestMethodInfo> interfaceGenericHttpMethods;
readonly JsonSerializer serializer;
readonly IContentSerializer serializer;
readonly RefitSettings settings;
public Type TargetType { get; }
@ -34,7 +33,7 @@ namespace Refit
Type targetInterface = typeof(TApi);
settings = refitSettings ?? new RefitSettings();
serializer = JsonSerializer.Create(settings.JsonSerializerSettings);
serializer = settings.ContentSerializer;
interfaceGenericHttpMethods = new ConcurrentDictionary<CloseGenericMethodKey, RestMethodInfo>();
if (targetInterface == null || !targetInterface.GetTypeInfo().IsInterface)
@ -201,8 +200,7 @@ namespace Refit
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 @@ namespace Refit
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<T>(resp, str);
return (T)str;
}
if (serializedReturnType == typeof(string))
{
var str = (object)await reader.ReadToEndAsync().ConfigureAwait(false);
if (isApiResponse)
return ApiResponse.Create<T>(resp, str);
return (T)str;
}
using (var jsonReader = new JsonTextReader(reader))
{
var json = serializer.Deserialize(jsonReader, serializedReturnType);
if (isApiResponse)
return ApiResponse.Create<T>(resp, json);
return (T)json;
}
}
var json = await serializer.DeserializeAsync(content, serializedReturnType);
if (isApiResponse)
return ApiResponse.Create<T>(resp, json);
return (T)json;
}
finally
{
@ -467,24 +466,15 @@ namespace Refit
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;
}

Просмотреть файл

@ -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<object> 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<T>(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));
}
/// <summary>
/// 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.
/// </summary>
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; }
}
}