Update gRPC transcoding to use System.Text.Json (#413)

This commit is contained in:
James Newton-King 2022-02-03 12:09:35 +13:00 коммит произвёл GitHub
Родитель f66a80dd38
Коммит 80c9af5e76
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
59 изменённых файлов: 3301 добавлений и 187 удалений

4
.gitignore поставляемый
Просмотреть файл

@ -128,5 +128,7 @@ config.ps1
node_modules/
# Python Compile Outputs
*.pyc
*.pyc
# BenchmarkDotNet
BenchmarkDotNet.Artifacts/

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

@ -7,7 +7,7 @@
<PropertyGroup>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)eng\AspNetCore.snk</AssemblyOriginatorKeyFile>
<LangVersion>8.0</LangVersion>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

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

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29521.150
# Visual Studio Version 17
VisualStudioVersion = 17.1.32104.313
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7B36FA7E-3D3E-4D24-9690-8EBD66C23039}"
EndProject
@ -19,6 +19,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Grpc.S
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Grpc.Swagger.Tests", "test\Microsoft.AspNetCore.Grpc.Swagger.Tests\Microsoft.AspNetCore.Grpc.Swagger.Tests.csproj", "{6EDCC241-95A6-4DFE-BD65-A524483208AD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{E274D80F-3217-466E-A920-BAA3D41FC0BE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Grpc.Microbenchmarks", "perf\Microsoft.AspNetCore.Grpc.Microbenchmarks\Microsoft.AspNetCore.Grpc.Microbenchmarks.csproj", "{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -89,6 +93,18 @@ Global
{6EDCC241-95A6-4DFE-BD65-A524483208AD}.Release|x64.Build.0 = Release|Any CPU
{6EDCC241-95A6-4DFE-BD65-A524483208AD}.Release|x86.ActiveCfg = Release|Any CPU
{6EDCC241-95A6-4DFE-BD65-A524483208AD}.Release|x86.Build.0 = Release|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Debug|x64.ActiveCfg = Debug|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Debug|x64.Build.0 = Debug|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Debug|x86.ActiveCfg = Debug|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Debug|x86.Build.0 = Debug|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Release|Any CPU.Build.0 = Release|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Release|x64.ActiveCfg = Release|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Release|x64.Build.0 = Release|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Release|x86.ActiveCfg = Release|Any CPU
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -100,6 +116,7 @@ Global
{02592779-E4F6-4B92-B17F-E333941CD7C2} = {E2811248-FE3F-4E6D-8063-3142CC2023A1}
{B54A4A8B-DD71-463A-8EEE-A8996677CC8D} = {7B36FA7E-3D3E-4D24-9690-8EBD66C23039}
{6EDCC241-95A6-4DFE-BD65-A524483208AD} = {F2DEA5D2-835F-4FE9-B231-DF0DAFAB04C6}
{9C3A04B6-5CDA-4F09-8FC4-39122DB9DCA8} = {E274D80F-3217-466E-A920-BAA3D41FC0BE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {ED16A561-5A6A-421E-8C8E-83423ACEF2EB}

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

@ -5,7 +5,7 @@
<PropertyGroup>
<GoogleApiCommonProtosPackageVersion>2.2.0</GoogleApiCommonProtosPackageVersion>
<GoogleProtobufPackageVersion>3.14.0</GoogleProtobufPackageVersion>
<GoogleProtobufPackageVersion>3.18.0</GoogleProtobufPackageVersion>
<GrpcAspNetCoreServerPackageVersion>2.33.1</GrpcAspNetCoreServerPackageVersion>
<GrpcCorePackageVersion>2.33.1</GrpcCorePackageVersion>
<NUnitPackageVersion>3.12.0</NUnitPackageVersion>

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

@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Toolchains.CsProj;
using BenchmarkDotNet.Toolchains.DotNetCli;
using BenchmarkDotNet.Validators;
namespace Microsoft.AspNetCore.Grpc.Microbenchmarks
{
internal class DefaultCoreConfig : ManualConfig
{
public DefaultCoreConfig()
{
AddLogger(ConsoleLogger.Default);
AddExporter(MarkdownExporter.GitHub);
AddDiagnoser(MemoryDiagnoser.Default);
AddColumn(StatisticColumn.OperationsPerSecond);
AddColumnProvider(DefaultColumnProviders.Instance);
AddValidator(JitOptimizationsValidator.FailOnError);
AddJob(Job.Default
.WithToolchain(CsProjCoreToolchain.From(new NetCoreAppSettings("net6.0", null, ".NET 6")))
.WithGcMode(new GcMode { Server = true })
.WithStrategy(RunStrategy.Throughput));
}
}
}

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

@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using BenchmarkDotNet.Configs;
namespace Microsoft.AspNetCore.Grpc.Microbenchmarks
{
[AttributeUsage(AttributeTargets.Assembly)]
public class DefaultCoreConfigAttribute : Attribute, IConfigSource
{
public IConfig Config => new DefaultCoreConfig();
}
}

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

@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using Google.Protobuf;
using Greet;
using Microsoft.AspNetCore.Grpc.HttpApi;
using Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json;
namespace Microsoft.AspNetCore.Grpc.Microbenchmarks.Json
{
public class JsonReading
{
private string _requestJson = default!;
private JsonSerializerOptions _serializerOptions = default!;
private JsonParser _jsonFormatter = default!;
[GlobalSetup]
public void GlobalSetup()
{
_requestJson = (new HelloRequest() { Name = "Hello world" }).ToString();
_serializerOptions = JsonConverterHelper.CreateSerializerOptions(new JsonSettings { WriteIndented = false });
_jsonFormatter = new JsonParser(new JsonParser.Settings(recursionLimit: 100));
}
[Benchmark]
public void ReadMessage_JsonSerializer()
{
JsonSerializer.Deserialize(_requestJson, typeof(HelloRequest), _serializerOptions);
}
[Benchmark]
public void ReadMessage_JsonFormatter()
{
_jsonFormatter.Parse(_requestJson, HelloRequest.Descriptor);
}
}
}

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

@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using Google.Protobuf;
using Greet;
using Microsoft.AspNetCore.Grpc.HttpApi;
using Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json;
namespace Microsoft.AspNetCore.Grpc.Microbenchmarks.Json
{
public class JsonWriting
{
private HelloRequest _request = default!;
private JsonSerializerOptions _serializerOptions = default!;
private JsonFormatter _jsonFormatter = default!;
[GlobalSetup]
public void GlobalSetup()
{
_request = new HelloRequest() { Name = "Hello world" };
_serializerOptions = JsonConverterHelper.CreateSerializerOptions(new JsonSettings { WriteIndented = false });
_jsonFormatter = new JsonFormatter(new JsonFormatter.Settings(formatDefaultValues: false));
}
[Benchmark]
public void WriteMessage_JsonSerializer()
{
JsonSerializer.Serialize(_request, _serializerOptions);
}
[Benchmark]
public void WriteMessage_JsonFormatter()
{
_jsonFormatter.Format(_request);
}
}
}

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

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Grpc.HttpApi\Microsoft.AspNetCore.Grpc.HttpApi.csproj" />
</ItemGroup>
<ItemGroup>
<Protobuf Include=".\Proto\chat.proto" GrpcServices="Server" />
<Protobuf Include=".\Proto\greet.proto" GrpcServices="Client" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="Google.Protobuf" Version="$(GoogleProtobufPackageVersion)" />
<PackageReference Include="Grpc.Tools" Version="$(GrpcCorePackageVersion)">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

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

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using BenchmarkDotNet.Running;
namespace Microsoft.AspNetCore.Grpc.Microbenchmarks
{
public class Program
{
static void Main(string[] args)
{
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
}
}

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

@ -0,0 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Grpc.Microbenchmarks;
[assembly: DefaultCoreConfigAttribute]

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

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
syntax = "proto3";
package chat;
// The greeting service definition.
service Chatter {
// Sends a greeting
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
// The chat message.
message ChatMessage {
string name = 1;
string message = 2;
}

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

@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
syntax = "proto3";
package greet;
import "google/protobuf/timestamp.proto";
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
rpc SayHellos (HelloRequest) returns (stream HelloReply);
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
google.protobuf.Timestamp timestamp = 2;
}

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

@ -1,8 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Google.Protobuf;
namespace Microsoft.AspNetCore.Grpc.HttpApi
{
/// <summary>
@ -10,18 +8,9 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
/// </summary>
public class GrpcHttpApiOptions
{
// grpc-gateway V2 writes default values by default
// https://github.com/grpc-ecosystem/grpc-gateway/pull/1377
private static readonly JsonFormatter DefaultFormatter = new JsonFormatter(new JsonFormatter.Settings(formatDefaultValues: true));
/// <summary>
/// Gets or sets the <see cref="Google.Protobuf.JsonFormatter"/> used to serialize outgoing messages.
/// Gets or sets the <see cref="HttpApi.JsonSettings"/> used to serialize messages.
/// </summary>
public JsonFormatter JsonFormatter { get; set; } = DefaultFormatter;
/// <summary>
/// Gets or sets the <see cref="Google.Protobuf.JsonParser"/> used to deserialize incoming messages.
/// </summary>
public JsonParser JsonParser { get; set; } = JsonParser.Default;
public JsonSettings JsonSettings { get; set; } = new JsonSettings();
}
}

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

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using Google.Api;
using Google.Protobuf.Reflection;
using Grpc.AspNetCore.Server;
@ -28,6 +29,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
private readonly GrpcServiceOptions<TService> _serviceOptions;
private readonly IGrpcServiceActivator<TService> _serviceActivator;
private readonly GrpcHttpApiOptions _httpApiOptions;
private readonly JsonSerializerOptions _serializerOptions;
private readonly ILogger _logger;
internal HttpApiProviderServiceBinder(
@ -39,7 +41,8 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
IServiceProvider serviceProvider,
ILoggerFactory loggerFactory,
IGrpcServiceActivator<TService> serviceActivator,
GrpcHttpApiOptions httpApiOptions)
GrpcHttpApiOptions httpApiOptions,
JsonSerializerOptions serializerOptions)
{
_context = context;
_declaringType = declaringType;
@ -48,6 +51,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
_serviceOptions = serviceOptions;
_serviceActivator = serviceActivator;
_httpApiOptions = httpApiOptions;
_serializerOptions = serializerOptions;
_logger = loggerFactory.CreateLogger<HttpApiProviderServiceBinder<TService>>();
}
@ -165,7 +169,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
bodyDescriptorRepeated,
bodyFieldDescriptors,
routeParameterDescriptors,
_httpApiOptions);
_serializerOptions);
_context.AddMethod<TRequest, TResponse>(method, routePattern, metadata, unaryServerCallHandler.HandleCallAsync);
}

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

@ -109,7 +109,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
protected override AuthContext AuthContextCore => throw new NotImplementedException();
protected override IDictionary<object, object> UserStateCore => _httpContext.Items;
protected override IDictionary<object, object?> UserStateCore => _httpContext.Items;
protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions options)
{

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

@ -3,11 +3,13 @@
using System;
using System.Reflection;
using System.Text.Json;
using Google.Protobuf.Reflection;
using Grpc.AspNetCore.Server;
using Grpc.AspNetCore.Server.Model;
using Grpc.Shared.HttpApi;
using Grpc.Shared.Server;
using Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -22,6 +24,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
private readonly ILoggerFactory _loggerFactory;
private readonly IServiceProvider _serviceProvider;
private readonly IGrpcServiceActivator<TService> _serviceActivator;
private readonly JsonSerializerOptions _serializerOptions;
public HttpApiServiceMethodProvider(
ILoggerFactory loggerFactory,
@ -38,6 +41,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
_loggerFactory = loggerFactory;
_serviceProvider = serviceProvider;
_serviceActivator = serviceActivator;
_serializerOptions = JsonConverterHelper.CreateSerializerOptions(_httpApiOptions.JsonSettings);
}
public void OnServiceMethodDiscovery(ServiceMethodProviderContext<TService> context)
@ -71,7 +75,8 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
_serviceProvider,
_loggerFactory,
_serviceActivator,
_httpApiOptions);
_httpApiOptions,
_serializerOptions);
try
{

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

@ -0,0 +1,101 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class AnyConverter<TMessage> : JsonConverter<TMessage> where TMessage : IMessage, new()
{
internal const string AnyTypeUrlField = "@type";
internal const string AnyWellKnownTypeValueField = "value";
private readonly JsonSettings _settings;
public AnyConverter(JsonSettings settings)
{
_settings = settings;
}
public override TMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var d = JsonDocument.ParseValue(ref reader);
if (!d.RootElement.TryGetProperty(AnyTypeUrlField, out var urlField))
{
throw new InvalidOperationException("Any message with no @type");
}
var message = new TMessage();
var typeUrl = urlField.GetString();
var typeName = Any.GetTypeName(typeUrl);
var descriptor = _settings.TypeRegistry.Find(typeName);
if (descriptor == null)
{
throw new InvalidOperationException($"Type registry has no descriptor for type name '{typeName}'");
}
IMessage data;
if (JsonConverterHelper.IsWellKnownType(descriptor))
{
if (!d.RootElement.TryGetProperty(AnyWellKnownTypeValueField, out var valueField))
{
throw new InvalidOperationException($"Expected '{AnyWellKnownTypeValueField}' property for well-known type Any body");
}
data = (IMessage)JsonSerializer.Deserialize(valueField, descriptor.ClrType, options)!;
}
else
{
data = (IMessage)JsonSerializer.Deserialize(d.RootElement, descriptor.ClrType, options)!;
}
message.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.SetValue(message, typeUrl);
message.Descriptor.Fields[Any.ValueFieldNumber].Accessor.SetValue(message, data.ToByteString());
return message;
}
public override void Write(Utf8JsonWriter writer, TMessage value, JsonSerializerOptions options)
{
var typeUrl = (string)value.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.GetValue(value);
var data = (ByteString)value.Descriptor.Fields[Any.ValueFieldNumber].Accessor.GetValue(value);
var typeName = Any.GetTypeName(typeUrl);
var descriptor = _settings.TypeRegistry.Find(typeName);
if (descriptor == null)
{
throw new InvalidOperationException($"Type registry has no descriptor for type name '{typeName}'");
}
var valueMessage = descriptor.Parser.ParseFrom(data);
writer.WriteStartObject();
writer.WriteString(AnyTypeUrlField, typeUrl);
if (JsonConverterHelper.IsWellKnownType(descriptor))
{
writer.WritePropertyName(AnyWellKnownTypeValueField);
if (JsonConverterHelper.IsWrapperType(descriptor))
{
var wrappedValue = valueMessage.Descriptor.Fields[JsonConverterHelper.WrapperValueFieldNumber].Accessor.GetValue(valueMessage);
JsonSerializer.Serialize(writer, wrappedValue, wrappedValue.GetType(), options);
}
else
{
JsonSerializer.Serialize(writer, valueMessage, valueMessage.GetType(), options);
}
}
else
{
MessageConverter<Any>.WriteMessageFields(writer, valueMessage, _settings, options);
}
writer.WriteEndObject();
}
}
}

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

@ -0,0 +1,28 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class BoolConverter : JsonConverter<bool>
{
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.GetBoolean();
}
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
public override void WriteAsPropertyName(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WritePropertyName(value ? "true" : "false");
}
}
}

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

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class ByteStringConverter : JsonConverter<ByteString>
{
public override ByteString? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// TODO - handle base64 strings without padding
return UnsafeByteOperations.UnsafeWrap(reader.GetBytesFromBase64());
}
public override void Write(Utf8JsonWriter writer, ByteString value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToBase64());
}
}
}

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

@ -0,0 +1,61 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class DurationConverter<TMessage> : JsonConverter<TMessage> where TMessage : IMessage, new()
{
public DurationConverter(JsonSettings settings)
{
}
public override TMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
{
throw new InvalidOperationException("Expected string value for Duration");
}
var (seconds, nanos) = Legacy.ParseDuration(reader.GetString()!);
var message = new TMessage();
if (message is Duration duration)
{
duration.Seconds = seconds;
duration.Nanos = nanos;
}
else
{
message.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.SetValue(message, seconds);
message.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.SetValue(message, nanos);
}
return message;
}
public override void Write(Utf8JsonWriter writer, TMessage value, JsonSerializerOptions options)
{
int nanos;
long seconds;
if (value is Duration duration)
{
nanos = duration.Nanos;
seconds = duration.Seconds;
}
else
{
nanos = (int)value.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.GetValue(value);
seconds = (long)value.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.GetValue(value);
}
var text = Legacy.GetDurationText(nanos, seconds);
writer.WriteStringValue(text);
}
}
}

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

@ -0,0 +1,131 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf.Reflection;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class EnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : Enum
{
private readonly JsonSettings _settings;
public EnumConverter(JsonSettings settings)
{
_settings = settings;
}
public override TEnum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.String:
var enumDescriptor = ResolveEnumDescriptor(typeToConvert);
if (enumDescriptor == null)
{
throw new InvalidOperationException($"Unable to resolve descriptor for {typeToConvert}.");
}
var valueDescriptor = enumDescriptor.FindValueByName(reader.GetString()!);
return ConvertFromInteger(valueDescriptor.Number);
case JsonTokenType.Number:
return ConvertFromInteger(reader.GetInt32());
case JsonTokenType.Null:
return default;
default:
throw new InvalidOperationException($"Unexpected JSON token: {reader.TokenType}");
}
}
private static EnumDescriptor? ResolveEnumDescriptor(Type typeToConvert)
{
var containingType = typeToConvert?.DeclaringType?.DeclaringType;
if (containingType != null)
{
var messageDescriptor = JsonConverterHelper.GetMessageDescriptor(containingType);
if (messageDescriptor != null)
{
for (var i = 0; i < messageDescriptor.EnumTypes.Count; i++)
{
if (messageDescriptor.EnumTypes[i].ClrType == typeToConvert)
{
return messageDescriptor.EnumTypes[i];
}
}
}
}
return null;
}
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
if (_settings.FormatEnumsAsIntegers)
{
writer.WriteNumberValue(ConvertToInteger(value));
}
else
{
var name = Legacy.OriginalEnumValueHelper.GetOriginalName(value);
if (name != null)
{
writer.WriteStringValue(name);
}
else
{
writer.WriteNumberValue(ConvertToInteger(value));
}
}
}
private static TEnum ConvertFromInteger(int integer)
{
if (!TryConvertToEnum(integer, out var value))
{
throw new InvalidOperationException($"Integer can't be converted to enum {typeof(TEnum).FullName}.");
}
return value;
}
private static int ConvertToInteger(TEnum value)
{
if (!TryConvertToInteger(value, out var integer))
{
throw new InvalidOperationException($"Enum {typeof(TEnum).FullName} can't be converted to integer.");
}
return integer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryConvertToInteger(TEnum value, out int integer)
{
if (Unsafe.SizeOf<int>() == Unsafe.SizeOf<TEnum>())
{
integer = Unsafe.As<TEnum, int>(ref value);
return true;
}
integer = default;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryConvertToEnum(int integer, [NotNullWhen(true)] out TEnum? value)
{
if (Unsafe.SizeOf<int>() == Unsafe.SizeOf<TEnum>())
{
value = Unsafe.As<int, TEnum>(ref integer);
return true;
}
value = default;
return false;
}
}
}

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

@ -0,0 +1,157 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class FieldMaskConverter<TMessage> : JsonConverter<TMessage> where TMessage : IMessage, new()
{
private static readonly char[] FieldMaskPathSeparators = new[] { ',' };
public FieldMaskConverter(JsonSettings settings)
{
}
public override TMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var message = new TMessage();
if (reader.TokenType != JsonTokenType.String)
{
throw new InvalidOperationException("Expected string value for FieldMask");
}
// TODO: Do we *want* to remove empty entries? Probably okay to treat "" as "no paths", but "foo,,bar"?
string[] jsonPaths = reader.GetString()!.Split(FieldMaskPathSeparators, StringSplitOptions.RemoveEmptyEntries);
IList messagePaths = (IList)message.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(message);
foreach (var path in jsonPaths)
{
messagePaths.Add(ToSnakeCase(path));
}
return message;
}
public override void Write(Utf8JsonWriter writer, TMessage value, JsonSerializerOptions options)
{
var paths = (IList<string>)value.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(value);
var firstInvalid = paths.FirstOrDefault(p => !IsPathValid(p));
if (firstInvalid == null)
{
writer.WriteStringValue(string.Join(",", paths.Select(ToJsonName)));
}
else
{
throw new InvalidOperationException($"Invalid field mask to be converted to JSON: {firstInvalid}");
}
}
internal static string ToJsonName(string name)
{
StringBuilder result = new StringBuilder(name.Length);
bool isNextUpperCase = false;
foreach (char ch in name)
{
if (ch == '_')
{
isNextUpperCase = true;
}
else if (isNextUpperCase)
{
result.Append(char.ToUpperInvariant(ch));
isNextUpperCase = false;
}
else
{
result.Append(ch);
}
}
return result.ToString();
}
/// <summary>
/// Checks whether the given path is valid for a field mask.
/// </summary>
/// <returns>true if the path is valid; false otherwise</returns>
private static bool IsPathValid(string input)
{
for (int i = 0; i < input.Length; i++)
{
char c = input[i];
if (c >= 'A' && c <= 'Z')
{
return false;
}
if (c == '_' && i < input.Length - 1)
{
char next = input[i + 1];
if (next < 'a' || next > 'z')
{
return false;
}
}
}
return true;
}
// Ported from src/google/protobuf/util/internal/utility.cc
private static string ToSnakeCase(string text)
{
var builder = new StringBuilder(text.Length * 2);
// Note: this is probably unnecessary now, but currently retained to be as close as possible to the
// C++, whilst still throwing an exception on underscores.
bool wasNotUnderscore = false; // Initialize to false for case 1 (below)
bool wasNotCap = false;
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if (c >= 'A' && c <= 'Z') // ascii_isupper
{
// Consider when the current character B is capitalized:
// 1) At beginning of input: "B..." => "b..."
// (e.g. "Biscuit" => "biscuit")
// 2) Following a lowercase: "...aB..." => "...a_b..."
// (e.g. "gBike" => "g_bike")
// 3) At the end of input: "...AB" => "...ab"
// (e.g. "GoogleLAB" => "google_lab")
// 4) Followed by a lowercase: "...ABc..." => "...a_bc..."
// (e.g. "GBike" => "g_bike")
if (wasNotUnderscore && // case 1 out
(wasNotCap || // case 2 in, case 3 out
(i + 1 < text.Length && // case 3 out
(text[i + 1] >= 'a' && text[i + 1] <= 'z')))) // ascii_islower(text[i + 1])
{ // case 4 in
// We add an underscore for case 2 and case 4.
builder.Append('_');
}
// ascii_tolower, but we already know that c *is* an upper case ASCII character...
builder.Append((char)(c + 'a' - 'A'));
wasNotUnderscore = true;
wasNotCap = false;
}
else
{
builder.Append(c);
if (c == '_')
{
throw new InvalidOperationException($"Invalid field mask: {text}");
}
wasNotUnderscore = true;
wasNotCap = true;
}
}
return builder.ToString();
}
}
}

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

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class Int64Converter : JsonConverter<long>
{
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
return long.Parse(reader.GetString()!);
}
return reader.GetInt64();
}
public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("d", CultureInfo.InvariantCulture));
}
}
}

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

@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal class JsonConverterFactoryForEnum : JsonConverterFactory
{
private readonly JsonSettings _settings;
public JsonConverterFactoryForEnum(JsonSettings settings)
{
_settings = settings;
}
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsEnum;
}
public override JsonConverter CreateConverter(
Type typeToConvert, JsonSerializerOptions options)
{
var converter = (JsonConverter)Activator.CreateInstance(
typeof(EnumConverter<>).MakeGenericType(new Type[] { typeToConvert }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: new object[] { _settings },
culture: null)!;
return converter;
}
}
}

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

@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal class JsonConverterFactoryForMessage : JsonConverterFactory
{
private readonly JsonSettings _settings;
public JsonConverterFactoryForMessage(JsonSettings settings)
{
_settings = settings;
}
public override bool CanConvert(Type typeToConvert)
{
return typeof(IMessage).IsAssignableFrom(typeToConvert);
}
public override JsonConverter CreateConverter(
Type typeToConvert, JsonSerializerOptions options)
{
JsonConverter converter = (JsonConverter)Activator.CreateInstance(
typeof(MessageConverter<>).MakeGenericType(new Type[] { typeToConvert }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: new object[] { _settings },
culture: null)!;
return converter;
}
}
}

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

@ -0,0 +1,67 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal class JsonConverterFactoryForWellKnownTypes : JsonConverterFactory
{
private readonly JsonSettings _settings;
public JsonConverterFactoryForWellKnownTypes(JsonSettings settings)
{
_settings = settings;
}
public override bool CanConvert(Type typeToConvert)
{
if (!typeof(IMessage).IsAssignableFrom(typeToConvert))
{
return false;
}
var descriptor = JsonConverterHelper.GetMessageDescriptor(typeToConvert);
if (descriptor == null)
{
return false;
}
return WellKnownTypeNames.ContainsKey(descriptor.FullName);
}
public override JsonConverter CreateConverter(
Type typeToConvert, JsonSerializerOptions options)
{
var descriptor = JsonConverterHelper.GetMessageDescriptor(typeToConvert)!;
var converterType = WellKnownTypeNames[descriptor.FullName];
var converter = (JsonConverter)Activator.CreateInstance(
converterType.MakeGenericType(new Type[] { typeToConvert }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: new object[] { _settings },
culture: null)!;
return converter;
}
private static readonly Dictionary<string, Type> WellKnownTypeNames = new Dictionary<string, Type>
{
[Any.Descriptor.FullName] = typeof(AnyConverter<>),
[Duration.Descriptor.FullName] = typeof(DurationConverter<>),
[Timestamp.Descriptor.FullName] = typeof(TimestampConverter<>),
[FieldMask.Descriptor.FullName] = typeof(FieldMaskConverter<>),
[Struct.Descriptor.FullName] = typeof(StructConverter<>),
[ListValue.Descriptor.FullName] = typeof(ListValueConverter<>),
[Value.Descriptor.FullName] = typeof(ValueConverter<>),
};
}
}

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

@ -0,0 +1,51 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal class JsonConverterFactoryForWrappers : JsonConverterFactory
{
private readonly JsonSettings _settings;
public JsonConverterFactoryForWrappers(JsonSettings settings)
{
_settings = settings;
}
public override bool CanConvert(Type typeToConvert)
{
if (!typeof(IMessage).IsAssignableFrom(typeToConvert))
{
return false;
}
var descriptor = JsonConverterHelper.GetMessageDescriptor(typeToConvert);
if (descriptor == null)
{
return false;
}
return JsonConverterHelper.IsWrapperType(descriptor);
}
public override JsonConverter CreateConverter(
Type typeToConvert, JsonSerializerOptions options)
{
var converter = (JsonConverter)Activator.CreateInstance(
typeof(WrapperConverter<>).MakeGenericType(new Type[] { typeToConvert }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: new object[] { _settings },
culture: null)!;
return converter;
}
}
}

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

@ -0,0 +1,153 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Encodings.Web;
using System.Text.Json;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal static class JsonConverterHelper
{
internal const int WrapperValueFieldNumber = Int32Value.ValueFieldNumber;
private static readonly HashSet<string> WellKnownTypeNames = new HashSet<string>
{
"google/protobuf/any.proto",
"google/protobuf/api.proto",
"google/protobuf/duration.proto",
"google/protobuf/empty.proto",
"google/protobuf/wrappers.proto",
"google/protobuf/timestamp.proto",
"google/protobuf/field_mask.proto",
"google/protobuf/source_context.proto",
"google/protobuf/struct.proto",
"google/protobuf/type.proto",
};
internal static JsonSerializerOptions CreateSerializerOptions(JsonSettings settings)
{
var options = new JsonSerializerOptions
{
WriteIndented = settings.WriteIndented,
NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
options.Converters.Add(new NullValueConverter());
options.Converters.Add(new ByteStringConverter());
options.Converters.Add(new Int64Converter());
options.Converters.Add(new UInt64Converter());
options.Converters.Add(new BoolConverter());
options.Converters.Add(new JsonConverterFactoryForEnum(settings));
options.Converters.Add(new JsonConverterFactoryForWrappers(settings));
options.Converters.Add(new JsonConverterFactoryForWellKnownTypes(settings));
options.Converters.Add(new JsonConverterFactoryForMessage(settings));
return options;
}
internal static Type GetFieldType(FieldDescriptor descriptor)
{
switch (descriptor.FieldType)
{
case FieldType.Bool:
return typeof(bool);
case FieldType.Bytes:
return typeof(ByteString);
case FieldType.String:
return typeof(string);
case FieldType.Double:
return typeof(double);
case FieldType.SInt32:
case FieldType.Int32:
case FieldType.SFixed32:
return typeof(int);
case FieldType.Enum:
return descriptor.EnumType.ClrType;
case FieldType.Fixed32:
case FieldType.UInt32:
return typeof(uint);
case FieldType.Fixed64:
case FieldType.UInt64:
return typeof(ulong);
case FieldType.SFixed64:
case FieldType.Int64:
case FieldType.SInt64:
return typeof(long);
case FieldType.Float:
return typeof(float);
case FieldType.Message:
case FieldType.Group: // Never expect to get this, but...
if (IsWrapperType(descriptor.MessageType))
{
var t = GetFieldType(descriptor.MessageType.Fields[WrapperValueFieldNumber]);
if (t.IsValueType)
{
return typeof(Nullable<>).MakeGenericType(t);
}
return t;
}
return descriptor.MessageType.ClrType;
default:
throw new ArgumentException("Invalid field type");
}
}
internal static MessageDescriptor? GetMessageDescriptor(Type typeToConvert)
{
var property = typeToConvert.GetProperty("Descriptor", BindingFlags.Static | BindingFlags.Public, binder: null, typeof(MessageDescriptor), Type.EmptyTypes, modifiers: null);
if (property == null)
{
return null;
}
return property.GetValue(null, null) as MessageDescriptor;
}
public static void PopulateMap(ref Utf8JsonReader reader, JsonSerializerOptions options, IMessage message, FieldDescriptor fieldDescriptor)
{
var mapFields = fieldDescriptor.MessageType.Fields.InFieldNumberOrder();
var mapKey = mapFields[0];
var mapValue = mapFields[1];
var keyType = GetFieldType(mapKey);
var valueType = GetFieldType(mapValue);
var repeatedFieldType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
var newValues = (IDictionary)JsonSerializer.Deserialize(ref reader, repeatedFieldType, options)!;
var existingValue = (IDictionary)fieldDescriptor.Accessor.GetValue(message);
foreach (DictionaryEntry item in newValues)
{
existingValue[item.Key] = item.Value;
}
}
public static void PopulateList(ref Utf8JsonReader reader, JsonSerializerOptions options, IMessage message, FieldDescriptor fieldDescriptor)
{
var fieldType = GetFieldType(fieldDescriptor);
var repeatedFieldType = typeof(List<>).MakeGenericType(fieldType);
var newValues = (IList)JsonSerializer.Deserialize(ref reader, repeatedFieldType, options)!;
var existingValue = (IList)fieldDescriptor.Accessor.GetValue(message);
foreach (var item in newValues)
{
existingValue.Add(item);
}
}
internal static bool IsWellKnownType(MessageDescriptor messageDescriptor) => messageDescriptor.File.Package == "google.protobuf" &&
WellKnownTypeNames.Contains(messageDescriptor.File.Name);
internal static bool IsWrapperType(MessageDescriptor messageDescriptor) => messageDescriptor.File.Package == "google.protobuf" &&
messageDescriptor.File.Name == "google/protobuf/wrappers.proto";
}
}

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

@ -0,0 +1,307 @@
#region Copyright notice and license
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc. All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using Google.Protobuf.Reflection;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
// Source here is from https://github.com/protocolbuffers/protobuf
// Most of this code will be replaced over time with optimized implementations.
internal static class Legacy
{
private static readonly Regex TimestampRegex = new Regex(@"^(?<datetime>[0-9]{4}-[01][0-9]-[0-3][0-9]T[012][0-9]:[0-5][0-9]:[0-5][0-9])(?<subseconds>\.[0-9]{1,9})?(?<offset>(Z|[+-][0-1][0-9]:[0-5][0-9]))$", RegexOptions.Compiled);
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// Constants determined programmatically, but then hard-coded so they can be constant expressions.
private const long BclSecondsAtUnixEpoch = 62135596800;
internal const long UnixSecondsAtBclMaxValue = 253402300799;
internal const long UnixSecondsAtBclMinValue = -BclSecondsAtUnixEpoch;
internal const int MaxNanos = Duration.NanosecondsPerSecond - 1;
private static readonly int[] SubsecondScalingFactors = { 0, 100000000, 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10, 1 };
private static readonly Regex DurationRegex = new Regex(@"^(?<sign>-)?(?<int>[0-9]{1,12})(?<subseconds>\.[0-9]{1,9})?s$", RegexOptions.Compiled);
public static (long seconds, int nanos) ParseTimestamp(string value)
{
var match = TimestampRegex.Match(value);
if (!match.Success)
{
throw new InvalidOperationException($"Invalid Timestamp value: {value}");
}
var dateTime = match.Groups["datetime"].Value;
var subseconds = match.Groups["subseconds"].Value;
var offset = match.Groups["offset"].Value;
try
{
DateTime parsed = DateTime.ParseExact(
dateTime,
"yyyy-MM-dd'T'HH:mm:ss",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
// TODO: It would be nice not to have to create all these objects... easy to optimize later though.
Timestamp timestamp = Timestamp.FromDateTime(parsed);
int nanosToAdd = 0;
if (subseconds != "")
{
// This should always work, as we've got 1-9 digits.
int parsedFraction = int.Parse(subseconds.Substring(1), CultureInfo.InvariantCulture);
nanosToAdd = parsedFraction * SubsecondScalingFactors[subseconds.Length];
}
int secondsToAdd = 0;
if (offset != "Z")
{
// This is the amount we need to *subtract* from the local time to get to UTC - hence - => +1 and vice versa.
int sign = offset[0] == '-' ? 1 : -1;
int hours = int.Parse(offset.Substring(1, 2), CultureInfo.InvariantCulture);
int minutes = int.Parse(offset.Substring(4, 2));
int totalMinutes = hours * 60 + minutes;
if (totalMinutes > 18 * 60)
{
throw new InvalidOperationException($"Invalid Timestamp value: {value}");
}
if (totalMinutes == 0 && sign == 1)
{
// This is an offset of -00:00, which means "unknown local offset". It makes no sense for a timestamp.
throw new InvalidOperationException($"Invalid Timestamp value: {value}");
}
// We need to *subtract* the offset from local time to get UTC.
secondsToAdd = sign * totalMinutes * 60;
}
// Ensure we've got the right signs. Currently unnecessary, but easy to do.
if (secondsToAdd < 0 && nanosToAdd > 0)
{
secondsToAdd++;
nanosToAdd = nanosToAdd - Duration.NanosecondsPerSecond;
}
if (secondsToAdd != 0 || nanosToAdd != 0)
{
timestamp += new Duration { Nanos = nanosToAdd, Seconds = secondsToAdd };
// The resulting timestamp after offset change would be out of our expected range. Currently the Timestamp message doesn't validate this
// anywhere, but we shouldn't parse it.
if (timestamp.Seconds < UnixSecondsAtBclMinValue || timestamp.Seconds > UnixSecondsAtBclMaxValue)
{
throw new InvalidOperationException($"Invalid Timestamp value: {value}");
}
}
return (timestamp.Seconds, timestamp.Nanos);
}
catch (FormatException)
{
throw new InvalidOperationException($"Invalid Timestamp value: {value}");
}
}
private static bool IsNormalized(long seconds, int nanoseconds) =>
nanoseconds >= 0 &&
nanoseconds <= MaxNanos &&
seconds >= UnixSecondsAtBclMinValue &&
seconds <= UnixSecondsAtBclMaxValue;
public static string GetTimestampText(int nanos, long seconds)
{
if (IsNormalized(seconds, nanos))
{
// Use .NET's formatting for the value down to the second, including an opening double quote (as it's a string value)
DateTime dateTime = UnixEpoch.AddSeconds(seconds);
var builder = new StringBuilder();
builder.Append(dateTime.ToString("yyyy'-'MM'-'dd'T'HH:mm:ss", CultureInfo.InvariantCulture));
if (nanos != 0)
{
builder.Append('.');
// Output to 3, 6 or 9 digits.
if (nanos % 1000000 == 0)
{
builder.Append((nanos / 1000000).ToString("d3", CultureInfo.InvariantCulture));
}
else if (nanos % 1000 == 0)
{
builder.Append((nanos / 1000).ToString("d6", CultureInfo.InvariantCulture));
}
else
{
builder.Append(nanos.ToString("d9", CultureInfo.InvariantCulture));
}
}
builder.Append("Z");
return builder.ToString();
}
else
{
throw new InvalidOperationException("Non-normalized timestamp value");
}
}
public static (long seconds, int nanos) ParseDuration(string value)
{
var match = DurationRegex.Match(value);
if (!match.Success)
{
throw new InvalidOperationException("Invalid Duration value: " + value);
}
var sign = match.Groups["sign"].Value;
var secondsText = match.Groups["int"].Value;
// Prohibit leading insignficant zeroes
if (secondsText[0] == '0' && secondsText.Length > 1)
{
throw new InvalidOperationException("Invalid Duration value: " + value);
}
var subseconds = match.Groups["subseconds"].Value;
var multiplier = sign == "-" ? -1 : 1;
try
{
long seconds = long.Parse(secondsText, CultureInfo.InvariantCulture) * multiplier;
int nanos = 0;
if (subseconds != "")
{
// This should always work, as we've got 1-9 digits.
int parsedFraction = int.Parse(subseconds.Substring(1));
nanos = parsedFraction * SubsecondScalingFactors[subseconds.Length] * multiplier;
}
if (!IsNormalized(seconds, nanos))
{
throw new InvalidOperationException("Invalid Duration value: " + value);
}
return (seconds, nanos);
}
catch (FormatException)
{
throw new InvalidOperationException("Invalid Duration value: " + value);
}
}
public static string GetDurationText(int nanos, long seconds)
{
if (IsNormalized(seconds, nanos))
{
var builder = new StringBuilder();
// The seconds part will normally provide the minus sign if we need it, but not if it's 0...
if (seconds == 0 && nanos < 0)
{
builder.Append('-');
}
builder.Append(seconds.ToString("d", CultureInfo.InvariantCulture));
AppendNanoseconds(builder, Math.Abs(nanos));
builder.Append("s");
return builder.ToString();
}
else
{
throw new InvalidOperationException("Non-normalized duration value");
}
}
/// <summary>
/// Appends a number of nanoseconds to a StringBuilder. Either 0 digits are added (in which
/// case no "." is appended), or 3 6 or 9 digits. This is internal for use in Timestamp as well
/// as Duration.
/// </summary>
internal static void AppendNanoseconds(StringBuilder builder, int nanos)
{
if (nanos != 0)
{
builder.Append('.');
// Output to 3, 6 or 9 digits.
if (nanos % 1000000 == 0)
{
builder.Append((nanos / 1000000).ToString("d3", CultureInfo.InvariantCulture));
}
else if (nanos % 1000 == 0)
{
builder.Append((nanos / 1000).ToString("d6", CultureInfo.InvariantCulture));
}
else
{
builder.Append(nanos.ToString("d9", CultureInfo.InvariantCulture));
}
}
}
// Effectively a cache of mapping from enum values to the original name as specified in the proto file,
// fetched by reflection.
// The need for this is unfortunate, as is its unbounded size, but realistically it shouldn't cause issues.
internal static class OriginalEnumValueHelper
{
private static readonly ConcurrentDictionary<Type, Dictionary<object, string>> _dictionaries
= new ConcurrentDictionary<Type, Dictionary<object, string>>();
internal static string? GetOriginalName(object value)
{
var enumType = value.GetType();
Dictionary<object, string>? nameMapping;
lock (_dictionaries)
{
if (!_dictionaries.TryGetValue(enumType, out nameMapping))
{
nameMapping = GetNameMapping(enumType);
_dictionaries[enumType] = nameMapping;
}
}
string? originalName;
// If this returns false, originalName will be null, which is what we want.
nameMapping.TryGetValue(value, out originalName);
return originalName;
}
private static Dictionary<object, string> GetNameMapping(Type enumType)
{
return enumType.GetTypeInfo().DeclaredFields
.Where(f => f.IsStatic)
.Where(f => f.GetCustomAttributes<OriginalNameAttribute>()
.FirstOrDefault()?.PreferredAlias ?? true)
.ToDictionary(f => f.GetValue(null)!,
f => f.GetCustomAttributes<OriginalNameAttribute>()
.FirstOrDefault()
// If the attribute hasn't been applied, fall back to the name of the field.
?.Name ?? f.Name);
}
}
}
}

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

@ -0,0 +1,35 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class ListValueConverter<TMessage> : JsonConverter<TMessage> where TMessage : IMessage, new()
{
public ListValueConverter(JsonSettings settings)
{
}
public override TMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var message = new TMessage();
JsonConverterHelper.PopulateList(ref reader, options, message, message.Descriptor.Fields[ListValue.ValuesFieldNumber]);
return message;
}
public override void Write(Utf8JsonWriter writer, TMessage value, JsonSerializerOptions options)
{
var list = (IList)value.Descriptor.Fields[ListValue.ValuesFieldNumber].Accessor.GetValue(value);
JsonSerializer.Serialize(writer, list, list.GetType(), options);
}
}
}

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

@ -0,0 +1,191 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class MessageConverter<TMessage> : JsonConverter<TMessage> where TMessage : IMessage, new()
{
private readonly JsonSettings _settings;
private readonly Dictionary<string, FieldDescriptor> _jsonFieldMap;
public MessageConverter(JsonSettings settings)
{
_settings = settings;
_jsonFieldMap = CreateJsonFieldMap((new TMessage()).Descriptor.Fields.InFieldNumberOrder());
}
public override TMessage Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
var message = new TMessage();
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new InvalidOperationException($"Unexpected JSON token: {reader.TokenType}");
}
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.EndObject:
return message;
case JsonTokenType.PropertyName:
if (_jsonFieldMap.TryGetValue(reader.GetString()!, out var fieldDescriptor))
{
if (fieldDescriptor.ContainingOneof != null)
{
if (fieldDescriptor.ContainingOneof.Accessor.GetCaseFieldDescriptor(message) != null)
{
throw new InvalidOperationException($"Multiple values specified for oneof {fieldDescriptor.ContainingOneof.Name}");
}
}
if (fieldDescriptor.IsMap)
{
JsonConverterHelper.PopulateMap(ref reader, options, message, fieldDescriptor);
}
else if (fieldDescriptor.IsRepeated)
{
JsonConverterHelper.PopulateList(ref reader, options, message, fieldDescriptor);
}
else
{
var fieldType = JsonConverterHelper.GetFieldType(fieldDescriptor);
var propertyValue = JsonSerializer.Deserialize(ref reader, fieldType, options);
fieldDescriptor.Accessor.SetValue(message, propertyValue);
}
}
else
{
reader.Skip();
}
break;
case JsonTokenType.Comment:
// Ignore
break;
default:
throw new InvalidOperationException($"Unexpected JSON token: {reader.TokenType}");
}
}
throw new Exception();
}
public override void Write(
Utf8JsonWriter writer,
TMessage value,
JsonSerializerOptions options)
{
WriteMessage(writer, value, options);
}
private void WriteMessage(Utf8JsonWriter writer, IMessage message, JsonSerializerOptions options)
{
writer.WriteStartObject();
WriteMessageFields(writer, message, _settings, options);
writer.WriteEndObject();
}
internal static void WriteMessageFields(Utf8JsonWriter writer, IMessage message, JsonSettings settings, JsonSerializerOptions options)
{
var fields = message.Descriptor.Fields;
foreach (var field in fields.InFieldNumberOrder())
{
var accessor = field.Accessor;
var value = accessor.GetValue(message);
if (!ShouldFormatFieldValue(message, field, value, settings.FormatDefaultValues))
{
continue;
}
writer.WritePropertyName(accessor.Descriptor.JsonName);
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
private static Dictionary<string, FieldDescriptor> CreateJsonFieldMap(IList<FieldDescriptor> fields)
{
var map = new Dictionary<string, FieldDescriptor>();
foreach (var field in fields)
{
map[field.Name] = field;
map[field.JsonName] = field;
}
return new Dictionary<string, FieldDescriptor>(map);
}
/// <summary>
/// Determines whether or not a field value should be serialized according to the field,
/// its value in the message, and the settings of this formatter.
/// </summary>
private static bool ShouldFormatFieldValue(IMessage message, FieldDescriptor field, object value, bool formatDefaultValues) =>
field.HasPresence
// Fields that support presence *just* use that
? field.Accessor.HasValue(message)
// Otherwise, format if either we've been asked to format default values, or if it's
// not a default value anyway.
: formatDefaultValues || !IsDefaultValue(field, value);
private static bool IsDefaultValue(FieldDescriptor descriptor, object value)
{
if (descriptor.IsMap)
{
IDictionary dictionary = (IDictionary)value;
return dictionary.Count == 0;
}
if (descriptor.IsRepeated)
{
IList list = (IList)value;
return list.Count == 0;
}
switch (descriptor.FieldType)
{
case FieldType.Bool:
return (bool)value == false;
case FieldType.Bytes:
return (ByteString)value == ByteString.Empty;
case FieldType.String:
return (string)value == "";
case FieldType.Double:
return (double)value == 0.0;
case FieldType.SInt32:
case FieldType.Int32:
case FieldType.SFixed32:
case FieldType.Enum:
return (int)value == 0;
case FieldType.Fixed32:
case FieldType.UInt32:
return (uint)value == 0;
case FieldType.Fixed64:
case FieldType.UInt64:
return (ulong)value == 0;
case FieldType.SFixed64:
case FieldType.Int64:
case FieldType.SInt64:
return (long)value == 0;
case FieldType.Float:
return (float)value == 0f;
case FieldType.Message:
case FieldType.Group: // Never expect to get this, but...
return value == null;
default:
throw new ArgumentException("Invalid field type");
}
}
}
}

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

@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class NullValueConverter : JsonConverter<NullValue>
{
public override bool HandleNull => true;
public override NullValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.String:
if (reader.GetString() == "NULL_VALUE")
{
return NullValue.NullValue;
}
else
{
throw new InvalidOperationException($"Invalid enum value: {reader.GetString()} for enum type: google.protobuf.NullValue");
}
case JsonTokenType.Number:
return (NullValue)reader.GetInt32();
case JsonTokenType.Null:
return NullValue.NullValue;
default:
throw new InvalidOperationException($"Unexpected JSON token: {reader.TokenType}");
}
}
public override void Write(Utf8JsonWriter writer, NullValue value, JsonSerializerOptions options)
{
writer.WriteNullValue();
}
}
}

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

@ -0,0 +1,49 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class StructConverter<TMessage> : JsonConverter<TMessage> where TMessage : IMessage, new()
{
public StructConverter(JsonSettings settings)
{
}
public override TMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var message = new TMessage();
JsonConverterHelper.PopulateMap(ref reader, options, message, message.Descriptor.Fields[Struct.FieldsFieldNumber]);
return message;
}
public override void Write(Utf8JsonWriter writer, TMessage value, JsonSerializerOptions options)
{
writer.WriteStartObject();
var fields = (IDictionary)value.Descriptor.Fields[Struct.FieldsFieldNumber].Accessor.GetValue(value);
foreach (DictionaryEntry entry in fields)
{
var k = (string)entry.Key;
var v = (IMessage?)entry.Value;
if (string.IsNullOrEmpty(k) || v == null)
{
throw new InvalidOperationException("Struct fields cannot have an empty key or a null value.");
}
writer.WritePropertyName(k);
JsonSerializer.Serialize(writer, v, v.GetType(), options);
}
writer.WriteEndObject();
}
}
}

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

@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class TimestampConverter<TMessage> : JsonConverter<TMessage> where TMessage : IMessage, new()
{
public TimestampConverter(JsonSettings settings)
{
}
public override TMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
{
throw new InvalidOperationException("Expected string value for Timestamp");
}
var (seconds, nanos) = Legacy.ParseTimestamp(reader.GetString()!);
var message = new TMessage();
if (message is Timestamp timestamp)
{
timestamp.Seconds = seconds;
timestamp.Nanos = nanos;
}
else
{
message.Descriptor.Fields[Timestamp.SecondsFieldNumber].Accessor.SetValue(message, seconds);
message.Descriptor.Fields[Timestamp.NanosFieldNumber].Accessor.SetValue(message, nanos);
}
return message;
}
public override void Write(Utf8JsonWriter writer, TMessage value, JsonSerializerOptions options)
{
int nanos;
long seconds;
if (value is Timestamp timestamp)
{
nanos = timestamp.Nanos;
seconds = timestamp.Seconds;
}
else
{
nanos = (int)value.Descriptor.Fields[Timestamp.NanosFieldNumber].Accessor.GetValue(value);
seconds = (long)value.Descriptor.Fields[Timestamp.SecondsFieldNumber].Accessor.GetValue(value);
}
var text = Legacy.GetTimestampText(nanos, seconds);
writer.WriteStringValue(text);
}
}
}

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

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class UInt64Converter : JsonConverter<ulong>
{
public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
return ulong.Parse(reader.GetString()!);
}
return reader.GetUInt64();
}
public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("d", CultureInfo.InvariantCulture));
}
}
}

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

@ -0,0 +1,90 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class ValueConverter<TMessage> : JsonConverter<TMessage> where TMessage : IMessage, new()
{
public ValueConverter(JsonSettings settings)
{
}
public override TMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var message = new TMessage();
var fields = message.Descriptor.Fields;
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
{
var field = fields[Value.StructValueFieldNumber];
var structMessage = JsonSerializer.Deserialize(ref reader, field.MessageType.ClrType, options);
field.Accessor.SetValue(message, structMessage);
break;
}
case JsonTokenType.StartArray:
{
var field = fields[Value.ListValueFieldNumber];
var list = JsonSerializer.Deserialize(ref reader, field.MessageType.ClrType, options);
field.Accessor.SetValue(message, list);
break;
}
case JsonTokenType.Comment:
break;
case JsonTokenType.String:
fields[Value.StringValueFieldNumber].Accessor.SetValue(message, reader.GetString()!);
break;
case JsonTokenType.Number:
fields[Value.NumberValueFieldNumber].Accessor.SetValue(message, reader.GetDouble());
break;
case JsonTokenType.True:
fields[Value.BoolValueFieldNumber].Accessor.SetValue(message, true);
break;
case JsonTokenType.False:
fields[Value.BoolValueFieldNumber].Accessor.SetValue(message, false);
break;
case JsonTokenType.Null:
fields[Value.NullValueFieldNumber].Accessor.SetValue(message, 0);
break;
default:
throw new InvalidOperationException("Unexpected token type: " + reader.TokenType);
}
return message;
}
public override void Write(Utf8JsonWriter writer, TMessage value, JsonSerializerOptions options)
{
var specifiedField = value.Descriptor.Oneofs[0].Accessor.GetCaseFieldDescriptor(value);
if (specifiedField == null)
{
throw new InvalidOperationException("Value message must contain a value for the oneof.");
}
object v = specifiedField.Accessor.GetValue(value);
switch (specifiedField.FieldNumber)
{
case Value.BoolValueFieldNumber:
case Value.StringValueFieldNumber:
case Value.NumberValueFieldNumber:
case Value.StructValueFieldNumber:
case Value.ListValueFieldNumber:
JsonSerializer.Serialize(writer, v, v.GetType(), options);
break;
case Value.NullValueFieldNumber:
writer.WriteNullValue();
break;
default:
throw new InvalidOperationException("Unexpected case in struct field: " + specifiedField.FieldNumber);
}
}
}
}

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

@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Text.Json;
using System.Text.Json.Serialization;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Type = System.Type;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json
{
internal sealed class WrapperConverter<TMessage> : JsonConverter<TMessage> where TMessage : IMessage, new()
{
public WrapperConverter(JsonSettings settings)
{
}
public override TMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var message = new TMessage();
var valueDescriptor = message.Descriptor.Fields[JsonConverterHelper.WrapperValueFieldNumber];
var t = JsonConverterHelper.GetFieldType(valueDescriptor);
var value = JsonSerializer.Deserialize(ref reader, t, options);
valueDescriptor.Accessor.SetValue(message, value);
return message;
}
public override void Write(Utf8JsonWriter writer, TMessage value, JsonSerializerOptions options)
{
var valueDescriptor = value.Descriptor.Fields[JsonConverterHelper.WrapperValueFieldNumber];
var innerValue = valueDescriptor.Accessor.GetValue(value);
JsonSerializer.Serialize(writer, innerValue, innerValue.GetType(), options);
}
}
}

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

@ -0,0 +1,80 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal
{
internal static class JsonRequestHelpers
{
public const string JsonContentType = "application/json";
public const string JsonContentTypeWithCharset = "application/json; charset=utf-8";
public static bool HasJsonContentType(HttpRequest request, out StringSegment charset)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (!MediaTypeHeaderValue.TryParse(request.ContentType, out var mt))
{
charset = StringSegment.Empty;
return false;
}
// Matches application/json
if (mt.MediaType.Equals(JsonContentType, StringComparison.OrdinalIgnoreCase))
{
charset = mt.Charset;
return true;
}
// Matches +json, e.g. application/ld+json
if (mt.Suffix.Equals("json", StringComparison.OrdinalIgnoreCase))
{
charset = mt.Charset;
return true;
}
charset = StringSegment.Empty;
return false;
}
public static (Stream stream, bool usesTranscodingStream) GetStream(Stream innerStream, Encoding? encoding)
{
if (encoding == null || encoding.CodePage == Encoding.UTF8.CodePage)
{
return (innerStream, false);
}
var stream = Encoding.CreateTranscodingStream(innerStream, encoding, Encoding.UTF8, leaveOpen: true);
return (stream, true);
}
public static Encoding? GetEncodingFromCharset(StringSegment charset)
{
if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase))
{
// This is an optimization for utf-8 that prevents the Substring caused by
// charset.Value
return Encoding.UTF8;
}
try
{
// charset.Value might be an invalid encoding name as in charset=invalid.
return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Unable to read the request as JSON because the request content type charset '{charset}' is not a known encoding.", ex);
}
}
}
}

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

@ -1,39 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using Microsoft.AspNetCore.WebUtilities;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Internal
{
internal sealed class PropertyWrappingTextReader : TextReader
{
private readonly HttpRequestStreamReader _inner;
private readonly string _prefix;
private int _index;
private bool _finished;
public PropertyWrappingTextReader(HttpRequestStreamReader inner, string propertyName)
{
_inner = inner;
_prefix = @"{""" + propertyName + @""":";
}
public override int Read()
{
if (_index < _prefix.Length)
{
return _prefix[_index++];
}
var c = _inner.Read();
if (c == -1 && !_finished)
{
_finished = true;
return '}';
}
return c;
}
}
}

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

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Google.Protobuf.Reflection;
namespace Microsoft.AspNetCore.Grpc.HttpApi
{
// TODO - improve names. boolean property values should aim to be false
public class JsonSettings
{
/// <summary>
/// Whether fields which would otherwise not be included in the formatted data
/// should be formatted even when the value is not present, or has the default value.
/// This option only affects fields which don't support "presence" (e.g.
/// singular non-optional proto3 primitive fields).
/// </summary>
public bool FormatDefaultValues { get; set; } = true;
public bool FormatEnumsAsIntegers { get; set; }
public TypeRegistry TypeRegistry { get; set; } = TypeRegistry.Empty;
public bool WriteIndented { get; set; } = true;
}
}

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

@ -6,7 +6,7 @@
<IsPackable>true</IsPackable>
<IsShipping>true</IsShipping>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<!-- Disable analysis for ConfigureAwait(false) -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CA2007</WarningsNotAsErrors>

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

@ -9,3 +9,10 @@ using System.Runtime.CompilerServices;
"d333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307" +
"e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c3" +
"08055da9")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Grpc.Microbenchmarks,PublicKey=" +
"0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67" +
"871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0b" +
"d333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307" +
"e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c3" +
"08055da9")]

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

@ -2,13 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Text.Json;
using System.Threading.Tasks;
using Google.Protobuf;
using Google.Protobuf.Reflection;
@ -17,8 +18,9 @@ using Grpc.Gateway.Runtime;
using Grpc.Shared.HttpApi;
using Grpc.Shared.Server;
using Microsoft.AspNetCore.Grpc.HttpApi.Internal;
using Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace Microsoft.AspNetCore.Grpc.HttpApi
{
@ -30,14 +32,14 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
private readonly UnaryServerMethodInvoker<TService, TRequest, TResponse> _unaryMethodInvoker;
private readonly FieldDescriptor? _responseBodyDescriptor;
private readonly MessageDescriptor? _bodyDescriptor;
private readonly bool _bodyDescriptorRepeated;
private readonly List<FieldDescriptor>? _bodyFieldDescriptors;
private readonly string? _bodyFieldDescriptorsPath;
private readonly Dictionary<string, List<FieldDescriptor>> _routeParameterDescriptors;
private readonly JsonFormatter _jsonFormatter;
private readonly JsonParser _jsonParser;
private readonly ConcurrentDictionary<string, List<FieldDescriptor>?> _pathDescriptorsCache;
private readonly List<FieldDescriptor>? _resolvedBodyFieldDescriptors;
private readonly JsonSerializerOptions _options;
[MemberNotNull(nameof(_bodyFieldDescriptors))]
private bool BodyDescriptorRepeated { get; }
public UnaryServerCallHandler(
UnaryServerMethodInvoker<TService, TRequest, TResponse> unaryMethodInvoker,
@ -46,25 +48,20 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
bool bodyDescriptorRepeated,
List<FieldDescriptor>? bodyFieldDescriptors,
Dictionary<string, List<FieldDescriptor>> routeParameterDescriptors,
GrpcHttpApiOptions httpApiOptions)
JsonSerializerOptions options)
{
_unaryMethodInvoker = unaryMethodInvoker;
_responseBodyDescriptor = responseBodyDescriptor;
_bodyDescriptor = bodyDescriptor;
_bodyDescriptorRepeated = bodyDescriptorRepeated;
BodyDescriptorRepeated = bodyDescriptorRepeated;
_bodyFieldDescriptors = bodyFieldDescriptors;
if (_bodyFieldDescriptors != null)
{
_bodyFieldDescriptorsPath = string.Join('.', _bodyFieldDescriptors.Select(d => d.Name));
}
if (_bodyDescriptorRepeated && _bodyFieldDescriptors != null)
{
_resolvedBodyFieldDescriptors = _bodyFieldDescriptors.Take(_bodyFieldDescriptors.Count - 1).ToList();
}
_routeParameterDescriptors = routeParameterDescriptors;
_jsonFormatter = httpApiOptions.JsonFormatter;
_jsonParser = httpApiOptions.JsonParser;
_pathDescriptorsCache = new ConcurrentDictionary<string, List<FieldDescriptor>?>(StringComparer.Ordinal);
_options = options;
}
public async Task HandleCallAsync(HttpContext httpContext)
@ -118,45 +115,26 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
private async Task<(IMessage? requestMessage, StatusCode statusCode, string? errorMessage)> CreateMessage(HttpRequest request)
{
IMessage? requestMessage;
IMessage requestMessage;
if (_bodyDescriptor != null)
{
if (request.ContentType == null ||
!request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
if (!JsonRequestHelpers.HasJsonContentType(request, out var charset))
{
return (null, StatusCode.InvalidArgument, "Request content-type of application/json is required.");
}
if (!request.Body.CanSeek)
var encoding = JsonRequestHelpers.GetEncodingFromCharset(charset);
var (stream, usesTranscodingStream) = JsonRequestHelpers.GetStream(request.HttpContext.Request.Body, encoding);
try
{
// JsonParser does synchronous reads. In order to avoid blocking on the stream, we asynchronously
// read everything into a buffer, and then seek back to the beginning.
request.EnableBuffering();
Debug.Assert(request.Body.CanSeek);
await request.Body.DrainAsync(CancellationToken.None);
request.Body.Seek(0L, SeekOrigin.Begin);
}
var encoding = RequestEncoding.SelectCharacterEncoding(request);
// TODO: Handle unsupported encoding
using (var requestReader = new HttpRequestStreamReader(request.Body, encoding))
{
if (_bodyDescriptorRepeated)
if (BodyDescriptorRepeated)
{
var containingMessage = ParseRepeatedContent(requestReader);
requestMessage = (IMessage)Activator.CreateInstance<TRequest>();
var repeatedContent = await ParseRepeatedContentAsync(stream);
if (_resolvedBodyFieldDescriptors!.Count > 0)
{
requestMessage = (IMessage)Activator.CreateInstance<TRequest>();
ServiceDescriptorHelpers.RecursiveSetValue(requestMessage, _resolvedBodyFieldDescriptors, containingMessage);
}
else
{
requestMessage = containingMessage;
}
ServiceDescriptorHelpers.RecursiveSetValue(requestMessage, _bodyFieldDescriptors, repeatedContent);
}
else
{
@ -164,13 +142,13 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
try
{
bodyContent = _jsonParser.Parse(requestReader, _bodyDescriptor);
bodyContent = (IMessage)(await JsonSerializer.DeserializeAsync(stream, _bodyDescriptor.ClrType, _options))!;
}
catch (InvalidJsonException)
catch (JsonException)
{
return (null, StatusCode.InvalidArgument, "Request JSON payload is not correctly formatted.");
}
catch (InvalidProtocolBufferException exception)
catch (Exception exception)
{
return (null, StatusCode.InvalidArgument, exception.Message);
}
@ -178,7 +156,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
if (_bodyFieldDescriptors != null)
{
requestMessage = (IMessage)Activator.CreateInstance<TRequest>();
ServiceDescriptorHelpers.RecursiveSetValue(requestMessage, _bodyFieldDescriptors, bodyContent);
ServiceDescriptorHelpers.RecursiveSetValue(requestMessage, _bodyFieldDescriptors, bodyContent!); // TODO - check nullability
}
else
{
@ -186,6 +164,13 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
}
}
}
finally
{
if (usesTranscodingStream)
{
await stream.DisposeAsync();
}
}
}
else
{
@ -227,21 +212,12 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
});
}
private IMessage ParseRepeatedContent(HttpRequestStreamReader requestReader)
private async ValueTask<IList> ParseRepeatedContentAsync(Stream inputStream)
{
// The following code is SUPER hacky.
//
// Problem:
// JsonParser doesn't provide a way to directly parse a JSON array to repeated fields.
// JsonParser's Parse methods only support reading JSON objects as methods.
//
// Solution:
// To get around this limitation a wrapping TextReader is created that inserts a wrapping
// object into the JSON passed to the parser. The parser returns the containing message
// with the repeated fields set on it.
var containingType = _bodyFieldDescriptors.Last()!.ContainingType;
var type = JsonConverterHelper.GetFieldType(_bodyFieldDescriptors!.Last());
var listType = typeof(List<>).MakeGenericType(type);
return _jsonParser.Parse(new PropertyWrappingTextReader(requestReader, _bodyFieldDescriptors.Last().JsonName), containingType);
return (IList)(await JsonSerializer.DeserializeAsync(inputStream, listType, _options))!;
}
private async Task SendResponse(HttpResponse response, Encoding encoding, TResponse message)
@ -254,7 +230,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
}
response.StatusCode = StatusCodes.Status200OK;
response.ContentType = "application/json";
response.ContentType = MediaType.ReplaceEncoding("application/json", encoding);
await WriteResponseMessage(response, encoding, responseBody);
}
@ -269,28 +245,25 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi
};
response.StatusCode = MapStatusCodeToHttpStatus(statusCode);
response.ContentType = "application/json";
response.ContentType = MediaType.ReplaceEncoding("application/json", encoding);
await WriteResponseMessage(response, encoding, e);
}
private async Task WriteResponseMessage(HttpResponse response, Encoding encoding, object responseBody)
{
using (var writer = new HttpResponseStreamWriter(response.Body, encoding))
{
if (responseBody is IMessage responseMessage)
{
_jsonFormatter.Format(responseMessage, writer);
}
else
{
_jsonFormatter.WriteValue(writer, responseBody);
}
var (stream, usesTranscodingStream) = JsonRequestHelpers.GetStream(response.Body, encoding);
// Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
// buffers. This is better than just letting dispose handle it (which would result in a synchronous
// write).
await writer.FlushAsync();
try
{
await JsonSerializer.SerializeAsync(stream, responseBody, _options);
}
finally
{
if (usesTranscodingStream)
{
await stream.DisposeAsync();
}
}
}

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

@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Grpc.Swagger.Internal
apiDescription.HttpMethod = verb;
apiDescription.ActionDescriptor = new ActionDescriptor
{
RouteValues = new Dictionary<string, string>
RouteValues = new Dictionary<string, string?>
{
// Swagger uses this to group endpoints together.
// Group methods together using the service name.

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

@ -6,7 +6,7 @@
<IsPackable>true</IsPackable>
<IsShipping>true</IsShipping>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<!-- Disable analysis for ConfigureAwait(false) -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CA2007</WarningsNotAsErrors>

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

@ -156,6 +156,13 @@ namespace Grpc.Shared.HttpApi
list.Add(ConvertValue(value, field));
}
}
else if (values is IList listValues)
{
foreach (var value in listValues)
{
list.Add(ConvertValue(value, field));
}
}
else
{
list.Add(ConvertValue(values, field));

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

@ -0,0 +1,481 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using Google.Protobuf.WellKnownTypes;
using HttpApi;
using Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests.ConverterTests
{
public class JsonConverterReadTests
{
private readonly ITestOutputHelper _output;
public JsonConverterReadTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void ReadObjectProperties()
{
var json = @"{
""name"": ""test"",
""age"": 1
}";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void RepeatedStrings()
{
var json = @"{
""name"": ""test"",
""repeatedStrings"": [
""One"",
""Two"",
""Three""
]
}";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void DataTypes_DefaultValues()
{
var json = @"{
""singleInt32"": 0,
""singleInt64"": ""0"",
""singleUint32"": 0,
""singleUint64"": ""0"",
""singleSint32"": 0,
""singleSint64"": ""0"",
""singleFixed32"": 0,
""singleFixed64"": ""0"",
""singleSfixed32"": 0,
""singleSfixed64"": ""0"",
""singleFloat"": 0,
""singleDouble"": 0,
""singleBool"": false,
""singleString"": """",
""singleBytes"": """",
""singleEnum"": ""NESTED_ENUM_UNSPECIFIED""
}";
AssertReadJson<HelloRequest.Types.DataTypes>(json);
}
[Theory]
[InlineData(1)]
[InlineData(-1)]
[InlineData(100)]
public void Enum_ReadNumber(int value)
{
var json = @"{ ""singleEnum"": " + value + " }";
AssertReadJson<HelloRequest.Types.DataTypes>(json);
}
[Fact]
public void Timestamp_Nested()
{
var json = @"{ ""timestampValue"": ""2020-12-01T00:30:00Z"" }";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void Duration_Nested()
{
var json = @"{ ""durationValue"": ""43200s"" }";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void Value_Nested()
{
var json = @"{
""valueValue"": {
""enabled"": true,
""metadata"": [
""value1"",
""value2""
]
}
}";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void Value_Root()
{
var json = @"{
""enabled"": true,
""metadata"": [
""value1"",
""value2""
]
}";
AssertReadJson<Value>(json);
}
[Fact]
public void Struct_Nested()
{
var json = @"{
""structValue"": {
""enabled"": true,
""metadata"": [
""value1"",
""value2""
]
}
}";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void Struct_Root()
{
var json = @"{
""enabled"": true,
""metadata"": [
""value1"",
""value2""
]
}";
AssertReadJson<Struct>(json);
}
[Fact]
public void ListValue_Nested()
{
var json = @"{
""listValue"": [
true,
""value1"",
""value2""
]
}";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void ListValue_Root()
{
var json = @"[
true,
""value1"",
""value2""
]";
AssertReadJson<ListValue>(json);
}
[Fact]
public void Int64_ReadNumber()
{
var json = @"{
""singleInt64"": 1,
""singleUint64"": 2,
""singleSint64"": 3,
""singleFixed64"": 4,
""singleSfixed64"": 5
}";
AssertReadJson<HelloRequest.Types.DataTypes>(json);
}
[Fact]
public void RepeatedDoubleValues()
{
var json = @"{
""repeatedDoubleValues"": [
1,
1.1
]
}";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void Any()
{
var json = @"{
""@type"": ""type.googleapis.com/http_api.HelloRequest"",
""name"": ""In any!""
}";
var any = AssertReadJson<Any>(json);
var helloRequest = any.Unpack<HelloRequest>();
Assert.Equal("In any!", helloRequest.Name);
}
[Fact]
public void Any_WellKnownType_Timestamp()
{
var json = @"{
""@type"": ""type.googleapis.com/google.protobuf.Timestamp"",
""value"": ""1970-01-01T00:00:00Z""
}";
var any = AssertReadJson<Any>(json);
var timestamp = any.Unpack<Timestamp>();
Assert.Equal(DateTimeOffset.UnixEpoch, timestamp.ToDateTimeOffset());
}
[Fact]
public void Any_WellKnownType_Int32()
{
var json = @"{
""@type"": ""type.googleapis.com/google.protobuf.Int32Value"",
""value"": 2147483647
}";
var any = AssertReadJson<Any>(json);
var value = any.Unpack<Int32Value>();
Assert.Equal(2147483647, value.Value);
}
[Fact]
public void MapMessages()
{
var json = @"{
""mapMessage"": {
""name1"": {
""subfield"": ""value1""
},
""name2"": {
""subfield"": ""value2""
}
}
}";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void MapKeyBool()
{
var json = @"{
""mapKeybool"": {
""true"": ""value1"",
""false"": ""value2""
}
}";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void MapKeyInt()
{
var json = @"{
""mapKeyint"": {
""-1"": ""value1"",
""0"": ""value3""
}
}";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void OneOf_Success()
{
var json = @"{
""oneofName1"": ""test""
}";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void OneOf_Failure()
{
var json = @"{
""oneofName1"": ""test"",
""oneofName2"": ""test""
}";
AssertReadJsonError<HelloRequest>(json, ex => Assert.Equal("Multiple values specified for oneof oneof_test", ex.Message));
}
[Fact]
public void NullableWrappers_NaN()
{
var json = @"{
""doubleValue"": ""NaN""
}";
AssertReadJson<HelloRequest.Types.Wrappers>(json);
}
[Fact]
public void NullableWrappers_Null()
{
var json = @"{
""stringValue"": null,
""int32Value"": null,
""int64Value"": null,
""floatValue"": null,
""doubleValue"": null,
""boolValue"": null,
""uint32Value"": null,
""uint64Value"": null,
""bytesValue"": null
}";
AssertReadJson<HelloRequest.Types.Wrappers>(json);
}
[Fact]
public void NullableWrappers()
{
var json = @"{
""stringValue"": ""A string"",
""int32Value"": 1,
""int64Value"": ""2"",
""floatValue"": 1.2,
""doubleValue"": 1.1,
""boolValue"": true,
""uint32Value"": 3,
""uint64Value"": ""4"",
""bytesValue"": ""SGVsbG8gd29ybGQ=""
}";
AssertReadJson<HelloRequest.Types.Wrappers>(json);
}
[Fact]
public void NullValue_Default_Null()
{
var json = @"{ ""nullValue"": null }";
AssertReadJson<NullValueContainer>(json);
}
[Fact]
public void NullValue_Default_String()
{
var json = @"{ ""nullValue"": ""NULL_VALUE"" }";
AssertReadJson<NullValueContainer>(json);
}
[Fact]
public void NullValue_NonDefaultValue_Int()
{
var json = @"{ ""nullValue"": 1 }";
AssertReadJson<NullValueContainer>(json);
}
[Fact]
public void NullValue_NonDefaultValue_String()
{
var json = @"{ ""nullValue"": ""MONKEY"" }";
AssertReadJsonError<NullValueContainer>(json, ex => Assert.Equal("Invalid enum value: MONKEY for enum type: google.protobuf.NullValue", ex.Message));
}
[Fact]
public void FieldMask_Nested()
{
var json = @"{ ""fieldMaskValue"": ""value1,value2,value3.nestedValue"" }";
AssertReadJson<HelloRequest>(json);
}
[Fact]
public void FieldMask_Root()
{
var json = @"""value1,value2,value3.nestedValue""";
AssertReadJson<FieldMask>(json);
}
[Fact]
public void NullableWrapper_Root_Int32()
{
var json = @"1";
AssertReadJson<Int32Value>(json);
}
[Fact]
public void NullableWrapper_Root_Int64()
{
var json = @"""1""";
AssertReadJson<Int64Value>(json);
}
private TValue AssertReadJson<TValue>(string value, JsonSettings? settings = null) where TValue : IMessage, new()
{
var typeRegistery = TypeRegistry.FromFiles(
HelloRequest.Descriptor.File,
Timestamp.Descriptor.File);
var formatter = new JsonParser(new JsonParser.Settings(
recursionLimit: int.MaxValue,
typeRegistery));
var objectOld = formatter.Parse<TValue>(value);
var jsonSerializerOptions = CreateSerializerOptions(settings, typeRegistery);
var objectNew = JsonSerializer.Deserialize<TValue>(value, jsonSerializerOptions)!;
_output.WriteLine("New:");
_output.WriteLine(objectNew.ToString());
_output.WriteLine("Old:");
_output.WriteLine(objectOld.ToString());
Assert.True(objectNew.Equals(objectOld));
return objectNew;
}
private void AssertReadJsonError<TValue>(string value, Action<Exception> assertException, JsonSettings? settings = null) where TValue : IMessage, new()
{
var typeRegistery = TypeRegistry.FromFiles(
HelloRequest.Descriptor.File,
Timestamp.Descriptor.File);
var jsonSerializerOptions = CreateSerializerOptions(settings, typeRegistery);
var ex = Assert.ThrowsAny<Exception>(() => JsonSerializer.Deserialize<TValue>(value, jsonSerializerOptions));
assertException(ex);
var formatter = new JsonParser(new JsonParser.Settings(
recursionLimit: int.MaxValue,
typeRegistery));
ex = Assert.ThrowsAny<Exception>(() => formatter.Parse<TValue>(value));
assertException(ex);
}
internal static JsonSerializerOptions CreateSerializerOptions(JsonSettings? settings, TypeRegistry typeRegistery)
{
var resolvedSettings = settings ?? new JsonSettings { TypeRegistry = typeRegistery };
return JsonConverterHelper.CreateSerializerOptions(resolvedSettings);
}
}
}

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

@ -0,0 +1,468 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using System.Text.Json;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using Google.Protobuf.WellKnownTypes;
using HttpApi;
using Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests.ConverterTests
{
public class JsonConverterWriteTests
{
private readonly ITestOutputHelper _output;
public JsonConverterWriteTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void NonAsciiString()
{
var helloRequest = new HelloRequest
{
Name = "This is a test 激光這兩個字是甚麼意思 string"
};
AssertWrittenJson(helloRequest, compareRawStrings: true);
}
[Fact]
public void RepeatedStrings()
{
var helloRequest = new HelloRequest
{
Name = "test",
RepeatedStrings =
{
"One",
"Two",
"Three"
}
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void RepeatedDoubleValues()
{
var helloRequest = new HelloRequest
{
RepeatedDoubleValues =
{
1,
1.1
}
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void MapStrings()
{
var helloRequest = new HelloRequest
{
MapStrings =
{
["name1"] = "value1",
["name2"] = "value2"
}
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void MapKeyBool()
{
var helloRequest = new HelloRequest
{
MapKeybool =
{
[true] = "value1",
[false] = "value2"
}
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void MapKeyInt()
{
var helloRequest = new HelloRequest
{
MapKeyint =
{
[-1] = "value1",
[0] = "value2",
[0] = "value3"
}
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void MapMessages()
{
var helloRequest = new HelloRequest
{
MapMessage =
{
["name1"] = new HelloRequest.Types.SubMessage { Subfield = "value1" },
["name2"] = new HelloRequest.Types.SubMessage { Subfield = "value2" }
}
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void DataTypes_DefaultValues()
{
var wrappers = new HelloRequest.Types.DataTypes();
AssertWrittenJson(wrappers, new JsonSettings { FormatDefaultValues = true });
}
[Fact]
public void NullableWrappers_NaN()
{
var wrappers = new HelloRequest.Types.Wrappers
{
DoubleValue = double.NaN
};
AssertWrittenJson(wrappers);
}
[Fact]
public void NullValue_Default()
{
var m = new NullValueContainer();
AssertWrittenJson(m, new JsonSettings { FormatDefaultValues = true });
}
[Fact]
public void NullValue_NonDefaultValue()
{
var m = new NullValueContainer
{
NullValue = (NullValue)1
};
AssertWrittenJson(m, new JsonSettings { FormatDefaultValues = true });
}
[Fact]
public void NullableWrappers()
{
var wrappers = new HelloRequest.Types.Wrappers
{
BoolValue = true,
BytesValue = ByteString.CopyFrom(Encoding.UTF8.GetBytes("Hello world")),
DoubleValue = 1.1,
FloatValue = 1.2f,
Int32Value = 1,
Int64Value = 2L,
StringValue = "A string",
Uint32Value = 3U,
Uint64Value = 4UL
};
AssertWrittenJson(wrappers);
}
[Fact]
public void NullableWrapper_Root_Int32()
{
var v = new Int32Value { Value = 1 };
AssertWrittenJson(v);
}
[Fact]
public void NullableWrapper_Root_Int64()
{
var v = new Int64Value { Value = 1 };
AssertWrittenJson(v);
}
[Fact]
public void Any()
{
var helloRequest = new HelloRequest
{
Name = "In any!"
};
var any = Google.Protobuf.WellKnownTypes.Any.Pack(helloRequest);
AssertWrittenJson(any);
}
[Fact]
public void Any_WellKnownType_Timestamp()
{
var timestamp = Timestamp.FromDateTimeOffset(DateTimeOffset.UnixEpoch);
var any = Google.Protobuf.WellKnownTypes.Any.Pack(timestamp);
AssertWrittenJson(any);
}
[Fact]
public void Any_WellKnownType_Int32()
{
var value = new Int32Value() { Value = int.MaxValue };
var any = Google.Protobuf.WellKnownTypes.Any.Pack(value);
AssertWrittenJson(any);
}
[Fact]
public void Timestamp_Nested()
{
var helloRequest = new HelloRequest
{
TimestampValue = Timestamp.FromDateTimeOffset(new DateTimeOffset(2020, 12, 1, 12, 30, 0, TimeSpan.FromHours(12)))
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void Timestamp_Root()
{
var ts = Timestamp.FromDateTimeOffset(new DateTimeOffset(2020, 12, 1, 12, 30, 0, TimeSpan.FromHours(12)));
AssertWrittenJson(ts);
}
[Fact]
public void Duration_Nested()
{
var helloRequest = new HelloRequest
{
DurationValue = Duration.FromTimeSpan(TimeSpan.FromHours(12))
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void Duration_Root()
{
var duration = Duration.FromTimeSpan(TimeSpan.FromHours(12));
AssertWrittenJson(duration);
}
[Fact]
public void Value_Nested()
{
var helloRequest = new HelloRequest
{
ValueValue = Value.ForStruct(new Struct
{
Fields =
{
["enabled"] = Value.ForBool(true),
["metadata"] = Value.ForList(
Value.ForString("value1"),
Value.ForString("value2"))
}
})
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void Value_Root()
{
var value = Value.ForStruct(new Struct
{
Fields =
{
["enabled"] = Value.ForBool(true),
["metadata"] = Value.ForList(
Value.ForString("value1"),
Value.ForString("value2"))
}
});
AssertWrittenJson(value);
}
[Fact]
public void Struct_Nested()
{
var helloRequest = new HelloRequest
{
StructValue = new Struct
{
Fields =
{
["enabled"] = Value.ForBool(true),
["metadata"] = Value.ForList(
Value.ForString("value1"),
Value.ForString("value2"))
}
}
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void Struct_Root()
{
var value = new Struct
{
Fields =
{
["enabled"] = Value.ForBool(true),
["metadata"] = Value.ForList(
Value.ForString("value1"),
Value.ForString("value2"))
}
};
AssertWrittenJson(value);
}
[Fact]
public void ListValue_Nested()
{
var helloRequest = new HelloRequest
{
ListValue = new ListValue
{
Values =
{
Value.ForBool(true),
Value.ForString("value1"),
Value.ForString("value2")
}
}
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void ListValue_Root()
{
var value = new ListValue
{
Values =
{
Value.ForBool(true),
Value.ForString("value1"),
Value.ForString("value2")
}
};
AssertWrittenJson(value);
}
[Fact]
public void FieldMask_Nested()
{
var helloRequest = new HelloRequest
{
FieldMaskValue = FieldMask.FromString("value1,value2,value3.nested_value"),
};
AssertWrittenJson(helloRequest);
}
[Fact]
public void FieldMask_Root()
{
var m = FieldMask.FromString("value1,value2,value3.nested_value");
AssertWrittenJson(m);
}
[Theory]
[InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Unspecified)]
[InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Bar)]
[InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Neg)]
[InlineData((HelloRequest.Types.DataTypes.Types.NestedEnum)100)]
public void Enum(HelloRequest.Types.DataTypes.Types.NestedEnum value)
{
var dataTypes = new HelloRequest.Types.DataTypes
{
SingleEnum = value
};
AssertWrittenJson(dataTypes);
}
[Theory]
[InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Unspecified)]
[InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Bar)]
[InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Neg)]
[InlineData((HelloRequest.Types.DataTypes.Types.NestedEnum)100)]
public void Enum_WriteNumber(HelloRequest.Types.DataTypes.Types.NestedEnum value)
{
var dataTypes = new HelloRequest.Types.DataTypes
{
SingleEnum = value
};
AssertWrittenJson(dataTypes, new JsonSettings { FormatEnumsAsIntegers = true, FormatDefaultValues = false });
}
private void AssertWrittenJson<TValue>(TValue value, JsonSettings? settings = null, bool? compareRawStrings = null) where TValue : IMessage
{
var typeRegistery = TypeRegistry.FromFiles(
HelloRequest.Descriptor.File,
Timestamp.Descriptor.File);
settings = settings ?? new JsonSettings { TypeRegistry = typeRegistery, FormatDefaultValues = false };
var jsonSerializerOptions = CreateSerializerOptions(settings, typeRegistery);
var formatterSettings = new JsonFormatter.Settings(
formatDefaultValues: settings.FormatDefaultValues,
typeRegistery);
formatterSettings = formatterSettings.WithFormatEnumsAsIntegers(settings.FormatEnumsAsIntegers);
var formatter = new JsonFormatter(formatterSettings);
var jsonOld = formatter.Format(value);
_output.WriteLine("Old:");
_output.WriteLine(jsonOld);
var jsonNew = JsonSerializer.Serialize(value, jsonSerializerOptions);
_output.WriteLine("New:");
_output.WriteLine(jsonNew);
using var doc1 = JsonDocument.Parse(jsonNew);
using var doc2 = JsonDocument.Parse(jsonOld);
var comparer = new JsonElementComparer(maxHashDepth: -1, compareRawStrings: compareRawStrings ?? false);
Assert.True(comparer.Equals(doc1.RootElement, doc2.RootElement));
}
internal static JsonSerializerOptions CreateSerializerOptions(JsonSettings? settings, TypeRegistry typeRegistery)
{
var resolvedSettings = settings ?? new JsonSettings { TypeRegistry = typeRegistery };
return JsonConverterHelper.CreateSerializerOptions(resolvedSettings);
}
}
}

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

@ -0,0 +1,141 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests.ConverterTests
{
public class JsonElementComparer : IEqualityComparer<JsonElement>
{
public JsonElementComparer() : this(maxHashDepth: -1, compareRawStrings: false) { }
public JsonElementComparer(int maxHashDepth, bool compareRawStrings)
{
MaxHashDepth = maxHashDepth;
CompareRawStrings = compareRawStrings;
}
private int MaxHashDepth { get; }
private bool CompareRawStrings { get; }
#region IEqualityComparer<JsonElement> Members
public bool Equals(JsonElement x, JsonElement y)
{
if (x.ValueKind != y.ValueKind)
return false;
switch (x.ValueKind)
{
case JsonValueKind.Null:
case JsonValueKind.True:
case JsonValueKind.False:
case JsonValueKind.Undefined:
return true;
// Compare the raw values of numbers, and the text of strings.
// Note this means that 0.0 will differ from 0.00 -- which may be correct as deserializing either to `decimal` will result in subtly different results.
// Newtonsoft's JValue.Compare(JTokenType valueType, object? objA, object? objB) has logic for detecting "equivalent" values,
// you may want to examine it to see if anything there is required here.
// https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Linq/JValue.cs#L246
case JsonValueKind.Number:
return x.GetRawText() == y.GetRawText();
case JsonValueKind.String:
if (CompareRawStrings)
{
return x.GetRawText() == y.GetRawText();
}
else
{
// Automatically resolve JSON escape sequences to their corresponding characters.
return x.GetString() == y.GetString();
}
case JsonValueKind.Array:
return x.EnumerateArray().SequenceEqual(y.EnumerateArray(), this);
case JsonValueKind.Object:
{
// Surprisingly, JsonDocument fully supports duplicate property names.
// I.e. it's perfectly happy to parse {"Value":"a", "Value" : "b"} and will store both
// key/value pairs inside the document!
// A close reading of https://www.rfc-editor.org/rfc/rfc8259#section-4 seems to indicate that
// such objects are allowed but not recommended, and when they arise, interpretation of
// identically-named properties is order-dependent.
// So stably sorting by name then comparing values seems the way to go.
var xPropertiesUnsorted = x.EnumerateObject().ToList();
var yPropertiesUnsorted = y.EnumerateObject().ToList();
if (xPropertiesUnsorted.Count != yPropertiesUnsorted.Count)
return false;
var xProperties = xPropertiesUnsorted.OrderBy(p => p.Name, StringComparer.Ordinal);
var yProperties = yPropertiesUnsorted.OrderBy(p => p.Name, StringComparer.Ordinal);
foreach (var (px, py) in xProperties.Zip(yProperties))
{
if (px.Name != py.Name)
return false;
if (!Equals(px.Value, py.Value))
return false;
}
return true;
}
default:
throw new JsonException(string.Format("Unknown JsonValueKind {0}", x.ValueKind));
}
}
public int GetHashCode(JsonElement obj)
{
var hash = new HashCode(); // New in .Net core: https://docs.microsoft.com/en-us/dotnet/api/system.hashcode
ComputeHashCode(obj, ref hash, 0);
return hash.ToHashCode();
}
void ComputeHashCode(JsonElement obj, ref HashCode hash, int depth)
{
hash.Add(obj.ValueKind);
switch (obj.ValueKind)
{
case JsonValueKind.Null:
case JsonValueKind.True:
case JsonValueKind.False:
case JsonValueKind.Undefined:
break;
case JsonValueKind.Number:
hash.Add(obj.GetRawText());
break;
case JsonValueKind.String:
hash.Add(obj.GetString());
break;
case JsonValueKind.Array:
if (depth != MaxHashDepth)
foreach (var item in obj.EnumerateArray())
ComputeHashCode(item, ref hash, depth + 1);
else
hash.Add(obj.GetArrayLength());
break;
case JsonValueKind.Object:
foreach (var property in obj.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
{
hash.Add(property.Name);
if (depth != MaxHashDepth)
ComputeHashCode(property.Value, ref hash, depth + 1);
}
break;
default:
throw new JsonException(string.Format("Unknown JsonValueKind {0}", obj.ValueKind));
}
}
#endregion
}
}

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

@ -23,8 +23,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests
var serviceProvider = services.BuildServiceProvider();
var options1 = serviceProvider.GetRequiredService<IOptions<GrpcHttpApiOptions>>().Value;
Assert.NotNull(options1.JsonFormatter);
Assert.NotNull(options1.JsonParser);
Assert.NotNull(options1.JsonSettings);
var options2 = serviceProvider.GetRequiredService<IOptions<GrpcHttpApiOptions>>().Value;
@ -35,24 +34,21 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests
public void AddGrpcHttpApi_OverrideOptions_OptionsApplied()
{
// Arrange
var jsonFormatter = new JsonFormatter(new JsonFormatter.Settings(formatDefaultValues: false));
var jsonParser = new JsonParser(new JsonParser.Settings(recursionLimit: 1));
var settings = new JsonSettings();
var services = new ServiceCollection();
// Act
services.AddGrpcHttpApi(o =>
{
o.JsonFormatter = jsonFormatter;
o.JsonParser = jsonParser;
o.JsonSettings = settings;
});
// Assert
var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<GrpcHttpApiOptions>>().Value;
Assert.Equal(jsonFormatter, options.JsonFormatter);
Assert.Equal(jsonParser, options.JsonParser);
Assert.Equal(settings, options.JsonSettings);
}
}
}

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

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests
// Assert
var endpoint = FindGrpcEndpoint(endpoints, nameof(HttpApiGreeterService.SayHello));
Assert.Equal("GET", endpoint.Metadata.GetMetadata<IHttpMethodMetadata>().HttpMethods.Single());
Assert.Equal("GET", endpoint.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods.Single());
Assert.Equal("/v1/greeter/{name}", endpoint.RoutePattern.RawText);
Assert.Equal(1, endpoint.RoutePattern.Parameters.Count);
Assert.Equal("name", endpoint.RoutePattern.Parameters[0].Name);
@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests
var endpoint = FindGrpcEndpoint(endpoints, nameof(HttpApiGreeterService.Custom));
Assert.Equal("/v1/greeter/{name}", endpoint.RoutePattern.RawText);
Assert.Equal("HEAD", endpoint.Metadata.GetMetadata<IHttpMethodMetadata>().HttpMethods.Single());
Assert.Equal("HEAD", endpoint.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods.Single());
}
[Fact]
@ -58,13 +58,13 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests
Assert.Equal(2, matchedEndpoints.Count);
var getMethodModel = matchedEndpoints[0];
Assert.Equal("GET", getMethodModel.Metadata.GetMetadata<IHttpMethodMetadata>().HttpMethods.Single());
Assert.Equal("/v1/additional_bindings/{name}", getMethodModel.Metadata.GetMetadata<GrpcHttpMetadata>().HttpRule.Get);
Assert.Equal("GET", getMethodModel.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods.Single());
Assert.Equal("/v1/additional_bindings/{name}", getMethodModel.Metadata.GetMetadata<GrpcHttpMetadata>()?.HttpRule.Get);
Assert.Equal("/v1/additional_bindings/{name}", getMethodModel.RoutePattern.RawText);
var additionalMethodModel = matchedEndpoints[1];
Assert.Equal("DELETE", additionalMethodModel.Metadata.GetMetadata<IHttpMethodMetadata>().HttpMethods.Single());
Assert.Equal("/v1/additional_bindings/{name}", additionalMethodModel.Metadata.GetMetadata<GrpcHttpMetadata>().HttpRule.Delete);
Assert.Equal("DELETE", additionalMethodModel.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods.Single());
Assert.Equal("/v1/additional_bindings/{name}", additionalMethodModel.Metadata.GetMetadata<GrpcHttpMetadata>()?.HttpRule.Delete);
Assert.Equal("/v1/additional_bindings/{name}", additionalMethodModel.RoutePattern.RawText);
}

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

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

@ -6,6 +6,10 @@ syntax = "proto3";
import "google/api/annotations.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/field_mask.proto";
package http_api;
@ -141,6 +145,23 @@ message HelloRequest {
Wrappers wrappers = 4;
repeated string repeated_strings = 5;
google.protobuf.Any any_message = 6;
map<string, string> map_strings = 7;
map<string, SubMessage> map_message = 8;
map<bool, string> map_keybool = 9;
map<int32, string> map_keyint = 10;
oneof oneof_test {
string oneof_name1 = 11;
string oneof_name2 = 12;
}
int32 age = 13;
repeated google.protobuf.DoubleValue repeated_double_values = 14;
google.protobuf.Timestamp timestamp_value = 15;
google.protobuf.Duration duration_value = 16;
google.protobuf.Value value_value = 17;
google.protobuf.Struct struct_value = 18;
google.protobuf.ListValue list_value = 19;
google.protobuf.NullValue null_value = 20;
google.protobuf.FieldMask field_mask_value = 21;
}
message HelloReply {
@ -149,3 +170,7 @@ message HelloReply {
google.protobuf.StringValue nullable_message = 3;
google.protobuf.Any any_message = 4;
}
message NullValueContainer {
google.protobuf.NullValue null_value = 1;
}

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

@ -21,6 +21,7 @@ using Grpc.Shared.HttpApi;
using Grpc.Shared.Server;
using Grpc.Tests.Shared;
using HttpApi;
using Microsoft.AspNetCore.Grpc.HttpApi.Internal.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
@ -403,8 +404,8 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests
[Theory]
[InlineData("{malformed_json}", "Request JSON payload is not correctly formatted.")]
[InlineData("{\"name\": 1234}", "Unsupported conversion from JSON number for field type String")]
[InlineData("{\"abcd\": 1234}", "Unknown field: abcd")]
[InlineData("{\"name\": 1234}", "Request JSON payload is not correctly formatted.")]
//[InlineData("{\"abcd\": 1234}", "Unknown field: abcd")]
public async Task HandleCallAsync_MalformedRequestBody_BadRequestReturned(string json, string expectedError)
{
// Arrange
@ -792,22 +793,24 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests
var typeRegistry = TypeRegistry.FromMessages(StringValue.Descriptor, Int32Value.Descriptor);
var jsonFormatter = new JsonFormatter(new JsonFormatter.Settings(formatDefaultValues: true, typeRegistry));
var jsonParser = new JsonParser(new JsonParser.Settings(recursionLimit: 100, typeRegistry));
var unaryServerCallHandler = CreateCallHandler(
invoker,
bodyDescriptor: HelloRequest.Descriptor,
httpApiOptions: new GrpcHttpApiOptions
{
JsonFormatter = jsonFormatter,
JsonParser = jsonParser
JsonSettings = new JsonSettings
{
TypeRegistry = typeRegistry
}
});
var httpContext = CreateHttpContext();
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(jsonFormatter.Format(new HelloRequest
var requestJson = jsonFormatter.Format(new HelloRequest
{
Name = "Test",
AnyMessage = Any.Pack(new Int32Value { Value = 123 })
})));
});
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(requestJson));
httpContext.Request.ContentType = "application/json";
// Act
@ -892,7 +895,9 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests
CreateServiceMethod<HelloRequest, HelloReply>("TestMethodName", HelloRequest.Parser, HelloReply.Parser),
MethodOptions.Create(new[] { serviceOptions }),
new TestGrpcServiceActivator<HttpApiGreeterService>());
var jsonSettings = httpApiOptions?.JsonSettings ?? new JsonSettings();
return new UnaryServerCallHandler<HttpApiGreeterService, HelloRequest, HelloReply>(
unaryServerCallInvoker,
responseBodyDescriptor,
@ -900,7 +905,7 @@ namespace Microsoft.AspNetCore.Grpc.HttpApi.Tests
bodyDescriptorRepeated ?? false,
bodyFieldDescriptors,
routeParameterDescriptors ?? new Dictionary<string, List<FieldDescriptor>>(),
httpApiOptions ?? new GrpcHttpApiOptions());
JsonConverterHelper.CreateSerializerOptions(jsonSettings));
}
private class HttpApiGreeterService : HttpApiGreeter.HttpApiGreeterBase

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

@ -49,8 +49,8 @@ namespace Microsoft.AspNetCore.Grpc.Swagger.Tests
private class TestWebHostEnvironment : IWebHostEnvironment
{
public IFileProvider? WebRootFileProvider { get; set; }
public string? WebRootPath { get; set; }
public IFileProvider WebRootFileProvider { get; set; } = default!;
public string WebRootPath { get; set; } = default!;
public string? ApplicationName { get; set; }
public IFileProvider? ContentRootFileProvider { get; set; }
public string? ContentRootPath { get; set; }

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

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

@ -1,20 +1,5 @@
#region Copyright notice and license
// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#endregion
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
@ -34,4 +19,4 @@ namespace Grpc.Tests.Shared
return default;
}
}
}
}

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
</PropertyGroup>