зеркало из https://github.com/dotnet/aspnetcore.git
Add gRPC JSON transcoding (#40242)
This commit is contained in:
Родитель
8b742dc672
Коммит
b4b43e39bc
205
AspNetCore.sln
205
AspNetCore.sln
|
@ -559,7 +559,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Grpc", "Grpc", "{8DAC59BE-C
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E763DA15-8F4E-446C-99B8-309053C75598}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropTests", "src\Grpc\test\InteropTests\InteropTests.csproj", "{3ADC50B9-2EBB-422A-8424-F9FC67841CA1}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropTests", "src\Grpc\Interop\test\InteropTests\InteropTests.csproj", "{3ADC50B9-2EBB-422A-8424-F9FC67841CA1}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{05A169C7-4F20-4516-B10A-B13C5649D346}"
|
||||
EndProject
|
||||
|
@ -1318,9 +1318,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Object
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{00B2DD87-7E2A-4460-BE1B-5E18B1062B7F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropClient", "src\Grpc\test\testassets\InteropClient\InteropClient.csproj", "{C3A0F425-669F-46A8-893F-CF449A6DAE56}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropClient", "src\Grpc\Interop\test\testassets\InteropClient\InteropClient.csproj", "{C3A0F425-669F-46A8-893F-CF449A6DAE56}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropWebsite", "src\Grpc\test\testassets\InteropWebsite\InteropWebsite.csproj", "{19189670-E206-471D-94F8-7D3D545E5020}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropWebsite", "src\Grpc\Interop\test\testassets\InteropWebsite\InteropWebsite.csproj", "{19189670-E206-471D-94F8-7D3D545E5020}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasm.Performance.ConsoleHost", "src\Components\benchmarkapps\Wasm.Performance\ConsoleHost\Wasm.Performance.ConsoleHost.csproj", "{E9408723-E6A9-4715-B906-3B25B0238ABA}"
|
||||
EndProject
|
||||
|
@ -1654,16 +1654,48 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SDK-Analyzers", "SDK-Analyz
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Components", "Components", "{CC45FA2D-128B-485D-BA6D-DFD9735CB3C3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.SdkAnalyzers", "src\Tools\SDK-Analyzers\Components\src\Microsoft.AspNetCore.Components.SdkAnalyzers.csproj", "{825BCF97-67A9-4834-B3A8-C3DC97A90E41}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.SdkAnalyzers", "src\Tools\SDK-Analyzers\Components\src\Microsoft.AspNetCore.Components.SdkAnalyzers.csproj", "{825BCF97-67A9-4834-B3A8-C3DC97A90E41}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.SdkAnalyzers.Tests", "src\Tools\SDK-Analyzers\Components\test\Microsoft.AspNetCore.Components.SdkAnalyzers.Tests.csproj", "{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.SdkAnalyzers.Tests", "src\Tools\SDK-Analyzers\Components\test\Microsoft.AspNetCore.Components.SdkAnalyzers.Tests.csproj", "{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LinkabilityChecker", "LinkabilityChecker", "{94F95276-7CDF-44A8-B159-D09702EF6794}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinkabilityChecker", "src\Tools\LinkabilityChecker\LinkabilityChecker.csproj", "{EA7D844B-C180-41C7-9D55-273AD88BF71F}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{7A331A1C-E2C4-4E37-B0A0-B5AA10661229}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "JsonTranscoding", "JsonTranscoding", "{DD076DDA-7956-4361-A7D4-2B8025AB3DFD}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{5CDB8ABC-9DD0-4A9F-8948-EED5FFE89F67}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{151E6F9E-107B-4DDC-A2B1-95115801FD14}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B43BE3EB-9846-4484-88D8-05165202A0FC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{9A8AE587-A3DB-4211-8354-430C4CCBEB9B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestsWebsite", "src\Grpc\JsonTranscoding\test\testassets\IntegrationTestsWebsite\IntegrationTestsWebsite.csproj", "{2E28881D-A188-47AF-800A-B5877AD8C288}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "src\Grpc\JsonTranscoding\test\testassets\Sandbox\Sandbox.csproj", "{A53696E8-6065-41BA-84FB-E89E0DACFF6C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Grpc.JsonTranscoding", "src\Grpc\JsonTranscoding\src\Microsoft.AspNetCore.Grpc.JsonTranscoding\Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj", "{109C702D-DACE-4F82-A490-15E5AFA94005}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Grpc.Swagger", "src\Grpc\JsonTranscoding\src\Microsoft.AspNetCore.Grpc.Swagger\Microsoft.AspNetCore.Grpc.Swagger.csproj", "{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Grpc.Swagger.Tests", "src\Grpc\JsonTranscoding\test\Microsoft.AspNetCore.Grpc.Swagger.Tests\Microsoft.AspNetCore.Grpc.Swagger.Tests.csproj", "{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests", "src\Grpc\JsonTranscoding\test\Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests\Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests.csproj", "{F18E97AE-3A3F-424D-8DC2-4D001A167F98}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests", "src\Grpc\JsonTranscoding\test\Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests\Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests.csproj", "{8C3E422A-F281-4B93-A567-88C7A1ED0412}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Grpc.Microbenchmarks", "src\Grpc\JsonTranscoding\perf\Microsoft.AspNetCore.Grpc.Microbenchmarks\Microsoft.AspNetCore.Grpc.Microbenchmarks.csproj", "{EB14F068-AD55-4970-B9B4-1FBE33704243}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.LongTests", "src\Servers\IIS\IIS\test\IIS.LongTests\IIS.LongTests.csproj", "{B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildAfterTargetingPack", "BuildAfterTargetingPack", "{489020F2-80D9-4468-A5D3-07E785837A5D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildAfterTargetingPack", "src\BuildAfterTargetingPack\BuildAfterTargetingPack.csproj", "{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -9959,6 +9991,134 @@ Global
|
|||
{EA7D844B-C180-41C7-9D55-273AD88BF71F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EA7D844B-C180-41C7-9D55-273AD88BF71F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EA7D844B-C180-41C7-9D55-273AD88BF71F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Release|x64.Build.0 = Release|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Release|x64.Build.0 = Release|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11}.Release|x86.Build.0 = Release|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412}.Release|x86.Build.0 = Release|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
|
@ -9975,6 +10135,22 @@ Global
|
|||
{B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -10247,7 +10423,7 @@ Global
|
|||
{58955E85-0D55-45FF-97EE-BDD096522954} = {BCD032DD-D088-4F72-B80F-48D0EA845F87}
|
||||
{D708256C-4A68-4B15-AAE5-6EFA41223A70} = {BCD032DD-D088-4F72-B80F-48D0EA845F87}
|
||||
{8DAC59BE-CB96-4F04-909C-56C22E7665EB} = {017429CC-C5FB-48B4-9C46-034E29EE2F06}
|
||||
{E763DA15-8F4E-446C-99B8-309053C75598} = {8DAC59BE-CB96-4F04-909C-56C22E7665EB}
|
||||
{E763DA15-8F4E-446C-99B8-309053C75598} = {7A331A1C-E2C4-4E37-B0A0-B5AA10661229}
|
||||
{3ADC50B9-2EBB-422A-8424-F9FC67841CA1} = {E763DA15-8F4E-446C-99B8-309053C75598}
|
||||
{05A169C7-4F20-4516-B10A-B13C5649D346} = {017429CC-C5FB-48B4-9C46-034E29EE2F06}
|
||||
{3D06E2C9-44F7-408D-802C-42D7E55F08E7} = {05A169C7-4F20-4516-B10A-B13C5649D346}
|
||||
|
@ -10725,6 +10901,7 @@ Global
|
|||
{DF4637DA-5F07-4903-8461-4E2DAB235F3C} = {7F99E967-3DC1-4198-9D55-47CD9471D0B6}
|
||||
{AAB50C64-39AA-4AED-8E9C-50D68E7751AD} = {7F99E967-3DC1-4198-9D55-47CD9471D0B6}
|
||||
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38} = {EB5E294B-9ED5-43BF-AFA9-1CD2327F3DC1}
|
||||
{11BE4471-6C3D-4758-881A-97B6A16F21F6} = {EB5E294B-9ED5-43BF-AFA9-1CD2327F3DC1}
|
||||
{022B4B80-E813-4256-8034-11A68146F4EF} = {E5963C9F-20A6-4385-B364-814D2581FADF}
|
||||
{FF413F1C-A998-4FA2-823F-52AC0916B35C} = {022B4B80-E813-4256-8034-11A68146F4EF}
|
||||
{3A1EC883-EF9C-43E8-95E5-6B527428867B} = {022B4B80-E813-4256-8034-11A68146F4EF}
|
||||
|
@ -10797,7 +10974,23 @@ Global
|
|||
{DC349A25-0DBF-4468-99E1-B95C22D3A7EF} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3}
|
||||
{94F95276-7CDF-44A8-B159-D09702EF6794} = {0B200A66-B809-4ED3-A790-CB1C2E80975E}
|
||||
{EA7D844B-C180-41C7-9D55-273AD88BF71F} = {94F95276-7CDF-44A8-B159-D09702EF6794}
|
||||
{7A331A1C-E2C4-4E37-B0A0-B5AA10661229} = {8DAC59BE-CB96-4F04-909C-56C22E7665EB}
|
||||
{DD076DDA-7956-4361-A7D4-2B8025AB3DFD} = {8DAC59BE-CB96-4F04-909C-56C22E7665EB}
|
||||
{5CDB8ABC-9DD0-4A9F-8948-EED5FFE89F67} = {DD076DDA-7956-4361-A7D4-2B8025AB3DFD}
|
||||
{151E6F9E-107B-4DDC-A2B1-95115801FD14} = {DD076DDA-7956-4361-A7D4-2B8025AB3DFD}
|
||||
{B43BE3EB-9846-4484-88D8-05165202A0FC} = {DD076DDA-7956-4361-A7D4-2B8025AB3DFD}
|
||||
{9A8AE587-A3DB-4211-8354-430C4CCBEB9B} = {B43BE3EB-9846-4484-88D8-05165202A0FC}
|
||||
{2E28881D-A188-47AF-800A-B5877AD8C288} = {9A8AE587-A3DB-4211-8354-430C4CCBEB9B}
|
||||
{A53696E8-6065-41BA-84FB-E89E0DACFF6C} = {9A8AE587-A3DB-4211-8354-430C4CCBEB9B}
|
||||
{109C702D-DACE-4F82-A490-15E5AFA94005} = {151E6F9E-107B-4DDC-A2B1-95115801FD14}
|
||||
{E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11} = {151E6F9E-107B-4DDC-A2B1-95115801FD14}
|
||||
{90CF4DC6-AC53-459F-9EAB-623A11EADAA3} = {B43BE3EB-9846-4484-88D8-05165202A0FC}
|
||||
{F18E97AE-3A3F-424D-8DC2-4D001A167F98} = {B43BE3EB-9846-4484-88D8-05165202A0FC}
|
||||
{8C3E422A-F281-4B93-A567-88C7A1ED0412} = {B43BE3EB-9846-4484-88D8-05165202A0FC}
|
||||
{EB14F068-AD55-4970-B9B4-1FBE33704243} = {5CDB8ABC-9DD0-4A9F-8948-EED5FFE89F67}
|
||||
{B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C}
|
||||
{489020F2-80D9-4468-A5D3-07E785837A5D} = {017429CC-C5FB-48B4-9C46-034E29EE2F06}
|
||||
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85} = {489020F2-80D9-4468-A5D3-07E785837A5D}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
|
||||
|
|
|
@ -239,6 +239,7 @@
|
|||
<Import Project="artifacts\bin\GenerateFiles\Directory.Build.props" Condition=" '$(MSBuildProjectName)' != 'GenerateFiles' " />
|
||||
<Import Project="eng\Dependencies.props" />
|
||||
<Import Project="eng\ProjectReferences.props" />
|
||||
<Import Project="eng\RequiresDelayedBuildProjects.props" />
|
||||
<Import Project="eng\SharedFramework.Local.props" />
|
||||
<Import Project="eng\SharedFramework.External.props" />
|
||||
<Import Project="eng\targets\Cpp.Common.props" Condition="'$(MSBuildProjectExtension)' == '.vcxproj'" />
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
<Project>
|
||||
<Import Project="Common.props" />
|
||||
|
||||
<!--
|
||||
@(RequiresDelayedBuild) projects can be extended many ways but this isn't fully automated because the new
|
||||
projects likely aren't referenced initially. To add new projects, edit RequiresDelayedBuildProjects.props
|
||||
manually, update the $(BuildMainlyReferenceProviders)' == 'true' item group near the bottom of this file,
|
||||
or edit BuildAfterTargetingPack.csproj. Then run GenerateProjectList.ps1 (even for the first option to ensure
|
||||
the format is correct) and undo any temporary changes. When complete, only BuildAfterTargetingPack.csproj and
|
||||
other @(RequiresDelayedBuild) projects should mention projects listed in RequiresDelayedBuildProjects.props.
|
||||
-->
|
||||
<Import Project="RequiresDelayedBuildProjects.props" />
|
||||
|
||||
<!-- These projects are always excluded, even when -projects is specified on command line. -->
|
||||
<ItemGroup>
|
||||
<!-- Explicitly excluded projects -->
|
||||
|
@ -16,8 +26,11 @@
|
|||
<!-- Exclude the websockets samples for now because they use classic .csproj, which is not yet supported in our build. -->
|
||||
<ProjectToExclude Include="$(RepoRoot)src\Middleware\WebSockets\samples\**\*.csproj" />
|
||||
|
||||
<!-- These projects are meant to be referenced only by tests. -->
|
||||
<ProjectToExclude Include="$(RepoRoot)src\**\testassets\**\*.*proj"
|
||||
<!-- These projects are meant to be referenced only by tests or via BuildAfterTargetingPack.csproj. -->
|
||||
<ProjectToExclude
|
||||
Include="$(RepoRoot)src\**\testassets\**\*.*proj;
|
||||
@(RequiresDelayedBuild);
|
||||
"
|
||||
Exclude="$(RepoRoot)src\Components\WebAssembly\testassets\WasmLinkerTest\*.*proj;
|
||||
$(RepoRoot)src\Components\WebView\Samples\PhotinoPlatform\testassets\PhotinoTestApp\*.*proj;
|
||||
$(RepoRoot)src\Http\Routing\test\testassets\RoutingSandbox\*.*proj;
|
||||
|
@ -190,6 +203,7 @@
|
|||
$(RepoRoot)src\ProjectTemplates\*\*.csproj;
|
||||
$(RepoRoot)src\submodules\spa-templates\src\*.csproj;
|
||||
$(RepoRoot)src\Extensions\**\*.csproj;
|
||||
$(RepoRoot)src\BuildAfterTargetingPack\*.csproj;
|
||||
"
|
||||
Exclude="
|
||||
@(ProjectToBuild);
|
||||
|
@ -229,6 +243,7 @@
|
|||
$(RepoRoot)src\HealthChecks\**\src\*.csproj;
|
||||
$(RepoRoot)src\Testing\**\src\*.csproj;
|
||||
$(RepoRoot)src\Extensions\**\src\*.csproj;
|
||||
$(RepoRoot)src\BuildAfterTargetingPack\*.csproj;
|
||||
"
|
||||
Exclude="
|
||||
@(ProjectToBuild);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<BuildManaged>true</BuildManaged>
|
||||
<RepoRoot
|
||||
|
@ -17,13 +16,15 @@
|
|||
BuildInParallel="true"
|
||||
SkipNonexistentTargets="true"
|
||||
SkipNonexistentProjects="true">
|
||||
<Output TaskParameter="TargetOutputs" ItemName="_ProjectReferenceProvider" />
|
||||
<Output TaskParameter="TargetOutputs" ItemName="_ProvidesReferenceOrRequiresDelay" />
|
||||
</MSBuild>
|
||||
|
||||
<ItemGroup>
|
||||
<_SharedFrameworkAndPackageRef Include="@(_ProjectReferenceProvider->WithMetadataValue('IsAspNetCoreApp','true')->WithMetadataValue('IsPackable', 'true')->Distinct())" />
|
||||
<_SharedFrameworkRef Include="@(_ProjectReferenceProvider->WithMetadataValue('IsAspNetCoreApp','true')->WithMetadataValue('IsPackable', 'false')->Distinct())" />
|
||||
<_TrimmableProject Include="@(_ProjectReferenceProvider->WithMetadataValue('Trimmable', 'true')->Distinct())" />
|
||||
<_ProjectReferenceProvider Include="@(_ProvidesReferenceOrRequiresDelay->WithMetadataValue('IsProjectReferenceProvider','true')->Distinct())" />
|
||||
<_RequiresDelayedBuild Include="@(_ProvidesReferenceOrRequiresDelay->WithMetadataValue('RequiresDelayedBuild','true')->Distinct())" />
|
||||
<_SharedFrameworkAndPackageRef Include="@(_ProjectReferenceProvider->WithMetadataValue('IsAspNetCoreApp','true')->WithMetadataValue('IsPackable', 'true'))" />
|
||||
<_SharedFrameworkRef Include="@(_ProjectReferenceProvider->WithMetadataValue('IsAspNetCoreApp','true')->WithMetadataValue('IsPackable', 'false'))" />
|
||||
<_TrimmableProject Include="@(_ProjectReferenceProvider->WithMetadataValue('Trimmable', 'true'))" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
@ -57,7 +58,7 @@
|
|||
|
||||
This file contains a complete list of the assemblies which are part of the shared framework.
|
||||
|
||||
This project is generated using the <IsAspNetCoreApp> and <IsPackable> properties from each .csproj in this repository.
|
||||
This file is generated using the <IsAspNetCoreApp/> and <IsPackable/> properties from each .csproj in this repository.
|
||||
-->
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
|
@ -97,6 +98,30 @@
|
|||
|
||||
<WriteLinesToFile File="$(TrimmableProjectsList)" Lines="$(TrimmableProjectsListContent)" Overwrite="true" />
|
||||
<Message Importance="High" Text="Generated $(TrimmableProjectsList)" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<DelayedBuildFile>$(MSBuildThisFileDirectory)RequiresDelayedBuildProjects.props</DelayedBuildFile>
|
||||
<DelayedBuildContent><![CDATA[<!--
|
||||
This file is automatically generated. Run `./eng/scripts/GenerateProjectList.ps1` to update.
|
||||
|
||||
This file contains a list of projects that must be restored etc. after App.Ref and App.Runtime are fully built.
|
||||
|
||||
This file is generated using <RequiresDelayedBuild/> properties. Content may overlap ProjectReferences.csproj
|
||||
but that is not required (projects that are not project reference providers are also supported).
|
||||
-->
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
@(_RequiresDelayedBuild->'<RequiresDelayedBuild Include="%24(RepoRoot)%(ProjectFileRelativePath)" />', '%0A ')
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
]]></DelayedBuildContent>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Workaround https://github.com/Microsoft/msbuild/issues/1024 -->
|
||||
<WriteLinesToFile Condition="'$(OS)' == 'Windows_NT'"
|
||||
File="$(DelayedBuildFile)" Lines="$([MSBuild]::Escape($(DelayedBuildContent)))" Overwrite="true" />
|
||||
<Exec Condition="'$(OS)' != 'Windows_NT'"
|
||||
Command="echo '$(DelayedBuildContent.Replace('\t','\\t'))' > $(DelayedBuildFile)" />
|
||||
<Message Importance="High" Text="Generated $(DelayedBuildFile)" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
|
@ -178,8 +178,10 @@ and are generated based on the last package release.
|
|||
<LatestPackageReference Include="Duende.IdentityServer.EntityFramework.Storage" />
|
||||
<LatestPackageReference Include="Duende.IdentityServer.Storage" />
|
||||
<LatestPackageReference Include="FSharp.Core" />
|
||||
<LatestPackageReference Include="Google.Api.CommonProtos" />
|
||||
<LatestPackageReference Include="Google.Protobuf" />
|
||||
<LatestPackageReference Include="Grpc.AspNetCore" />
|
||||
<LatestPackageReference Include="Grpc.AspNetCore.Server" />
|
||||
<LatestPackageReference Include="Grpc.Auth" />
|
||||
<LatestPackageReference Include="Grpc.Net.Client" />
|
||||
<LatestPackageReference Include="Grpc.Tools" />
|
||||
|
@ -202,6 +204,7 @@ and are generated based on the last package release.
|
|||
<LatestPackageReference Include="Serilog.Extensions.Logging" />
|
||||
<LatestPackageReference Include="Serilog.Sinks.File" />
|
||||
<LatestPackageReference Include="StackExchange.Redis" />
|
||||
<LatestPackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<LatestPackageReference Include="System.Reactive.Linq" />
|
||||
<LatestPackageReference Include="xunit.abstractions" />
|
||||
<LatestPackageReference Include="xunit.analyzers" />
|
||||
|
|
|
@ -157,5 +157,7 @@
|
|||
<ProjectReferenceProvider Include="Microsoft.Extensions.Diagnostics.HealthChecks" ProjectPath="$(RepoRoot)src\HealthChecks\HealthChecks\src\Microsoft.Extensions.Diagnostics.HealthChecks.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Testing" ProjectPath="$(RepoRoot)src\Testing\src\Microsoft.AspNetCore.Testing.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.Extensions.Features" ProjectPath="$(RepoRoot)src\Extensions\Features\src\Microsoft.Extensions.Features.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Grpc.JsonTranscoding" ProjectPath="$(RepoRoot)src\Grpc\JsonTranscoding\src\Microsoft.AspNetCore.Grpc.JsonTranscoding\Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Grpc.Swagger" ProjectPath="$(RepoRoot)src\Grpc\JsonTranscoding\src\Microsoft.AspNetCore.Grpc.Swagger\Microsoft.AspNetCore.Grpc.Swagger.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<!--
|
||||
This file is automatically generated. Run `./eng/scripts/GenerateProjectList.ps1` to update.
|
||||
|
||||
This file contains a list of projects that must be restored etc. after App.Ref and App.Runtime are fully built.
|
||||
|
||||
This file is generated using <RequiresDelayedBuild/> properties. Content may overlap ProjectReferences.csproj
|
||||
but that is not required (projects that are not project reference providers are also supported).
|
||||
-->
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<RequiresDelayedBuild Include="$(RepoRoot)src\Grpc\JsonTranscoding\perf\Microsoft.AspNetCore.Grpc.Microbenchmarks\Microsoft.AspNetCore.Grpc.Microbenchmarks.csproj" />
|
||||
<RequiresDelayedBuild Include="$(RepoRoot)src\Grpc\JsonTranscoding\src\Microsoft.AspNetCore.Grpc.JsonTranscoding\Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj" />
|
||||
<RequiresDelayedBuild Include="$(RepoRoot)src\Grpc\JsonTranscoding\src\Microsoft.AspNetCore.Grpc.Swagger\Microsoft.AspNetCore.Grpc.Swagger.csproj" />
|
||||
<RequiresDelayedBuild Include="$(RepoRoot)src\Grpc\JsonTranscoding\test\Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests\Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests.csproj" />
|
||||
<RequiresDelayedBuild Include="$(RepoRoot)src\Grpc\JsonTranscoding\test\Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests\Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests.csproj" />
|
||||
<RequiresDelayedBuild Include="$(RepoRoot)src\Grpc\JsonTranscoding\test\Microsoft.AspNetCore.Grpc.Swagger.Tests\Microsoft.AspNetCore.Grpc.Swagger.Tests.csproj" />
|
||||
<RequiresDelayedBuild Include="$(RepoRoot)src\Grpc\JsonTranscoding\test\testassets\IntegrationTestsWebsite\IntegrationTestsWebsite.csproj" />
|
||||
<RequiresDelayedBuild Include="$(RepoRoot)src\Grpc\JsonTranscoding\test\testassets\Sandbox\Sandbox.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
This file contains a complete list of the assemblies which are part of the shared framework.
|
||||
|
||||
This project is generated using the <IsAspNetCoreApp> and <IsPackable> properties from each .csproj in this repository.
|
||||
This file is generated using the <IsAspNetCoreApp/> and <IsPackable/> properties from each .csproj in this repository.
|
||||
-->
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -238,11 +238,13 @@
|
|||
<CastleCoreVersion>4.2.1</CastleCoreVersion>
|
||||
<CommandLineParserVersion>2.3.0</CommandLineParserVersion>
|
||||
<FSharpCoreVersion>6.0.0</FSharpCoreVersion>
|
||||
<GoogleProtobufVersion>3.18.0</GoogleProtobufVersion>
|
||||
<GrpcAspNetCoreVersion>2.40.0</GrpcAspNetCoreVersion>
|
||||
<GrpcAuthVersion>2.40.0</GrpcAuthVersion>
|
||||
<GrpcNetClientVersion>2.40.0</GrpcNetClientVersion>
|
||||
<GrpcToolsVersion>2.40.0</GrpcToolsVersion>
|
||||
<GoogleApiCommonProtosVersion>2.5.0</GoogleApiCommonProtosVersion>
|
||||
<GoogleProtobufVersion>3.18.1</GoogleProtobufVersion>
|
||||
<GrpcAspNetCoreVersion>2.43.0</GrpcAspNetCoreVersion>
|
||||
<GrpcAspNetCoreServerVersion>2.43.0</GrpcAspNetCoreServerVersion>
|
||||
<GrpcAuthVersion>2.43.0</GrpcAuthVersion>
|
||||
<GrpcNetClientVersion>2.43.0</GrpcNetClientVersion>
|
||||
<GrpcToolsVersion>2.43.0</GrpcToolsVersion>
|
||||
<DuendeIdentityServerAspNetIdentityVersion>5.2.0</DuendeIdentityServerAspNetIdentityVersion>
|
||||
<DuendeIdentityServerEntityFrameworkVersion>5.2.0</DuendeIdentityServerEntityFrameworkVersion>
|
||||
<DuendeIdentityServerVersion>5.2.0</DuendeIdentityServerVersion>
|
||||
|
|
|
@ -311,12 +311,18 @@
|
|||
<Warning Condition=" '$(IsProjectReferenceProvider)' == 'true' AND '$(AssemblyName)' != '$(MSBuildProjectName)' "
|
||||
Text="Project name "$(MSBuildProjectName)" is confusing; assembly is named "$(AssemblyName)"." />
|
||||
|
||||
<ItemGroup Condition=" '$(IsProjectReferenceProvider)' == 'true' ">
|
||||
<ItemGroup Condition=" '$(IsProjectReferenceProvider)' == 'true' OR '$(RequiresDelayedBuild)' == 'true' ">
|
||||
<ProvidesReference Include="$(AssemblyName)">
|
||||
<IsAspNetCoreApp>$([MSBuild]::ValueOrDefault($(IsAspNetCoreApp),'false'))</IsAspNetCoreApp>
|
||||
<IsPackable>$([MSBuild]::ValueOrDefault($(IsPackable),'false'))</IsPackable>
|
||||
<ProjectFileRelativePath>$([MSBuild]::MakeRelative($(RepoRoot), $(MSBuildProjectFullPath)))</ProjectFileRelativePath>
|
||||
<Trimmable>$([MSBuild]::ValueOrDefault($(Trimmable),'false'))</Trimmable>
|
||||
|
||||
<!-- True if the project may be referenced using a @(Reference) item. -->
|
||||
<IsProjectReferenceProvider>$([MSBuild]::ValueOrDefault($(IsProjectReferenceProvider),'false'))</IsProjectReferenceProvider>
|
||||
|
||||
<!-- True if project must be restored etc. after App.Ref and App.Runtime are fully built. -->
|
||||
<RequiresDelayedBuild>$([MSBuild]::ValueOrDefault($(RequiresDelayedBuild),'false'))</RequiresDelayedBuild>
|
||||
</ProvidesReference>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
<Project>
|
||||
<!-- Fake a unit test project unless that would prevent building this project. -->
|
||||
<PropertyGroup>
|
||||
<IsUnitTestProject Condition=" '$(SkipTestBuild)' != 'true' ">true</IsUnitTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
|
||||
|
||||
<!--
|
||||
Forward targets to @(RequiresDelayedBuild) projects after App.Ref has built. Intended for projects that
|
||||
cannot restore without our App.Ref layout and aren't referenced elsewhere in the repo. For projects
|
||||
referenced elsewhere, use something else e.g. @(TestAssetProjectReference) items and
|
||||
FunctionalTestWithAssets.targets for test asset projects that need special handling.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
|
||||
<!-- Nothing from this project ships whether we're faking a unit test project or not. -->
|
||||
<IsShipping>false</IsShipping>
|
||||
|
||||
<!-- Build.props is main thing that references this project. -->
|
||||
<IsProjectReferenceProvider>false</IsProjectReferenceProvider>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--
|
||||
When adding new projects to @(RequiresDelayedBuild), temporarily reference the projects here, run
|
||||
GenerateProjectList.ps1, then undo changes in this file. See comments in Build.props for other options.
|
||||
-->
|
||||
<!-- RequiresDelayedBuild Include="..." / -->
|
||||
|
||||
<!-- Enforce build order. Need shared Fx before building the important projects. -->
|
||||
<ProjectReference Include="$(RepoRoot)\src\Framework\App.Ref\src\Microsoft.AspNetCore.App.Ref.csproj"
|
||||
Private="false"
|
||||
ReferenceOutputAssembly="false"
|
||||
SkipGetTargetFrameworkProperties="true" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Cannot build in source-build because that does not create an App.Ref layout. -->
|
||||
<Target Name="BuildDelayedProjects"
|
||||
BeforeTargets="Build"
|
||||
Condition=" '$(DotNetBuildFromSource)' != 'true' "
|
||||
Returns="@(TargetPathWithTargetPlatformMoniker)">
|
||||
<MSBuild Projects="@(RequiresDelayedBuild)"
|
||||
BuildInParallel="$(BuildInParallel)"
|
||||
Properties="MSBuildRestoreSessionId=$([System.Guid]::NewGuid())"
|
||||
Targets="Restore" />
|
||||
<MSBuild Projects="@(RequiresDelayedBuild)" BuildInParallel="$(BuildInParallel)" Targets="Build">
|
||||
<Output TaskParameter="TargetOutputs" ItemName="TargetPathWithTargetPlatformMoniker" />
|
||||
</MSBuild>
|
||||
</Target>
|
||||
|
||||
<Target Name="CleanDelayedProjects" BeforeTargets="Clean" Condition=" '$(DotNetBuildFromSource)' != 'true' ">
|
||||
<MSBuild Projects="@(RequiresDelayedBuild)" BuildInParallel="$(BuildInParallel)" Targets="Clean" />
|
||||
</Target>
|
||||
|
||||
<Target Name="CreateHelixPayloadDelayedProjects"
|
||||
BeforeTargets="CreateHelixPayload"
|
||||
Condition=" '$(DotNetBuildFromSource)' != 'true' "
|
||||
Returns="@(HelixWorkItem)">
|
||||
<MSBuild Projects="@(RequiresDelayedBuild)"
|
||||
BuildInParallel="$(BuildInParallel)"
|
||||
SkipNonexistentTargets="true"
|
||||
Targets="CreateHelixPayload">
|
||||
<Output TaskParameter="TargetOutputs" ItemName="HelixWorkItem" />
|
||||
</MSBuild>
|
||||
</Target>
|
||||
|
||||
<Target Name="GetReferencesProvidedDelayedProjects"
|
||||
BeforeTargets="GetReferencesProvided"
|
||||
Returns="@(ProvidesReference)">
|
||||
<MSBuild Projects="@(RequiresDelayedBuild)" BuildInParallel="$(BuildInParallel)" Targets="GetReferencesProvided">
|
||||
<Output TaskParameter="TargetOutputs" ItemName="ProvidesReference" />
|
||||
</MSBuild>
|
||||
</Target>
|
||||
|
||||
<Target Name="PackDelayedProjects" BeforeTargets="Pack" Condition=" '$(DotNetBuildFromSource)' != 'true' ">
|
||||
<MSBuild Projects="@(RequiresDelayedBuild)" BuildInParallel="$(BuildInParallel)" Targets="Pack" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishDelayedProjects" BeforeTargets="Publish" Condition=" '$(DotNetBuildFromSource)' != 'true' ">
|
||||
<MSBuild Projects="@(RequiresDelayedBuild)" BuildInParallel="$(BuildInParallel)" Targets="Publish" />
|
||||
</Target>
|
||||
|
||||
<Target Name="TestDelayedProjects" BeforeTargets="Test" Condition=" '$(DotNetBuildFromSource)' != 'true' ">
|
||||
<MSBuild Projects="@(RequiresDelayedBuild)" BuildInParallel="$(BuildInParallel)" Targets="Test" />
|
||||
</Target>
|
||||
|
||||
<Target Name="VSTestDelayedProjects" BeforeTargets="VSTest" Condition=" '$(DotNetBuildFromSource)' != 'true' ">
|
||||
<MSBuild Projects="@(RequiresDelayedBuild)" BuildInParallel="$(BuildInParallel)" Targets="VSTest" />
|
||||
</Target>
|
||||
|
||||
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
|
||||
</Project>
|
|
@ -1,11 +1,32 @@
|
|||
{
|
||||
{
|
||||
"solution": {
|
||||
"path": "..\\..\\AspNetCore.sln",
|
||||
"projects" : [
|
||||
"src\\Grpc\\test\\InteropTests\\InteropTests.csproj",
|
||||
"src\\Grpc\\test\\testassets\\InteropWebsite\\InteropWebsite.csproj",
|
||||
"src\\Grpc\\test\\testassets\\InteropClient\\InteropClient.csproj",
|
||||
"projects": [
|
||||
"src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj",
|
||||
"src\\Grpc\\JsonTranscoding\\perf\\Microsoft.AspNetCore.Grpc.Microbenchmarks\\Microsoft.AspNetCore.Grpc.Microbenchmarks.csproj",
|
||||
"src\\Grpc\\JsonTranscoding\\src\\Microsoft.AspNetCore.Grpc.JsonTranscoding\\Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj",
|
||||
"src\\Grpc\\JsonTranscoding\\src\\Microsoft.AspNetCore.Grpc.Swagger\\Microsoft.AspNetCore.Grpc.Swagger.csproj",
|
||||
"src\\Grpc\\JsonTranscoding\\test\\Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests\\Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests.csproj",
|
||||
"src\\Grpc\\JsonTranscoding\\test\\Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests\\Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests.csproj",
|
||||
"src\\Grpc\\JsonTranscoding\\test\\Microsoft.AspNetCore.Grpc.Swagger.Tests\\Microsoft.AspNetCore.Grpc.Swagger.Tests.csproj",
|
||||
"src\\Grpc\\JsonTranscoding\\test\\testassets\\IntegrationTestsWebsite\\IntegrationTestsWebsite.csproj",
|
||||
"src\\Grpc\\JsonTranscoding\\test\\testassets\\Sandbox\\Sandbox.csproj",
|
||||
"src\\Grpc\\Interop\\test\\InteropTests\\InteropTests.csproj",
|
||||
"src\\Grpc\\Interop\\test\\testassets\\InteropClient\\InteropClient.csproj",
|
||||
"src\\Grpc\\Interop\\test\\testassets\\InteropWebsite\\InteropWebsite.csproj",
|
||||
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
|
||||
"src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
|
||||
"src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
|
||||
"src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj",
|
||||
"src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
|
||||
"src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
|
||||
"src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
|
||||
"src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
|
||||
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
|
||||
"src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
|
||||
"src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj",
|
||||
"src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",
|
||||
"src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,8 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Xunit.Abstractions;
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Xunit.Abstractions;
|
||||
|
|
@ -1,12 +1,8 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using InteropTests.Helpers;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace InteropTests;
|
|
@ -1,12 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<ContainsFunctionalTestAssets>true</ContainsFunctionalTestAssets>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\..\Shared\Process\ProcessEx.cs" Link="Helpers\ProcessEx.cs" />
|
||||
<Compile Include="..\..\..\Shared\Process\ProcessExtensions.cs" Link="Helpers\ProcessExtensions.cs" />
|
||||
<Compile Include="..\..\..\..\Shared\Process\ProcessEx.cs" Link="Helpers\ProcessEx.cs" />
|
||||
<Compile Include="..\..\..\..\Shared\Process\ProcessExtensions.cs" Link="Helpers\ProcessExtensions.cs" />
|
||||
|
||||
<!-- Enforce build order. Need shared Fx before PublishTestAssets runs. -->
|
||||
<ProjectReference Include="$(RepoRoot)\src\Framework\App.Ref\src\Microsoft.AspNetCore.App.Ref.csproj"
|
|
@ -16,9 +16,7 @@
|
|||
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace InteropTestsClient;
|
||||
|
|
@ -16,9 +16,6 @@
|
|||
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Grpc.Core;
|
||||
|
||||
namespace InteropTestsClient;
|
|
@ -16,8 +16,6 @@
|
|||
|
||||
#endregion
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
|
|
@ -16,15 +16,9 @@
|
|||
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using Google.Apis.Auth.OAuth2;
|
||||
using Google.Protobuf;
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace InteropTestsClient;
|
|
@ -16,9 +16,6 @@
|
|||
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Grpc.Core;
|
||||
|
||||
namespace InteropTestsWebsite;
|
|
@ -16,13 +16,7 @@
|
|||
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace InteropTestsWebsite;
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:54288/",
|
||||
"sslPort": 44374
|
||||
"applicationUrl": "http://localhost:54462/",
|
||||
"sslPort": 44363
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
|
@ -24,4 +24,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,12 +16,8 @@
|
|||
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Grpc.Testing;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace InteropTestsWebsite;
|
|
@ -16,9 +16,6 @@
|
|||
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using Grpc.Core;
|
||||
using InteropTestsWebsite;
|
||||
|
@ -50,9 +47,10 @@ public class TestServiceImpl : TestService.TestServiceBase
|
|||
|
||||
foreach (var responseParam in request.ResponseParameters)
|
||||
{
|
||||
// TODO(JamesNK): Remove nullable override after Grpc.Core.Api update
|
||||
responseStream.WriteOptions = !(responseParam.Compressed?.Value ?? false)
|
||||
? new WriteOptions(WriteFlags.NoCompress)
|
||||
: null;
|
||||
: null!;
|
||||
|
||||
var response = new StreamingOutputCallResponse { Payload = CreateZerosPayload(responseParam.Size) };
|
||||
await responseStream.WriteAsync(response);
|
|
@ -0,0 +1,15 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Projects must be restored after App.Ref and App.Runtime layouts are complete. -->
|
||||
<RequiresDelayedBuild>true</RequiresDelayedBuild>
|
||||
|
||||
<!-- Required because Swashbuckle.AspNetCore uses shared runtime -->
|
||||
<UseAspNetCoreSharedRuntime>true</UseAspNetCoreSharedRuntime>
|
||||
|
||||
<!-- Workaround lack of Linux MUSL x64 support. -->
|
||||
<ExcludeFromBuild
|
||||
Condition=" '$(TargetOsName)' == 'linux-musl' and '$(TargetArchitecture)' == 'x64' ">true</ExcludeFromBuild>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -0,0 +1,39 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Greet;
|
||||
using Microsoft.AspNetCore.Grpc.JsonTranscoding;
|
||||
using Microsoft.AspNetCore.Grpc.JsonTranscoding.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 JsonContext(new GrpcJsonSettings { WriteIndented = false }, TypeRegistry.Empty));
|
||||
_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,40 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Greet;
|
||||
using Microsoft.AspNetCore.Grpc.JsonTranscoding;
|
||||
using Microsoft.AspNetCore.Grpc.JsonTranscoding.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 JsonContext(new GrpcJsonSettings { WriteIndented = false }, TypeRegistry.Empty));
|
||||
_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,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<TieredCompilation>false</TieredCompilation>
|
||||
<DefineConstants>$(DefineConstants);IS_BENCHMARKS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include=".\Proto\chat.proto" GrpcServices="Server" />
|
||||
<Protobuf Include=".\Proto\greet.proto" GrpcServices="Client" />
|
||||
|
||||
<Reference Include="BenchmarkDotNet" />
|
||||
<Reference Include="Google.Protobuf" />
|
||||
<Reference Include="Grpc.Tools" PrivateAssets="All" />
|
||||
<Reference Include="Microsoft.AspNetCore.Grpc.JsonTranscoding" />
|
||||
|
||||
<Compile Include="$(SharedSourceRoot)BenchmarkRunner\*.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,4 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding;
|
||||
|
||||
/// <summary>
|
||||
/// Provides settings for serializing JSON.
|
||||
/// </summary>
|
||||
public sealed class GrpcJsonSettings
|
||||
{
|
||||
/// <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 IgnoreDefaultValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether <see cref="Enum"/> values are written as integers instead of strings.
|
||||
/// Default value is false.
|
||||
/// </summary>
|
||||
public bool WriteEnumsAsIntegers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether <see cref="Int64"/> and <see cref="UInt64"/> values are written as strings instead of numbers.
|
||||
/// Default value is false.
|
||||
/// </summary>
|
||||
public bool WriteInt64sAsStrings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether JSON should use pretty printing.
|
||||
/// Default value is false.
|
||||
/// </summary>
|
||||
public bool WriteIndented { get; set; }
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Google.Api;
|
||||
using Google.Protobuf.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata for a gRPC JSON transcoding endpoint.
|
||||
/// </summary>
|
||||
public sealed class GrpcJsonTranscodingMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="GrpcJsonTranscodingMetadata"/> with the provided Protobuf
|
||||
/// <see cref="Google.Protobuf.Reflection.MethodDescriptor"/> and <see cref="Google.Api.HttpRule"/>.
|
||||
/// </summary>
|
||||
/// <param name="methodDescriptor">The Protobuf <see cref="Google.Protobuf.Reflection.MethodDescriptor"/>.</param>
|
||||
/// <param name="httpRule">The <see cref="Google.Api.HttpRule"/>.</param>
|
||||
public GrpcJsonTranscodingMetadata(MethodDescriptor methodDescriptor, HttpRule httpRule)
|
||||
{
|
||||
MethodDescriptor = methodDescriptor;
|
||||
HttpRule = httpRule;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Protobuf <see cref="Google.Protobuf.Reflection.MethodDescriptor"/>.
|
||||
/// </summary>
|
||||
public MethodDescriptor MethodDescriptor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Google.Api.HttpRule"/>.
|
||||
/// </summary>
|
||||
public HttpRule HttpRule { get; }
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding;
|
||||
|
||||
/// <summary>
|
||||
/// Options used to configure gRPC JSON transcoding service instances.
|
||||
/// </summary>
|
||||
public sealed class GrpcJsonTranscodingOptions
|
||||
{
|
||||
private readonly Lazy<JsonSerializerOptions> _unaryOptions;
|
||||
private readonly Lazy<JsonSerializerOptions> _serverStreamingOptions;
|
||||
|
||||
public GrpcJsonTranscodingOptions()
|
||||
{
|
||||
_unaryOptions = new Lazy<JsonSerializerOptions>(
|
||||
() => JsonConverterHelper.CreateSerializerOptions(new JsonContext(JsonSettings, TypeRegistry)),
|
||||
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
_serverStreamingOptions = new Lazy<JsonSerializerOptions>(
|
||||
() => JsonConverterHelper.CreateSerializerOptions(new JsonContext(JsonSettings, TypeRegistry), isStreamingOptions: true),
|
||||
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
}
|
||||
|
||||
internal JsonSerializerOptions UnarySerializerOptions => _unaryOptions.Value;
|
||||
internal JsonSerializerOptions ServerStreamingSerializerOptions => _serverStreamingOptions.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Google.Protobuf.Reflection.TypeRegistry"/> used to lookup types from type names.
|
||||
/// </summary>
|
||||
public TypeRegistry TypeRegistry { get; set; } = TypeRegistry.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="GrpcJsonSettings"/> used to serialize messages.
|
||||
/// </summary>
|
||||
public GrpcJsonSettings JsonSettings { get; set; } = new GrpcJsonSettings();
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Grpc.AspNetCore.Server;
|
||||
using Grpc.AspNetCore.Server.Model;
|
||||
using Microsoft.AspNetCore.Grpc.JsonTranscoding;
|
||||
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Binding;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the gRPC JSON transcoding services.
|
||||
/// </summary>
|
||||
public static class GrpcJsonTranscodingServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds gRPC JSON transcoding services to the specified <see cref="IGrpcServerBuilder" />.
|
||||
/// </summary>
|
||||
/// <param name="grpcBuilder">The <see cref="IGrpcServerBuilder"/>.</param>
|
||||
/// <returns>The same instance of the <see cref="IGrpcServerBuilder"/> for chaining.</returns>
|
||||
public static IGrpcServerBuilder AddJsonTranscoding(this IGrpcServerBuilder grpcBuilder)
|
||||
{
|
||||
if (grpcBuilder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(grpcBuilder));
|
||||
}
|
||||
|
||||
grpcBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IServiceMethodProvider<>), typeof(JsonTranscodingServiceMethodProvider<>)));
|
||||
|
||||
return grpcBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds gRPC JSON transcoding services to the specified <see cref="IGrpcServerBuilder" />.
|
||||
/// </summary>
|
||||
/// <param name="grpcBuilder">The <see cref="IGrpcServerBuilder"/>.</param>
|
||||
/// <param name="configureOptions">An <see cref="Action{GrpcJsonTranscodingOptions}"/> to configure the provided <see cref="GrpcJsonTranscodingOptions"/>.</param>
|
||||
/// <returns>The same instance of the <see cref="IGrpcServerBuilder"/> for chaining.</returns>
|
||||
public static IGrpcServerBuilder AddJsonTranscoding(this IGrpcServerBuilder grpcBuilder, Action<GrpcJsonTranscodingOptions> configureOptions)
|
||||
{
|
||||
if (grpcBuilder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(grpcBuilder));
|
||||
}
|
||||
|
||||
grpcBuilder.Services.Configure(configureOptions);
|
||||
return grpcBuilder.AddJsonTranscoding();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,294 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Google.Api;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Grpc.AspNetCore.Server;
|
||||
using Grpc.AspNetCore.Server.Model;
|
||||
using Grpc.Core;
|
||||
using Grpc.Shared;
|
||||
using Grpc.Shared.Server;
|
||||
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.CallHandlers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MethodOptions = global::Grpc.Shared.Server.MethodOptions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Binding;
|
||||
|
||||
internal sealed partial class JsonTranscodingProviderServiceBinder<TService> : ServiceBinderBase where TService : class
|
||||
{
|
||||
private delegate (RequestDelegate RequestDelegate, List<object> Metadata) CreateRequestDelegate<TRequest, TResponse>(
|
||||
Method<TRequest, TResponse> method,
|
||||
string httpVerb,
|
||||
HttpRule httpRule,
|
||||
MethodDescriptor methodDescriptor,
|
||||
CallHandlerDescriptorInfo descriptorInfo,
|
||||
MethodOptions methodOptions);
|
||||
|
||||
private readonly ServiceMethodProviderContext<TService> _context;
|
||||
private readonly IServiceInvokerResolver<TService> _invokerResolver;
|
||||
private readonly ServiceDescriptor _serviceDescriptor;
|
||||
private readonly GrpcServiceOptions _globalOptions;
|
||||
private readonly GrpcServiceOptions<TService> _serviceOptions;
|
||||
private readonly IGrpcServiceActivator<TService> _serviceActivator;
|
||||
private readonly GrpcJsonTranscodingOptions _JsonTranscodingOptions;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
internal JsonTranscodingProviderServiceBinder(
|
||||
ServiceMethodProviderContext<TService> context,
|
||||
IServiceInvokerResolver<TService> invokerResolver,
|
||||
ServiceDescriptor serviceDescriptor,
|
||||
GrpcServiceOptions globalOptions,
|
||||
GrpcServiceOptions<TService> serviceOptions,
|
||||
ILoggerFactory loggerFactory,
|
||||
IGrpcServiceActivator<TService> serviceActivator,
|
||||
GrpcJsonTranscodingOptions JsonTranscodingOptions)
|
||||
{
|
||||
_context = context;
|
||||
_invokerResolver = invokerResolver;
|
||||
_serviceDescriptor = serviceDescriptor;
|
||||
_globalOptions = globalOptions;
|
||||
_serviceOptions = serviceOptions;
|
||||
_serviceActivator = serviceActivator;
|
||||
_JsonTranscodingOptions = JsonTranscodingOptions;
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = loggerFactory.CreateLogger<JsonTranscodingProviderServiceBinder<TService>>();
|
||||
}
|
||||
|
||||
public override void AddMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, ClientStreamingServerMethod<TRequest, TResponse> handler)
|
||||
{
|
||||
if (TryGetMethodDescriptor(method.Name, out var methodDescriptor) &&
|
||||
ServiceDescriptorHelpers.TryGetHttpRule(methodDescriptor, out _))
|
||||
{
|
||||
Log.StreamingMethodNotSupported(_logger, method.Name, method.ServiceName);
|
||||
}
|
||||
}
|
||||
|
||||
public override void AddMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, DuplexStreamingServerMethod<TRequest, TResponse> handler)
|
||||
{
|
||||
if (TryGetMethodDescriptor(method.Name, out var methodDescriptor) &&
|
||||
ServiceDescriptorHelpers.TryGetHttpRule(methodDescriptor, out _))
|
||||
{
|
||||
Log.StreamingMethodNotSupported(_logger, method.Name, method.ServiceName);
|
||||
}
|
||||
}
|
||||
|
||||
public override void AddMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, ServerStreamingServerMethod<TRequest, TResponse> handler)
|
||||
{
|
||||
if (TryGetMethodDescriptor(method.Name, out var methodDescriptor))
|
||||
{
|
||||
if (ServiceDescriptorHelpers.TryGetHttpRule(methodDescriptor, out var httpRule))
|
||||
{
|
||||
LogMethodHttpRule(method, httpRule);
|
||||
ProcessHttpRule(method, methodDescriptor, httpRule, CreateServerStreamingRequestDelegate);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Consider setting to enable mapping to methods without HttpRule
|
||||
// AddMethodCore(method, method.FullName, "GET", string.Empty, string.Empty, methodDescriptor);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.MethodDescriptorNotFound(_logger, method.Name, typeof(TService));
|
||||
}
|
||||
}
|
||||
|
||||
public override void AddMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, UnaryServerMethod<TRequest, TResponse> handler)
|
||||
{
|
||||
if (TryGetMethodDescriptor(method.Name, out var methodDescriptor))
|
||||
{
|
||||
if (ServiceDescriptorHelpers.TryGetHttpRule(methodDescriptor, out var httpRule))
|
||||
{
|
||||
LogMethodHttpRule(method, httpRule);
|
||||
ProcessHttpRule(method, methodDescriptor, httpRule, CreateUnaryRequestDelegate);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Consider setting to enable mapping to methods without HttpRule
|
||||
// AddMethodCore(method, method.FullName, "GET", string.Empty, string.Empty, methodDescriptor);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.MethodDescriptorNotFound(_logger, method.Name, typeof(TService));
|
||||
}
|
||||
}
|
||||
|
||||
private void LogMethodHttpRule(IMethod method, HttpRule httpRule)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
Log.HttpRuleFound(_logger, method.Name, method.ServiceName, httpRule.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessHttpRule<TRequest, TResponse>(
|
||||
Method<TRequest, TResponse> method,
|
||||
MethodDescriptor methodDescriptor,
|
||||
HttpRule httpRule,
|
||||
CreateRequestDelegate<TRequest, TResponse> createRequestDelegate)
|
||||
where TRequest : class
|
||||
where TResponse : class
|
||||
{
|
||||
if (ServiceDescriptorHelpers.TryResolvePattern(httpRule, out var pattern, out var httpVerb))
|
||||
{
|
||||
AddMethodCore(method, httpRule, pattern, httpVerb, httpRule.Body, httpRule.ResponseBody, methodDescriptor, createRequestDelegate);
|
||||
}
|
||||
|
||||
foreach (var additionalRule in httpRule.AdditionalBindings)
|
||||
{
|
||||
ProcessHttpRule(method, methodDescriptor, additionalRule, createRequestDelegate);
|
||||
}
|
||||
}
|
||||
|
||||
private (RequestDelegate RequestDelegate, List<object> Metadata) CreateUnaryRequestDelegate<TRequest, TResponse>(
|
||||
Method<TRequest, TResponse> method,
|
||||
string httpVerb,
|
||||
HttpRule httpRule,
|
||||
MethodDescriptor methodDescriptor,
|
||||
CallHandlerDescriptorInfo descriptorInfo,
|
||||
MethodOptions methodOptions)
|
||||
where TRequest : class
|
||||
where TResponse : class
|
||||
{
|
||||
var (invoker, metadata) = _invokerResolver.CreateModelCore<UnaryServerMethod<TService, TRequest, TResponse>>(
|
||||
method.Name,
|
||||
new[] { typeof(TRequest), typeof(ServerCallContext) },
|
||||
httpVerb,
|
||||
httpRule,
|
||||
methodDescriptor);
|
||||
|
||||
var methodInvoker = new UnaryServerMethodInvoker<TService, TRequest, TResponse>(invoker, method, methodOptions, _serviceActivator);
|
||||
var callHandler = new UnaryServerCallHandler<TService, TRequest, TResponse>(
|
||||
methodInvoker,
|
||||
_loggerFactory,
|
||||
descriptorInfo,
|
||||
_JsonTranscodingOptions.UnarySerializerOptions);
|
||||
|
||||
return (callHandler.HandleCallAsync, metadata);
|
||||
}
|
||||
|
||||
private (RequestDelegate RequestDelegate, List<object> Metadata) CreateServerStreamingRequestDelegate<TRequest, TResponse>(
|
||||
Method<TRequest, TResponse> method,
|
||||
string httpVerb,
|
||||
HttpRule httpRule,
|
||||
MethodDescriptor methodDescriptor,
|
||||
CallHandlerDescriptorInfo descriptorInfo,
|
||||
MethodOptions methodOptions)
|
||||
where TRequest : class
|
||||
where TResponse : class
|
||||
{
|
||||
var (invoker, metadata) = _invokerResolver.CreateModelCore<ServerStreamingServerMethod<TService, TRequest, TResponse>>(
|
||||
method.Name,
|
||||
new[] { typeof(TRequest), typeof(IServerStreamWriter<TResponse>), typeof(ServerCallContext) },
|
||||
httpVerb,
|
||||
httpRule,
|
||||
methodDescriptor);
|
||||
|
||||
var methodInvoker = new ServerStreamingServerMethodInvoker<TService, TRequest, TResponse>(invoker, method, methodOptions, _serviceActivator);
|
||||
var callHandler = new ServerStreamingServerCallHandler<TService, TRequest, TResponse>(
|
||||
methodInvoker,
|
||||
_loggerFactory,
|
||||
descriptorInfo,
|
||||
_JsonTranscodingOptions.ServerStreamingSerializerOptions);
|
||||
|
||||
return (callHandler.HandleCallAsync, metadata);
|
||||
}
|
||||
|
||||
private void AddMethodCore<TRequest, TResponse>(
|
||||
Method<TRequest, TResponse> method,
|
||||
HttpRule httpRule,
|
||||
string pattern,
|
||||
string httpVerb,
|
||||
string body,
|
||||
string responseBody,
|
||||
MethodDescriptor methodDescriptor,
|
||||
CreateRequestDelegate<TRequest, TResponse> createRequestDelegate)
|
||||
where TRequest : class
|
||||
where TResponse : class
|
||||
{
|
||||
try
|
||||
{
|
||||
var (routePattern, descriptorInfo) = ParseRoute(pattern, body, responseBody, methodDescriptor);
|
||||
var methodOptions = MethodOptions.Create(new[] { _globalOptions, _serviceOptions });
|
||||
|
||||
var (requestDelegate, metadata) = createRequestDelegate(method, httpVerb, httpRule, methodDescriptor, descriptorInfo, methodOptions);
|
||||
|
||||
_context.AddMethod<TRequest, TResponse>(method, routePattern, metadata, requestDelegate);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Error binding {method.Name} on {typeof(TService).Name} to HTTP API.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static (RoutePattern routePattern, CallHandlerDescriptorInfo descriptorInfo) ParseRoute(string pattern, string body, string responseBody, MethodDescriptor methodDescriptor)
|
||||
{
|
||||
if (!pattern.StartsWith('/'))
|
||||
{
|
||||
// This validation is consistent with grpc-gateway code generation.
|
||||
// We should match their validation to be a good member of the eco-system.
|
||||
throw new InvalidOperationException($"Path template '{pattern}' must start with a '/'.");
|
||||
}
|
||||
|
||||
var routePattern = RoutePatternFactory.Parse(pattern);
|
||||
return (RoutePatternFactory.Parse(pattern), CreateDescriptorInfo(body, responseBody, methodDescriptor, routePattern));
|
||||
}
|
||||
|
||||
private static CallHandlerDescriptorInfo CreateDescriptorInfo(string body, string responseBody, MethodDescriptor methodDescriptor, RoutePattern routePattern)
|
||||
{
|
||||
var routeParameterDescriptors = ServiceDescriptorHelpers.ResolveRouteParameterDescriptors(routePattern, methodDescriptor.InputType);
|
||||
|
||||
var bodyDescriptor = ServiceDescriptorHelpers.ResolveBodyDescriptor(body, typeof(TService), methodDescriptor);
|
||||
|
||||
FieldDescriptor? responseBodyDescriptor = null;
|
||||
if (!string.IsNullOrEmpty(responseBody))
|
||||
{
|
||||
responseBodyDescriptor = methodDescriptor.OutputType.FindFieldByName(responseBody);
|
||||
if (responseBodyDescriptor == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Couldn't find matching field for response body '{responseBody}' on {methodDescriptor.OutputType.Name}.");
|
||||
}
|
||||
}
|
||||
|
||||
var descriptorInfo = new CallHandlerDescriptorInfo(
|
||||
responseBodyDescriptor,
|
||||
bodyDescriptor?.Descriptor,
|
||||
bodyDescriptor?.IsDescriptorRepeated ?? false,
|
||||
bodyDescriptor?.FieldDescriptors,
|
||||
routeParameterDescriptors);
|
||||
return descriptorInfo;
|
||||
}
|
||||
|
||||
private bool TryGetMethodDescriptor(string methodName, [NotNullWhen(true)]out MethodDescriptor? methodDescriptor)
|
||||
{
|
||||
for (var i = 0; i < _serviceDescriptor.Methods.Count; i++)
|
||||
{
|
||||
var method = _serviceDescriptor.Methods[i];
|
||||
if (method.Name == methodName)
|
||||
{
|
||||
methodDescriptor = method;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
methodDescriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static partial class Log
|
||||
{
|
||||
[LoggerMessage(1, LogLevel.Warning, "Unable to find method descriptor for {MethodName} on {ServiceType}.", EventName = "MethodDescriptorNotFound")]
|
||||
public static partial void MethodDescriptorNotFound(ILogger logger, string methodName, Type serviceType);
|
||||
|
||||
[LoggerMessage(2, LogLevel.Warning, "Unable to bind {MethodName} on {ServiceName} to gRPC JSON transcoding. Client and bidirectional streaming methods are not supported.", EventName = "StreamingMethodNotSupported")]
|
||||
public static partial void StreamingMethodNotSupported(ILogger logger, string methodName, string serviceName);
|
||||
|
||||
[LoggerMessage(3, LogLevel.Trace, "Found HttpRule mapping. Method {MethodName} on {ServiceName}. HttpRule payload: {HttpRulePayload}", EventName = "HttpRuleFound", SkipEnabledCheck = true)]
|
||||
public static partial void HttpRuleFound(ILogger logger, string methodName, string serviceName, string httpRulePayload);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Google.Protobuf.Reflection;
|
||||
using Grpc.AspNetCore.Server;
|
||||
using Grpc.AspNetCore.Server.Model;
|
||||
using Grpc.Shared;
|
||||
using Grpc.Shared.Server;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Binding;
|
||||
|
||||
internal sealed partial class JsonTranscodingServiceMethodProvider<TService> : IServiceMethodProvider<TService> where TService : class
|
||||
{
|
||||
private readonly ILogger<JsonTranscodingServiceMethodProvider<TService>> _logger;
|
||||
private readonly GrpcServiceOptions _globalOptions;
|
||||
private readonly GrpcServiceOptions<TService> _serviceOptions;
|
||||
private readonly GrpcJsonTranscodingOptions _JsonTranscodingOptions;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IGrpcServiceActivator<TService> _serviceActivator;
|
||||
|
||||
public JsonTranscodingServiceMethodProvider(
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<GrpcServiceOptions> globalOptions,
|
||||
IOptions<GrpcServiceOptions<TService>> serviceOptions,
|
||||
IGrpcServiceActivator<TService> serviceActivator,
|
||||
IOptions<GrpcJsonTranscodingOptions> JsonTranscodingOptions)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<JsonTranscodingServiceMethodProvider<TService>>();
|
||||
_globalOptions = globalOptions.Value;
|
||||
_serviceOptions = serviceOptions.Value;
|
||||
_JsonTranscodingOptions = JsonTranscodingOptions.Value;
|
||||
_loggerFactory = loggerFactory;
|
||||
_serviceActivator = serviceActivator;
|
||||
}
|
||||
|
||||
public void OnServiceMethodDiscovery(ServiceMethodProviderContext<TService> context)
|
||||
{
|
||||
var bindMethodInfo = BindMethodFinder.GetBindMethod(typeof(TService));
|
||||
|
||||
// Invoke BindService(ServiceBinderBase, BaseType)
|
||||
if (bindMethodInfo != null)
|
||||
{
|
||||
// The second parameter is always the service base type
|
||||
var serviceParameter = bindMethodInfo.GetParameters()[1];
|
||||
|
||||
ServiceDescriptor? serviceDescriptor = null;
|
||||
try
|
||||
{
|
||||
serviceDescriptor = ServiceDescriptorHelpers.GetServiceDescriptor(bindMethodInfo.DeclaringType!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.ServiceDescriptorError(_logger, typeof(TService), ex);
|
||||
}
|
||||
|
||||
if (serviceDescriptor != null)
|
||||
{
|
||||
var binder = new JsonTranscodingProviderServiceBinder<TService>(
|
||||
context,
|
||||
new ReflectionServiceInvokerResolver<TService>(serviceParameter.ParameterType),
|
||||
serviceDescriptor,
|
||||
_globalOptions,
|
||||
_serviceOptions,
|
||||
_loggerFactory,
|
||||
_serviceActivator,
|
||||
_JsonTranscodingOptions);
|
||||
|
||||
try
|
||||
{
|
||||
bindMethodInfo.Invoke(null, new object?[] { binder, null });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Error binding gRPC service '{typeof(TService).Name}'.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.BindMethodNotFound(_logger, typeof(TService));
|
||||
}
|
||||
}
|
||||
|
||||
private static partial class Log
|
||||
{
|
||||
[LoggerMessage(1, LogLevel.Warning, "Could not find bind method for {ServiceType}.", EventName = "BindMethodNotFound")]
|
||||
public static partial void BindMethodNotFound(ILogger logger, Type serviceType);
|
||||
|
||||
[LoggerMessage(2, LogLevel.Warning, "Error getting service descriptor for {ServiceType}.", EventName = "ServiceDescriptorError")]
|
||||
public static partial void ServiceDescriptorError(ILogger logger, Type serviceType, Exception ex);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Google.Api;
|
||||
using Google.Protobuf.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Binding;
|
||||
|
||||
internal interface IServiceInvokerResolver<TService> where TService : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a service invoker delegate and associated metadata using the service type, method name, and HTTP binding.
|
||||
/// </summary>
|
||||
(TDelegate invoker, List<object> metadata) CreateModelCore<TDelegate>(
|
||||
string methodName,
|
||||
Type[] methodParameters,
|
||||
string verb,
|
||||
HttpRule httpRule,
|
||||
MethodDescriptor methodDescriptor) where TDelegate : Delegate;
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Reflection;
|
||||
using Google.Api;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Binding;
|
||||
|
||||
internal sealed class ReflectionServiceInvokerResolver<TService>
|
||||
: IServiceInvokerResolver<TService> where TService : class
|
||||
{
|
||||
private readonly Type _declaringType;
|
||||
|
||||
public ReflectionServiceInvokerResolver(Type declaringType)
|
||||
{
|
||||
_declaringType = declaringType;
|
||||
}
|
||||
|
||||
public (TDelegate invoker, List<object> metadata) CreateModelCore<TDelegate>(
|
||||
string methodName,
|
||||
Type[] methodParameters,
|
||||
string verb,
|
||||
HttpRule httpRule,
|
||||
MethodDescriptor methodDescriptor) where TDelegate : Delegate
|
||||
{
|
||||
var handlerMethod = GetMethod(methodName, methodParameters);
|
||||
|
||||
if (handlerMethod == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not find '{methodName}' on {typeof(TService)}.");
|
||||
}
|
||||
|
||||
var invoker = (TDelegate)Delegate.CreateDelegate(typeof(TDelegate), handlerMethod);
|
||||
|
||||
var metadata = new List<object>();
|
||||
// Add type metadata first so it has a lower priority
|
||||
metadata.AddRange(typeof(TService).GetCustomAttributes(inherit: true));
|
||||
// Add method metadata last so it has a higher priority
|
||||
metadata.AddRange(handlerMethod.GetCustomAttributes(inherit: true));
|
||||
metadata.Add(new HttpMethodMetadata(new[] { verb }));
|
||||
|
||||
// Add protobuf service method descriptor.
|
||||
// Is used by swagger generation to identify gRPC JSON transcoding APIs.
|
||||
metadata.Add(new GrpcJsonTranscodingMetadata(methodDescriptor, httpRule));
|
||||
|
||||
return (invoker, metadata);
|
||||
}
|
||||
|
||||
private MethodInfo? GetMethod(string methodName, Type[] methodParameters)
|
||||
{
|
||||
Type? currentType = typeof(TService);
|
||||
while (currentType != null)
|
||||
{
|
||||
var matchingMethod = currentType.GetMethod(
|
||||
methodName,
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
binder: null,
|
||||
types: methodParameters,
|
||||
modifiers: null);
|
||||
|
||||
if (matchingMethod == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate that the method overrides the virtual method on the base service type.
|
||||
// If there is a method with the same name it will hide the base method. Ignore it,
|
||||
// and continue searching on the base type.
|
||||
if (matchingMethod.IsVirtual)
|
||||
{
|
||||
var baseDefinitionMethod = matchingMethod.GetBaseDefinition();
|
||||
if (baseDefinitionMethod != null && baseDefinitionMethod.DeclaringType == _declaringType)
|
||||
{
|
||||
return matchingMethod;
|
||||
}
|
||||
}
|
||||
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Google.Protobuf.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.CallHandlers;
|
||||
|
||||
internal sealed class CallHandlerDescriptorInfo
|
||||
{
|
||||
public CallHandlerDescriptorInfo(
|
||||
FieldDescriptor? responseBodyDescriptor,
|
||||
MessageDescriptor? bodyDescriptor,
|
||||
bool bodyDescriptorRepeated,
|
||||
List<FieldDescriptor>? bodyFieldDescriptors,
|
||||
Dictionary<string, List<FieldDescriptor>> routeParameterDescriptors)
|
||||
{
|
||||
ResponseBodyDescriptor = responseBodyDescriptor;
|
||||
BodyDescriptor = bodyDescriptor;
|
||||
BodyDescriptorRepeated = bodyDescriptorRepeated;
|
||||
BodyFieldDescriptors = bodyFieldDescriptors;
|
||||
RouteParameterDescriptors = routeParameterDescriptors;
|
||||
if (BodyFieldDescriptors != null)
|
||||
{
|
||||
BodyFieldDescriptorsPath = string.Join('.', BodyFieldDescriptors.Select(d => d.Name));
|
||||
}
|
||||
PathDescriptorsCache = new ConcurrentDictionary<string, List<FieldDescriptor>?>();
|
||||
}
|
||||
|
||||
public FieldDescriptor? ResponseBodyDescriptor { get; }
|
||||
public MessageDescriptor? BodyDescriptor { get; }
|
||||
[MemberNotNullWhen(true, nameof(BodyFieldDescriptors), nameof(BodyFieldDescriptorsPath))]
|
||||
public bool BodyDescriptorRepeated { get; }
|
||||
public List<FieldDescriptor>? BodyFieldDescriptors { get; }
|
||||
public Dictionary<string, List<FieldDescriptor>> RouteParameterDescriptors { get; }
|
||||
public ConcurrentDictionary<string, List<FieldDescriptor>?> PathDescriptorsCache { get; }
|
||||
public string? BodyFieldDescriptorsPath { get; }
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using Grpc.AspNetCore.Server;
|
||||
using Grpc.Core;
|
||||
using Grpc.Shared.Server;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.CallHandlers;
|
||||
|
||||
internal abstract class ServerCallHandlerBase<TService, TRequest, TResponse>
|
||||
where TService : class
|
||||
where TRequest : class
|
||||
where TResponse : class
|
||||
{
|
||||
private const string LoggerName = "Grpc.AspNetCore.Grpc.JsonTranscoding.ServerCallHandler";
|
||||
|
||||
protected ServerMethodInvokerBase<TService, TRequest, TResponse> MethodInvoker { get; }
|
||||
public CallHandlerDescriptorInfo DescriptorInfo { get; }
|
||||
public JsonSerializerOptions SerializerOptions { get; }
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
protected ServerCallHandlerBase(
|
||||
ServerMethodInvokerBase<TService, TRequest, TResponse> methodInvoker,
|
||||
ILoggerFactory loggerFactory,
|
||||
CallHandlerDescriptorInfo descriptorInfo,
|
||||
JsonSerializerOptions serializerOptions)
|
||||
{
|
||||
MethodInvoker = methodInvoker;
|
||||
DescriptorInfo = descriptorInfo;
|
||||
SerializerOptions = serializerOptions;
|
||||
Logger = loggerFactory.CreateLogger(LoggerName);
|
||||
}
|
||||
|
||||
public Task HandleCallAsync(HttpContext httpContext)
|
||||
{
|
||||
var serverCallContext = new JsonTranscodingServerCallContext(httpContext, MethodInvoker.Options, MethodInvoker.Method, DescriptorInfo, Logger);
|
||||
httpContext.Features.Set<IServerCallContextFeature>(serverCallContext);
|
||||
|
||||
try
|
||||
{
|
||||
serverCallContext.Initialize();
|
||||
|
||||
var handleCallTask = HandleCallAsyncCore(httpContext, serverCallContext);
|
||||
|
||||
if (handleCallTask.IsCompletedSuccessfully)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
return AwaitHandleCall(serverCallContext, MethodInvoker.Method, IsStreaming, SerializerOptions, handleCallTask);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return serverCallContext.ProcessHandlerErrorAsync(ex, MethodInvoker.Method.Name, IsStreaming, SerializerOptions);
|
||||
}
|
||||
|
||||
static async Task AwaitHandleCall(JsonTranscodingServerCallContext serverCallContext, Method<TRequest, TResponse> method, bool isStreaming, JsonSerializerOptions serializerOptions, Task handleCall)
|
||||
{
|
||||
try
|
||||
{
|
||||
await handleCall;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await serverCallContext.ProcessHandlerErrorAsync(ex, method.Name, isStreaming, serializerOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task HandleCallAsyncCore(HttpContext httpContext, JsonTranscodingServerCallContext serverCallContext);
|
||||
|
||||
protected virtual bool IsStreaming => false;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using Grpc.Shared.Server;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.CallHandlers;
|
||||
|
||||
internal sealed class ServerStreamingServerCallHandler<TService, TRequest, TResponse> : ServerCallHandlerBase<TService, TRequest, TResponse>
|
||||
where TService : class
|
||||
where TRequest : class
|
||||
where TResponse : class
|
||||
{
|
||||
private readonly ServerStreamingServerMethodInvoker<TService, TRequest, TResponse> _invoker;
|
||||
|
||||
public ServerStreamingServerCallHandler(
|
||||
ServerStreamingServerMethodInvoker<TService, TRequest, TResponse> unaryMethodInvoker,
|
||||
ILoggerFactory loggerFactory,
|
||||
CallHandlerDescriptorInfo descriptorInfo,
|
||||
JsonSerializerOptions options) : base(unaryMethodInvoker, loggerFactory, descriptorInfo, options)
|
||||
{
|
||||
_invoker = unaryMethodInvoker;
|
||||
}
|
||||
|
||||
protected override async Task HandleCallAsyncCore(HttpContext httpContext, JsonTranscodingServerCallContext serverCallContext)
|
||||
{
|
||||
// Decode request
|
||||
var request = await JsonRequestHelpers.ReadMessage<TRequest>(serverCallContext, SerializerOptions);
|
||||
|
||||
var streamWriter = new HttpContextStreamWriter<TResponse>(serverCallContext, SerializerOptions);
|
||||
try
|
||||
{
|
||||
await _invoker.Invoke(httpContext, serverCallContext, request, streamWriter);
|
||||
}
|
||||
finally
|
||||
{
|
||||
streamWriter.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool IsStreaming => true;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using Grpc.Core;
|
||||
using Grpc.Shared.Server;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.CallHandlers;
|
||||
|
||||
internal sealed class UnaryServerCallHandler<TService, TRequest, TResponse> : ServerCallHandlerBase<TService, TRequest, TResponse>
|
||||
where TService : class
|
||||
where TRequest : class
|
||||
where TResponse : class
|
||||
{
|
||||
private readonly UnaryServerMethodInvoker<TService, TRequest, TResponse> _invoker;
|
||||
|
||||
public UnaryServerCallHandler(
|
||||
UnaryServerMethodInvoker<TService, TRequest, TResponse> unaryMethodInvoker,
|
||||
ILoggerFactory loggerFactory,
|
||||
CallHandlerDescriptorInfo descriptorInfo,
|
||||
JsonSerializerOptions options) : base(unaryMethodInvoker, loggerFactory, descriptorInfo, options)
|
||||
{
|
||||
_invoker = unaryMethodInvoker;
|
||||
}
|
||||
|
||||
protected override async Task HandleCallAsyncCore(HttpContext httpContext, JsonTranscodingServerCallContext serverCallContext)
|
||||
{
|
||||
var request = await JsonRequestHelpers.ReadMessage<TRequest>(serverCallContext, SerializerOptions);
|
||||
|
||||
var response = await _invoker.Invoke(httpContext, serverCallContext, request);
|
||||
|
||||
if (serverCallContext.Status.StatusCode != StatusCode.OK)
|
||||
{
|
||||
throw new RpcException(serverCallContext.Status);
|
||||
}
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
// This is consistent with Grpc.Core when a null value is returned
|
||||
throw new RpcException(new Status(StatusCode.Cancelled, "No message returned from method."));
|
||||
}
|
||||
|
||||
serverCallContext.EnsureResponseHeaders();
|
||||
|
||||
await JsonRequestHelpers.SendMessage(serverCallContext, SerializerOptions, response);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
|
||||
|
||||
internal static class CommonGrpcProtocolHelpers
|
||||
{
|
||||
public static string ConvertToRpcExceptionMessage(Exception ex)
|
||||
{
|
||||
// RpcException doesn't allow for an inner exception. To ensure the user is getting enough information about the
|
||||
// error we will concatenate any inner exception messages together.
|
||||
return ex.InnerException == null ? $"{ex.GetType().Name}: {ex.Message}" : BuildErrorMessage(ex);
|
||||
}
|
||||
|
||||
private static string BuildErrorMessage(Exception ex)
|
||||
{
|
||||
// Concatenate inner exceptions messages together.
|
||||
var sb = new StringBuilder();
|
||||
var first = true;
|
||||
Exception? current = ex;
|
||||
do
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
sb.Append(' ');
|
||||
}
|
||||
else
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
sb.Append(current.GetType().Name);
|
||||
sb.Append(": ");
|
||||
sb.Append(current.Message);
|
||||
}
|
||||
while ((current = current.InnerException) != null);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
|
||||
|
||||
internal static class ErrorMessageHelper
|
||||
{
|
||||
internal static string BuildErrorMessage(string message, Exception exception, bool? includeExceptionDetails)
|
||||
{
|
||||
if (includeExceptionDetails ?? false)
|
||||
{
|
||||
return message + " " + CommonGrpcProtocolHelpers.ConvertToRpcExceptionMessage(exception);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
|
||||
|
||||
internal static class GrpcProtocolConstants
|
||||
{
|
||||
internal const string TimeoutHeader = "grpc-timeout";
|
||||
internal const string MessageEncodingHeader = "grpc-encoding";
|
||||
internal const string MessageAcceptEncodingHeader = "grpc-accept-encoding";
|
||||
internal static readonly ReadOnlyMemory<byte> StreamingDelimiter = new byte[] { (byte)'\n' };
|
||||
|
||||
internal static readonly HashSet<string> FilteredHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
MessageEncodingHeader,
|
||||
MessageAcceptEncodingHeader,
|
||||
TimeoutHeader,
|
||||
HeaderNames.ContentType,
|
||||
HeaderNames.TE,
|
||||
HeaderNames.Host,
|
||||
HeaderNames.AcceptEncoding
|
||||
};
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
|
||||
|
||||
internal static class GrpcProtocolHelpers
|
||||
{
|
||||
public static byte[] ParseBinaryHeader(string base64)
|
||||
{
|
||||
string decodable;
|
||||
switch (base64.Length % 4)
|
||||
{
|
||||
case 0:
|
||||
// base64 has the required padding
|
||||
decodable = base64;
|
||||
break;
|
||||
case 2:
|
||||
// 2 chars padding
|
||||
decodable = base64 + "==";
|
||||
break;
|
||||
case 3:
|
||||
// 3 chars padding
|
||||
decodable = base64 + "=";
|
||||
break;
|
||||
default:
|
||||
// length%4 == 1 should be illegal
|
||||
throw new FormatException("Invalid base64 header value");
|
||||
}
|
||||
|
||||
return Convert.FromBase64String(decodable);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Grpc.Core;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
|
||||
|
||||
internal static partial class GrpcServerLog
|
||||
{
|
||||
[LoggerMessage(1, LogLevel.Information, "Request content-type of '{ContentType}' is not supported.", EventName = "UnsupportedRequestContentType")]
|
||||
public static partial void UnsupportedRequestContentType(ILogger logger, string? contentType);
|
||||
|
||||
[LoggerMessage(2, LogLevel.Error, "Error when executing service method '{ServiceMethod}'.", EventName = "ErrorExecutingServiceMethod")]
|
||||
public static partial void ErrorExecutingServiceMethod(ILogger logger, string serviceMethod, Exception ex);
|
||||
|
||||
[LoggerMessage(3, LogLevel.Information, "Error status code '{StatusCode}' with detail '{Detail}' raised.", EventName = "RpcConnectionError")]
|
||||
public static partial void RpcConnectionError(ILogger logger, StatusCode statusCode, string detail);
|
||||
|
||||
[LoggerMessage(4, LogLevel.Debug, "Reading message.", EventName = "ReadingMessage")]
|
||||
public static partial void ReadingMessage(ILogger logger);
|
||||
|
||||
[LoggerMessage(5, LogLevel.Trace, "Deserializing to '{MessageType}'.", EventName = "DeserializingMessage")]
|
||||
public static partial void DeserializingMessage(ILogger logger, Type messageType);
|
||||
|
||||
[LoggerMessage(6, LogLevel.Trace, "Received message.", EventName = "ReceivedMessage")]
|
||||
public static partial void ReceivedMessage(ILogger logger);
|
||||
|
||||
[LoggerMessage(7, LogLevel.Information, "Error reading message.", EventName = "ErrorReadingMessage")]
|
||||
public static partial void ErrorReadingMessage(ILogger logger, Exception ex);
|
||||
|
||||
[LoggerMessage(8, LogLevel.Debug, "Sending message.", EventName = "SendingMessage")]
|
||||
public static partial void SendingMessage(ILogger logger);
|
||||
|
||||
[LoggerMessage(9, LogLevel.Debug, "Message sent.", EventName = "MessageSent")]
|
||||
public static partial void MessageSent(ILogger logger);
|
||||
|
||||
[LoggerMessage(10, LogLevel.Information, "Error sending message.", EventName = "ErrorSendingMessage")]
|
||||
public static partial void ErrorSendingMessage(ILogger logger, Exception ex);
|
||||
|
||||
[LoggerMessage(11, LogLevel.Trace, "Serialized '{MessageType}'.", EventName = "SerializedMessage")]
|
||||
public static partial void SerializedMessage(ILogger logger, Type messageType);
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Grpc.AspNetCore.Server;
|
||||
using Grpc.Core;
|
||||
using Grpc.Shared;
|
||||
using Grpc.Shared.Server;
|
||||
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.CallHandlers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
|
||||
|
||||
internal sealed class JsonTranscodingServerCallContext : ServerCallContext, IServerCallContextFeature
|
||||
{
|
||||
// TODO(JamesNK): Remove nullable override after Grpc.Core.Api update
|
||||
private static readonly AuthContext UnauthenticatedContext = new AuthContext(null!, new Dictionary<string, List<AuthProperty>>());
|
||||
|
||||
private readonly IMethod _method;
|
||||
|
||||
public HttpContext HttpContext { get; }
|
||||
public MethodOptions Options { get; }
|
||||
public CallHandlerDescriptorInfo DescriptorInfo { get; }
|
||||
public bool IsJsonRequestContent { get; private set; }
|
||||
// Default request encoding to UTF8 so an encoding is available
|
||||
// if the request sends an invalid/unsupported encoding.
|
||||
public Encoding RequestEncoding { get; private set; } = Encoding.UTF8;
|
||||
|
||||
internal ILogger Logger { get; }
|
||||
|
||||
private string? _peer;
|
||||
private Metadata? _requestHeaders;
|
||||
private AuthContext? _authContext;
|
||||
|
||||
public JsonTranscodingServerCallContext(HttpContext httpContext, MethodOptions options, IMethod method, CallHandlerDescriptorInfo descriptorInfo, ILogger logger)
|
||||
{
|
||||
HttpContext = httpContext;
|
||||
Options = options;
|
||||
_method = method;
|
||||
DescriptorInfo = descriptorInfo;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
IsJsonRequestContent = JsonRequestHelpers.HasJsonContentType(HttpContext.Request, out var charset);
|
||||
RequestEncoding = JsonRequestHelpers.GetEncodingFromCharset(charset) ?? Encoding.UTF8;
|
||||
|
||||
// HttpContext.Items is publically exposed as ServerCallContext.UserState.
|
||||
// Because this is a custom ServerCallContext, HttpContext must be added to UserState so GetHttpContext() continues to work.
|
||||
// https://github.com/grpc/grpc-dotnet/blob/7ef184f3c4cd62fbc3cde55e4bb3e16b58258ca1/src/Grpc.AspNetCore.Server/ServerCallContextExtensions.cs#L53-L61
|
||||
HttpContext.Items["__HttpContext"] = HttpContext;
|
||||
}
|
||||
|
||||
public ServerCallContext ServerCallContext => this;
|
||||
|
||||
protected override string MethodCore => _method.FullName;
|
||||
|
||||
protected override string HostCore => HttpContext.Request.Host.Value;
|
||||
|
||||
protected override string PeerCore
|
||||
{
|
||||
get
|
||||
{
|
||||
// Follows the standard at https://github.com/grpc/grpc/blob/master/doc/naming.md
|
||||
if (_peer == null)
|
||||
{
|
||||
_peer = BuildPeer();
|
||||
}
|
||||
|
||||
return _peer;
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildPeer()
|
||||
{
|
||||
var connection = HttpContext.Connection;
|
||||
if (connection.RemoteIpAddress != null)
|
||||
{
|
||||
switch (connection.RemoteIpAddress.AddressFamily)
|
||||
{
|
||||
case AddressFamily.InterNetwork:
|
||||
return $"ipv4:{connection.RemoteIpAddress}:{connection.RemotePort}";
|
||||
case AddressFamily.InterNetworkV6:
|
||||
return $"ipv6:[{connection.RemoteIpAddress}]:{connection.RemotePort}";
|
||||
default:
|
||||
// TODO(JamesNK) - Test what should be output when used with UDS and named pipes
|
||||
return $"unknown:{connection.RemoteIpAddress}:{connection.RemotePort}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return "unknown"; // Match Grpc.Core
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task ProcessHandlerErrorAsync(Exception ex, string method, bool isStreaming, JsonSerializerOptions options)
|
||||
{
|
||||
Status status;
|
||||
if (ex is RpcException rpcException)
|
||||
{
|
||||
// RpcException is thrown by client code to modify the status returned from the server.
|
||||
// Log the status and detail. Don't log the exception to reduce log verbosity.
|
||||
GrpcServerLog.RpcConnectionError(Logger, rpcException.StatusCode, rpcException.Status.Detail);
|
||||
|
||||
status = rpcException.Status;
|
||||
}
|
||||
else
|
||||
{
|
||||
GrpcServerLog.ErrorExecutingServiceMethod(Logger, method, ex);
|
||||
|
||||
var message = ErrorMessageHelper.BuildErrorMessage("Exception was thrown by handler.", ex, Options.EnableDetailedErrors);
|
||||
|
||||
// Note that the exception given to status won't be returned to the client.
|
||||
// It is still useful to set in case an interceptor accesses the status on the server.
|
||||
status = new Status(StatusCode.Unknown, message, ex);
|
||||
}
|
||||
|
||||
await JsonRequestHelpers.SendErrorResponse(HttpContext.Response, RequestEncoding, status, options);
|
||||
if (isStreaming)
|
||||
{
|
||||
await HttpContext.Response.Body.WriteAsync(GrpcProtocolConstants.StreamingDelimiter);
|
||||
}
|
||||
}
|
||||
|
||||
// Deadline returns max value when there isn't a deadline.
|
||||
protected override DateTime DeadlineCore => DateTime.MaxValue;
|
||||
|
||||
protected override Metadata RequestHeadersCore
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_requestHeaders == null)
|
||||
{
|
||||
_requestHeaders = new Metadata();
|
||||
|
||||
foreach (var header in HttpContext.Request.Headers)
|
||||
{
|
||||
// gRPC metadata contains a subset of the request headers
|
||||
// Filter out pseudo headers (start with :) and other known headers
|
||||
if (header.Key.StartsWith(':') || GrpcProtocolConstants.FilteredHeaders.Contains(header.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (header.Key.EndsWith(Metadata.BinaryHeaderSuffix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_requestHeaders.Add(header.Key, GrpcProtocolHelpers.ParseBinaryHeader(header.Value!));
|
||||
}
|
||||
else
|
||||
{
|
||||
_requestHeaders.Add(header.Key, header.Value!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _requestHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
protected override CancellationToken CancellationTokenCore => HttpContext.RequestAborted;
|
||||
|
||||
protected override Metadata ResponseTrailersCore => throw new NotImplementedException();
|
||||
|
||||
protected override Status StatusCore { get; set; }
|
||||
|
||||
protected override WriteOptions WriteOptionsCore
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override AuthContext AuthContextCore
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_authContext == null)
|
||||
{
|
||||
var clientCertificate = HttpContext.Connection.ClientCertificate;
|
||||
|
||||
_authContext = clientCertificate == null
|
||||
? UnauthenticatedContext
|
||||
: AuthContextHelpers.CreateAuthContext(clientCertificate);
|
||||
}
|
||||
|
||||
return _authContext;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(JamesNK): Remove nullable override after Grpc.Core.Api update
|
||||
protected override IDictionary<object, object> UserStateCore => HttpContext.Items!;
|
||||
|
||||
protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions? options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
|
||||
{
|
||||
// Headers can only be written once. Throw on subsequent call to write response header instead of silent no-op.
|
||||
if (HttpContext.Response.HasStarted)
|
||||
{
|
||||
throw new InvalidOperationException("Response headers can only be sent once per call.");
|
||||
}
|
||||
|
||||
if (responseHeaders != null)
|
||||
{
|
||||
foreach (var entry in responseHeaders)
|
||||
{
|
||||
if (entry.IsBinary)
|
||||
{
|
||||
HttpContext.Response.Headers[entry.Key] = Convert.ToBase64String(entry.ValueBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Response.Headers[entry.Key] = entry.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EnsureResponseHeaders();
|
||||
|
||||
return HttpContext.Response.BodyWriter.FlushAsync().GetAsTask();
|
||||
}
|
||||
|
||||
internal void EnsureResponseHeaders()
|
||||
{
|
||||
if (!HttpContext.Response.HasStarted)
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||
HttpContext.Response.ContentType = MediaType.ReplaceEncoding("application/json", RequestEncoding);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using Grpc.Core;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
|
||||
|
||||
internal sealed class HttpContextStreamWriter<TResponse> : IServerStreamWriter<TResponse>
|
||||
where TResponse : class
|
||||
{
|
||||
private readonly JsonTranscodingServerCallContext _context;
|
||||
private readonly JsonSerializerOptions _serializerOptions;
|
||||
private readonly object _writeLock;
|
||||
private Task? _writeTask;
|
||||
private bool _completed;
|
||||
|
||||
public HttpContextStreamWriter(JsonTranscodingServerCallContext context, JsonSerializerOptions serializerOptions)
|
||||
{
|
||||
_context = context;
|
||||
_serializerOptions = serializerOptions;
|
||||
_writeLock = new object();
|
||||
}
|
||||
|
||||
public WriteOptions WriteOptions
|
||||
{
|
||||
get => _context.WriteOptions;
|
||||
set => _context.WriteOptions = value;
|
||||
}
|
||||
|
||||
public Task WriteAsync(TResponse message)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
return Task.FromException(new ArgumentNullException(nameof(message)));
|
||||
}
|
||||
|
||||
if (_completed || _context.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromException(new InvalidOperationException("Can't write the message because the request is complete."));
|
||||
}
|
||||
|
||||
lock (_writeLock)
|
||||
{
|
||||
// Pending writes need to be awaited first
|
||||
if (IsWriteInProgressUnsynchronized)
|
||||
{
|
||||
return Task.FromException(new InvalidOperationException("Can't write the message because the previous write is in progress."));
|
||||
}
|
||||
|
||||
// Save write task to track whether it is complete. Must be set inside lock.
|
||||
_writeTask = WriteMessageAndDelimiter(message);
|
||||
}
|
||||
|
||||
return _writeTask;
|
||||
}
|
||||
|
||||
private async Task WriteMessageAndDelimiter(TResponse message)
|
||||
{
|
||||
await JsonRequestHelpers.SendMessage(_context, _serializerOptions, message);
|
||||
await _context.HttpContext.Response.Body.WriteAsync(GrpcProtocolConstants.StreamingDelimiter);
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A value indicating whether there is an async write already in progress.
|
||||
/// Should only check this property when holding the write lock.
|
||||
/// </summary>
|
||||
private bool IsWriteInProgressUnsynchronized
|
||||
{
|
||||
get
|
||||
{
|
||||
var writeTask = _writeTask;
|
||||
return writeTask != null && !writeTask.IsCompleted;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Shared;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class AnyConverter<TMessage> : SettingsConverterBase<TMessage> where TMessage : IMessage, new()
|
||||
{
|
||||
internal const string AnyTypeUrlField = "@type";
|
||||
internal const string AnyWellKnownTypeValueField = "value";
|
||||
|
||||
public AnyConverter(JsonContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
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 typeUrl = urlField.GetString();
|
||||
var typeName = Any.GetTypeName(typeUrl);
|
||||
|
||||
var descriptor = Context.TypeRegistry.Find(typeName);
|
||||
if (descriptor == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Type registry has no descriptor for type name '{typeName}'.");
|
||||
}
|
||||
|
||||
IMessage data;
|
||||
if (ServiceDescriptorHelpers.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)!;
|
||||
}
|
||||
|
||||
var message = new TMessage();
|
||||
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 = Context.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 (ServiceDescriptorHelpers.IsWellKnownType(descriptor))
|
||||
{
|
||||
writer.WritePropertyName(AnyWellKnownTypeValueField);
|
||||
if (ServiceDescriptorHelpers.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, Context.Settings, options);
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.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,23 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Google.Protobuf;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.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,58 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class DurationConverter<TMessage> : SettingsConverterBase<TMessage> where TMessage : IMessage, new()
|
||||
{
|
||||
public DurationConverter(JsonContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
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,125 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class EnumConverter<TEnum> : SettingsConverterBase<TEnum> where TEnum : Enum
|
||||
{
|
||||
public EnumConverter(JsonContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
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 (Context.Settings.WriteEnumsAsIntegers)
|
||||
{
|
||||
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,53 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class FieldMaskConverter<TMessage> : SettingsConverterBase<TMessage> where TMessage : IMessage, new()
|
||||
{
|
||||
public FieldMaskConverter(JsonContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
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"?
|
||||
// Note: This logic replicates Google.Protobuf. Should follow their lead.
|
||||
var jsonPaths = reader.GetString()!.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
var messagePaths = (IList)message.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(message);
|
||||
foreach (var path in jsonPaths)
|
||||
{
|
||||
messagePaths.Add(Legacy.ToSnakeCase(path));
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, TMessage value, JsonSerializerOptions options)
|
||||
{
|
||||
// Note: This logic replicates Google.Protobuf. Should follow their lead.
|
||||
var paths = (IList<string>)value.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(value);
|
||||
var firstInvalid = paths.FirstOrDefault(p => !Legacy.IsPathValid(p));
|
||||
if (firstInvalid == null)
|
||||
{
|
||||
writer.WriteStringValue(string.Join(",", paths.Select(Legacy.ToJsonName)));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid field mask to be converted to JSON: {firstInvalid}.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class Int64Converter : SettingsConverterBase<long>
|
||||
{
|
||||
public Int64Converter(JsonContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
return long.Parse(reader.GetString()!, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return reader.GetInt64();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
|
||||
{
|
||||
if (!Context.Settings.WriteInt64sAsStrings)
|
||||
{
|
||||
writer.WriteNumberValue(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStringValue(value.ToString("d", CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Google.Protobuf.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class JsonContext
|
||||
{
|
||||
public JsonContext(GrpcJsonSettings settings, TypeRegistry typeRegistry)
|
||||
{
|
||||
Settings = settings;
|
||||
TypeRegistry = typeRegistry;
|
||||
}
|
||||
|
||||
public GrpcJsonSettings Settings { get; }
|
||||
public TypeRegistry TypeRegistry { get; }
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal class JsonConverterFactoryForEnum : JsonConverterFactory
|
||||
{
|
||||
private readonly JsonContext _context;
|
||||
|
||||
public JsonConverterFactoryForEnum(JsonContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
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[] { _context },
|
||||
culture: null)!;
|
||||
|
||||
return converter;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Google.Protobuf;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal class JsonConverterFactoryForMessage : JsonConverterFactory
|
||||
{
|
||||
private readonly JsonContext _context;
|
||||
|
||||
public JsonConverterFactoryForMessage(JsonContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
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[] { _context },
|
||||
culture: null)!;
|
||||
|
||||
return converter;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
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.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal class JsonConverterFactoryForWellKnownTypes : JsonConverterFactory
|
||||
{
|
||||
private readonly JsonContext _context;
|
||||
|
||||
public JsonConverterFactoryForWellKnownTypes(JsonContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
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[] { _context },
|
||||
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,50 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Google.Protobuf;
|
||||
using Grpc.Shared;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal class JsonConverterFactoryForWrappers : JsonConverterFactory
|
||||
{
|
||||
private readonly JsonContext _context;
|
||||
|
||||
public JsonConverterFactoryForWrappers(JsonContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
if (!typeof(IMessage).IsAssignableFrom(typeToConvert))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var descriptor = JsonConverterHelper.GetMessageDescriptor(typeToConvert);
|
||||
if (descriptor == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ServiceDescriptorHelpers.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[] { _context },
|
||||
culture: null)!;
|
||||
|
||||
return converter;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Shared;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal static class JsonConverterHelper
|
||||
{
|
||||
internal const int WrapperValueFieldNumber = Int32Value.ValueFieldNumber;
|
||||
|
||||
internal static JsonSerializerOptions CreateSerializerOptions(JsonContext context, bool isStreamingOptions = false)
|
||||
{
|
||||
// Streaming is line delimited between messages. That means JSON can't be indented as it adds new lines.
|
||||
// For streaming to work, indenting must be disabled when streaming.
|
||||
var writeIndented = !isStreamingOptions ? context.Settings.WriteIndented : false;
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = writeIndented,
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
options.Converters.Add(new NullValueConverter());
|
||||
options.Converters.Add(new ByteStringConverter());
|
||||
options.Converters.Add(new Int64Converter(context));
|
||||
options.Converters.Add(new UInt64Converter(context));
|
||||
options.Converters.Add(new BoolConverter());
|
||||
options.Converters.Add(new JsonConverterFactoryForEnum(context));
|
||||
options.Converters.Add(new JsonConverterFactoryForWrappers(context));
|
||||
options.Converters.Add(new JsonConverterFactoryForWellKnownTypes(context));
|
||||
options.Converters.Add(new JsonConverterFactoryForMessage(context));
|
||||
|
||||
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 (ServiceDescriptorHelpers.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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,403 @@
|
|||
#pragma warning disable IDE0073 // The file header does not match the required text
|
||||
#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
|
||||
#pragma warning restore IDE0073 // The file header does not match the required text
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
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.JsonTranscoding.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.AsSpan(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.AsSpan(1, 2), CultureInfo.InvariantCulture);
|
||||
int minutes = int.Parse(offset.AsSpan(4, 2), CultureInfo.InvariantCulture);
|
||||
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 insignificant 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.AsSpan(1), CultureInfo.InvariantCulture);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ported from src/google/protobuf/util/internal/utility.cc
|
||||
internal 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();
|
||||
}
|
||||
|
||||
internal static string ToJsonName(string name)
|
||||
{
|
||||
var result = new StringBuilder(name.Length);
|
||||
var isNextUpperCase = false;
|
||||
foreach (var 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>
|
||||
internal static bool IsPathValid(string input)
|
||||
{
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var c = input[i];
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (c == '_' && i < input.Length - 1)
|
||||
{
|
||||
var next = input[i + 1];
|
||||
if (next < 'a' || next > 'z')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// If this returns false, originalName will be null, which is what we want.
|
||||
nameMapping.TryGetValue(value, out var 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,32 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections;
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class ListValueConverter<TMessage> : SettingsConverterBase<TMessage> where TMessage : IMessage, new()
|
||||
{
|
||||
public ListValueConverter(JsonContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
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,187 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections;
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
// This converter should be temporary until System.Text.Json supports overriding contacts.
|
||||
// We want to eliminate this converter because System.Text.Json has to buffer content in converters.
|
||||
internal sealed class MessageConverter<TMessage> : SettingsConverterBase<TMessage> where TMessage : IMessage, new()
|
||||
{
|
||||
private readonly Dictionary<string, FieldDescriptor> _jsonFieldMap;
|
||||
|
||||
public MessageConverter(JsonContext context) : base(context)
|
||||
{
|
||||
_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, Context.Settings, options);
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
internal static void WriteMessageFields(Utf8JsonWriter writer, IMessage message, GrpcJsonSettings 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.IgnoreDefaultValues))
|
||||
{
|
||||
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,41 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.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,16 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal abstract class SettingsConverterBase<T> : JsonConverter<T>
|
||||
{
|
||||
public SettingsConverterBase(JsonContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public JsonContext Context { get; }
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections;
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class StructConverter<TMessage> : SettingsConverterBase<TMessage> where TMessage : IMessage, new()
|
||||
{
|
||||
public StructConverter(JsonContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
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,57 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class TimestampConverter<TMessage> : SettingsConverterBase<TMessage> where TMessage : IMessage, new()
|
||||
{
|
||||
public TimestampConverter(JsonContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
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,38 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class UInt64Converter : SettingsConverterBase<ulong>
|
||||
{
|
||||
public UInt64Converter(JsonContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
return ulong.Parse(reader.GetString()!, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return reader.GetUInt64();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options)
|
||||
{
|
||||
if (!Context.Settings.WriteInt64sAsStrings)
|
||||
{
|
||||
writer.WriteNumberValue(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStringValue(value.ToString("d", CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class ValueConverter<TMessage> : SettingsConverterBase<TMessage> where TMessage : IMessage, new()
|
||||
{
|
||||
public ValueConverter(JsonContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
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,33 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
|
||||
internal sealed class WrapperConverter<TMessage> : SettingsConverterBase<TMessage> where TMessage : IMessage, new()
|
||||
{
|
||||
public WrapperConverter(JsonContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
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,346 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Grpc.Core;
|
||||
using Grpc.Gateway.Runtime;
|
||||
using Grpc.Shared;
|
||||
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.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 = default;
|
||||
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 = default;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendErrorResponse(HttpResponse response, Encoding encoding, Status status, JsonSerializerOptions options)
|
||||
{
|
||||
if (!response.HasStarted)
|
||||
{
|
||||
response.StatusCode = MapStatusCodeToHttpStatus(status.StatusCode);
|
||||
response.ContentType = MediaType.ReplaceEncoding("application/json", encoding);
|
||||
}
|
||||
|
||||
var e = new Error
|
||||
{
|
||||
Error_ = status.Detail,
|
||||
Message = status.Detail,
|
||||
Code = (int)status.StatusCode
|
||||
};
|
||||
|
||||
await WriteResponseMessage(response, encoding, e, options);
|
||||
}
|
||||
|
||||
public static int MapStatusCodeToHttpStatus(StatusCode statusCode)
|
||||
{
|
||||
switch (statusCode)
|
||||
{
|
||||
case StatusCode.OK:
|
||||
return StatusCodes.Status200OK;
|
||||
case StatusCode.Cancelled:
|
||||
return StatusCodes.Status408RequestTimeout;
|
||||
case StatusCode.Unknown:
|
||||
return StatusCodes.Status500InternalServerError;
|
||||
case StatusCode.InvalidArgument:
|
||||
return StatusCodes.Status400BadRequest;
|
||||
case StatusCode.DeadlineExceeded:
|
||||
return StatusCodes.Status504GatewayTimeout;
|
||||
case StatusCode.NotFound:
|
||||
return StatusCodes.Status404NotFound;
|
||||
case StatusCode.AlreadyExists:
|
||||
return StatusCodes.Status409Conflict;
|
||||
case StatusCode.PermissionDenied:
|
||||
return StatusCodes.Status403Forbidden;
|
||||
case StatusCode.Unauthenticated:
|
||||
return StatusCodes.Status401Unauthorized;
|
||||
case StatusCode.ResourceExhausted:
|
||||
return StatusCodes.Status429TooManyRequests;
|
||||
case StatusCode.FailedPrecondition:
|
||||
// Note, this deliberately doesn't translate to the similarly named '412 Precondition Failed' HTTP response status.
|
||||
return StatusCodes.Status400BadRequest;
|
||||
case StatusCode.Aborted:
|
||||
return StatusCodes.Status409Conflict;
|
||||
case StatusCode.OutOfRange:
|
||||
return StatusCodes.Status400BadRequest;
|
||||
case StatusCode.Unimplemented:
|
||||
return StatusCodes.Status501NotImplemented;
|
||||
case StatusCode.Internal:
|
||||
return StatusCodes.Status500InternalServerError;
|
||||
case StatusCode.Unavailable:
|
||||
return StatusCodes.Status503ServiceUnavailable;
|
||||
case StatusCode.DataLoss:
|
||||
return StatusCodes.Status500InternalServerError;
|
||||
}
|
||||
|
||||
return StatusCodes.Status500InternalServerError;
|
||||
}
|
||||
|
||||
public static async Task WriteResponseMessage(HttpResponse response, Encoding encoding, object responseBody, JsonSerializerOptions options)
|
||||
{
|
||||
var (stream, usesTranscodingStream) = GetStream(response.Body, encoding);
|
||||
|
||||
try
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(stream, responseBody, options);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (usesTranscodingStream)
|
||||
{
|
||||
await stream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<TRequest> ReadMessage<TRequest>(JsonTranscodingServerCallContext serverCallContext, JsonSerializerOptions serializerOptions) where TRequest : class
|
||||
{
|
||||
try
|
||||
{
|
||||
GrpcServerLog.ReadingMessage(serverCallContext.Logger);
|
||||
|
||||
IMessage requestMessage;
|
||||
if (serverCallContext.DescriptorInfo.BodyDescriptor != null)
|
||||
{
|
||||
if (!serverCallContext.IsJsonRequestContent)
|
||||
{
|
||||
GrpcServerLog.UnsupportedRequestContentType(serverCallContext.Logger, serverCallContext.HttpContext.Request.ContentType);
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "Request content-type of application/json is required."));
|
||||
}
|
||||
|
||||
var (stream, usesTranscodingStream) = GetStream(serverCallContext.HttpContext.Request.Body, serverCallContext.RequestEncoding);
|
||||
|
||||
try
|
||||
{
|
||||
if (serverCallContext.DescriptorInfo.BodyDescriptorRepeated)
|
||||
{
|
||||
requestMessage = (IMessage)Activator.CreateInstance<TRequest>();
|
||||
|
||||
// TODO: JsonSerializer currently doesn't support deserializing values onto an existing object or collection.
|
||||
// Either update this to use new functionality in JsonSerializer or improve work-around perf.
|
||||
var type = JsonConverterHelper.GetFieldType(serverCallContext.DescriptorInfo.BodyFieldDescriptors.Last());
|
||||
var listType = typeof(List<>).MakeGenericType(type);
|
||||
|
||||
GrpcServerLog.DeserializingMessage(serverCallContext.Logger, listType);
|
||||
var repeatedContent = (IList)(await JsonSerializer.DeserializeAsync(stream, listType, serializerOptions))!;
|
||||
|
||||
ServiceDescriptorHelpers.RecursiveSetValue(requestMessage, serverCallContext.DescriptorInfo.BodyFieldDescriptors, repeatedContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
IMessage bodyContent;
|
||||
|
||||
try
|
||||
{
|
||||
GrpcServerLog.DeserializingMessage(serverCallContext.Logger, serverCallContext.DescriptorInfo.BodyDescriptor.ClrType);
|
||||
bodyContent = (IMessage)(await JsonSerializer.DeserializeAsync(stream, serverCallContext.DescriptorInfo.BodyDescriptor.ClrType, serializerOptions))!;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "Request JSON payload is not correctly formatted."));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, exception.Message));
|
||||
}
|
||||
|
||||
if (serverCallContext.DescriptorInfo.BodyFieldDescriptors != null)
|
||||
{
|
||||
requestMessage = (IMessage)Activator.CreateInstance<TRequest>();
|
||||
ServiceDescriptorHelpers.RecursiveSetValue(requestMessage, serverCallContext.DescriptorInfo.BodyFieldDescriptors, bodyContent!); // TODO - check nullability
|
||||
}
|
||||
else
|
||||
{
|
||||
requestMessage = bodyContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (usesTranscodingStream)
|
||||
{
|
||||
await stream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
requestMessage = (IMessage)Activator.CreateInstance<TRequest>();
|
||||
}
|
||||
|
||||
foreach (var parameterDescriptor in serverCallContext.DescriptorInfo.RouteParameterDescriptors)
|
||||
{
|
||||
var routeValue = serverCallContext.HttpContext.Request.RouteValues[parameterDescriptor.Key];
|
||||
if (routeValue != null)
|
||||
{
|
||||
ServiceDescriptorHelpers.RecursiveSetValue(requestMessage, parameterDescriptor.Value, routeValue);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in serverCallContext.HttpContext.Request.Query)
|
||||
{
|
||||
if (CanBindQueryStringVariable(serverCallContext, item.Key))
|
||||
{
|
||||
var pathDescriptors = GetPathDescriptors(serverCallContext, requestMessage, item.Key);
|
||||
|
||||
if (pathDescriptors != null)
|
||||
{
|
||||
var value = item.Value.Count == 1 ? (object?)item.Value[0] : item.Value;
|
||||
ServiceDescriptorHelpers.RecursiveSetValue(requestMessage, pathDescriptors, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GrpcServerLog.ReceivedMessage(serverCallContext.Logger);
|
||||
return (TRequest)requestMessage;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GrpcServerLog.ErrorReadingMessage(serverCallContext.Logger, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<FieldDescriptor>? GetPathDescriptors(JsonTranscodingServerCallContext serverCallContext, IMessage requestMessage, string path)
|
||||
{
|
||||
return serverCallContext.DescriptorInfo.PathDescriptorsCache.GetOrAdd(path, p =>
|
||||
{
|
||||
ServiceDescriptorHelpers.TryResolveDescriptors(requestMessage.Descriptor, p, out var pathDescriptors);
|
||||
return pathDescriptors;
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task SendMessage<TResponse>(JsonTranscodingServerCallContext serverCallContext, JsonSerializerOptions serializerOptions, TResponse message) where TResponse : class
|
||||
{
|
||||
var response = serverCallContext.HttpContext.Response;
|
||||
|
||||
try
|
||||
{
|
||||
GrpcServerLog.SendingMessage(serverCallContext.Logger);
|
||||
|
||||
object responseBody;
|
||||
Type responseType;
|
||||
|
||||
if (serverCallContext.DescriptorInfo.ResponseBodyDescriptor != null)
|
||||
{
|
||||
// TODO: Support recursive response body?
|
||||
responseBody = serverCallContext.DescriptorInfo.ResponseBodyDescriptor.Accessor.GetValue((IMessage)message);
|
||||
responseType = JsonConverterHelper.GetFieldType(serverCallContext.DescriptorInfo.ResponseBodyDescriptor);
|
||||
}
|
||||
else
|
||||
{
|
||||
responseBody = message;
|
||||
responseType = message.GetType();
|
||||
}
|
||||
|
||||
await JsonRequestHelpers.WriteResponseMessage(response, serverCallContext.RequestEncoding, responseBody, serializerOptions);
|
||||
|
||||
GrpcServerLog.SerializedMessage(serverCallContext.Logger, responseType);
|
||||
GrpcServerLog.MessageSent(serverCallContext.Logger);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GrpcServerLog.ErrorSendingMessage(serverCallContext.Logger, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanBindQueryStringVariable(JsonTranscodingServerCallContext serverCallContext, string variable)
|
||||
{
|
||||
if (serverCallContext.DescriptorInfo.BodyDescriptor != null)
|
||||
{
|
||||
if (serverCallContext.DescriptorInfo.BodyFieldDescriptors == null || serverCallContext.DescriptorInfo.BodyFieldDescriptors.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (variable == serverCallContext.DescriptorInfo.BodyFieldDescriptorsPath)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (variable.StartsWith(serverCallContext.DescriptorInfo.BodyFieldDescriptorsPath!, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (serverCallContext.DescriptorInfo.RouteParameterDescriptors.ContainsKey(variable))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
syntax = "proto3";
|
||||
package grpc.gateway.runtime;
|
||||
option go_package = "internal";
|
||||
|
||||
import "google/protobuf/any.proto";
|
||||
|
||||
// Error is the generic error returned from unary RPCs.
|
||||
message Error {
|
||||
string error = 1;
|
||||
// This is to make the error more compatible with users that expect errors to be Status objects:
|
||||
// https://github.com/grpc/grpc/blob/master/src/proto/grpc/status/status.proto
|
||||
// It should be the exact same message as the Error field.
|
||||
int32 code = 2;
|
||||
string message = 3;
|
||||
repeated google.protobuf.Any details = 4;
|
||||
}
|
||||
|
||||
// StreamError is a response type which is returned when
|
||||
// streaming rpc returns an error.
|
||||
message StreamError {
|
||||
int32 grpc_code = 1;
|
||||
int32 http_code = 2;
|
||||
string message = 3;
|
||||
string http_status = 4;
|
||||
repeated google.protobuf.Any details = 5;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Description>HTTP API for gRPC ASP.NET Core</Description>
|
||||
<PackageTags>gRPC RPC HTTP/2 REST</PackageTags>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests" />
|
||||
<InternalsVisibleTo Include="Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests" />
|
||||
<InternalsVisibleTo Include="Microsoft.AspNetCore.Grpc.Microbenchmarks" />
|
||||
|
||||
<Compile Include="..\Shared\Server\BindMethodFinder.cs" Link="Internal\Shared\Server\BindMethodFinder.cs" />
|
||||
<Compile Include="..\Shared\Server\ClientStreamingServerMethodInvoker.cs" Link="Internal\Shared\Server\ClientStreamingServerMethodInvoker.cs" />
|
||||
<Compile Include="..\Shared\Server\DuplexStreamingServerMethodInvoker.cs" Link="Internal\Shared\Server\DuplexStreamingServerMethodInvoker.cs" />
|
||||
<Compile Include="..\Shared\Server\InterceptorPipelineBuilder.cs" Link="Internal\Shared\Server\InterceptorPipelineBuilder.cs" />
|
||||
<Compile Include="..\Shared\Server\MethodOptions.cs" Link="Internal\Shared\Server\MethodOptions.cs" />
|
||||
<Compile Include="..\Shared\Server\ServerMethodInvokerBase.cs" Link="Internal\Shared\Server\ServerMethodInvokerBase.cs" />
|
||||
<Compile Include="..\Shared\Server\ServerStreamingServerMethodInvoker.cs" Link="Internal\Shared\Server\ServerStreamingServerMethodInvoker.cs" />
|
||||
<Compile Include="..\Shared\Server\UnaryServerMethodInvoker.cs" Link="Internal\Shared\Server\UnaryServerMethodInvoker.cs" />
|
||||
<Compile Include="..\Shared\AuthContextHelpers.cs" Link="Internal\Shared\AuthContextHelpers.cs" />
|
||||
<Compile Include="..\Shared\ServiceDescriptorHelpers.cs" Link="Internal\Shared\ServiceDescriptorHelpers.cs" />
|
||||
<Compile Include="..\Shared\X509CertificateHelpers.cs" Link="Internal\Shared\X509CertificateHelpers.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" LinkBase="Internal\Shared" />
|
||||
|
||||
<Protobuf Include="Internal\Protos\errors.proto" Access="Internal" />
|
||||
|
||||
<Reference Include="Google.Api.CommonProtos" />
|
||||
<Reference Include="Google.Protobuf" />
|
||||
<Reference Include="Grpc.AspNetCore.Server" />
|
||||
<Reference Include="Grpc.Tools" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1 @@
|
|||
#nullable enable
|
|
@ -0,0 +1,24 @@
|
|||
#nullable enable
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.GrpcJsonSettings() -> void
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.IgnoreDefaultValues.get -> bool
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.IgnoreDefaultValues.set -> void
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.WriteEnumsAsIntegers.get -> bool
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.WriteEnumsAsIntegers.set -> void
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.WriteIndented.get -> bool
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.WriteIndented.set -> void
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.WriteInt64sAsStrings.get -> bool
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.WriteInt64sAsStrings.set -> void
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonTranscodingMetadata
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonTranscodingMetadata.GrpcJsonTranscodingMetadata(Google.Protobuf.Reflection.MethodDescriptor! methodDescriptor, Google.Api.HttpRule! httpRule) -> void
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonTranscodingMetadata.HttpRule.get -> Google.Api.HttpRule!
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonTranscodingMetadata.MethodDescriptor.get -> Google.Protobuf.Reflection.MethodDescriptor!
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonTranscodingOptions
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonTranscodingOptions.GrpcJsonTranscodingOptions() -> void
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonTranscodingOptions.JsonSettings.get -> Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings!
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonTranscodingOptions.JsonSettings.set -> void
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonTranscodingOptions.TypeRegistry.get -> Google.Protobuf.Reflection.TypeRegistry!
|
||||
Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonTranscodingOptions.TypeRegistry.set -> void
|
||||
Microsoft.Extensions.DependencyInjection.GrpcJsonTranscodingServiceExtensions
|
||||
static Microsoft.Extensions.DependencyInjection.GrpcJsonTranscodingServiceExtensions.AddJsonTranscoding(this Grpc.AspNetCore.Server.IGrpcServerBuilder! grpcBuilder) -> Grpc.AspNetCore.Server.IGrpcServerBuilder!
|
||||
static Microsoft.Extensions.DependencyInjection.GrpcJsonTranscodingServiceExtensions.AddJsonTranscoding(this Grpc.AspNetCore.Server.IGrpcServerBuilder! grpcBuilder, System.Action<Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonTranscodingOptions!>! configureOptions) -> Grpc.AspNetCore.Server.IGrpcServerBuilder!
|
|
@ -0,0 +1,78 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Xml.XPath;
|
||||
using Microsoft.AspNetCore.Grpc.Swagger.Internal.XmlComments;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the gRPC JSON transcoding services.
|
||||
/// </summary>
|
||||
public static class GrpcSwaggerGenOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files
|
||||
/// </summary>
|
||||
/// <param name="swaggerGenOptions"></param>
|
||||
/// <param name="xmlDocFactory">A factory method that returns XML Comments as an XPathDocument</param>
|
||||
public static void IncludeGrpcXmlComments(
|
||||
this SwaggerGenOptions swaggerGenOptions,
|
||||
Func<XPathDocument> xmlDocFactory)
|
||||
{
|
||||
swaggerGenOptions.IncludeGrpcXmlComments(xmlDocFactory, includeControllerXmlComments: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files
|
||||
/// </summary>
|
||||
/// <param name="swaggerGenOptions"></param>
|
||||
/// <param name="xmlDocFactory">A factory method that returns XML Comments as an XPathDocument</param>
|
||||
/// <param name="includeControllerXmlComments">
|
||||
/// Flag to indicate if controller XML comments (i.e. summary) should be used to assign Tag descriptions.
|
||||
/// Don't set this flag if you're customizing the default tag for operations via TagActionsBy.
|
||||
/// </param>
|
||||
public static void IncludeGrpcXmlComments(
|
||||
this SwaggerGenOptions swaggerGenOptions,
|
||||
Func<XPathDocument> xmlDocFactory,
|
||||
bool includeControllerXmlComments)
|
||||
{
|
||||
var xmlDoc = xmlDocFactory();
|
||||
swaggerGenOptions.OperationFilter<GrpcXmlCommentsOperationFilter>(xmlDoc);
|
||||
|
||||
if (includeControllerXmlComments)
|
||||
{
|
||||
swaggerGenOptions.DocumentFilter<GrpcXmlCommentsDocumentFilter>(xmlDoc);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files
|
||||
/// </summary>
|
||||
/// <param name="swaggerGenOptions"></param>
|
||||
/// <param name="filePath">An absolute path to the file that contains XML Comments</param>
|
||||
public static void IncludeGrpcXmlComments(
|
||||
this SwaggerGenOptions swaggerGenOptions,
|
||||
string filePath)
|
||||
{
|
||||
swaggerGenOptions.IncludeGrpcXmlComments(() => new XPathDocument(filePath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files
|
||||
/// </summary>
|
||||
/// <param name="swaggerGenOptions"></param>
|
||||
/// <param name="filePath">An absolute path to the file that contains XML Comments</param>
|
||||
/// <param name="includeControllerXmlComments">
|
||||
/// Flag to indicate if controller XML comments (i.e. summary) should be used to assign Tag descriptions.
|
||||
/// Don't set this flag if you're customizing the default tag for operations via TagActionsBy.
|
||||
/// </param>
|
||||
public static void IncludeGrpcXmlComments(
|
||||
this SwaggerGenOptions swaggerGenOptions,
|
||||
string filePath,
|
||||
bool includeControllerXmlComments)
|
||||
{
|
||||
swaggerGenOptions.IncludeGrpcXmlComments(() => new XPathDocument(filePath), includeControllerXmlComments);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Grpc.Swagger.Internal;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the gRPC JSON transcoding services.
|
||||
/// </summary>
|
||||
public static class GrpcSwaggerServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds gRPC JSON transcoding services to the specified <see cref="IServiceCollection" />.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
|
||||
public static IServiceCollection AddGrpcSwagger(this IServiceCollection services)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
services.AddGrpc().AddJsonTranscoding();
|
||||
|
||||
services.TryAddEnumerable(ServiceDescriptor.Transient<IApiDescriptionProvider, GrpcJsonTranscodingDescriptionProvider>());
|
||||
|
||||
// Register default description provider in case MVC is not registered
|
||||
services.TryAddSingleton<IApiDescriptionGroupCollectionProvider>(serviceProvider =>
|
||||
{
|
||||
var actionDescriptorCollectionProvider = serviceProvider.GetService<IActionDescriptorCollectionProvider>();
|
||||
var apiDescriptionProvider = serviceProvider.GetServices<IApiDescriptionProvider>();
|
||||
|
||||
return new ApiDescriptionGroupCollectionProvider(
|
||||
actionDescriptorCollectionProvider ?? new EmptyActionDescriptorCollectionProvider(),
|
||||
apiDescriptionProvider);
|
||||
});
|
||||
|
||||
// Add or replace contract resolver.
|
||||
services.Replace(ServiceDescriptor.Transient<ISerializerDataContractResolver>(s =>
|
||||
{
|
||||
var serializerOptions = s.GetService<IOptions<JsonOptions>>()?.Value?.JsonSerializerOptions ?? new JsonSerializerOptions();
|
||||
var innerContractResolver = new JsonSerializerDataContractResolver(serializerOptions);
|
||||
return new GrpcDataContractResolver(innerContractResolver);
|
||||
}));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
// Dummy type that is only used if MVC is not registered in the app
|
||||
private class EmptyActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
|
||||
{
|
||||
public ActionDescriptorCollection ActionDescriptors { get; } = new ActionDescriptorCollection(new List<ActionDescriptor>(), 1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Shared;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.Swagger.Internal;
|
||||
|
||||
internal class GrpcDataContractResolver : ISerializerDataContractResolver
|
||||
{
|
||||
private readonly ISerializerDataContractResolver _innerContractResolver;
|
||||
private readonly Dictionary<Type, MessageDescriptor> _messageTypeMapping;
|
||||
private readonly Dictionary<Type, EnumDescriptor> _enumTypeMapping;
|
||||
|
||||
public GrpcDataContractResolver(ISerializerDataContractResolver innerContractResolver)
|
||||
{
|
||||
_innerContractResolver = innerContractResolver;
|
||||
_messageTypeMapping = new Dictionary<Type, MessageDescriptor>();
|
||||
_enumTypeMapping = new Dictionary<Type, EnumDescriptor>();
|
||||
}
|
||||
|
||||
public DataContract GetDataContractForType(Type type)
|
||||
{
|
||||
if (!_messageTypeMapping.TryGetValue(type, out var messageDescriptor))
|
||||
{
|
||||
if (typeof(IMessage).IsAssignableFrom(type))
|
||||
{
|
||||
var property = type.GetProperty("Descriptor", BindingFlags.Public | BindingFlags.Static);
|
||||
messageDescriptor = property?.GetValue(null) as MessageDescriptor;
|
||||
|
||||
if (messageDescriptor == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Couldn't resolve message descriptor for {type}.");
|
||||
}
|
||||
|
||||
_messageTypeMapping[type] = messageDescriptor;
|
||||
}
|
||||
}
|
||||
|
||||
if (messageDescriptor != null)
|
||||
{
|
||||
return ConvertMessage(messageDescriptor);
|
||||
}
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
if (_enumTypeMapping.TryGetValue(type, out var enumDescriptor))
|
||||
{
|
||||
var values = enumDescriptor.Values.Select(v => v.Name).ToList();
|
||||
return DataContract.ForPrimitive(type, DataType.String, dataFormat: null, value =>
|
||||
{
|
||||
var match = enumDescriptor.Values.SingleOrDefault(v => v.Number == (int)value);
|
||||
var name = match?.Name ?? value.ToString();
|
||||
return @"""" + name + @"""";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return _innerContractResolver.GetDataContractForType(type);
|
||||
}
|
||||
|
||||
private DataContract ConvertMessage(MessageDescriptor messageDescriptor)
|
||||
{
|
||||
if (ServiceDescriptorHelpers.IsWellKnownType(messageDescriptor))
|
||||
{
|
||||
if (ServiceDescriptorHelpers.IsWrapperType(messageDescriptor))
|
||||
{
|
||||
var field = messageDescriptor.Fields[Int32Value.ValueFieldNumber];
|
||||
|
||||
return _innerContractResolver.GetDataContractForType(MessageDescriptorHelpers.ResolveFieldType(field));
|
||||
}
|
||||
if (messageDescriptor.FullName == Timestamp.Descriptor.FullName ||
|
||||
messageDescriptor.FullName == Duration.Descriptor.FullName ||
|
||||
messageDescriptor.FullName == FieldMask.Descriptor.FullName)
|
||||
{
|
||||
return DataContract.ForPrimitive(messageDescriptor.ClrType, DataType.String, dataFormat: null);
|
||||
}
|
||||
if (messageDescriptor.FullName == Struct.Descriptor.FullName)
|
||||
{
|
||||
return DataContract.ForObject(messageDescriptor.ClrType, Array.Empty<DataProperty>(), extensionDataType: typeof(Value));
|
||||
}
|
||||
if (messageDescriptor.FullName == ListValue.Descriptor.FullName)
|
||||
{
|
||||
return DataContract.ForArray(messageDescriptor.ClrType, typeof(Value));
|
||||
}
|
||||
if (messageDescriptor.FullName == Value.Descriptor.FullName)
|
||||
{
|
||||
return DataContract.ForPrimitive(messageDescriptor.ClrType, DataType.Unknown, dataFormat: null);
|
||||
}
|
||||
if (messageDescriptor.FullName == Any.Descriptor.FullName)
|
||||
{
|
||||
var anyProperties = new List<DataProperty>
|
||||
{
|
||||
new DataProperty("@type", typeof(string), isRequired: true)
|
||||
};
|
||||
return DataContract.ForObject(messageDescriptor.ClrType, anyProperties, extensionDataType: typeof(Value));
|
||||
}
|
||||
}
|
||||
|
||||
var properties = new List<DataProperty>();
|
||||
|
||||
foreach (var field in messageDescriptor.Fields.InFieldNumberOrder())
|
||||
{
|
||||
// Enum type will later be used to call this contract resolver.
|
||||
// Register the enum type so we know to resolve its names from the descriptor.
|
||||
if (field.FieldType == FieldType.Enum)
|
||||
{
|
||||
_enumTypeMapping.TryAdd(field.EnumType.ClrType, field.EnumType);
|
||||
}
|
||||
|
||||
Type fieldType;
|
||||
if (field.IsMap)
|
||||
{
|
||||
var mapFields = field.MessageType.Fields.InFieldNumberOrder();
|
||||
var valueType = MessageDescriptorHelpers.ResolveFieldType(mapFields[1]);
|
||||
fieldType = typeof(IDictionary<,>).MakeGenericType(typeof(string), valueType);
|
||||
}
|
||||
else if (field.IsRepeated)
|
||||
{
|
||||
fieldType = typeof(IList<>).MakeGenericType(MessageDescriptorHelpers.ResolveFieldType(field));
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldType = MessageDescriptorHelpers.ResolveFieldType(field);
|
||||
}
|
||||
|
||||
var propertyName = ServiceDescriptorHelpers.FormatUnderscoreName(field.Name, pascalCase: true, preservePeriod: false);
|
||||
var propertyInfo = messageDescriptor.ClrType.GetProperty(propertyName);
|
||||
|
||||
properties.Add(new DataProperty(field.JsonName, fieldType, memberInfo: propertyInfo));
|
||||
}
|
||||
|
||||
var schema = DataContract.ForObject(messageDescriptor.ClrType, properties: properties);
|
||||
|
||||
return schema;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Linq;
|
||||
using Google.Api;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Grpc.AspNetCore.Server;
|
||||
using Grpc.Shared;
|
||||
using Microsoft.AspNetCore.Grpc.JsonTranscoding;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.Swagger.Internal;
|
||||
|
||||
internal sealed class GrpcJsonTranscodingDescriptionProvider : IApiDescriptionProvider
|
||||
{
|
||||
private readonly EndpointDataSource _endpointDataSource;
|
||||
|
||||
public GrpcJsonTranscodingDescriptionProvider(EndpointDataSource endpointDataSource)
|
||||
{
|
||||
_endpointDataSource = endpointDataSource;
|
||||
}
|
||||
|
||||
// Executes after ASP.NET Core
|
||||
public int Order => -900;
|
||||
|
||||
public void OnProvidersExecuting(ApiDescriptionProviderContext context)
|
||||
{
|
||||
var endpoints = _endpointDataSource.Endpoints;
|
||||
|
||||
foreach (var endpoint in endpoints)
|
||||
{
|
||||
if (endpoint is RouteEndpoint routeEndpoint)
|
||||
{
|
||||
var grpcMetadata = endpoint.Metadata.GetMetadata<GrpcJsonTranscodingMetadata>();
|
||||
|
||||
if (grpcMetadata != null)
|
||||
{
|
||||
var httpRule = grpcMetadata.HttpRule;
|
||||
var methodDescriptor = grpcMetadata.MethodDescriptor;
|
||||
|
||||
if (ServiceDescriptorHelpers.TryResolvePattern(grpcMetadata.HttpRule, out var pattern, out var verb))
|
||||
{
|
||||
var apiDescription = CreateApiDescription(routeEndpoint, httpRule, methodDescriptor, pattern, verb);
|
||||
|
||||
context.Results.Add(apiDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, HttpRule httpRule, MethodDescriptor methodDescriptor, string pattern, string verb)
|
||||
{
|
||||
var apiDescription = new ApiDescription();
|
||||
apiDescription.HttpMethod = verb;
|
||||
apiDescription.ActionDescriptor = new ActionDescriptor
|
||||
{
|
||||
RouteValues = new Dictionary<string, string?>
|
||||
{
|
||||
// Swagger uses this to group endpoints together.
|
||||
// Group methods together using the service name.
|
||||
["controller"] = methodDescriptor.Service.FullName
|
||||
},
|
||||
EndpointMetadata = routeEndpoint.Metadata.ToList()
|
||||
};
|
||||
apiDescription.RelativePath = pattern.TrimStart('/');
|
||||
apiDescription.SupportedRequestFormats.Add(new ApiRequestFormat { MediaType = "application/json" });
|
||||
apiDescription.SupportedResponseTypes.Add(new ApiResponseType
|
||||
{
|
||||
ApiResponseFormats = { new ApiResponseFormat { MediaType = "application/json" } },
|
||||
ModelMetadata = new GrpcModelMetadata(ModelMetadataIdentity.ForType(methodDescriptor.OutputType.ClrType)),
|
||||
StatusCode = 200
|
||||
});
|
||||
var explorerSettings = routeEndpoint.Metadata.GetMetadata<ApiExplorerSettingsAttribute>();
|
||||
if (explorerSettings != null)
|
||||
{
|
||||
apiDescription.GroupName = explorerSettings.GroupName;
|
||||
}
|
||||
|
||||
var methodMetadata = routeEndpoint.Metadata.GetMetadata<GrpcMethodMetadata>()!;
|
||||
var routeParameters = ServiceDescriptorHelpers.ResolveRouteParameterDescriptors(routeEndpoint.RoutePattern, methodDescriptor.InputType);
|
||||
|
||||
foreach (var routeParameter in routeParameters)
|
||||
{
|
||||
var field = routeParameter.Value.Last();
|
||||
var parameterName = ServiceDescriptorHelpers.FormatUnderscoreName(field.Name, pascalCase: true, preservePeriod: false);
|
||||
var propertyInfo = field.ContainingType.ClrType.GetProperty(parameterName);
|
||||
|
||||
// If from a property, create model as property to get its XML comments.
|
||||
var identity = propertyInfo != null
|
||||
? ModelMetadataIdentity.ForProperty(propertyInfo, MessageDescriptorHelpers.ResolveFieldType(field), field.ContainingType.ClrType)
|
||||
: ModelMetadataIdentity.ForType(MessageDescriptorHelpers.ResolveFieldType(field));
|
||||
|
||||
apiDescription.ParameterDescriptions.Add(new ApiParameterDescription
|
||||
{
|
||||
Name = routeParameter.Key,
|
||||
ModelMetadata = new GrpcModelMetadata(identity),
|
||||
Source = BindingSource.Path,
|
||||
DefaultValue = string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
var bodyDescriptor = ServiceDescriptorHelpers.ResolveBodyDescriptor(httpRule.Body, methodMetadata.ServiceType, methodDescriptor);
|
||||
if (bodyDescriptor != null)
|
||||
{
|
||||
// If from a property, create model as property to get its XML comments.
|
||||
var identity = bodyDescriptor.PropertyInfo != null
|
||||
? ModelMetadataIdentity.ForProperty(bodyDescriptor.PropertyInfo, bodyDescriptor.Descriptor.ClrType, bodyDescriptor.PropertyInfo.DeclaringType!)
|
||||
: ModelMetadataIdentity.ForType(bodyDescriptor.Descriptor.ClrType);
|
||||
|
||||
// Or if from a parameter, create model as parameter to get its XML comments.
|
||||
var parameterDescriptor = bodyDescriptor.ParameterInfo != null
|
||||
? new ControllerParameterDescriptor { ParameterInfo = bodyDescriptor.ParameterInfo }
|
||||
: null;
|
||||
|
||||
apiDescription.ParameterDescriptions.Add(new ApiParameterDescription
|
||||
{
|
||||
Name = "Input",
|
||||
ModelMetadata = new GrpcModelMetadata(identity),
|
||||
Source = BindingSource.Body,
|
||||
ParameterDescriptor = parameterDescriptor!
|
||||
});
|
||||
}
|
||||
|
||||
return apiDescription;
|
||||
}
|
||||
|
||||
public void OnProvidersExecuted(ApiDescriptionProviderContext context)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.Swagger.Internal;
|
||||
|
||||
internal class GrpcModelMetadata : ModelMetadata
|
||||
{
|
||||
public GrpcModelMetadata(ModelMetadataIdentity identity) : base(identity)
|
||||
{
|
||||
IsBindingAllowed = true;
|
||||
}
|
||||
|
||||
public override IReadOnlyDictionary<object, object> AdditionalValues { get; }
|
||||
public override string BinderModelName { get; }
|
||||
public override Type BinderType { get; }
|
||||
public override BindingSource BindingSource { get; }
|
||||
public override bool ConvertEmptyStringToNull { get; }
|
||||
public override string DataTypeName { get; }
|
||||
public override string Description { get; }
|
||||
public override string DisplayFormatString { get; }
|
||||
public override string DisplayName { get; }
|
||||
public override string EditFormatString { get; }
|
||||
public override ModelMetadata ElementMetadata { get; }
|
||||
public override IEnumerable<KeyValuePair<EnumGroupAndName, string>> EnumGroupedDisplayNamesAndValues { get; }
|
||||
public override IReadOnlyDictionary<string, string> EnumNamesAndValues { get; }
|
||||
public override bool HasNonDefaultEditFormat { get; }
|
||||
public override bool HideSurroundingHtml { get; }
|
||||
public override bool HtmlEncode { get; }
|
||||
public override bool IsBindingAllowed { get; }
|
||||
public override bool IsBindingRequired { get; }
|
||||
public override bool IsEnum { get; }
|
||||
public override bool IsFlagsEnum { get; }
|
||||
public override bool IsReadOnly { get; }
|
||||
public override bool IsRequired { get; }
|
||||
public override ModelBindingMessageProvider ModelBindingMessageProvider { get; }
|
||||
public override string NullDisplayText { get; }
|
||||
public override int Order { get; }
|
||||
public override string Placeholder { get; }
|
||||
public override ModelPropertyCollection Properties { get; }
|
||||
public override IPropertyFilterProvider PropertyFilterProvider { get; }
|
||||
public override Func<object, object> PropertyGetter { get; }
|
||||
public override Action<object, object> PropertySetter { get; }
|
||||
public override bool ShowForDisplay { get; }
|
||||
public override bool ShowForEdit { get; }
|
||||
public override string SimpleDisplayProperty { get; }
|
||||
public override string TemplateHint { get; }
|
||||
public override bool ValidateChildren { get; }
|
||||
public override IReadOnlyList<object> ValidatorMetadata { get; }
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Google.Protobuf.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.Swagger.Internal;
|
||||
|
||||
internal static class MessageDescriptorHelpers
|
||||
{
|
||||
public static Type ResolveFieldType(FieldDescriptor field)
|
||||
{
|
||||
switch (field.FieldType)
|
||||
{
|
||||
case FieldType.Double:
|
||||
return typeof(double);
|
||||
case FieldType.Float:
|
||||
return typeof(float);
|
||||
case FieldType.Int64:
|
||||
return typeof(long);
|
||||
case FieldType.UInt64:
|
||||
return typeof(ulong);
|
||||
case FieldType.Int32:
|
||||
return typeof(int);
|
||||
case FieldType.Fixed64:
|
||||
return typeof(long);
|
||||
case FieldType.Fixed32:
|
||||
return typeof(int);
|
||||
case FieldType.Bool:
|
||||
return typeof(bool);
|
||||
case FieldType.String:
|
||||
return typeof(string);
|
||||
case FieldType.Bytes:
|
||||
return typeof(string);
|
||||
case FieldType.UInt32:
|
||||
return typeof(uint);
|
||||
case FieldType.SFixed32:
|
||||
return typeof(int);
|
||||
case FieldType.SFixed64:
|
||||
return typeof(long);
|
||||
case FieldType.SInt32:
|
||||
return typeof(int);
|
||||
case FieldType.SInt64:
|
||||
return typeof(long);
|
||||
case FieldType.Enum:
|
||||
return field.EnumType.ClrType;
|
||||
case FieldType.Message:
|
||||
return field.MessageType.ClrType;
|
||||
default:
|
||||
throw new InvalidOperationException("Unexpected field type: " + field.FieldType);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.XPath;
|
||||
using Grpc.AspNetCore.Server;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.Swagger.Internal.XmlComments;
|
||||
|
||||
internal class GrpcXmlCommentsDocumentFilter : IDocumentFilter
|
||||
{
|
||||
private const string MemberXPath = "/doc/members/member[@name='{0}']";
|
||||
private const string SummaryTag = "summary";
|
||||
|
||||
private readonly XPathNavigator _xmlNavigator;
|
||||
|
||||
public GrpcXmlCommentsDocumentFilter(XPathDocument xmlDoc)
|
||||
{
|
||||
_xmlNavigator = xmlDoc.CreateNavigator();
|
||||
}
|
||||
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
// Get unique services
|
||||
var nameAndServiceDescriptor = context.ApiDescriptions
|
||||
.Select(apiDesc => apiDesc.ActionDescriptor)
|
||||
.Where(actionDesc => actionDesc != null && (actionDesc.EndpointMetadata?.Any(m => m is GrpcMethodMetadata) ?? false))
|
||||
.GroupBy(actionDesc => actionDesc.RouteValues["controller"]!)
|
||||
.Select(group => new KeyValuePair<string, ActionDescriptor>(group.Key, group.First()));
|
||||
|
||||
foreach (var nameAndType in nameAndServiceDescriptor)
|
||||
{
|
||||
var grpcMethodMetadata = nameAndType.Value.EndpointMetadata.OfType<GrpcMethodMetadata>().First();
|
||||
if (TryAdd(swaggerDoc, nameAndType, grpcMethodMetadata.ServiceType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (grpcMethodMetadata.ServiceType.BaseType?.DeclaringType is { } staticService)
|
||||
{
|
||||
if (TryAdd(swaggerDoc, nameAndType, staticService))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryAdd(OpenApiDocument swaggerDoc, KeyValuePair<string, ActionDescriptor> nameAndType, Type type)
|
||||
{
|
||||
var memberName = XmlCommentsNodeNameHelper.GetMemberNameForType(type);
|
||||
var typeNode = _xmlNavigator.SelectSingleNode(string.Format(CultureInfo.InvariantCulture, MemberXPath, memberName));
|
||||
|
||||
if (typeNode != null)
|
||||
{
|
||||
var summaryNode = typeNode.SelectSingleNode(SummaryTag);
|
||||
if (summaryNode != null)
|
||||
{
|
||||
if (swaggerDoc.Tags == null)
|
||||
{
|
||||
swaggerDoc.Tags = new List<OpenApiTag>();
|
||||
}
|
||||
|
||||
swaggerDoc.Tags.Add(new OpenApiTag
|
||||
{
|
||||
Name = nameAndType.Key,
|
||||
Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml)
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Xml.XPath;
|
||||
using Grpc.AspNetCore.Server;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Microsoft.AspNetCore.Grpc.Swagger.Internal.XmlComments;
|
||||
|
||||
internal class GrpcXmlCommentsOperationFilter : IOperationFilter
|
||||
{
|
||||
private readonly XPathNavigator _xmlNavigator;
|
||||
|
||||
public GrpcXmlCommentsOperationFilter(XPathDocument xmlDoc)
|
||||
{
|
||||
_xmlNavigator = xmlDoc.CreateNavigator();
|
||||
}
|
||||
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var grpcMetadata = context.ApiDescription.ActionDescriptor.EndpointMetadata.OfType<GrpcMethodMetadata>().FirstOrDefault();
|
||||
if (grpcMetadata == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var methodInfo = grpcMetadata.ServiceType.GetMethod(grpcMetadata.Method.Name);
|
||||
if (methodInfo == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If method is from a constructed generic type, look for comments from the generic type method
|
||||
var targetMethod = methodInfo.DeclaringType!.IsConstructedGenericType
|
||||
? methodInfo.GetUnderlyingGenericTypeMethod()
|
||||
: methodInfo;
|
||||
|
||||
if (targetMethod == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Base service never has response tags.
|
||||
ApplyServiceTags(operation, targetMethod.DeclaringType!);
|
||||
|
||||
if (TryApplyMethodTags(operation, targetMethod))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetMethod.IsVirtual && targetMethod.GetBaseDefinition() is { } baseMethod)
|
||||
{
|
||||
if (TryApplyMethodTags(operation, baseMethod))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyServiceTags(OpenApiOperation operation, Type controllerType)
|
||||
{
|
||||
var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(controllerType);
|
||||
var responseNodes = _xmlNavigator.Select($"/doc/members/member[@name='{typeMemberName}']/response");
|
||||
ApplyResponseTags(operation, responseNodes);
|
||||
}
|
||||
|
||||
private bool TryApplyMethodTags(OpenApiOperation operation, MethodInfo methodInfo)
|
||||
{
|
||||
var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(methodInfo);
|
||||
var methodNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{methodMemberName}']");
|
||||
|
||||
if (methodNode == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var summaryNode = methodNode.SelectSingleNode("summary");
|
||||
if (summaryNode != null)
|
||||
{
|
||||
operation.Summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
|
||||
}
|
||||
|
||||
var remarksNode = methodNode.SelectSingleNode("remarks");
|
||||
if (remarksNode != null)
|
||||
{
|
||||
operation.Description = XmlCommentsTextHelper.Humanize(remarksNode.InnerXml);
|
||||
}
|
||||
|
||||
var responseNodes = methodNode.Select("response");
|
||||
ApplyResponseTags(operation, responseNodes);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ApplyResponseTags(OpenApiOperation operation, XPathNodeIterator responseNodes)
|
||||
{
|
||||
while (responseNodes.MoveNext())
|
||||
{
|
||||
var code = responseNodes.Current!.GetAttribute("code", "");
|
||||
var response = operation.Responses.ContainsKey(code)
|
||||
? operation.Responses[code]
|
||||
: operation.Responses[code] = new OpenApiResponse();
|
||||
|
||||
response.Description = XmlCommentsTextHelper.Humanize(responseNodes.Current.InnerXml);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Description>Swagger for gRPC ASP.NET Core</Description>
|
||||
<PackageTags>gRPC RPC HTTP/2 REST Swagger OpenAPI</PackageTags>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsShipping>false</IsShipping>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.AspNetCore.Grpc.Swagger.Tests" />
|
||||
|
||||
<Compile Include="..\Shared\ServiceDescriptorHelpers.cs" Link="Internal\Shared\ServiceDescriptorHelpers.cs" />
|
||||
|
||||
<Reference Include="Microsoft.AspNetCore.Grpc.JsonTranscoding" />
|
||||
<Reference Include="Swashbuckle.AspNetCore" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1 @@
|
|||
#nullable enable
|
|
@ -0,0 +1,8 @@
|
|||
#nullable enable
|
||||
Microsoft.Extensions.DependencyInjection.GrpcSwaggerGenOptionsExtensions
|
||||
Microsoft.Extensions.DependencyInjection.GrpcSwaggerServiceExtensions
|
||||
static Microsoft.Extensions.DependencyInjection.GrpcSwaggerGenOptionsExtensions.IncludeGrpcXmlComments(this Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions! swaggerGenOptions, System.Func<System.Xml.XPath.XPathDocument!>! xmlDocFactory) -> void
|
||||
static Microsoft.Extensions.DependencyInjection.GrpcSwaggerGenOptionsExtensions.IncludeGrpcXmlComments(this Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions! swaggerGenOptions, System.Func<System.Xml.XPath.XPathDocument!>! xmlDocFactory, bool includeControllerXmlComments) -> void
|
||||
static Microsoft.Extensions.DependencyInjection.GrpcSwaggerGenOptionsExtensions.IncludeGrpcXmlComments(this Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions! swaggerGenOptions, string! filePath) -> void
|
||||
static Microsoft.Extensions.DependencyInjection.GrpcSwaggerGenOptionsExtensions.IncludeGrpcXmlComments(this Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions! swaggerGenOptions, string! filePath, bool includeControllerXmlComments) -> void
|
||||
static Microsoft.Extensions.DependencyInjection.GrpcSwaggerServiceExtensions.AddGrpcSwagger(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
|
|
@ -0,0 +1,3 @@
|
|||
[*.{cs,vb}]
|
||||
# IDE0073: File header
|
||||
dotnet_diagnostic.IDE0073.severity = none
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче