Improve CanRead/CanWriteResult method.

Add LZ4MessagePackInput/Output formatter.
Add AspNetCoreMvcFormatter tests.
This commit is contained in:
spacekey 2019-01-09 22:35:29 +09:00
Родитель 3765ddf7cf
Коммит 9559449a8e
8 изменённых файлов: 552 добавлений и 95 удалений

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

@ -55,6 +55,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestData2", "sandbox\TestDa
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessagePack.UniversalCodeGenerator", "src\MessagePack.UniversalCodeGenerator\MessagePack.UniversalCodeGenerator.csproj", "{10AD85DD-929D-49B8-BD43-45242C2644B7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessagePack.AspNetCoreMvcFormatter.Tests", "tests\MessagePack.AspNetCoreMvcFormatter.Tests\MessagePack.AspNetCoreMvcFormatter.Tests.csproj", "{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -245,6 +247,18 @@ Global
{10AD85DD-929D-49B8-BD43-45242C2644B7}.Release|x64.Build.0 = Release|Any CPU
{10AD85DD-929D-49B8-BD43-45242C2644B7}.Release|x86.ActiveCfg = Release|Any CPU
{10AD85DD-929D-49B8-BD43-45242C2644B7}.Release|x86.Build.0 = Release|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Debug|Any CPU.Build.0 = Debug|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Debug|x64.ActiveCfg = Debug|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Debug|x64.Build.0 = Debug|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Debug|x86.ActiveCfg = Debug|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Debug|x86.Build.0 = Debug|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Release|Any CPU.ActiveCfg = Release|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Release|Any CPU.Build.0 = Release|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Release|x64.ActiveCfg = Release|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Release|x64.Build.0 = Release|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Release|x86.ActiveCfg = Release|Any CPU
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -265,6 +279,7 @@ Global
{814F94D6-1413-4ACB-B1B5-A3488CAA1E6B} = {BF4C4202-5015-4FBD-80E6-D0F36A06F700}
{2A32A538-BA26-4D89-85D0-E4249AFA0837} = {BF4C4202-5015-4FBD-80E6-D0F36A06F700}
{10AD85DD-929D-49B8-BD43-45242C2644B7} = {86309CF6-0054-4CE3-BFD3-CA0AA7DB17BC}
{79C2B2CB-872A-4BA9-82DC-60F6DD77F940} = {19FE674A-AC94-4E7E-B24C-2285D1D04CDE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {26F0752B-06F7-44AD-BFEE-8F2E36B3AA27}

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

@ -1,95 +0,0 @@
using Microsoft.AspNetCore.Mvc.Formatters;
using System.Threading.Tasks;
namespace MessagePack.AspNetCoreMvcFormatter
{
public class MessagePackOutputFormatter : IOutputFormatter //, IApiResponseTypeMetadataProvider
{
const string ContentType = "application/x-msgpack";
static readonly string[] SupportedContentTypes = new[] { ContentType };
readonly IFormatterResolver resolver;
public MessagePackOutputFormatter()
: this(null)
{
}
public MessagePackOutputFormatter(IFormatterResolver resolver)
{
this.resolver = resolver ?? MessagePackSerializer.DefaultResolver;
}
//public IReadOnlyList<string> GetSupportedContentTypes(string contentType, Type objectType)
//{
// return SupportedContentTypes;
//}
public bool CanWriteResult(OutputFormatterCanWriteContext context)
{
return true;
}
public Task WriteAsync(OutputFormatterWriteContext context)
{
context.HttpContext.Response.ContentType = ContentType;
// 'object' want to use anonymous type serialize, etc...
if (context.ObjectType == typeof(object))
{
if (context.Object == null)
{
context.HttpContext.Response.Body.WriteByte(MessagePackCode.Nil);
return Task.CompletedTask;
}
else
{
// use concrete type.
MessagePackSerializer.NonGeneric.Serialize(context.Object.GetType(), context.HttpContext.Response.Body, context.Object, resolver);
return Task.CompletedTask;
}
}
else
{
MessagePackSerializer.NonGeneric.Serialize(context.ObjectType, context.HttpContext.Response.Body, context.Object, resolver);
return Task.CompletedTask;
}
}
}
public class MessagePackInputFormatter : IInputFormatter // , IApiRequestFormatMetadataProvider
{
const string ContentType = "application/x-msgpack";
static readonly string[] SupportedContentTypes = new[] { ContentType };
readonly IFormatterResolver resolver;
public MessagePackInputFormatter()
: this(null)
{
}
public MessagePackInputFormatter(IFormatterResolver resolver)
{
this.resolver = resolver ?? MessagePackSerializer.DefaultResolver;
}
//public IReadOnlyList<string> GetSupportedContentTypes(string contentType, Type objectType)
//{
// return SupportedContentTypes;
//}
public bool CanRead(InputFormatterContext context)
{
return true;
}
public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
var request = context.HttpContext.Request;
var result = MessagePackSerializer.NonGeneric.Deserialize(context.ModelType, request.Body, resolver);
return InputFormatterResult.SuccessAsync(result);
}
}
}

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

@ -0,0 +1,30 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace MessagePack.AspNetCoreMvcFormatter
{
public class LZ4MessagePackInputFormatter : IInputFormatter
{
private const string ContentType = "application/x-msgpack";
private readonly IFormatterResolver resolver;
public LZ4MessagePackInputFormatter() : this(null)
{
}
public LZ4MessagePackInputFormatter(IFormatterResolver resolver)
{
this.resolver = resolver ?? MessagePackSerializer.DefaultResolver;
}
public bool CanRead(InputFormatterContext context) =>
context.HttpContext.Request.ContentType == ContentType;
public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
var request = context.HttpContext.Request;
var result = LZ4MessagePackSerializer.NonGeneric.Deserialize(context.ModelType, request.Body, resolver);
return InputFormatterResult.SuccessAsync(result);
}
}
}

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

@ -0,0 +1,47 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace MessagePack.AspNetCoreMvcFormatter
{
public class LZ4MessagePackOutputFormatter : IOutputFormatter
{
private const string ContentType = "application/x-msgpack";
private readonly IFormatterResolver resolver;
public LZ4MessagePackOutputFormatter() : this(null)
{
}
public LZ4MessagePackOutputFormatter(IFormatterResolver resolver)
{
this.resolver = resolver ?? MessagePackSerializer.DefaultResolver;
}
public bool CanWriteResult(OutputFormatterCanWriteContext context) =>
context.HttpContext.Request.ContentType == ContentType;
public Task WriteAsync(OutputFormatterWriteContext context)
{
context.HttpContext.Response.ContentType = ContentType;
if (context.ObjectType == typeof(object))
{
if (context.Object == null)
{
context.HttpContext.Response.Body.WriteByte(MessagePackCode.Nil);
return Task.CompletedTask;
}
else
{
LZ4MessagePackSerializer.NonGeneric.Serialize(context.Object.GetType(), context.HttpContext.Response.Body, context.Object, resolver);
return Task.CompletedTask;
}
}
else
{
LZ4MessagePackSerializer.NonGeneric.Serialize(context.ObjectType, context.HttpContext.Response.Body, context.Object, resolver);
return Task.CompletedTask;
}
}
}
}

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

@ -0,0 +1,30 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace MessagePack.AspNetCoreMvcFormatter
{
public class MessagePackInputFormatter : IInputFormatter
{
private const string ContentType = "application/x-msgpack";
private readonly IFormatterResolver resolver;
public MessagePackInputFormatter() : this(null)
{
}
public MessagePackInputFormatter(IFormatterResolver resolver)
{
this.resolver = resolver ?? MessagePackSerializer.DefaultResolver;
}
public bool CanRead(InputFormatterContext context) =>
context.HttpContext.Request.ContentType == ContentType;
public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
var request = context.HttpContext.Request;
var result = MessagePackSerializer.NonGeneric.Deserialize(context.ModelType, request.Body, resolver);
return InputFormatterResult.SuccessAsync(result);
}
}
}

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

@ -0,0 +1,47 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace MessagePack.AspNetCoreMvcFormatter
{
public class MessagePackOutputFormatter : IOutputFormatter
{
private const string ContentType = "application/x-msgpack";
private readonly IFormatterResolver resolver;
public MessagePackOutputFormatter() : this(null)
{
}
public MessagePackOutputFormatter(IFormatterResolver resolver)
{
this.resolver = resolver ?? MessagePackSerializer.DefaultResolver;
}
public bool CanWriteResult(OutputFormatterCanWriteContext context) =>
context.HttpContext.Request.ContentType == ContentType;
public Task WriteAsync(OutputFormatterWriteContext context)
{
context.HttpContext.Response.ContentType = ContentType;
if (context.ObjectType == typeof(object))
{
if (context.Object == null)
{
context.HttpContext.Response.Body.WriteByte(MessagePackCode.Nil);
return Task.CompletedTask;
}
else
{
MessagePackSerializer.NonGeneric.Serialize(context.Object.GetType(), context.HttpContext.Response.Body, context.Object, resolver);
return Task.CompletedTask;
}
}
else
{
MessagePackSerializer.NonGeneric.Serialize(context.ObjectType, context.HttpContext.Response.Body, context.Object, resolver);
return Task.CompletedTask;
}
}
}
}

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

@ -0,0 +1,360 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MessagePack.AspNetCoreMvcFormatter;
using MessagePack.Resolvers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Moq;
using Newtonsoft.Json;
using Xunit;
namespace MessagePack.Tests.ExtensionTests
{
public class AspNetCoreMvcFormatterTest
{
private const string MsgPackContentType = "application/x-msgpack";
[Fact]
public async Task MessagePackFormatter()
{
var person = new User
{
UserId = 1,
FullName = "John Denver",
Age = 35,
Description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
};
var messagePackBinary = MessagePackSerializer.Serialize(person);
// OutputFormatter
var outputFormatterContext = GetOutputFormatterContext(person, typeof(User), MsgPackContentType);
var outputFormatter = new MessagePackOutputFormatter(StandardResolver.Instance);
outputFormatter.CanWriteResult(outputFormatterContext).IsTrue();
await outputFormatter.WriteAsync(outputFormatterContext);
var body = outputFormatterContext.HttpContext.Response.Body;
Assert.NotNull(body);
body.Position = 0;
using (var ms = new MemoryStream())
{
body.CopyTo(ms);
Assert.Equal(messagePackBinary, ms.ToArray());
}
// InputFormatter
var httpContext = new DefaultHttpContext();
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
httpContext.Request.Body = new NonSeekableReadStream(messagePackBinary);
httpContext.Request.ContentType = MsgPackContentType;
var inputFormatterContext = CreateInputFormatterContext(typeof(User), httpContext);
var inputFormatter = new MessagePackInputFormatter();
inputFormatter.CanRead(inputFormatterContext).IsTrue();
var result = await inputFormatter.ReadAsync(inputFormatterContext);
Assert.False(result.HasError);
var userModel = Assert.IsType<User>(result.Model);
userModel.IsStructuralEqual(person);
}
[Fact]
public void MessagePackFormatterCanNotRead()
{
var person = new User();
// OutputFormatter
var outputFormatterContext = GetOutputFormatterContext(person, typeof(User), "application/json");
var outputFormatter = new MessagePackOutputFormatter(StandardResolver.Instance);
outputFormatter.CanWriteResult(outputFormatterContext).IsFalse();
// InputFormatter
var httpContext = new DefaultHttpContext();
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
httpContext.Request.Body = new NonSeekableReadStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(person)));
httpContext.Request.ContentType = "application/json";
var inputFormatterContext = CreateInputFormatterContext(typeof(User), httpContext);
var inputFormatter = new MessagePackInputFormatter();
inputFormatter.CanRead(inputFormatterContext).IsFalse();
}
[Fact]
public async Task LZ4MessagePackFormatter()
{
var person = new User
{
UserId = 1,
FullName = "John Denver",
Age = 35,
Description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
};
var messagePackBinary = MessagePackSerializer.Serialize(person);
var lz4MessagePackBinary = LZ4MessagePackSerializer.Serialize(person);
// OutputFormatter
var outputFormatterContext = GetOutputFormatterContext(person, typeof(User), MsgPackContentType);
var outputFormatter = new LZ4MessagePackOutputFormatter(StandardResolver.Instance);
await outputFormatter.WriteAsync(outputFormatterContext);
var body = outputFormatterContext.HttpContext.Response.Body;
Assert.NotNull(body);
body.Position = 0;
using (var ms = new MemoryStream())
{
body.CopyTo(ms);
var binary = ms.ToArray();
binary.IsNot(messagePackBinary);
binary.Is(lz4MessagePackBinary);
}
// InputFormatter
var httpContext = new DefaultHttpContext();
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
httpContext.Request.Body = new NonSeekableReadStream(messagePackBinary);
httpContext.Request.ContentType = MsgPackContentType;
var inputFormatterContext = CreateInputFormatterContext(typeof(User), httpContext);
var inputFormatter = new LZ4MessagePackInputFormatter();
var result = await inputFormatter.ReadAsync(inputFormatterContext);
Assert.False(result.HasError);
var userModel = Assert.IsType<User>(result.Model);
userModel.IsStructuralEqual(person);
}
[Fact]
public void LZ4MessagePackFormatterCanNotRead()
{
var person = new User();
// OutputFormatter
var outputFormatterContext = GetOutputFormatterContext(person, typeof(User), "application/json");
var outputFormatter = new LZ4MessagePackOutputFormatter(StandardResolver.Instance);
outputFormatter.CanWriteResult(outputFormatterContext).IsFalse();
// InputFormatter
var httpContext = new DefaultHttpContext();
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
httpContext.Request.Body = new NonSeekableReadStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(person)));
httpContext.Request.ContentType = "application/json";
var inputFormatterContext = CreateInputFormatterContext(typeof(User), httpContext);
var inputFormatter = new LZ4MessagePackInputFormatter();
inputFormatter.CanRead(inputFormatterContext).IsFalse();
}
/// <summary>
/// <see href="https://github.com/aspnet/Mvc/blob/master/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonOutputFormatterTests.cs#L453">JsonOutputFormatterTests.cs#L453</see>
/// </summary>
private static OutputFormatterWriteContext GetOutputFormatterContext(
object outputValue,
Type outputType,
string contentType = "application/xml; charset=utf-8",
MemoryStream responseStream = null)
{
var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType);
var actionContext = GetActionContext(mediaTypeHeaderValue, responseStream);
return new OutputFormatterWriteContext(
actionContext.HttpContext,
new TestHttpResponseStreamWriterFactory().CreateWriter,
outputType,
outputValue)
{
ContentType = new StringSegment(contentType),
};
}
/// <summary>
/// <see href="https://github.com/aspnet/Mvc/blob/master/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonOutputFormatterTests.cs#L472">JsonOutputFormatterTests.cs#L472</see>
/// </summary>
private static ActionContext GetActionContext(
MediaTypeHeaderValue contentType,
MemoryStream responseStream = null)
{
var request = new Mock<HttpRequest>();
var headers = new HeaderDictionary();
request.Setup(r => r.ContentType).Returns(contentType.ToString());
request.SetupGet(r => r.Headers).Returns(headers);
headers[HeaderNames.AcceptCharset] = contentType.Charset.ToString();
var response = new Mock<HttpResponse>();
response.SetupGet(f => f.Body).Returns(responseStream ?? new MemoryStream());
var httpContext = new Mock<HttpContext>();
httpContext.SetupGet(c => c.Request).Returns(request.Object);
httpContext.SetupGet(c => c.Response).Returns(response.Object);
return new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor());
}
/// <summary>
/// <see href="https://github.com/aspnet/Mvc/blob/master/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs#L717">JsonInputFormatterTest.cs#L717</see>
/// </summary>
private InputFormatterContext CreateInputFormatterContext(
Type modelType,
HttpContext httpContext,
string modelName = null,
bool treatEmptyInputAsDefaultValue = false)
{
var provider = new EmptyModelMetadataProvider();
var metadata = provider.GetMetadataForType(modelType);
return new InputFormatterContext(
httpContext,
modelName: modelName ?? string.Empty,
modelState: new ModelStateDictionary(),
metadata: metadata,
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader,
treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue);
}
/// <summary>
/// <see href="https://github.com/aspnet/Mvc/blob/master/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs#L791">JsonInputFormatterTest.cs#L791</see>
/// </summary>
private class TestResponseFeature : HttpResponseFeature
{
public override void OnCompleted(Func<object, Task> callback, object state)
{
// do not do anything
}
}
/// <summary>
/// <see href="https://github.com/aspnet/Mvc/blob/master/test/Microsoft.AspNetCore.Mvc.Core.TestCommon/TestHttpResponseStreamWriterFactory.cs">TestHttpResponseStreamWriterFactory.cs</see>
/// </summary>
private class TestHttpResponseStreamWriterFactory : IHttpResponseStreamWriterFactory
{
private const int DefaultBufferSize = 16 * 1024;
public TextWriter CreateWriter(Stream stream, Encoding encoding)
{
return new HttpResponseStreamWriter(stream, encoding, DefaultBufferSize);
}
}
/// <summary>
/// <see href="https://github.com/aspnet/Mvc/blob/master/test/Microsoft.AspNetCore.Mvc.Core.TestCommon/TestHttpRequestStreamReaderFactory.cs">TestHttpRequestStreamReaderFactory.cs</see>
/// </summary>
private class TestHttpRequestStreamReaderFactory : IHttpRequestStreamReaderFactory
{
public TextReader CreateReader(Stream stream, Encoding encoding)
{
return new HttpRequestStreamReader(stream, encoding);
}
}
/// <summary>
/// <see href="https://github.com/aspnet/Mvc/blob/master/test/Microsoft.AspNetCore.Mvc.Core.TestCommon/NonSeekableReadableStream.cs">NonSeekableReadableStream.cs</see>
/// </summary>
private class NonSeekableReadStream : Stream
{
private Stream _inner;
public NonSeekableReadStream(byte[] data)
: this(new MemoryStream(data))
{
}
public NonSeekableReadStream(Stream inner)
{
_inner = inner;
}
public override bool CanRead => _inner.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length
{
get { throw new NotSupportedException(); }
}
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public override void Flush()
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
count = Math.Max(count, 1);
return _inner.Read(buffer, offset, count);
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
count = Math.Max(count, 1);
return _inner.ReadAsync(buffer, offset, count, cancellationToken);
}
}
}
[MessagePackObject]
public class User
{
[Key(0)]
public int UserId { get; set; }
[Key(1)]
public string FullName { get; set; }
[Key(2)]
public int Age { get; set; }
[Key(3)]
public string Description { get; set; }
}
}

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

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Moq" Version="4.10.1" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MessagePack.AspNetCoreMvcFormatter\MessagePack.AspNetCoreMvcFormatter.csproj" />
<ProjectReference Include="..\..\src\MessagePack\MessagePack.csproj" />
<ProjectReference Include="..\MessagePack.Tests\MessagePack.Tests.csproj" />
</ItemGroup>
</Project>