Update gRPC transcoding to use System.Text.Json (#413)
This commit is contained in:
Родитель
f66a80dd38
Коммит
80c9af5e76
|
@ -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>
|
||||
|
|
Загрузка…
Ссылка в новой задаче