├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── nuget_org_only.config ├── .gitignore ├── changelog.md ├── img ├── github-banner.png ├── grpc-curl.ico ├── grpc-curl.png └── grpc-curl.svg ├── license.txt ├── readme.md └── src ├── Directory.Build.props ├── Directory.Build.targets ├── DynamicGrpc ├── DynamicAny.cs ├── DynamicFileDescriptorSet.cs ├── DynamicGrpc.csproj ├── DynamicGrpcClient.cs ├── DynamicGrpcClientContext.cs ├── DynamicGrpcClientException.cs ├── DynamicGrpcClientOptions.cs ├── DynamicGrpcPrinter.cs ├── DynamicGrpcPrinterOptions.cs ├── DynamicMessageSerializer.cs └── DynamicServiceDescriptor.cs ├── GrpcCurl.Tests ├── BasicTests.cs ├── CurlTests.cs ├── GrpcCurl.Tests.csproj ├── GrpcTestBase.cs └── Proto │ ├── GreeterProto2Impl.cs │ ├── GreeterServiceImpl.cs │ ├── PrimitiveServiceImpl.cs │ ├── Primitives.proto │ ├── Primitives.tt │ ├── greet.proto │ └── greet_proto2.proto ├── GrpcCurl ├── GrpcCurl.csproj ├── GrpcCurlApp.cs ├── GrpcCurlOptions.cs └── Program.cs ├── common.props ├── dotnet-releaser.toml ├── global.json ├── grpc-curl.sln └── grpc-curl.sln.DotSettings /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [xoofx] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'doc/**' 7 | - 'img/**' 8 | - 'changelog.md' 9 | - 'readme.md' 10 | pull_request: 11 | 12 | jobs: 13 | build: 14 | runs-on: windows-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | with: 20 | submodules: true 21 | fetch-depth: 0 22 | 23 | - name: Install .NET 8.0 24 | uses: actions/setup-dotnet@v4 25 | with: 26 | dotnet-version: '8.0.x' 27 | 28 | - name: Build, Test, Pack, Publish 29 | shell: bash 30 | run: | 31 | dotnet tool install -g dotnet-releaser --configfile .github/workflows/nuget_org_only.config 32 | dotnet-releaser run --skip-app-packages-for-build-only --nuget-token "${{secrets.NUGET_TOKEN}}" --github-token "${{secrets.GITHUB_TOKEN}}" --github-token-extra "${{secrets.TOKEN_GITHUB}}" src/dotnet-releaser.toml 33 | -------------------------------------------------------------------------------- /.github/workflows/nuget_org_only.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # Rider 14 | .idea/ 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | build/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | [Ll]ogs/ 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Visual Studio 2017 auto generated files 45 | Generated\ Files/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUnit 52 | *.VisualState.xml 53 | TestResult.xml 54 | nunit-*.xml 55 | 56 | # Build Results of an ATL Project 57 | [Dd]ebugPS/ 58 | [Rr]eleasePS/ 59 | dlldata.c 60 | 61 | # Benchmark Results 62 | BenchmarkDotNet.Artifacts/ 63 | 64 | # .NET Core 65 | project.lock.json 66 | project.fragment.lock.json 67 | artifacts/ 68 | 69 | # ASP.NET Scaffolding 70 | ScaffoldingReadMe.txt 71 | 72 | # StyleCop 73 | StyleCopReport.xml 74 | 75 | # Files built by Visual Studio 76 | *_i.c 77 | *_p.c 78 | *_h.h 79 | *.ilk 80 | *.meta 81 | *.obj 82 | *.iobj 83 | *.pch 84 | *.pdb 85 | *.ipdb 86 | *.pgc 87 | *.pgd 88 | *.rsp 89 | *.sbr 90 | *.tlb 91 | *.tli 92 | *.tlh 93 | *.tmp 94 | *.tmp_proj 95 | *_wpftmp.csproj 96 | *.log 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*[.json, .xml, .info] 150 | 151 | # Visual Studio code coverage results 152 | *.coverage 153 | *.coveragexml 154 | 155 | # NCrunch 156 | _NCrunch_* 157 | .*crunch*.local.xml 158 | nCrunchTemp_* 159 | 160 | # MightyMoose 161 | *.mm.* 162 | AutoTest.Net/ 163 | 164 | # Web workbench (sass) 165 | .sass-cache/ 166 | 167 | # Installshield output folder 168 | [Ee]xpress/ 169 | 170 | # DocProject is a documentation generator add-in 171 | DocProject/buildhelp/ 172 | DocProject/Help/*.HxT 173 | DocProject/Help/*.HxC 174 | DocProject/Help/*.hhc 175 | DocProject/Help/*.hhk 176 | DocProject/Help/*.hhp 177 | DocProject/Help/Html2 178 | DocProject/Help/html 179 | 180 | # Click-Once directory 181 | publish/ 182 | 183 | # Publish Web Output 184 | *.[Pp]ublish.xml 185 | *.azurePubxml 186 | # Note: Comment the next line if you want to checkin your web deploy settings, 187 | # but database connection strings (with potential passwords) will be unencrypted 188 | *.pubxml 189 | *.publishproj 190 | 191 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 192 | # checkin your Azure Web App publish settings, but sensitive information contained 193 | # in these scripts will be unencrypted 194 | PublishScripts/ 195 | 196 | # NuGet Packages 197 | *.nupkg 198 | # NuGet Symbol Packages 199 | *.snupkg 200 | # The packages folder can be ignored because of Package Restore 201 | **/[Pp]ackages/* 202 | # except build/, which is used as an MSBuild target. 203 | !**/[Pp]ackages/build/ 204 | # Uncomment if necessary however generally it will be regenerated when needed 205 | #!**/[Pp]ackages/repositories.config 206 | # NuGet v3's project.json files produces more ignorable files 207 | *.nuget.props 208 | *.nuget.targets 209 | 210 | # Microsoft Azure Build Output 211 | csx/ 212 | *.build.csdef 213 | 214 | # Microsoft Azure Emulator 215 | ecf/ 216 | rcf/ 217 | 218 | # Windows Store app package directories and files 219 | AppPackages/ 220 | BundleArtifacts/ 221 | Package.StoreAssociation.xml 222 | _pkginfo.txt 223 | *.appx 224 | *.appxbundle 225 | *.appxupload 226 | 227 | # Visual Studio cache files 228 | # files ending in .cache can be ignored 229 | *.[Cc]ache 230 | # but keep track of directories ending in .cache 231 | !?*.[Cc]ache/ 232 | 233 | # Others 234 | ClientBin/ 235 | ~$* 236 | *~ 237 | *.dbmdl 238 | *.dbproj.schemaview 239 | *.jfm 240 | *.pfx 241 | *.publishsettings 242 | orleans.codegen.cs 243 | 244 | # Including strong name files can present a security risk 245 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 246 | #*.snk 247 | 248 | # Since there are multiple workflows, uncomment next line to ignore bower_components 249 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 250 | #bower_components/ 251 | 252 | # RIA/Silverlight projects 253 | Generated_Code/ 254 | 255 | # Backup & report files from converting an old project file 256 | # to a newer Visual Studio version. Backup files are not needed, 257 | # because we have git ;-) 258 | _UpgradeReport_Files/ 259 | Backup*/ 260 | UpgradeLog*.XML 261 | UpgradeLog*.htm 262 | ServiceFabricBackup/ 263 | *.rptproj.bak 264 | 265 | # SQL Server files 266 | *.mdf 267 | *.ldf 268 | *.ndf 269 | 270 | # Business Intelligence projects 271 | *.rdl.data 272 | *.bim.layout 273 | *.bim_*.settings 274 | *.rptproj.rsuser 275 | *- [Bb]ackup.rdl 276 | *- [Bb]ackup ([0-9]).rdl 277 | *- [Bb]ackup ([0-9][0-9]).rdl 278 | 279 | # Microsoft Fakes 280 | FakesAssemblies/ 281 | 282 | # GhostDoc plugin setting file 283 | *.GhostDoc.xml 284 | 285 | # Node.js Tools for Visual Studio 286 | .ntvs_analysis.dat 287 | node_modules/ 288 | 289 | # Visual Studio 6 build log 290 | *.plg 291 | 292 | # Visual Studio 6 workspace options file 293 | *.opt 294 | 295 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 296 | *.vbw 297 | 298 | # Visual Studio LightSwitch build output 299 | **/*.HTMLClient/GeneratedArtifacts 300 | **/*.DesktopClient/GeneratedArtifacts 301 | **/*.DesktopClient/ModelManifest.xml 302 | **/*.Server/GeneratedArtifacts 303 | **/*.Server/ModelManifest.xml 304 | _Pvt_Extensions 305 | 306 | # Paket dependency manager 307 | .paket/paket.exe 308 | paket-files/ 309 | 310 | # FAKE - F# Make 311 | .fake/ 312 | 313 | # CodeRush personal settings 314 | .cr/personal 315 | 316 | # Python Tools for Visual Studio (PTVS) 317 | __pycache__/ 318 | *.pyc 319 | 320 | # Cake - Uncomment if you are using it 321 | # tools/** 322 | # !tools/packages.config 323 | 324 | # Tabs Studio 325 | *.tss 326 | 327 | # Telerik's JustMock configuration file 328 | *.jmconfig 329 | 330 | # BizTalk build output 331 | *.btp.cs 332 | *.btm.cs 333 | *.odx.cs 334 | *.xsd.cs 335 | 336 | # OpenCover UI analysis results 337 | OpenCover/ 338 | 339 | # Azure Stream Analytics local run output 340 | ASALocalRun/ 341 | 342 | # MSBuild Binary and Structured Log 343 | *.binlog 344 | 345 | # NVidia Nsight GPU debugger configuration file 346 | *.nvuser 347 | 348 | # MFractors (Xamarin productivity tool) working folder 349 | .mfractor/ 350 | 351 | # Local History for Visual Studio 352 | .localhistory/ 353 | 354 | # BeatPulse healthcheck temp database 355 | healthchecksdb 356 | 357 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 358 | MigrationBackup/ 359 | 360 | # Ionide (cross platform F# VS Code tools) working folder 361 | .ionide/ 362 | 363 | # Rust 364 | /lib/blake3_dotnet/target 365 | Cargo.lock 366 | 367 | # Tmp folders 368 | tmp/ 369 | [Tt]emp/ 370 | 371 | # Remove artifacts produced by dotnet-releaser 372 | artifacts-dotnet-releaser/ -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > This changelog is no longer used for newer version. Please visits https://github.com/xoofx/grpc-curl/releases 4 | 5 | ## 1.3.5 (1 Feb 2022) 6 | - Update to latest CommandLineUtils to fix issue with RequiredAttribute 7 | - Bump to update formula with latest dotnet-releaser 8 | 9 | ## 1.3.4 (30 Jan 2022) 10 | - Fix exception with RequiredAttribute constructor not found 11 | 12 | ## 1.3.3 (30 Jan 2022) 13 | - Use CommandLineUtils to parse command line args 14 | - Add source link for DynamicGrpc library 15 | 16 | ## 1.3.2 (29 Jan 2022) 17 | - Use dotnet-releaser for releasing binaries 18 | 19 | ## 1.3.1 (27 Jan 2022) 20 | - Fix message fields having packed attribute 21 | - Fix warnings with trimming 22 | - Prepare application for self-contained app and trimming 23 | 24 | ## 1.3.0 (22 Jan 2022) 25 | - Add support for pretty printing all services and messages supported by a server with reflection (`--describe` with `grpc-curl`). 26 | - Add support for pretty printing proto descriptor back to proto language (`ToProtoString()` API with `DynamicGrpc`) 27 | 28 | ## 1.2.0 (21 Jan 2022) 29 | - Add support for all calling modes (unary, client streaming, server streaming and full-duplex) 30 | - Add cancellation token when fetching reflection from server. 31 | - Add support for default values. 32 | - Allow to parse data from stdin 33 | - Allow to force http if address:host is passed, use https by default 34 | 35 | ## 1.1.0 (21 Jan 2022) 36 | - Add support for any 37 | 38 | ## 1.0.0 (20 Jan 2022) 39 | 40 | - Initial version -------------------------------------------------------------------------------- /img/github-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoofx/grpc-curl/e6101a0c3aac1e2394fe54c657787d5fcd6acee8/img/github-banner.png -------------------------------------------------------------------------------- /img/grpc-curl.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoofx/grpc-curl/e6101a0c3aac1e2394fe54c657787d5fcd6acee8/img/grpc-curl.ico -------------------------------------------------------------------------------- /img/grpc-curl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoofx/grpc-curl/e6101a0c3aac1e2394fe54c657787d5fcd6acee8/img/grpc-curl.png -------------------------------------------------------------------------------- /img/grpc-curl.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, Alexandre Mutel 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification 5 | , are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # grpc-curl [![Build Status](https://github.com/xoofx/grpc-curl/workflows/ci/badge.svg?branch=main)](https://github.com/xoofx/grpc-curl/actions) [![Coverage Status](https://coveralls.io/repos/github/xoofx/grpc-curl/badge.svg?branch=main)](https://coveralls.io/github/xoofx/grpc-curl?branch=main) [![NuGet](https://img.shields.io/nuget/v/grpc-curl.svg)](https://www.nuget.org/packages/grpc-curl/) 2 | 3 | 4 | 5 | `grpc-curl` is a command line tool for interacting with gRPC servers. 6 | 7 | All the functionalities of `grpc-curl` are also accessible through the NuGet package [DynamicGrpc](https://www.nuget.org/packages/DynamicGrpc/) that is part of this repository. 8 | 9 | This tool is the .NET equivalent of the popular [gRPCurl](https://github.com/fullstorydev/grpcurl) written in Golang. 10 | 11 | > NOTE: `grpc-curl` doesn't not support yet all the features that `gRPCurl` is providing. 12 | ## Features 13 | 14 | - Allows to **invoke method services** for all gRPC calling modes (unary, client streaming, server streaming, full-duplex). 15 | - Allows to **print proto reflection descriptors** back to **proto language** (via `--describe` with `grpc-curl`, or via the API `.ToProtoString()` with `DynamicGrpc`) 16 | - Supports for plain Protocol Buffers naming conventions and JSON. 17 | - Supports for `google.protobuf.Any`: The type has to be encoded - and is decoded with the shadow property `@type` on a dictionary (e.g `@type = "type.googleapis.com/YourTypeName"`). 18 | - Build on top of the `DynamicGrpc` library available as a separate [NuGet package](https://www.nuget.org/packages/DynamicGrpc/). 19 | - Build for `net6.0+` 20 | - Available for multiple platforms. See binaries section below. 21 | 22 | ## Usage 23 | 24 | `grpc-curl` currently requires that the gRPC server has activated gRPC reflection. 25 | 26 | ``` 27 | Copyright (C) 2022 Alexandre Mutel. All Rights Reserved 28 | grpc-curl - Version: 1.3.6 29 | 30 | Usage: grpc-curl [options] address service/method 31 | 32 | address: A http/https URL or a simple host:address. 33 | If only host:address is used, HTTPS is used by default 34 | unless the options --http is passed. 35 | 36 | ## Options 37 | 38 | -d, --data=VALUE Data for string content. 39 | --http Use HTTP instead of HTTPS unless the protocol is 40 | specified directly on the address. 41 | --json Use JSON naming for input and output. 42 | --describe Describe the service or dump all services 43 | available. 44 | -v, --verbosity[=VALUE] Set verbosity. 45 | -h, --help Show this help. 46 | ``` 47 | 48 | ### Query a service 49 | 50 | ```powershell 51 | ./grpc-curl --json -d "{""getStatus"":{}}" http://192.168.100.1:9200 SpaceX.API.Device.Device/Handle 52 | ``` 53 | Will print the following result: 54 | 55 | ```json 56 | { 57 | "apiVersion": 4, 58 | "dishGetStatus": { 59 | "deviceInfo": { 60 | "id": "0000000000-00000000-00000000", 61 | "hardwareVersion": "rev2_proto3", 62 | "softwareVersion": "992cafb5-61c7-46a3-9ef7-5907c8cf90fd.uterm.release", 63 | "countryCode": "FR", 64 | "utcOffsetS": 1 65 | }, 66 | "deviceState": { 67 | "uptimeS": 667397 68 | }, 69 | "obstructionStats": { 70 | "fractionObstructed": 2.2786187E-06, 71 | "wedgeFractionObstructed": [ 72 | 0.0, 73 | 0.0, 74 | 0.0, 75 | 0.0, 76 | 0.0, 77 | 0.0, 78 | 0.0, 79 | 0.0, 80 | 0.0, 81 | 0.0, 82 | 0.0, 83 | 0.0 84 | ], 85 | "wedgeAbsFractionObstructed": [ 86 | 0.0, 87 | 0.0, 88 | 0.0, 89 | 0.0, 90 | 0.0, 91 | 0.0, 92 | 0.0, 93 | 0.0, 94 | 0.0, 95 | 0.0, 96 | 0.0, 97 | 0.0 98 | ], 99 | "validS": 667070.0, 100 | "avgProlongedObstructionIntervalS": "NaN" 101 | }, 102 | "alerts": { 103 | "roaming": true 104 | }, 105 | "downlinkThroughputBps": 461012.72, 106 | "uplinkThroughputBps": 294406.6, 107 | "popPingLatencyMs": 30.35, 108 | "boresightAzimuthDeg": 0.7464048, 109 | "boresightElevationDeg": 65.841354, 110 | "gpsStats": { 111 | "gpsValid": true, 112 | "gpsSats": 12 113 | } 114 | } 115 | } 116 | ``` 117 | 118 | ### Describe a service 119 | 120 | ```powershell 121 | ./grpc-curl --describe http://192.168.100.1:9200 SpaceX.API.Device.Device 122 | ``` 123 | Will print: 124 | 125 | ```proto 126 | // SpaceX.API.Device.Device is a service: 127 | service Device { 128 | rpc Stream ( .SpaceX.API.Device.ToDevice ) returns ( .SpaceX.API.Device.FromDevice ); 129 | rpc Handle ( .SpaceX.API.Device.Request ) returns ( .SpaceX.API.Device.Response ); 130 | } 131 | ``` 132 | 133 | ### Describe all proto files serviced via reflection 134 | 135 | ```powershell 136 | ./grpc-curl --describe http://192.168.100.1:9200 137 | ``` 138 | Will print: 139 | 140 | ```proto 141 | // spacex/api/common/status/status.proto is a proto file. 142 | syntax = "proto3"; 143 | 144 | package SpaceX.API.Status; 145 | 146 | // SpaceX.API.Status.Status is a message: 147 | message Status { 148 | int32 code = 1; 149 | string message = 2; 150 | } 151 | 152 | 153 | // spacex/api/device/command.proto is a proto file. 154 | syntax = "proto3"; 155 | 156 | package SpaceX.API.Device; 157 | 158 | // SpaceX.API.Device.PublicKey is a message: 159 | message PublicKey { 160 | string key = 1; 161 | repeated Capability capabilities = 2; 162 | } 163 | 164 | // ....... and more prints ........ 165 | ``` 166 | 167 | ## Usage API 168 | 169 | All the functionalities of `grpc-curl` are also accessible through the NuGet package [DynamicGrpc](https://www.nuget.org/packages/DynamicGrpc/). 170 | 171 | ```c# 172 | var channel = GrpcChannel.ForAddress("http://192.168.100.1:9200"); 173 | // Fetch reflection data from server 174 | var client = await DynamicGrpcClient.FromServerReflection(channel); 175 | 176 | // Call the method `Handle` on the service `SpaceX.API.Device.Device` 177 | var result = await client.AsyncUnaryCall("SpaceX.API.Device.Device", "Handle", new Dictionary() 178 | { 179 | { "get_status", new Dictionary() } 180 | }); 181 | 182 | // Print a proto descriptor 183 | FileDescriptor descriptor = client.Files[0]; 184 | Console.WriteLine(descriptor.ToProtoString()); 185 | ``` 186 | ## Binaries 187 | 188 | `grpc-curl` is available on multiple platforms: 189 | 190 | 191 | | Platform | Packages | 192 | |-----------------------------------------|------------------| 193 | | `win-x64`, `win-arm`, `win-arm64` | `zip` 194 | | `linux-x64`, `linux-arm`, `linux-arm64` | `deb`, `tar` 195 | | `rhel-x64` | `rpm`, `tar` 196 | | `osx-x64`, `osx-arm64` | `tar` 197 | 198 | 199 | If you have dotnet 6.0 installed, you can install this tool via NuGet: 200 | 201 | ``` 202 | dotnet tool install --global grpc-curl 203 | ``` 204 | 205 | Otherwise, you can install native binaries to Windows, Linux, and macOS with the various debian/rpm/zip packages available directly from the [releases](https://github.com/xoofx/grpc-curl/releases). 206 | 207 | grpc-curl is also available via homebrew for macOS and Linux: 208 | 209 | ``` 210 | $ brew tap xoofx/grpc-curl 211 | $ brew install grpc-curl 212 | ``` 213 | 214 | ## License 215 | 216 | This software is released under the [BSD-Clause 2 license](https://opensource.org/licenses/BSD-2-Clause). 217 | 218 | ## Author 219 | 220 | Alexandre Mutel aka [xoofx](https://xoofx.github.io). 221 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Alexandre Mutel 4 | en-US 5 | Alexandre Mutel 6 | gRPC;RPC;HTTP/2;tool 7 | readme.md 8 | https://github.com/xoofx/grpc-curl/blob/master/changelog.md 9 | grpc-curl.png 10 | https://github.com/xoofx/grpc-curl 11 | BSD-2-Clause 12 | true 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | all 5 | runtime; build; native; contentfiles; analyzers 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/DynamicGrpc/DynamicAny.cs: -------------------------------------------------------------------------------- 1 | //using System.Dynamic; 2 | 3 | namespace DynamicGrpc; 4 | 5 | /// 6 | /// Used to serialize/deserialize back the Any type. 7 | /// 8 | public static class DynamicAnyExtensions 9 | { 10 | // https://github.com/protocolbuffers/protobuf/blob/41e22cde8d8a44c35127a26c19e08b180e0b30a4/src/google/protobuf/any.proto#L97-L124 11 | internal const string GoogleTypeAnyFullName = "google.protobuf.Any"; 12 | internal const string GoogleTypeUrlKey = "type_url"; 13 | internal const string GoogleValueKey = "value"; 14 | 15 | public const string TypeKey = "@type"; 16 | 17 | /// 18 | /// Adds the property @type to serialize a dictionary as any type 19 | /// 20 | /// Type of the dictionary. 21 | /// The any dictionary. 22 | /// The type associated to this dictionary. 23 | /// The input any dictionary with the proper @type information. 24 | public static TAny WithAny(this TAny any, string typeName) where TAny : IDictionary 25 | { 26 | any[TypeKey] = $"type.googleapis.com/{typeName}"; 27 | return any; 28 | } 29 | } -------------------------------------------------------------------------------- /src/DynamicGrpc/DynamicFileDescriptorSet.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Google.Protobuf; 4 | using Google.Protobuf.Reflection; 5 | using Grpc.Core; 6 | using Grpc.Reflection.V1Alpha; 7 | 8 | namespace DynamicGrpc; 9 | 10 | /// 11 | /// Internal class used to manage all serializers for a set of . 12 | /// 13 | internal sealed class DynamicFileDescriptorSet 14 | { 15 | private readonly FileDescriptor[] _descriptorSet; 16 | private readonly Dictionary _services; 17 | private readonly Dictionary _messageTypes; 18 | 19 | public DynamicFileDescriptorSet(FileDescriptor[] descriptorSet) 20 | { 21 | _descriptorSet = descriptorSet; 22 | _services = new Dictionary(); 23 | _messageTypes = new Dictionary(); 24 | Initialize(); 25 | } 26 | 27 | public FileDescriptor[] Files => _descriptorSet; 28 | 29 | private void Initialize() 30 | { 31 | foreach (var file in _descriptorSet) 32 | { 33 | foreach (var service in file.Services) 34 | { 35 | var dynamicService = new DynamicServiceDescriptor(service); 36 | var key = $"{file.Package}.{service.Name}"; 37 | if (!_services.ContainsKey(key)) 38 | { 39 | _services.Add(key, dynamicService); 40 | } 41 | } 42 | 43 | foreach (var message in file.MessageTypes) 44 | { 45 | ProcessMessageType(message, file.Package); 46 | } 47 | 48 | // TODO: Anything from file.Options? 49 | } 50 | } 51 | 52 | private void ProcessMessageType(MessageDescriptor messageDescriptor, string parentKey) 53 | { 54 | var keyType = $"{parentKey}.{messageDescriptor.Name}"; 55 | if (!_messageTypes.ContainsKey(keyType)) 56 | { 57 | _messageTypes.Add(keyType, new DynamicMessageSerializer(this, messageDescriptor)); 58 | } 59 | 60 | foreach (var messageDescriptorNestedType in messageDescriptor.NestedTypes) 61 | { 62 | ProcessMessageType(messageDescriptorNestedType, keyType); 63 | } 64 | } 65 | 66 | public (Marshaller> request, Marshaller> response) GetMarshaller(string serviceName, string methodName, DynamicGrpcClientContext context) 67 | { 68 | if (!TryFindMethodDescriptorProto(serviceName, methodName, out var methodProto)) 69 | { 70 | throw new InvalidOperationException($"The service/method `{serviceName}/{methodName}` was not found."); 71 | } 72 | 73 | if (!TryFindMessageDescriptorProto(methodProto.InputType.FullName, out var inputMessageProto)) 74 | { 75 | throw new InvalidOperationException($"The input message type`{methodProto.InputType}` for the service/method {serviceName}/{methodName} was not found."); 76 | } 77 | 78 | if (!TryFindMessageDescriptorProto(methodProto.OutputType.FullName, out var outputMessageProto)) 79 | { 80 | throw new InvalidOperationException($"The output message type `{methodProto.InputType}` for the service/method {serviceName}/{methodName} was not found."); 81 | } 82 | 83 | return (inputMessageProto.GetMarshaller(context), outputMessageProto.GetMarshaller(context)); 84 | } 85 | 86 | public static async Task FromServerReflection(CallInvoker callInvoker, int? timeoutInMillis, CancellationToken cancellationToken) 87 | { 88 | // Step 1 - Fetch all services we can interact with 89 | var client = new ServerReflection.ServerReflectionClient(callInvoker); 90 | var response = await SingleRequestAsync(client, new ServerReflectionRequest 91 | { 92 | ListServices = "" 93 | }, timeoutInMillis, cancellationToken); 94 | 95 | // Step 2 - Fetch all proto files associated with the service we got. 96 | // NOTE: The proto files are all transitive, but not correctly ordered! 97 | var protosLoaded = new Dictionary(); 98 | var listOfProtosToLoad = new List(); 99 | foreach (var service in response.ListServicesResponse.Service) 100 | { 101 | var serviceResponse = await SingleRequestAsync(client, new ServerReflectionRequest 102 | { 103 | FileContainingSymbol = service.Name 104 | }, timeoutInMillis, cancellationToken); 105 | 106 | listOfProtosToLoad.AddRange(serviceResponse.FileDescriptorResponse.FileDescriptorProto.ToList()); 107 | } 108 | 109 | // Workaround for https://github.com/protocolbuffers/protobuf/issues/9431 110 | // Step 3 - Order proto files correctly because of 2 problems: 111 | // 1) as FileContainingSymbol doesn't seem to return proto files in the correct order 112 | // 2) FileDescriptor.BuildFromByteStrings doesn't support passing files in random order, so we need to reorder them with protos 113 | // It is very unfortunate, as we are doubling the deserialization of FileDescriptorProto 114 | var resolved = new HashSet(); 115 | var orderedList = new List(); 116 | 117 | foreach (var buffer in listOfProtosToLoad) 118 | { 119 | var proto = FileDescriptorProto.Parser.ParseFrom(buffer.ToByteArray()); 120 | protosLoaded.TryAdd(proto.Name, (buffer, proto)); 121 | } 122 | 123 | while (protosLoaded.Count > 0) 124 | { 125 | var (buffer, nextProto) = protosLoaded.Values.FirstOrDefault(x => x.Item2.Dependency.All(dep => resolved.Contains(dep))); 126 | if (nextProto == null) 127 | { 128 | throw new InvalidOperationException($"Invalid proto dependencies. Unable to resolve remaining protos [{string.Join(",", protosLoaded.Values.Select(x => x.Item2.Name))}] that don't have all their dependencies available."); 129 | } 130 | 131 | resolved.Add(nextProto.Name); 132 | protosLoaded.Remove(nextProto.Name); 133 | orderedList.Add(buffer); 134 | } 135 | 136 | // Step 4 - Build FileDescriptor from properly ordered list 137 | var descriptors = FileDescriptor.BuildFromByteStrings(orderedList.ToList()); 138 | return new DynamicFileDescriptorSet(descriptors.ToArray()); 139 | } 140 | 141 | public static DynamicFileDescriptorSet FromFileDescriptorProtos(IEnumerable protos) 142 | { 143 | // Workaround for https://github.com/protocolbuffers/protobuf/issues/9431 144 | // Step 1 - FileDescriptor.BuildFromByteStrings doesn't support passing files in random order, so we need to reorder them with protos 145 | // It is very unfortunate, as we are doubling the deserialization of FileDescriptorProto 146 | var resolved = new HashSet(); 147 | var orderedList = new List(); 148 | var unorderedList = new List(protos); 149 | 150 | while (unorderedList.Count > 0) 151 | { 152 | var proto = unorderedList.FirstOrDefault(x => x.Dependency.All(dep => resolved.Contains(dep))); 153 | if (proto == null) 154 | { 155 | throw new InvalidOperationException($"Invalid proto dependencies. Unable to resolve remaining protos [{string.Join(",", unorderedList.Select(x => x.Name))}] that don't have all their dependencies available."); 156 | } 157 | 158 | resolved.Add(proto.Name); 159 | unorderedList.Remove(proto); 160 | orderedList.Add(proto.ToByteString()); 161 | } 162 | 163 | // Step 2 - Build FileDescriptor from properly ordered list 164 | var descriptors = FileDescriptor.BuildFromByteStrings(orderedList.ToList()); 165 | return new DynamicFileDescriptorSet(descriptors.ToArray()); 166 | } 167 | 168 | public static DynamicFileDescriptorSet FromFileDescriptors(IEnumerable descriptors) 169 | { 170 | // Workaround for https://github.com/protocolbuffers/protobuf/issues/9431 171 | // Step 1 - FileDescriptor.BuildFromByteStrings doesn't support passing files in random order, so we need to reorder them with protos 172 | // It is very unfortunate, as we are doubling the deserialization of FileDescriptorProto 173 | var resolved = new HashSet(); 174 | var orderedList = new List(); 175 | var unorderedList = new List(descriptors); 176 | 177 | while (unorderedList.Count > 0) 178 | { 179 | var descriptor = unorderedList.FirstOrDefault(x => x.Dependencies.All(dep => resolved.Contains(dep.Name))); 180 | if (descriptor == null) 181 | { 182 | throw new InvalidOperationException($"Invalid proto dependencies. Unable to resolve remaining protos [{string.Join(",", unorderedList.Select(x => x.Name))}] that don't have all their dependencies available."); 183 | } 184 | 185 | resolved.Add(descriptor.Name); 186 | unorderedList.Remove(descriptor); 187 | orderedList.Add(descriptor); 188 | } 189 | 190 | // Step 2 - Build FileDescriptor from properly ordered list 191 | return new DynamicFileDescriptorSet(orderedList.ToArray()); 192 | } 193 | 194 | private static async Task SingleRequestAsync(ServerReflection.ServerReflectionClient client, ServerReflectionRequest request, int? timeoutInMillis, CancellationToken cancellationToken) 195 | { 196 | using var call = client.ServerReflectionInfo(deadline: timeoutInMillis is > 0 ? DateTime.Now.ToUniversalTime().AddMilliseconds(timeoutInMillis.Value) : null, cancellationToken: cancellationToken); 197 | await call.RequestStream.WriteAsync(request); 198 | var result = await call.ResponseStream.MoveNext(); 199 | if (!result) 200 | { 201 | throw new InvalidOperationException(); 202 | } 203 | var response = call.ResponseStream.Current; 204 | await call.RequestStream.CompleteAsync(); 205 | 206 | result = await call.ResponseStream.MoveNext(); 207 | Debug.Assert(!result); 208 | 209 | return response; 210 | } 211 | 212 | public bool TryFindMethodDescriptorProto(string serviceName, string methodName, [NotNullWhen(true)] out MethodDescriptor? methodProto) 213 | { 214 | methodProto = null; 215 | return _services.TryGetValue($"{serviceName}", out var dynamicServiceDescriptor) && dynamicServiceDescriptor.TryGetValue(methodName, out methodProto); 216 | } 217 | 218 | 219 | public bool TryFindMessageDescriptorProto(string typeName, [NotNullWhen(true)] out DynamicMessageSerializer? messageDescriptorProto) 220 | { 221 | messageDescriptorProto = null; 222 | return _messageTypes.TryGetValue(typeName, out messageDescriptorProto); 223 | } 224 | } -------------------------------------------------------------------------------- /src/DynamicGrpc/DynamicGrpc.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | DynamicGrpc is a .NET 6.0+ library for interacting with gRPC servers. 8 | 9 | true 10 | true 11 | snupkg 12 | True 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/DynamicGrpc/DynamicGrpcClient.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Google.Protobuf.Reflection; 3 | using Grpc.Core; 4 | 5 | namespace DynamicGrpc; 6 | 7 | /// 8 | /// Main client interface for dynamically calling a gRPC service. 9 | /// 10 | public sealed class DynamicGrpcClient : ClientBase 11 | { 12 | private readonly DynamicFileDescriptorSet _dynamicDescriptorSet; 13 | private readonly DynamicGrpcClientOptions _options; 14 | 15 | private DynamicGrpcClient(CallInvoker callInvoker, DynamicFileDescriptorSet dynamicDescriptorSet, DynamicGrpcClientOptions options) : base(callInvoker) 16 | { 17 | _dynamicDescriptorSet = dynamicDescriptorSet; 18 | _options = options; 19 | } 20 | 21 | /// 22 | /// List of used by this instance. 23 | /// 24 | public IReadOnlyList Files => _dynamicDescriptorSet.Files; 25 | 26 | /// 27 | /// Tries to find the associated to the specified service and method name. 28 | /// 29 | /// The service. 30 | /// The method. 31 | /// The method descriptor or null if none found. 32 | /// true if the method was found; false otherwise. 33 | public bool TryFindMethod(string serviceName, string methodName, [NotNullWhen(true)] out MethodDescriptor? methodDescriptor) 34 | { 35 | return _dynamicDescriptorSet.TryFindMethodDescriptorProto(serviceName, methodName, out methodDescriptor); 36 | } 37 | 38 | /// 39 | /// Creates a client by using the specified . The descriptors must appear in reverse dependency order (if A depends on B, B should comes first). 40 | /// 41 | /// The gRPC channel to fetch reflection data from. 42 | /// The file descriptors./> 43 | /// Options for this client. 44 | /// A dynamic client gRPC instance. 45 | public static DynamicGrpcClient FromDescriptors(ChannelBase channel, FileDescriptor[] descriptors, DynamicGrpcClientOptions? options = null) 46 | { 47 | return FromDescriptors(channel.CreateCallInvoker(), descriptors, options); 48 | } 49 | 50 | /// 51 | /// Creates a client by using the specified . The descriptors must appear in reverse dependency order (if A depends on B, B should comes first). 52 | /// 53 | /// The gRPC CallInvoker to fetch reflection data from. 54 | /// The file descriptors./> 55 | /// Options for this client. 56 | /// A dynamic client gRPC instance. 57 | public static DynamicGrpcClient FromDescriptors(CallInvoker callInvoker, FileDescriptor[] descriptors, DynamicGrpcClientOptions? options = null) 58 | { 59 | options ??= new DynamicGrpcClientOptions(); 60 | return new DynamicGrpcClient(callInvoker, DynamicFileDescriptorSet.FromFileDescriptors(descriptors), options); 61 | } 62 | 63 | /// 64 | /// Creates a client by using the specified . The descriptors must appear in reverse dependency order (if A depends on B, B should comes first). 65 | /// 66 | /// The gRPC channel to fetch reflection data from. 67 | /// The file proto descriptors./> 68 | /// Options for this client. 69 | /// A dynamic client gRPC instance. 70 | public static DynamicGrpcClient FromDescriptorProtos(ChannelBase channel, FileDescriptorProto[] descriptorProtos, DynamicGrpcClientOptions? options = null) 71 | { 72 | return FromDescriptorProtos(channel.CreateCallInvoker(), descriptorProtos, options); 73 | } 74 | 75 | /// 76 | /// Creates a client by using the specified . The descriptors must appear in reverse dependency order (if A depends on B, B should comes first). 77 | /// 78 | /// The gRPC CallInvoker to fetch reflection data from. 79 | /// The file proto descriptors./> 80 | /// Options for this client. 81 | /// A dynamic client gRPC instance. 82 | public static DynamicGrpcClient FromDescriptorProtos(CallInvoker callInvoker, FileDescriptorProto[] descriptorProtos, DynamicGrpcClientOptions? options = null) 83 | { 84 | options ??= new DynamicGrpcClientOptions(); 85 | return new DynamicGrpcClient(callInvoker, DynamicFileDescriptorSet.FromFileDescriptorProtos(descriptorProtos), options); 86 | } 87 | 88 | /// 89 | /// Creates a client by fetching reflection data from the server. Might trigger an exception if the server doesn't support exception. 90 | /// 91 | /// The gRPC channel to fetch reflection data from. 92 | /// Options for this client. 93 | /// Timeout in milliseconds. Default is 10000ms (10 seconds). 94 | /// Optional cancellation token. 95 | /// A dynamic client gRPC instance. 96 | public static Task FromServerReflection(ChannelBase channel, DynamicGrpcClientOptions? options = null, int timeoutInMillis = 10000, CancellationToken cancellationToken = default) 97 | { 98 | return FromServerReflection(channel.CreateCallInvoker(), options, timeoutInMillis, cancellationToken); 99 | } 100 | 101 | /// 102 | /// Creates a client by fetching reflection data from the server. Might trigger an exception if the server doesn't support exception. 103 | /// 104 | /// The gRPC CallInvoker to fetch reflection data from. 105 | /// Options for this client. 106 | /// Timeout in milliseconds. Default is 10000ms (10 seconds). 107 | /// Optional cancellation token. 108 | /// A dynamic client gRPC instance. 109 | public static async Task FromServerReflection(CallInvoker callInvoker, DynamicGrpcClientOptions? options = null, int timeoutInMillis = 10000, CancellationToken cancellationToken = default) 110 | { 111 | options ??= new DynamicGrpcClientOptions(); 112 | var dynamicDescriptorSet = await DynamicFileDescriptorSet.FromServerReflection(callInvoker, timeoutInMillis, cancellationToken); 113 | return new DynamicGrpcClient(callInvoker, dynamicDescriptorSet, options); 114 | } 115 | 116 | /// Invokes a simple remote call in a blocking fashion. 117 | public IDictionary BlockingUnaryCall(string serviceName, string methodName, IDictionary request, string? host = null, CallOptions? options = null) 118 | { 119 | return CallInvoker.BlockingUnaryCall(GetMethod(serviceName, methodName), host, options ?? new CallOptions(), request); 120 | } 121 | 122 | /// 123 | /// Invoke the method asynchronously and adapt dynamically whether the method is a unary call, a client streaming, server streaming or full duplex. 124 | /// 125 | /// The name of the service to access. 126 | /// The name of the method in the service. 127 | /// The input. 128 | /// The result of the call. 129 | /// If the service/method was not found 130 | public async IAsyncEnumerable> AsyncDynamicCall(string serviceName, string methodName, IAsyncEnumerable> input, string? host = null, CallOptions? options = null) 131 | { 132 | if (!TryFindMethod(serviceName, methodName, out var methodDescriptor)) 133 | { 134 | throw new InvalidOperationException($"Unable to find the method `{serviceName}/{methodName}`"); 135 | } 136 | 137 | if (methodDescriptor.IsClientStreaming) 138 | { 139 | if (methodDescriptor.IsServerStreaming) 140 | { 141 | // Full streaming duplex 142 | var call = AsyncDuplexStreamingCall(serviceName, methodName, host, options); 143 | await foreach (var item in input) 144 | { 145 | await call.RequestStream.WriteAsync(item); 146 | } 147 | await call.RequestStream.CompleteAsync(); 148 | 149 | var responseStream = call.ResponseStream; 150 | while (await responseStream.MoveNext()) 151 | { 152 | yield return responseStream.Current; 153 | } 154 | } 155 | else 156 | { 157 | // Client streaming only 158 | var call = AsyncClientStreamingCall(serviceName, methodName, host, options); 159 | await foreach (var item in input) 160 | { 161 | await call.RequestStream.WriteAsync(item); 162 | } 163 | await call.RequestStream.CompleteAsync(); 164 | var result = await call.ResponseAsync; 165 | yield return result; 166 | } 167 | 168 | } 169 | else if (methodDescriptor.IsServerStreaming) 170 | { 171 | // Server streaming only 172 | IDictionary? firstInput = null; 173 | await foreach (var item in input) 174 | { 175 | firstInput = item; 176 | break; // Take only the first element 177 | } 178 | 179 | var call = AsyncServerStreamingCall(serviceName, methodName, firstInput ?? new Dictionary(), host, options); 180 | var responseStream = call.ResponseStream; 181 | while (await responseStream.MoveNext()) 182 | { 183 | yield return responseStream.Current; 184 | } 185 | } 186 | else 187 | { 188 | // Standard call 189 | IDictionary? firstInput = null; 190 | await foreach (var item in input) 191 | { 192 | firstInput = item; 193 | break; // Take only the first element 194 | } 195 | 196 | var result = await AsyncUnaryCall(serviceName, methodName, firstInput ?? new Dictionary(), host, options); 197 | yield return result; 198 | } 199 | } 200 | 201 | /// Invokes a simple remote call asynchronously. 202 | /// The name of the service to access. 203 | /// The name of the method in the service. 204 | /// The input. 205 | /// Override for the host. 206 | /// Optional options for this call. 207 | /// The result of the call. 208 | public AsyncUnaryCall> AsyncUnaryCall(string serviceName, string methodName, IDictionary request, string? host = null, CallOptions? options = null) 209 | { 210 | return CallInvoker.AsyncUnaryCall(GetMethod(serviceName, methodName), host, options ?? new CallOptions(), request); 211 | } 212 | 213 | /// 214 | /// Invokes a server streaming call asynchronously. 215 | /// In server streaming scenario, client sends on request and server responds with a stream of responses. 216 | /// 217 | /// The name of the service to access. 218 | /// The name of the method in the service. 219 | /// The input. 220 | /// Override for the host. 221 | /// Optional options for this call. 222 | /// A call object to interact with streaming request/response streams. 223 | public Grpc.Core.AsyncServerStreamingCall> AsyncServerStreamingCall( 224 | string serviceName, string methodName, 225 | IDictionary request, string? host = null, CallOptions? options = null) 226 | { 227 | return CallInvoker.AsyncServerStreamingCall(GetMethod(serviceName, methodName), host, options ?? new CallOptions(), request); 228 | } 229 | 230 | ///// 231 | ///// Invokes a client streaming call asynchronously. 232 | ///// In client streaming scenario, client sends a stream of requests and server responds with a single response. 233 | ///// 234 | /// The name of the service to access. 235 | /// The name of the method in the service. 236 | /// Override for the host. 237 | /// Optional options for this call. 238 | /// A call object to interact with streaming request/response streams. 239 | public Grpc.Core.AsyncClientStreamingCall, IDictionary> AsyncClientStreamingCall( 240 | string serviceName, string methodName, 241 | string? host = null, 242 | CallOptions? options = null) 243 | { 244 | return CallInvoker.AsyncClientStreamingCall(GetMethod(serviceName, methodName), host, options ?? new CallOptions()); 245 | } 246 | 247 | /// 248 | /// Invokes a duplex streaming call asynchronously. 249 | /// In duplex streaming scenario, client sends a stream of requests and server responds with a stream of responses. 250 | /// The response stream is completely independent and both side can be sending messages at the same time. 251 | /// 252 | /// The name of the service to access. 253 | /// The name of the method in the service. 254 | /// Override for the host. 255 | /// Optional options for this call. 256 | /// A call object to interact with streaming request/response streams. 257 | public Grpc.Core.AsyncDuplexStreamingCall, IDictionary> AsyncDuplexStreamingCall( 258 | string serviceName, string methodName, 259 | string? host = null, 260 | CallOptions? options = null) 261 | { 262 | return CallInvoker.AsyncDuplexStreamingCall(GetMethod(serviceName, methodName), host, options ?? new CallOptions()); 263 | } 264 | 265 | private Method, IDictionary> GetMethod(string serviceName, string methodName) 266 | { 267 | var (marshalIn, marshalOut) = _dynamicDescriptorSet.GetMarshaller(serviceName, methodName, new DynamicGrpcClientContext(_options)); 268 | return new Method, IDictionary>(MethodType.Unary, serviceName, methodName, marshalIn, marshalOut); 269 | } 270 | } -------------------------------------------------------------------------------- /src/DynamicGrpc/DynamicGrpcClientContext.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | 3 | namespace DynamicGrpc; 4 | 5 | /// 6 | /// Internal class used for passing options around and keep read tags. 7 | /// This class should reflect . 8 | /// 9 | internal sealed class DynamicGrpcClientContext 10 | { 11 | private readonly Queue _nextTags; 12 | 13 | public DynamicGrpcClientContext(DynamicGrpcClientOptions options) 14 | { 15 | UseJsonNaming = options.UseJsonNaming; 16 | UseNumberedEnums = options.UseNumberedEnums; 17 | Factory = options.MessageFactory; 18 | _nextTags = new Queue(); 19 | MapToAny = new Dictionary, IDictionary>(ReferenceEqualityComparer.Instance); 20 | } 21 | 22 | public bool UseJsonNaming { get; set; } 23 | 24 | public bool UseNumberedEnums { get; set; } 25 | 26 | public Func> Factory { get; set; } 27 | 28 | public Dictionary, IDictionary> MapToAny { get; } 29 | 30 | internal uint ReadTag(ref ParseContext input) 31 | { 32 | return _nextTags.Count > 0 ? _nextTags.Dequeue() : input.ReadTag(); 33 | } 34 | 35 | internal uint SkipTag(ref ParseContext input) 36 | { 37 | return _nextTags.Dequeue(); 38 | } 39 | 40 | internal uint PeekTak(ref ParseContext input) 41 | { 42 | if (_nextTags.Count > 0) return _nextTags.Peek(); 43 | var tag = input.ReadTag(); 44 | _nextTags.Enqueue(tag); 45 | return tag; 46 | } 47 | 48 | internal void EnqueueTag(uint tag) 49 | { 50 | _nextTags.Enqueue(tag); 51 | } 52 | } -------------------------------------------------------------------------------- /src/DynamicGrpc/DynamicGrpcClientException.cs: -------------------------------------------------------------------------------- 1 | namespace DynamicGrpc; 2 | 3 | /// 4 | /// Exception that can be thrown by . 5 | /// 6 | public sealed class DynamicGrpcClientException : Exception 7 | { 8 | /// 9 | /// Creates a new instance of this class. 10 | /// 11 | /// The message of the exception. 12 | public DynamicGrpcClientException(string? message) : base(message) 13 | { 14 | } 15 | 16 | /// 17 | /// Creates a new instance of this class. 18 | /// 19 | /// The message of the exception. 20 | /// The nested exception. 21 | public DynamicGrpcClientException(string? message, Exception? innerException) : base(message, innerException) 22 | { 23 | } 24 | } -------------------------------------------------------------------------------- /src/DynamicGrpc/DynamicGrpcClientOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | 3 | namespace DynamicGrpc; 4 | 5 | /// 6 | /// Options to use with 7 | /// 8 | public sealed class DynamicGrpcClientOptions 9 | { 10 | /// 11 | /// Creates a new instance of this class. 12 | /// 13 | public DynamicGrpcClientOptions() 14 | { 15 | MessageFactory = () => new ExpandoObject()!; 16 | } 17 | 18 | /// 19 | /// Gets or sets a boolean indicating whether to serialize/deserialize using JSON names. Default is false. 20 | /// 21 | public bool UseJsonNaming { get; set; } 22 | 23 | /// 24 | /// Gets or sets a boolean indicating whether to serialize/deserialize enum with numbers instead of strings. Default is false. 25 | /// 26 | public bool UseNumberedEnums { get; set; } 27 | 28 | /// 29 | /// Gets or sets the factory to instance deserialized messages. By default, creates a . 30 | /// 31 | public Func> MessageFactory { get; set; } 32 | 33 | /// 34 | /// Clones this instance. 35 | /// 36 | /// A clone of this instance. 37 | public DynamicGrpcClientOptions Clone() 38 | { 39 | return (DynamicGrpcClientOptions)this.MemberwiseClone(); 40 | } 41 | } -------------------------------------------------------------------------------- /src/DynamicGrpc/DynamicGrpcPrinter.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Reflection; 3 | using System.Text; 4 | using Google.Protobuf.Reflection; 5 | using FileOptions = Google.Protobuf.Reflection.FileOptions; 6 | 7 | namespace DynamicGrpc; 8 | 9 | /// 10 | /// Extension methods for printing descriptors back to proto language. 11 | /// 12 | public static class DynamicGrpcPrinter 13 | { 14 | /// 15 | /// Prints the proto description of the specified to a writer. 16 | /// 17 | /// The descriptor to print. 18 | /// The text writer. 19 | /// The printing options. 20 | public static void ToProtoString(this FileDescriptor file, TextWriter writer, DynamicGrpcPrinterOptions? options = null) 21 | { 22 | ToProtoString(file, new DynamicGrpcPrinterContext(writer, options ?? new DynamicGrpcPrinterOptions())); 23 | } 24 | 25 | /// 26 | /// Prints the proto description of the specified to a string. 27 | /// 28 | /// The descriptor to print. 29 | /// The printing options. 30 | /// A proto description of the specified descriptor. 31 | public static string ToProtoString(this FileDescriptor file, DynamicGrpcPrinterOptions? options = null) 32 | { 33 | var writer = new StringWriter(); 34 | ToProtoString(file, writer, options); 35 | return writer.ToString(); 36 | } 37 | 38 | /// 39 | /// Prints the proto description of the specified to a writer. 40 | /// 41 | /// The descriptor to print. 42 | /// The text writer. 43 | /// The printing options. 44 | public static void ToProtoString(this ServiceDescriptor service, TextWriter writer, DynamicGrpcPrinterOptions? options = null) 45 | { 46 | ToProtoString(service, new DynamicGrpcPrinterContext(writer, options ?? new DynamicGrpcPrinterOptions())); 47 | } 48 | 49 | /// 50 | /// Prints the proto description of the specified to a string. 51 | /// 52 | /// The descriptor to print. 53 | /// The printing options. 54 | /// A proto description of the specified descriptor. 55 | public static string ToProtoString(this ServiceDescriptor service, DynamicGrpcPrinterOptions? options = null) 56 | { 57 | var writer = new StringWriter(); 58 | ToProtoString(service, writer, options); 59 | return writer.ToString(); 60 | } 61 | 62 | /// 63 | /// Prints the proto description of the specified to a writer. 64 | /// 65 | /// The descriptor to print. 66 | /// The text writer. 67 | /// The printing options. 68 | public static void ToProtoString(this MessageDescriptor message, TextWriter writer, DynamicGrpcPrinterOptions? options = null) 69 | { 70 | ToProtoString(message, new DynamicGrpcPrinterContext(writer, options ?? new DynamicGrpcPrinterOptions())); 71 | } 72 | 73 | /// 74 | /// Prints the proto description of the specified to a string. 75 | /// 76 | /// The descriptor to print. 77 | /// The printing options. 78 | /// A proto description of the specified descriptor. 79 | public static string ToProtoString(this MessageDescriptor message, DynamicGrpcPrinterOptions? options = null) 80 | { 81 | var writer = new StringWriter(); 82 | ToProtoString(message, writer, options); 83 | return writer.ToString(); 84 | } 85 | 86 | /// 87 | /// Prints the proto description of the specified to a writer. 88 | /// 89 | /// The descriptor to print. 90 | /// The text writer. 91 | /// The printing options. 92 | public static void ToProtoString(this EnumDescriptor enumDesc, TextWriter writer, DynamicGrpcPrinterOptions? options = null) 93 | { 94 | ToProtoString(enumDesc, new DynamicGrpcPrinterContext(writer, options ?? new DynamicGrpcPrinterOptions())); 95 | } 96 | 97 | /// 98 | /// Prints the proto description of the specified to a string. 99 | /// 100 | /// The descriptor to print. 101 | /// The printing options. 102 | /// A proto description of the specified descriptor. 103 | public static string ToProtoString(this EnumDescriptor enumDesc, DynamicGrpcPrinterOptions? options = null) 104 | { 105 | var writer = new StringWriter(); 106 | ToProtoString(enumDesc, writer, options); 107 | return writer.ToString(); 108 | } 109 | 110 | 111 | private static string GetEnumName(Enum enumValue, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type enumType) 112 | { 113 | foreach (var field in enumType.GetFields(BindingFlags.Static | BindingFlags.Public)) 114 | { 115 | if (enumValue.Equals(field.GetValue(null))) 116 | { 117 | var originalNameAttribute = field.GetCustomAttribute(); 118 | if (originalNameAttribute != null) 119 | { 120 | return originalNameAttribute.Name; 121 | } 122 | } 123 | } 124 | 125 | return enumType.ToString(); 126 | } 127 | 128 | private static void ToProtoString(this FileDescriptor file, DynamicGrpcPrinterContext context) 129 | { 130 | if (context.Options.AddMetaComments) 131 | { 132 | context.WriteLine($"// {file.Name} is a proto file."); 133 | } 134 | 135 | bool requiresNewLine = false; 136 | // Write syntax 137 | switch (file.Syntax) 138 | { 139 | case Syntax.Proto2: 140 | context.WriteLine("syntax = \"proto2\";"); 141 | requiresNewLine = true; 142 | break; 143 | case Syntax.Proto3: 144 | context.WriteLine("syntax = \"proto3\";"); 145 | requiresNewLine = true; 146 | break; 147 | } 148 | 149 | var options = file.GetOptions(); 150 | if (options != null) 151 | { 152 | ToProtoString(options, context); 153 | } 154 | 155 | // Dump package 156 | if (requiresNewLine) context.WriteLine(); 157 | requiresNewLine = false; 158 | if (!string.IsNullOrWhiteSpace(file.Package)) 159 | { 160 | context.WriteLine($"package {file.Package};"); 161 | requiresNewLine = true; 162 | } 163 | context.PushContextName(file.Package); 164 | 165 | // Dump imports 166 | if (requiresNewLine) context.WriteLine(); 167 | requiresNewLine = false; 168 | foreach (var import in file.Dependencies) 169 | { 170 | context.WriteLine($"import \"{import.Name}\""); 171 | requiresNewLine = true; 172 | } 173 | 174 | // Dump services 175 | if (requiresNewLine) context.WriteLine(); 176 | requiresNewLine = false; 177 | foreach (var serviceDescriptor in file.Services) 178 | { 179 | ToProtoString(serviceDescriptor, context); 180 | context.WriteLine(); 181 | } 182 | 183 | // Dump message types 184 | foreach (var messageDescriptor in file.MessageTypes) 185 | { 186 | ToProtoString(messageDescriptor, context); 187 | context.WriteLine(); 188 | } 189 | 190 | // Dump message types 191 | foreach (var enumDescriptor in file.EnumTypes) 192 | { 193 | ToProtoString(enumDescriptor, context); 194 | context.WriteLine(); 195 | } 196 | 197 | context.PopContextName(file.Package); 198 | } 199 | 200 | private static void ToProtoString(this ServiceDescriptor service, DynamicGrpcPrinterContext context) 201 | { 202 | if (context.Options.AddMetaComments) 203 | { 204 | context.WriteLine($"// {service.FullName} is a service:"); 205 | } 206 | context.WriteLine($"service {service.Name} {{"); 207 | context.PushContextName(service.Name); 208 | context.Indent(); 209 | foreach (var method in service.Methods) 210 | { 211 | context.WriteLine($"rpc {method.Name} ( {context.GetTypeName(method.InputType)} ) returns ( {context.GetTypeName(method.OutputType)} );"); 212 | } 213 | context.UnIndent(); 214 | context.PopContextName(service.Name); 215 | context.WriteLine("}"); 216 | } 217 | 218 | private static void ToProtoString(this MessageDescriptor message, DynamicGrpcPrinterContext context) 219 | { 220 | bool isEmpty = message.Fields.InDeclarationOrder().Count == 0 && message.NestedTypes.Count == 0 && message.EnumTypes.Count == 0 && message.GetOptions() == null; 221 | 222 | if (context.Options.AddMetaComments) 223 | { 224 | context.WriteLine($"// {message.FullName} is {(isEmpty?"an empty":"a")} message:"); 225 | } 226 | 227 | // Compact form, if a message is empty, output a single line 228 | if (isEmpty) 229 | { 230 | context.WriteLine($"message {message.Name} {{}}"); 231 | return; 232 | } 233 | 234 | context.WriteLine($"message {message.Name} {{"); 235 | context.PushContextName(message.Name); 236 | context.Indent(); 237 | 238 | // handle options 239 | var options = message.GetOptions(); 240 | if (options != null) 241 | { 242 | // message_set_wire_format 243 | if (options.HasMessageSetWireFormat) context.WriteLine($"option message_set_wire_format = {options.MessageSetWireFormat.Bool()};"); 244 | // no_standard_descriptor_accessor 245 | if (options.HasNoStandardDescriptorAccessor) context.WriteLine($"option no_standard_descriptor_accessor = {options.NoStandardDescriptorAccessor.Bool()};"); 246 | // deprecated 247 | if (options.HasDeprecated) context.WriteLine($"option deprecated = {options.Deprecated.Bool()};"); 248 | // map_entry 249 | if (options.HasMapEntry) context.WriteLine($"option map_entry = {options.MapEntry.Bool()};"); 250 | } 251 | 252 | bool requiresNewLine = false; 253 | OneofDescriptor? currentOneOf = null; 254 | foreach (var field in message.Fields.InDeclarationOrder()) 255 | { 256 | var oneof = field.RealContainingOneof; 257 | if (currentOneOf != oneof) 258 | { 259 | if (currentOneOf is not null) 260 | { 261 | context.UnIndent(); 262 | context.WriteLine("}"); 263 | } 264 | 265 | if (oneof is not null) 266 | { 267 | context.WriteLine($"oneof {oneof.Name} {{"); 268 | context.Indent(); 269 | } 270 | } 271 | currentOneOf = oneof; 272 | 273 | // handle options 274 | var fieldOptions = field.GetOptions(); 275 | var fieldOptionsAsText = string.Empty; 276 | if (fieldOptions != null) 277 | { 278 | var fieldOptionList = new List(); 279 | 280 | // ctype 281 | if (fieldOptions.HasCtype) fieldOptionList.Add($"ctype = {GetEnumName(fieldOptions.Ctype, typeof(FieldOptions.Types.CType))}"); 282 | // packed 283 | if (fieldOptions.HasPacked) fieldOptionList.Add($"packed = {fieldOptions.Packed.Bool()}"); 284 | // jstype 285 | if (fieldOptions.HasJstype) fieldOptionList.Add($"jstype = {GetEnumName(fieldOptions.Jstype, typeof(FieldOptions.Types.JSType))}"); 286 | // lazy 287 | if (fieldOptions.HasLazy) fieldOptionList.Add($"lazy = {fieldOptions.Lazy.Bool()}"); 288 | // deprecated 289 | if (fieldOptions.Deprecated) fieldOptionList.Add($"deprecated = {fieldOptions.Deprecated.Bool()}"); 290 | // weak 291 | if (fieldOptions.HasWeak) context.WriteLine($"weak = {fieldOptions.Weak.Bool()}"); 292 | 293 | if (fieldOptionList.Count > 0) 294 | { 295 | fieldOptionsAsText = $" [ {string.Join(", ", fieldOptionList)} ]"; 296 | } 297 | } 298 | 299 | if (fieldOptions is { Deprecated: true }) 300 | { 301 | fieldOptionsAsText = " [ deprecated = true ]"; 302 | } 303 | 304 | context.WriteLine($"{context.GetTypeName(field)} {field.Name} = {field.FieldNumber}{fieldOptionsAsText};"); 305 | requiresNewLine = true; 306 | } 307 | 308 | if (currentOneOf is not null) 309 | { 310 | context.UnIndent(); 311 | context.WriteLine("}"); 312 | } 313 | 314 | if (message.NestedTypes.Count > 0) 315 | { 316 | if (requiresNewLine) context.WriteLine(); 317 | requiresNewLine = false; 318 | for (var index = 0; index < message.NestedTypes.Count; index++) 319 | { 320 | var nestedMessageType = message.NestedTypes[index]; 321 | ToProtoString(nestedMessageType, context); 322 | 323 | // Don't output a trailing \n for the last entry 324 | if (message.EnumTypes.Count > 0 || index + 1 < message.EnumTypes.Count) 325 | { 326 | context.WriteLine(); 327 | } 328 | } 329 | } 330 | 331 | if (message.EnumTypes.Count > 0) 332 | { 333 | if (requiresNewLine) context.WriteLine(); 334 | for (var index = 0; index < message.EnumTypes.Count; index++) 335 | { 336 | var enumDescriptor = message.EnumTypes[index]; 337 | ToProtoString(enumDescriptor, context); 338 | 339 | // Don't output a trailing \n for the last entry 340 | if (index + 1 < message.EnumTypes.Count) 341 | { 342 | context.WriteLine(); 343 | } 344 | } 345 | } 346 | 347 | context.UnIndent(); 348 | context.PopContextName(message.Name); 349 | context.WriteLine("}"); 350 | } 351 | 352 | private static string Bool(this bool value) => value ? "true" : "false"; 353 | 354 | private static void ToProtoString(this EnumDescriptor enumDescriptor, DynamicGrpcPrinterContext context) 355 | { 356 | if (context.Options.AddMetaComments) 357 | { 358 | context.WriteLine($"// {enumDescriptor.FullName} is an enum:"); 359 | } 360 | context.WriteLine($"enum {enumDescriptor.Name} {{"); 361 | context.Indent(); 362 | foreach (var item in enumDescriptor.Values) 363 | { 364 | context.WriteLine($"{item.Name} = {item.Number};"); 365 | } 366 | context.UnIndent(); 367 | context.WriteLine("}"); 368 | } 369 | 370 | private static void ToProtoString(FileOptions options, DynamicGrpcPrinterContext context) 371 | { 372 | // java_package 373 | if (options.HasJavaPackage) context.WriteLine($"option java_package = \"{options.JavaPackage}\";"); 374 | // java_outer_classname 375 | if (options.HasJavaOuterClassname) context.WriteLine($"option java_outer_classname = \"{options.JavaOuterClassname}\";"); 376 | // java_multiple_files 377 | if (options.HasJavaMultipleFiles) context.WriteLine($"option java_multiple_files = {options.JavaMultipleFiles.Bool()};"); 378 | // java_generate_equals_and_hash 379 | #pragma warning disable CS0612 // Type or member is obsolete 380 | if (options.HasJavaGenerateEqualsAndHash) context.WriteLine($"option java_generate_equals_and_hash = {options.JavaGenerateEqualsAndHash.Bool()};"); 381 | #pragma warning restore CS0612 // Type or member is obsolete 382 | // java_string_check_utf8 383 | if (options.HasJavaStringCheckUtf8) context.WriteLine($"option java_string_check_utf8 = {options.JavaStringCheckUtf8.Bool()};"); 384 | // optimize_for 385 | if (options.HasOptimizeFor) context.WriteLine($"option optimize_for = {GetEnumName(options.OptimizeFor, typeof(FileOptions.Types.OptimizeMode))};"); 386 | // go_package 387 | if (options.HasJavaMultipleFiles) context.WriteLine($"option go_package = {options.JavaMultipleFiles.Bool()};"); 388 | // cc_generic_services 389 | if (options.HasCcGenericServices) context.WriteLine($"option cc_generic_services = {options.CcGenericServices.Bool()};"); 390 | // java_generic_services 391 | if (options.HasJavaGenericServices) context.WriteLine($"option java_generic_services = {options.JavaGenericServices.Bool()};"); 392 | // py_generic_services 393 | if (options.HasPyGenericServices) context.WriteLine($"option py_generic_services = {options.PyGenericServices.Bool()};"); 394 | // php_generic_services 395 | if (options.HasPhpGenericServices) context.WriteLine($"option php_generic_services = {options.PhpGenericServices.Bool()};"); 396 | // deprecated 397 | if (options.HasDeprecated) context.WriteLine($"option deprecated = {options.Deprecated.Bool()};"); 398 | // cc_enable_arenas 399 | if (options.HasCcEnableArenas) context.WriteLine($"option cc_enable_arenas = {options.CcEnableArenas.Bool()};"); 400 | // objc_class_prefix 401 | if (options.HasObjcClassPrefix) context.WriteLine($"option objc_class_prefix = \"{options.ObjcClassPrefix}\";"); 402 | // csharp_namespace 403 | if (options.HasCsharpNamespace) context.WriteLine($"option csharp_namespace = \"{options.CsharpNamespace}\";"); 404 | // swift_prefix 405 | if (options.HasSwiftPrefix) context.WriteLine($"option swift_prefix = \"{options.SwiftPrefix}\";"); 406 | // php_class_prefix 407 | if (options.HasPhpClassPrefix) context.WriteLine($"option php_class_prefix = \"{options.PhpClassPrefix}\";"); 408 | // php_namespace 409 | if (options.HasPhpNamespace) context.WriteLine($"option php_namespace = \"{options.PhpNamespace}\";"); 410 | // php_metadata_namespace 411 | if (options.HasPhpMetadataNamespace) context.WriteLine($"option php_metadata_namespace = \"{options.PhpMetadataNamespace}\";"); 412 | // ruby_package 413 | if (options.HasRubyPackage) context.WriteLine($"option ruby_package = \"{options.RubyPackage}\";"); 414 | } 415 | 416 | private class DynamicGrpcPrinterContext 417 | { 418 | private readonly List _contextNames; 419 | 420 | public DynamicGrpcPrinterContext(TextWriter writer, DynamicGrpcPrinterOptions options) 421 | { 422 | _contextNames = new List(); 423 | Writer = writer; 424 | Options = options; 425 | } 426 | 427 | public DynamicGrpcPrinterOptions Options { get; } 428 | 429 | public int Level { get; set; } 430 | 431 | public void Indent() => Level++; 432 | 433 | public void UnIndent() => Level--; 434 | 435 | public TextWriter Writer { get; } 436 | 437 | public void WriteLine() 438 | { 439 | Writer.WriteLine(); 440 | } 441 | 442 | public void WriteLine(string text) 443 | { 444 | WriteIndent(); 445 | Writer.WriteLine(text); 446 | } 447 | 448 | private void WriteIndent() 449 | { 450 | var indent = Options.Indent; 451 | for (int i = 0; i < Level; i++) 452 | { 453 | Writer.Write(indent); 454 | } 455 | } 456 | 457 | public void PushContextName(string name) 458 | { 459 | if (string.IsNullOrEmpty(name)) return; 460 | foreach (var partName in name.Split('.')) 461 | { 462 | _contextNames.Add($"{partName}."); 463 | } 464 | } 465 | 466 | public void PopContextName(string name) 467 | { 468 | if (string.IsNullOrEmpty(name)) return; 469 | foreach (var _ in name.Split('.')) 470 | { 471 | _contextNames.RemoveAt(_contextNames.Count - 1); 472 | } 473 | } 474 | 475 | public string GetTypeName(MessageDescriptor descriptor) 476 | { 477 | return GetContextualTypeName(descriptor.FullName); 478 | } 479 | 480 | public string GetTypeName(FieldDescriptor field) 481 | { 482 | if (field.IsMap) 483 | { 484 | var subFields = field.MessageType.Fields.InFieldNumberOrder(); 485 | return $"map<{GetTypeName(subFields[0])}, {GetTypeName(subFields[1])}>"; 486 | } 487 | 488 | var builder = new StringBuilder(); 489 | if (field.IsRequired) builder.Append("required "); 490 | var options = field.GetOptions(); 491 | if (options == null) 492 | { 493 | if (field.File.Syntax == Syntax.Proto3) 494 | { 495 | if (field.IsRepeated) builder.Append("repeated "); 496 | } 497 | } 498 | else 499 | { 500 | if (field.File.Syntax != Syntax.Proto3 && field.IsPacked) builder.Append("packed "); 501 | if (field.IsRepeated) builder.Append("repeated "); 502 | } 503 | 504 | switch (field.FieldType) 505 | { 506 | case FieldType.Double: 507 | builder.Append("double"); 508 | break; 509 | case FieldType.Float: 510 | builder.Append("float"); 511 | break; 512 | case FieldType.Int64: 513 | builder.Append("int64"); 514 | break; 515 | case FieldType.UInt64: 516 | builder.Append("uint64"); 517 | break; 518 | case FieldType.Int32: 519 | builder.Append("int32"); 520 | break; 521 | case FieldType.Fixed64: 522 | builder.Append("fixed64"); 523 | break; 524 | case FieldType.Fixed32: 525 | builder.Append("fixed32"); 526 | break; 527 | case FieldType.Bool: 528 | builder.Append("bool"); 529 | break; 530 | case FieldType.String: 531 | builder.Append("string"); 532 | break; 533 | case FieldType.Group: 534 | break; 535 | case FieldType.Message: 536 | builder.Append(GetContextualTypeName(field.MessageType.FullName)); 537 | break; 538 | case FieldType.Bytes: 539 | builder.Append("bytes"); 540 | break; 541 | case FieldType.UInt32: 542 | builder.Append("uint32"); 543 | break; 544 | case FieldType.SFixed32: 545 | builder.Append("sfixed32"); 546 | break; 547 | case FieldType.SFixed64: 548 | builder.Append("sfixed64"); 549 | break; 550 | case FieldType.SInt32: 551 | builder.Append("sint32"); 552 | break; 553 | case FieldType.SInt64: 554 | builder.Append("sint64"); 555 | break; 556 | case FieldType.Enum: 557 | builder.Append(GetContextualTypeName(field.EnumType.FullName)); 558 | break; 559 | default: 560 | throw new ArgumentOutOfRangeException(); 561 | } 562 | 563 | return builder.ToString(); 564 | } 565 | 566 | private string GetContextualTypeName(string fullTypeName) 567 | { 568 | if (Options.FullyQualified) return $".{fullTypeName}"; 569 | 570 | int nextIndex = 0; 571 | foreach (var partName in _contextNames) 572 | { 573 | var currentIndex = fullTypeName.IndexOf(partName, nextIndex, StringComparison.OrdinalIgnoreCase); 574 | if (currentIndex != nextIndex) 575 | { 576 | break; 577 | } 578 | 579 | nextIndex = currentIndex + partName.Length; 580 | } 581 | 582 | return nextIndex > 0 ? fullTypeName.Substring(nextIndex) : $".{fullTypeName}"; 583 | } 584 | } 585 | } -------------------------------------------------------------------------------- /src/DynamicGrpc/DynamicGrpcPrinterOptions.cs: -------------------------------------------------------------------------------- 1 | namespace DynamicGrpc; 2 | 3 | public class DynamicGrpcPrinterOptions 4 | { 5 | internal static readonly DynamicGrpcPrinterOptions Default = new DynamicGrpcPrinterOptions(); 6 | 7 | public DynamicGrpcPrinterOptions() 8 | { 9 | Indent = " "; 10 | } 11 | 12 | public bool AddMetaComments { get; set; } 13 | 14 | public bool FullyQualified { get; set; } 15 | 16 | public string Indent { get; set; } 17 | } -------------------------------------------------------------------------------- /src/DynamicGrpc/DynamicMessageSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Diagnostics; 3 | using Google.Protobuf; 4 | using Google.Protobuf.Collections; 5 | using Google.Protobuf.Reflection; 6 | using Grpc.Core; 7 | 8 | namespace DynamicGrpc; 9 | 10 | /// 11 | /// Internal class used to serialize/deserialize a message. 12 | /// 13 | internal sealed class DynamicMessageSerializer 14 | { 15 | private readonly Dictionary _nameToField; 16 | private readonly Dictionary _jsonNameToField; 17 | private readonly Dictionary _tagToField; 18 | 19 | internal DynamicMessageSerializer(DynamicFileDescriptorSet descriptorSet, MessageDescriptor messageDescriptor) 20 | { 21 | DescriptorSet = descriptorSet; 22 | _tagToField = new Dictionary(); 23 | _nameToField = new Dictionary(); 24 | _jsonNameToField = new Dictionary(); 25 | Descriptor = messageDescriptor; 26 | Initialize(); 27 | } 28 | 29 | public DynamicFileDescriptorSet DescriptorSet { get; } 30 | 31 | public MessageDescriptor Descriptor { get; } 32 | 33 | private void Initialize() 34 | { 35 | var fields = Descriptor.Fields; 36 | 37 | foreach (var field in fields.InDeclarationOrder()) 38 | { 39 | var tag = GetTagForField(field); 40 | _tagToField.Add(tag, field); 41 | _nameToField.Add(field.Name, field); 42 | _jsonNameToField.Add(field.JsonName, field); 43 | } 44 | } 45 | 46 | internal Marshaller> GetMarshaller(DynamicGrpcClientContext context) 47 | { 48 | var parser = new MessageParser(() => 49 | { 50 | var value = context.Factory(); 51 | return new DynamicMessage(this, value, context); 52 | }); 53 | 54 | Func> deserializer = (ctx) => 55 | { 56 | var message = parser.ParseFrom(ctx.PayloadAsReadOnlySequence()); 57 | return message.Value; 58 | }; 59 | 60 | Action, SerializationContext> serializer = (value, ctx) => 61 | { 62 | var writer = ctx.GetBufferWriter(); 63 | var message = new DynamicMessage(this, value, context); 64 | message.WriteTo(writer); 65 | ctx.Complete(); 66 | }; 67 | 68 | return new Marshaller>(serializer, deserializer); 69 | } 70 | 71 | private static uint GetTagForField(FieldDescriptor field) 72 | { 73 | var isRepeatedPacked = IsRepeatedPacked(field); 74 | var wireType = GetWireType(field.FieldType, isRepeatedPacked); 75 | var tag = WireFormat.MakeTag(field.FieldNumber, wireType); 76 | return tag; 77 | } 78 | 79 | public IDictionary ReadFrom(ref ParseContext input, DynamicGrpcClientContext context) 80 | { 81 | var result = context.Factory(); 82 | 83 | while (true) 84 | { 85 | var tag = context.ReadTag(ref input); 86 | if (tag == 0) break; 87 | 88 | if (!_tagToField.ContainsKey(tag)) 89 | { 90 | throw new DynamicGrpcClientException($"Invalid tag 0x{tag:x8} received when deserializing message `{Descriptor.FullName}`."); 91 | } 92 | 93 | var fieldDescriptor = _tagToField[tag]; 94 | 95 | object value; 96 | 97 | if (fieldDescriptor.IsMap) 98 | { 99 | var map = new Dictionary(); 100 | 101 | var fieldMessageType = fieldDescriptor.MessageType; 102 | var keyField = fieldMessageType.Fields.InDeclarationOrder()[0]; 103 | var keyTag = GetTagForField(keyField); 104 | var valueField = fieldMessageType.Fields.InDeclarationOrder()[1]; 105 | var valueTag = GetTagForField(valueField); 106 | var keyDefaultValue = DefaultValueHelper.GetDefaultValue(keyField.FieldType); 107 | var valueDefaultValue = DefaultValueHelper.GetDefaultValue(valueField.FieldType); 108 | 109 | bool isFirst = true; 110 | while (true) 111 | { 112 | if (!isFirst) 113 | { 114 | var nextTag = context.PeekTak(ref input); 115 | 116 | // If the next tag is not the one that we expect, queue it to make it available for the next ReadTag 117 | if (nextTag != tag) 118 | { 119 | break; 120 | } 121 | 122 | context.SkipTag(ref input); 123 | } 124 | 125 | // Not used 126 | var length = input.ReadLength(); 127 | 128 | object? keyRead = keyDefaultValue; 129 | object? valueRead = valueDefaultValue; 130 | 131 | // Read key 132 | var tagRead = context.PeekTak(ref input); 133 | if (tagRead == keyTag) 134 | { 135 | context.SkipTag(ref input); 136 | keyRead = ReadFieldValue(ref input, fieldMessageType, keyField, context); 137 | } 138 | else 139 | { 140 | // We didn't have a key, so key is default value, try next the value directly 141 | goto readValue; 142 | } 143 | 144 | // Read value 145 | tagRead = context.PeekTak(ref input); 146 | readValue: 147 | 148 | if (tagRead == valueTag) 149 | { 150 | context.SkipTag(ref input); 151 | valueRead = ReadFieldValue(ref input, fieldMessageType, valueField, context); 152 | } 153 | 154 | Debug.Assert(keyRead != null); 155 | Debug.Assert(valueRead != null); 156 | map[keyRead!] = valueRead!; 157 | isFirst = false; 158 | } 159 | 160 | value = map; 161 | } 162 | else if (fieldDescriptor.IsRepeated) 163 | { 164 | switch (fieldDescriptor.FieldType) 165 | { 166 | case FieldType.Double: 167 | { 168 | var repeated = new RepeatedField(); 169 | repeated.AddEntriesFrom(ref input, FieldCodec.ForDouble(tag)); 170 | value = new List(repeated); 171 | } 172 | break; 173 | case FieldType.Float: 174 | { 175 | var repeated = new RepeatedField(); 176 | repeated.AddEntriesFrom(ref input, FieldCodec.ForFloat(tag)); 177 | value = new List(repeated); 178 | } 179 | break; 180 | case FieldType.Int64: 181 | { 182 | var repeated = new RepeatedField(); 183 | repeated.AddEntriesFrom(ref input, FieldCodec.ForInt64(tag)); 184 | value = new List(repeated); 185 | } 186 | break; 187 | case FieldType.UInt64: 188 | { 189 | var repeated = new RepeatedField(); 190 | repeated.AddEntriesFrom(ref input, FieldCodec.ForUInt64(tag)); 191 | value = new List(repeated); 192 | } 193 | break; 194 | case FieldType.Int32: 195 | { 196 | var repeated = new RepeatedField(); 197 | repeated.AddEntriesFrom(ref input, FieldCodec.ForInt32(tag)); 198 | value = new List(repeated); 199 | } 200 | break; 201 | case FieldType.Fixed64: 202 | { 203 | var repeated = new RepeatedField(); 204 | repeated.AddEntriesFrom(ref input, FieldCodec.ForFixed64(tag)); 205 | value = new List(repeated); 206 | } 207 | break; 208 | case FieldType.Fixed32: 209 | { 210 | var repeated = new RepeatedField(); 211 | repeated.AddEntriesFrom(ref input, FieldCodec.ForFixed32(tag)); 212 | value = new List(repeated); 213 | } 214 | break; 215 | case FieldType.Bool: 216 | { 217 | var repeated = new RepeatedField(); 218 | repeated.AddEntriesFrom(ref input, FieldCodec.ForBool(tag)); 219 | value = new List(repeated); 220 | } 221 | break; 222 | case FieldType.String: 223 | { 224 | var repeated = new RepeatedField(); 225 | repeated.AddEntriesFrom(ref input, FieldCodec.ForString(tag)); 226 | value = new List(repeated); 227 | } 228 | break; 229 | case FieldType.Group: 230 | { 231 | var endTag = GetGroupEndTag(tag); 232 | var descriptorForRepeatedMessage = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 233 | var parser = new MessageParser(() => new DynamicMessage(descriptorForRepeatedMessage, context.Factory(), context)); 234 | var repeated = new RepeatedField(); 235 | repeated.AddEntriesFrom(ref input, FieldCodec.ForGroup(tag, endTag, parser)); 236 | var dict = new List>(repeated.Count); 237 | foreach (var item in repeated) 238 | { 239 | dict.Add(item.Value); 240 | } 241 | 242 | value = dict; 243 | } 244 | break; 245 | case FieldType.Message: 246 | { 247 | var descriptorForRepeatedMessage = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 248 | var parser = new MessageParser(() => new DynamicMessage(descriptorForRepeatedMessage, context.Factory(), context)); 249 | var repeated = new RepeatedField(); 250 | repeated.AddEntriesFrom(ref input, FieldCodec.ForMessage(tag, parser)); 251 | var dict = new List>(repeated.Count); 252 | foreach (var item in repeated) 253 | { 254 | dict.Add(item.Value); 255 | } 256 | 257 | value = dict; 258 | } 259 | break; 260 | case FieldType.Bytes: 261 | { 262 | var repeated = new RepeatedField(); 263 | repeated.AddEntriesFrom(ref input, FieldCodec.ForBytes(tag)); 264 | var list = new List(); 265 | foreach (var item in repeated) 266 | { 267 | list.Add(item.ToByteArray()); 268 | } 269 | 270 | value = list; 271 | } 272 | break; 273 | case FieldType.UInt32: 274 | { 275 | var repeated = new RepeatedField(); 276 | repeated.AddEntriesFrom(ref input, FieldCodec.ForUInt32(tag)); 277 | value = new List(repeated); 278 | } 279 | break; 280 | case FieldType.SFixed32: 281 | { 282 | var repeated = new RepeatedField(); 283 | repeated.AddEntriesFrom(ref input, FieldCodec.ForSFixed32(tag)); 284 | value = new List(repeated); 285 | } 286 | break; 287 | case FieldType.SFixed64: 288 | { 289 | var repeated = new RepeatedField(); 290 | repeated.AddEntriesFrom(ref input, FieldCodec.ForSFixed64(tag)); 291 | value = new List(repeated); 292 | } 293 | break; 294 | case FieldType.SInt32: 295 | { 296 | var repeated = new RepeatedField(); 297 | repeated.AddEntriesFrom(ref input, FieldCodec.ForSInt32(tag)); 298 | value = new List(repeated); 299 | } 300 | break; 301 | case FieldType.SInt64: 302 | { 303 | var repeated = new RepeatedField(); 304 | repeated.AddEntriesFrom(ref input, FieldCodec.ForSInt64(tag)); 305 | value = new List(repeated); 306 | } 307 | break; 308 | case FieldType.Enum: 309 | { 310 | var enumType = fieldDescriptor.EnumType; 311 | var repeated = new RepeatedField(); 312 | repeated.AddEntriesFrom(ref input, FieldCodec.ForEnum(tag, null, i => i)); 313 | if (context.UseNumberedEnums) 314 | { 315 | value = new List(repeated); 316 | } 317 | else 318 | { 319 | var listString = new List(repeated.Count); 320 | foreach (var i in repeated) 321 | { 322 | listString.Add(enumType.FindValueByNumber(i).Name); 323 | } 324 | value = listString; 325 | } 326 | } 327 | break; 328 | default: 329 | throw new ArgumentOutOfRangeException(); 330 | } 331 | } 332 | else 333 | { 334 | value = ReadFieldValue(ref input, Descriptor, fieldDescriptor, context); 335 | } 336 | 337 | result.Add(context.UseJsonNaming ? fieldDescriptor.JsonName: fieldDescriptor.Name, value); 338 | } 339 | 340 | // Add default fields 341 | foreach (var fieldDescriptor in Descriptor.Fields.InDeclarationOrder()) 342 | { 343 | var name = context.UseJsonNaming ? fieldDescriptor.JsonName : fieldDescriptor.Name; 344 | var defaultValue = DefaultValueHelper.GetDefaultValue(fieldDescriptor.FieldType); 345 | if (defaultValue is not null && !result.ContainsKey(name)) 346 | { 347 | if (fieldDescriptor.FieldType == FieldType.Enum && !context.UseNumberedEnums) 348 | { 349 | defaultValue = fieldDescriptor.EnumType.FindValueByNumber(0).Name; 350 | } 351 | result[name] = defaultValue; 352 | 353 | } 354 | } 355 | 356 | // Special case for any, we are converting them on the fly 357 | if (Descriptor.FullName == DynamicAnyExtensions.GoogleTypeAnyFullName) 358 | { 359 | // The input type is an any. Expecting the property @type to be part of it 360 | if (result.TryGetValue(DynamicAnyExtensions.GoogleTypeUrlKey, out var typeUrlObject) && typeUrlObject is string typeUrl) 361 | { 362 | var typeName = Google.Protobuf.WellKnownTypes.Any.GetTypeName(typeUrl); 363 | if (DescriptorSet.TryFindMessageDescriptorProto(typeName, out var serializer)) 364 | { 365 | if (!result.TryGetValue(DynamicAnyExtensions.GoogleValueKey, out var dataValueObject) || dataValueObject is not byte[] dataValue) 366 | { 367 | throw new DynamicGrpcClientException($"Invalid message for type `Any`. `{DynamicAnyExtensions.GoogleValueKey}` not found or with an invalid type."); 368 | } 369 | 370 | var newResult = context.Factory(); 371 | var stream = new CodedInputStream(dataValue); 372 | var message = new DynamicMessage(serializer, newResult, context); 373 | stream.ReadRawMessage(message); 374 | newResult = message.Value; 375 | newResult[DynamicAnyExtensions.TypeKey] = typeUrl; 376 | result = newResult; 377 | } 378 | else 379 | { 380 | throw new DynamicGrpcClientException($"Invalid message type name `{typeName}` not found for the type `Any`."); 381 | } 382 | } 383 | else 384 | { 385 | throw new DynamicGrpcClientException($"Expecting the type `Any` but the input dictionary doesn't contain a `{DynamicAnyExtensions.GoogleTypeUrlKey}` property."); 386 | } 387 | } 388 | 389 | return result; 390 | } 391 | 392 | private static uint GetGroupEndTag(uint tag) 393 | { 394 | Debug.Assert(WireFormat.GetTagWireType(tag) == WireFormat.WireType.StartGroup); 395 | return WireFormat.MakeTag(WireFormat.GetTagFieldNumber(tag), WireFormat.WireType.EndGroup); 396 | } 397 | 398 | private object ReadFieldValue(ref ParseContext input, MessageDescriptor parentDescriptor, FieldDescriptor fieldDescriptor, DynamicGrpcClientContext context) 399 | { 400 | object value; 401 | switch (fieldDescriptor.FieldType) 402 | { 403 | case FieldType.Double: 404 | value = input.ReadDouble(); 405 | break; 406 | case FieldType.Float: 407 | value = input.ReadFloat(); 408 | break; 409 | case FieldType.Int64: 410 | value = input.ReadInt64(); 411 | break; 412 | case FieldType.UInt64: 413 | value = input.ReadUInt64(); 414 | break; 415 | case FieldType.Int32: 416 | value = input.ReadInt32(); 417 | break; 418 | case FieldType.Fixed64: 419 | value = input.ReadFixed64(); 420 | break; 421 | case FieldType.Fixed32: 422 | value = input.ReadFixed32(); 423 | break; 424 | case FieldType.Bool: 425 | value = input.ReadBool(); 426 | break; 427 | case FieldType.String: 428 | value = input.ReadString(); 429 | break; 430 | case FieldType.Group: 431 | { 432 | var descriptor = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 433 | var message = new DynamicMessage(descriptor, context.Factory(), context); 434 | input.ReadGroup(message); 435 | value = message.Value; 436 | } 437 | break; 438 | case FieldType.Message: 439 | { 440 | var descriptor = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 441 | var message = new DynamicMessage(descriptor, context.Factory(), context); 442 | input.ReadMessage(message); 443 | value = message.Value; 444 | } 445 | break; 446 | case FieldType.Bytes: 447 | value = input.ReadBytes().ToByteArray(); 448 | break; 449 | case FieldType.UInt32: 450 | value = input.ReadUInt32(); 451 | break; 452 | case FieldType.SFixed32: 453 | value = input.ReadSFixed32(); 454 | break; 455 | case FieldType.SFixed64: 456 | value = input.ReadSFixed64(); 457 | break; 458 | case FieldType.SInt32: 459 | value = input.ReadSInt32(); 460 | break; 461 | case FieldType.SInt64: 462 | value = input.ReadSInt64(); 463 | break; 464 | case FieldType.Enum: 465 | if (context.UseNumberedEnums) 466 | { 467 | value = input.ReadEnum(); 468 | } 469 | else 470 | { 471 | var number = input.ReadEnum(); 472 | value = fieldDescriptor.EnumType.FindValueByNumber(number).Name; 473 | } 474 | 475 | break; 476 | default: 477 | throw new ArgumentOutOfRangeException(); 478 | } 479 | 480 | return value; 481 | } 482 | 483 | /// 484 | /// return true if the field is a repeated packed field, false otherwise. 485 | /// 486 | public static bool IsRepeatedPacked(FieldDescriptor field) 487 | { 488 | // Workaround as field.IsPacked will throw when GetOptions() is null 489 | bool isRepeatedPacked = false; 490 | var options = field.GetOptions(); 491 | if (options == null) 492 | { 493 | if (field.File.Syntax == Syntax.Proto3) 494 | { 495 | isRepeatedPacked = field.IsRepeated; 496 | } 497 | } 498 | else 499 | { 500 | isRepeatedPacked = field.IsRepeated && field.IsPacked; 501 | } 502 | 503 | return isRepeatedPacked; 504 | } 505 | 506 | public void WriteTo(IDictionary value, ref WriteContext output, DynamicGrpcClientContext context) 507 | { 508 | var nameToField = context.UseJsonNaming ? _jsonNameToField : _nameToField; 509 | // Special case for any, we are converting them on the fly 510 | value = FilterAny(value, context); 511 | 512 | foreach (var keyValue in value) 513 | { 514 | if (!nameToField.TryGetValue(keyValue.Key, out var fieldDescriptor)) 515 | { 516 | throw new DynamicGrpcClientException($"Field `{keyValue.Key}` not found in message type `{Descriptor.FullName}`."); 517 | } 518 | 519 | var tag = GetTagForField(fieldDescriptor); 520 | 521 | if (fieldDescriptor.IsMap) 522 | { 523 | if (keyValue.Value is not IDictionary values) 524 | { 525 | throw new DynamicGrpcClientException($"Repeated Field `{keyValue.Key}` is expecting an IDictionary type instead of {keyValue.Value?.GetType()?.FullName}."); 526 | } 527 | 528 | var fieldMessageType = fieldDescriptor.MessageType; 529 | var keyField = fieldMessageType.Fields.InDeclarationOrder()[0]; 530 | var keyTag = GetTagForField(keyField); 531 | var valueField = fieldMessageType.Fields.InDeclarationOrder()[1]; 532 | var valueTag = GetTagForField(valueField); 533 | 534 | var it = values.GetEnumerator(); 535 | while (it.MoveNext()) 536 | { 537 | var keyFromMap = it.Key; 538 | var valueFromMap = it.Value; 539 | output.WriteTag(tag); 540 | 541 | var length = 0; 542 | var isKeyDefaultValue = DefaultValueHelper.IsDefaultValue(keyField.FieldType, keyFromMap); 543 | if (!isKeyDefaultValue) 544 | { 545 | length += CodedOutputStream.ComputeRawVarint32Size(keyTag) + ComputeSimpleFieldSize(keyTag, fieldMessageType, keyField, "key", keyFromMap, context); 546 | } 547 | var isValueDefaultValue = DefaultValueHelper.IsDefaultValue(valueField.FieldType, valueFromMap); 548 | if (!isValueDefaultValue) 549 | { 550 | Debug.Assert(valueFromMap is not null); 551 | length += CodedOutputStream.ComputeRawVarint32Size(valueTag) + ComputeSimpleFieldSize(valueTag, fieldMessageType, valueField, "value", valueFromMap!, context); 552 | } 553 | output.WriteLength(length); 554 | 555 | if (!isKeyDefaultValue) 556 | { 557 | WriteFieldValue(keyTag, fieldMessageType, keyField, "key", keyFromMap, ref output, context); 558 | } 559 | 560 | if (!isValueDefaultValue) 561 | { 562 | Debug.Assert(valueFromMap is not null); 563 | WriteFieldValue(valueTag, fieldMessageType, valueField, "value", valueFromMap!, ref output, context); 564 | } 565 | } 566 | } 567 | else if (fieldDescriptor.IsRepeated) 568 | { 569 | if (keyValue.Value is not IEnumerable values) 570 | { 571 | throw new DynamicGrpcClientException($"Repeated Field `{keyValue.Key}` is expecting an IEnumerable type instead of {keyValue.Value?.GetType()?.FullName}."); 572 | } 573 | 574 | // TODO: inefficient copy here to RepeatedField. We should have access to FieldCodec internals 575 | // https://github.com/protocolbuffers/protobuf/issues/9432 576 | switch (fieldDescriptor.FieldType) 577 | { 578 | case FieldType.Double: 579 | { 580 | var repeated = new RepeatedField(); 581 | foreach(var item in values) repeated.Add(Convert.ToDouble(item)); 582 | repeated.WriteTo(ref output, FieldCodec.ForDouble(tag)); 583 | } 584 | break; 585 | case FieldType.Float: 586 | { 587 | var repeated = new RepeatedField(); 588 | foreach (var item in values) repeated.Add(Convert.ToSingle(item)); 589 | repeated.WriteTo(ref output, FieldCodec.ForFloat(tag)); 590 | } 591 | break; 592 | case FieldType.Int64: 593 | { 594 | var repeated = new RepeatedField(); 595 | foreach (var item in values) repeated.Add(Convert.ToInt64(item)); 596 | repeated.WriteTo(ref output, FieldCodec.ForInt64(tag)); 597 | } 598 | break; 599 | case FieldType.UInt64: 600 | { 601 | var repeated = new RepeatedField(); 602 | foreach (var item in values) repeated.Add(Convert.ToUInt64(item)); 603 | repeated.WriteTo(ref output, FieldCodec.ForUInt64(tag)); 604 | } 605 | break; 606 | case FieldType.Int32: 607 | { 608 | var repeated = new RepeatedField(); 609 | foreach (var item in values) repeated.Add(Convert.ToInt32(item)); 610 | repeated.WriteTo(ref output, FieldCodec.ForInt32(tag)); 611 | } 612 | break; 613 | case FieldType.Fixed64: 614 | { 615 | var repeated = new RepeatedField(); 616 | foreach (var item in values) repeated.Add(Convert.ToUInt64(item)); 617 | repeated.WriteTo(ref output, FieldCodec.ForFixed64(tag)); 618 | } 619 | break; 620 | case FieldType.Fixed32: 621 | { 622 | var repeated = new RepeatedField(); 623 | foreach (var item in values) repeated.Add(Convert.ToUInt32(item)); 624 | repeated.WriteTo(ref output, FieldCodec.ForFixed32(tag)); 625 | } 626 | break; 627 | case FieldType.Bool: 628 | { 629 | var repeated = new RepeatedField(); 630 | foreach (var item in values) repeated.Add(Convert.ToBoolean(item)); 631 | repeated.WriteTo(ref output, FieldCodec.ForBool(tag)); 632 | } 633 | break; 634 | case FieldType.String: 635 | { 636 | var repeated = new RepeatedField(); 637 | foreach (var item in values) repeated.Add(Convert.ToString(item)); 638 | repeated.WriteTo(ref output, FieldCodec.ForString(tag)); 639 | } 640 | break; 641 | case FieldType.Group: 642 | { 643 | var descriptorForRepeatedMessage = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 644 | var parser = new MessageParser(() => new DynamicMessage(descriptorForRepeatedMessage, context.Factory(), context)); 645 | var repeated = new RepeatedField(); 646 | foreach (var item in values) 647 | { 648 | repeated.Add(new DynamicMessage(descriptorForRepeatedMessage, (IDictionary)item, context)); 649 | } 650 | 651 | repeated.WriteTo(ref output, FieldCodec.ForGroup(tag, GetGroupEndTag(tag), parser)); 652 | } 653 | break; 654 | case FieldType.Message: 655 | { 656 | var descriptorForRepeatedMessage = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 657 | var parser = new MessageParser(() => new DynamicMessage(descriptorForRepeatedMessage, context.Factory(), context)); 658 | var repeated = new RepeatedField(); 659 | foreach (var item in values) 660 | { 661 | repeated.Add(new DynamicMessage(descriptorForRepeatedMessage, (IDictionary)item, context)); 662 | } 663 | 664 | repeated.WriteTo(ref output, FieldCodec.ForMessage(tag, parser)); 665 | } 666 | break; 667 | case FieldType.Bytes: 668 | { 669 | var repeated = new RepeatedField(); 670 | foreach (var item in values) repeated.Add(ByteString.CopyFrom((byte[])item)); 671 | repeated.WriteTo(ref output, FieldCodec.ForBytes(tag)); 672 | } 673 | break; 674 | case FieldType.UInt32: 675 | { 676 | var repeated = new RepeatedField(); 677 | foreach (var item in values) repeated.Add(Convert.ToUInt32(item)); 678 | repeated.WriteTo(ref output, FieldCodec.ForUInt32(tag)); 679 | } 680 | break; 681 | case FieldType.SFixed32: 682 | { 683 | var repeated = new RepeatedField(); 684 | foreach (var item in values) repeated.Add(Convert.ToInt32(item)); 685 | repeated.WriteTo(ref output, FieldCodec.ForSFixed32(tag)); 686 | } 687 | break; 688 | case FieldType.SFixed64: 689 | { 690 | var repeated = new RepeatedField(); 691 | foreach (var item in values) repeated.Add(Convert.ToInt64(item)); 692 | repeated.WriteTo(ref output, FieldCodec.ForSFixed64(tag)); 693 | } 694 | break; 695 | case FieldType.SInt32: 696 | { 697 | var repeated = new RepeatedField(); 698 | foreach (var item in values) repeated.Add(Convert.ToInt32(item)); 699 | repeated.WriteTo(ref output, FieldCodec.ForSInt32(tag)); 700 | } 701 | break; 702 | case FieldType.SInt64: 703 | { 704 | var repeated = new RepeatedField(); 705 | foreach (var item in values) repeated.Add(Convert.ToInt64(item)); 706 | repeated.WriteTo(ref output, FieldCodec.ForSInt64(tag)); 707 | } 708 | break; 709 | case FieldType.Enum: 710 | { 711 | var repeated = new RepeatedField(); 712 | foreach (var item in values) 713 | { 714 | var enumInputValue = item; 715 | if (enumInputValue is Enum realEnum) 716 | { 717 | enumInputValue = realEnum.ToString(); 718 | } 719 | 720 | var rawEnumValue = enumInputValue is string enumAsText ? fieldDescriptor.EnumType.FindValueByName(enumAsText).Number : Convert.ToInt32(enumInputValue); 721 | repeated.Add(rawEnumValue); 722 | } 723 | repeated.WriteTo(ref output, FieldCodec.ForEnum(tag, o => o, i => i, 0)); 724 | } 725 | break; 726 | default: 727 | throw new ArgumentOutOfRangeException(); 728 | } 729 | } 730 | else 731 | { 732 | WriteFieldValue(tag, Descriptor, fieldDescriptor, keyValue.Key, keyValue.Value, ref output, context); 733 | } 734 | } 735 | } 736 | 737 | private void WriteFieldValue(uint tag, MessageDescriptor parentDescriptor, FieldDescriptor fieldDescriptor, string keyName, object value, ref WriteContext output, DynamicGrpcClientContext context) 738 | { 739 | output.WriteTag(tag); 740 | switch (fieldDescriptor.FieldType) 741 | { 742 | case FieldType.Double: 743 | output.WriteDouble(Convert.ToDouble(value)); 744 | break; 745 | case FieldType.Float: 746 | output.WriteFloat(Convert.ToSingle(value)); 747 | break; 748 | case FieldType.Int64: 749 | output.WriteInt64(Convert.ToInt64(value)); 750 | break; 751 | case FieldType.UInt64: 752 | output.WriteUInt64(Convert.ToUInt64(value)); 753 | break; 754 | case FieldType.Int32: 755 | output.WriteInt32(Convert.ToInt32(value)); 756 | break; 757 | case FieldType.Fixed64: 758 | output.WriteFixed64(Convert.ToUInt64(value)); 759 | break; 760 | case FieldType.Fixed32: 761 | output.WriteFixed32(Convert.ToUInt32(value)); 762 | break; 763 | case FieldType.Bool: 764 | output.WriteBool(Convert.ToBoolean(value)); 765 | break; 766 | case FieldType.String: 767 | output.WriteString(Convert.ToString(value)); 768 | break; 769 | case FieldType.Group: 770 | { 771 | var descriptor = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 772 | var valueToSerialize = (IDictionary)value; 773 | var sizeOfValueToSerialize = descriptor.ComputeSize(valueToSerialize, context); 774 | output.WriteLength(sizeOfValueToSerialize); 775 | descriptor.WriteTo((IDictionary)value, ref output, context); 776 | output.WriteTag(GetGroupEndTag(tag)); 777 | break; 778 | } 779 | case FieldType.Message: 780 | { 781 | var descriptor = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 782 | var valueToSerialize = (IDictionary)value; 783 | var sizeOfValueToSerialize = descriptor.ComputeSize(valueToSerialize, context); 784 | output.WriteLength(sizeOfValueToSerialize); 785 | descriptor.WriteTo((IDictionary)value, ref output, context); 786 | break; 787 | } 788 | case FieldType.Bytes: 789 | output.WriteBytes(ByteString.CopyFrom((byte[])value)); 790 | break; 791 | case FieldType.UInt32: 792 | output.WriteUInt32(Convert.ToUInt32(value)); 793 | break; 794 | case FieldType.SFixed32: 795 | output.WriteSFixed32(Convert.ToInt32(value)); 796 | break; 797 | case FieldType.SFixed64: 798 | output.WriteSFixed64(Convert.ToInt64(value)); 799 | break; 800 | case FieldType.SInt32: 801 | output.WriteSInt32(Convert.ToInt32(value)); 802 | break; 803 | case FieldType.SInt64: 804 | output.WriteSInt64(Convert.ToInt64(value)); 805 | break; 806 | case FieldType.Enum: 807 | var enumInputValue = value; 808 | if (enumInputValue is Enum realEnum) 809 | { 810 | enumInputValue = realEnum.ToString(); 811 | } 812 | 813 | int enumValue = enumInputValue is string enumAsText ? fieldDescriptor.EnumType.FindValueByName(enumAsText).Number : Convert.ToInt32(enumInputValue); 814 | output.WriteEnum(enumValue); 815 | break; 816 | default: 817 | throw new ArgumentOutOfRangeException($"Unsupported field type `{fieldDescriptor.FieldType}`= {(int)fieldDescriptor.FieldType} in message type `{parentDescriptor.FullName}`."); 818 | } 819 | } 820 | 821 | private IDictionary FilterAny(IDictionary value, DynamicGrpcClientContext context) 822 | { 823 | // Special case for any, we are converting them on the fly 824 | if (Descriptor.FullName == DynamicAnyExtensions.GoogleTypeAnyFullName) 825 | { 826 | if (context.MapToAny.TryGetValue(value, out var any)) 827 | { 828 | return any; 829 | } 830 | 831 | // The input type is an any. Expecting the property @type to be part of it 832 | if (value.TryGetValue(DynamicAnyExtensions.TypeKey, out var typeUrlObject) && typeUrlObject is string typeUrl) 833 | { 834 | var typeName = Google.Protobuf.WellKnownTypes.Any.GetTypeName(typeUrl); 835 | if (DescriptorSet.TryFindMessageDescriptorProto(typeName, out var serializer)) 836 | { 837 | var memoryStream = new MemoryStream(); 838 | { 839 | var copy = new Dictionary(value); 840 | copy.Remove(DynamicAnyExtensions.TypeKey); 841 | var stream = new CodedOutputStream(memoryStream); 842 | var message = new DynamicMessage(serializer, copy, context); 843 | stream.WriteRawMessage(message); 844 | stream.Flush(); 845 | } 846 | var byteBuffer = memoryStream.ToArray(); 847 | var newValue = new Dictionary 848 | { 849 | [DynamicAnyExtensions.GoogleTypeUrlKey] = typeUrl, 850 | [DynamicAnyExtensions.GoogleValueKey] = byteBuffer 851 | }; 852 | context.MapToAny[value] = newValue; 853 | value = newValue; 854 | } 855 | else 856 | { 857 | throw new DynamicGrpcClientException($"Invalid message type name `{typeName}` not found for the type `Any`."); 858 | } 859 | } 860 | else 861 | { 862 | throw new DynamicGrpcClientException($"Expecting the type `Any` but the input dictionary doesn't contain a `{DynamicAnyExtensions.TypeKey}` property."); 863 | } 864 | } 865 | return value; 866 | } 867 | 868 | public int ComputeSize(IDictionary value, DynamicGrpcClientContext context) 869 | { 870 | // Special case for any, we are converting them on the fly 871 | value = FilterAny(value, context); 872 | 873 | int size = 0; 874 | foreach (var keyValue in value) 875 | { 876 | var fieldDescriptor = _nameToField[keyValue.Key]; 877 | var tag = GetTagForField(fieldDescriptor); 878 | var fieldSize = ComputeFieldSize(tag, Descriptor, fieldDescriptor, keyValue.Key, keyValue.Value, context); 879 | size += fieldSize; 880 | } 881 | 882 | return size; 883 | } 884 | 885 | private int ComputeFieldSize(uint tag, MessageDescriptor parentDescriptor, FieldDescriptor fieldDescriptor, string key, object value, DynamicGrpcClientContext context) 886 | { 887 | int fieldSize; 888 | 889 | if (fieldDescriptor.IsMap) 890 | { 891 | if (value is not IDictionary values) 892 | { 893 | throw new DynamicGrpcClientException($"Repeated Field `{key}` is expecting an IDictionary type instead of {value?.GetType()?.FullName}."); 894 | } 895 | 896 | var fieldMessageType = fieldDescriptor.MessageType; 897 | var keyField = fieldMessageType.Fields.InDeclarationOrder()[0]; 898 | var keyTag = GetTagForField(keyField); 899 | var keyTagSize = CodedOutputStream.ComputeUInt32Size(keyTag); 900 | var valueField = fieldMessageType.Fields.InDeclarationOrder()[1]; 901 | var valueTag = GetTagForField(valueField); 902 | var valueTagSize = CodedOutputStream.ComputeUInt32Size(valueTag); 903 | var tagSize = CodedOutputStream.ComputeUInt32Size(tag); 904 | 905 | var it = values.GetEnumerator(); 906 | fieldSize = 0; 907 | while (it.MoveNext()) 908 | { 909 | var keyFromMap = it.Key; 910 | var valueFromMap = it.Value; 911 | fieldSize += tagSize; 912 | 913 | var keyValueSize = 0; 914 | var isKeyDefaultValue = DefaultValueHelper.IsDefaultValue(keyField.FieldType, keyFromMap); 915 | if (!isKeyDefaultValue) 916 | { 917 | keyValueSize += keyTagSize; 918 | keyValueSize += ComputeSimpleFieldSize(keyTag, fieldMessageType, keyField, "key", keyFromMap, context); 919 | } 920 | var isValueDefaultValue = DefaultValueHelper.IsDefaultValue(valueField.FieldType, valueFromMap); 921 | if (!isValueDefaultValue) 922 | { 923 | Debug.Assert(valueFromMap is not null); 924 | keyValueSize += valueTagSize; 925 | keyValueSize += ComputeSimpleFieldSize(valueTag, fieldMessageType, valueField, "value", valueFromMap!, context); 926 | } 927 | 928 | fieldSize += CodedOutputStream.ComputeLengthSize(keyValueSize); 929 | fieldSize += keyValueSize; 930 | } 931 | } 932 | else if (fieldDescriptor.IsRepeated) 933 | { 934 | if (value is not IEnumerable values) 935 | { 936 | throw new DynamicGrpcClientException($"Repeated Field `{key}` is expecting an IEnumerable type instead of {value?.GetType()?.FullName}."); 937 | } 938 | 939 | switch (fieldDescriptor.FieldType) 940 | { 941 | case FieldType.Double: 942 | { 943 | var repeated = new RepeatedField(); 944 | foreach (var item in values) repeated.Add(Convert.ToDouble(item)); 945 | fieldSize = repeated.CalculateSize(FieldCodec.ForDouble(tag)); 946 | } 947 | break; 948 | case FieldType.Float: 949 | { 950 | var repeated = new RepeatedField(); 951 | foreach (var item in values) repeated.Add(Convert.ToSingle(item)); 952 | fieldSize = repeated.CalculateSize(FieldCodec.ForFloat(tag)); 953 | } 954 | break; 955 | case FieldType.Int64: 956 | { 957 | var repeated = new RepeatedField(); 958 | foreach (var item in values) repeated.Add(Convert.ToInt64(item)); 959 | fieldSize = repeated.CalculateSize(FieldCodec.ForInt64(tag)); 960 | } 961 | break; 962 | case FieldType.UInt64: 963 | { 964 | var repeated = new RepeatedField(); 965 | foreach (var item in values) repeated.Add(Convert.ToUInt64(item)); 966 | fieldSize = repeated.CalculateSize(FieldCodec.ForUInt64(tag)); 967 | } 968 | break; 969 | case FieldType.Int32: 970 | { 971 | var repeated = new RepeatedField(); 972 | foreach (var item in values) repeated.Add(Convert.ToInt32(item)); 973 | fieldSize = repeated.CalculateSize(FieldCodec.ForInt32(tag)); 974 | } 975 | break; 976 | case FieldType.Fixed64: 977 | { 978 | var repeated = new RepeatedField(); 979 | foreach (var item in values) repeated.Add(Convert.ToUInt64(item)); 980 | fieldSize = repeated.CalculateSize(FieldCodec.ForFixed64(tag)); 981 | } 982 | break; 983 | case FieldType.Fixed32: 984 | { 985 | var repeated = new RepeatedField(); 986 | foreach (var item in values) repeated.Add(Convert.ToUInt32(item)); 987 | fieldSize = repeated.CalculateSize(FieldCodec.ForFixed32(tag)); 988 | } 989 | break; 990 | case FieldType.Bool: 991 | { 992 | var repeated = new RepeatedField(); 993 | foreach (var item in values) repeated.Add(Convert.ToBoolean(item)); 994 | fieldSize = repeated.CalculateSize(FieldCodec.ForBool(tag)); 995 | } 996 | break; 997 | case FieldType.String: 998 | { 999 | var repeated = new RepeatedField(); 1000 | foreach (var item in values) repeated.Add(Convert.ToString(item)); 1001 | fieldSize = repeated.CalculateSize(FieldCodec.ForString(tag)); 1002 | } 1003 | break; 1004 | case FieldType.Group: 1005 | { 1006 | var descriptorForRepeatedMessage = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 1007 | var parser = new MessageParser(() => new DynamicMessage(descriptorForRepeatedMessage, context.Factory(), context)); 1008 | var repeated = new RepeatedField(); 1009 | foreach (var item in values) 1010 | { 1011 | repeated.Add(new DynamicMessage(descriptorForRepeatedMessage, (IDictionary)item, context)); 1012 | } 1013 | fieldSize = repeated.CalculateSize(FieldCodec.ForGroup(tag, GetGroupEndTag(tag), parser)); 1014 | } 1015 | break; 1016 | case FieldType.Message: 1017 | { 1018 | var descriptorForRepeatedMessage = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 1019 | var parser = new MessageParser(() => new DynamicMessage(descriptorForRepeatedMessage, context.Factory(), context)); 1020 | var repeated = new RepeatedField(); 1021 | foreach (var item in values) 1022 | { 1023 | repeated.Add(new DynamicMessage(descriptorForRepeatedMessage, (IDictionary)item, context)); 1024 | } 1025 | fieldSize = repeated.CalculateSize(FieldCodec.ForMessage(tag, parser)); 1026 | } 1027 | break; 1028 | case FieldType.Bytes: 1029 | { 1030 | var repeated = new RepeatedField(); 1031 | foreach (var item in values) repeated.Add(ByteString.CopyFrom((byte[])item)); 1032 | fieldSize = repeated.CalculateSize(FieldCodec.ForBytes(tag)); 1033 | } 1034 | break; 1035 | case FieldType.UInt32: 1036 | { 1037 | var repeated = new RepeatedField(); 1038 | foreach (var item in values) repeated.Add(Convert.ToUInt32(item)); 1039 | fieldSize = repeated.CalculateSize(FieldCodec.ForUInt32(tag)); 1040 | } 1041 | break; 1042 | case FieldType.SFixed32: 1043 | { 1044 | var repeated = new RepeatedField(); 1045 | foreach (var item in values) repeated.Add(Convert.ToInt32(item)); 1046 | fieldSize = repeated.CalculateSize(FieldCodec.ForSFixed32(tag)); 1047 | } 1048 | break; 1049 | case FieldType.SFixed64: 1050 | { 1051 | var repeated = new RepeatedField(); 1052 | foreach (var item in values) repeated.Add(Convert.ToInt64(item)); 1053 | fieldSize = repeated.CalculateSize(FieldCodec.ForSFixed64(tag)); 1054 | } 1055 | break; 1056 | case FieldType.SInt32: 1057 | { 1058 | var repeated = new RepeatedField(); 1059 | foreach (var item in values) repeated.Add(Convert.ToInt32(item)); 1060 | fieldSize = repeated.CalculateSize(FieldCodec.ForSInt32(tag)); 1061 | } 1062 | break; 1063 | case FieldType.SInt64: 1064 | { 1065 | var repeated = new RepeatedField(); 1066 | foreach (var item in values) repeated.Add(Convert.ToInt64(item)); 1067 | fieldSize = repeated.CalculateSize(FieldCodec.ForSInt64(tag)); 1068 | } 1069 | break; 1070 | case FieldType.Enum: 1071 | { 1072 | var repeated = new RepeatedField(); 1073 | foreach (var item in values) 1074 | { 1075 | var enumInputValue = item; 1076 | if (enumInputValue is Enum realEnum) 1077 | { 1078 | enumInputValue = realEnum.ToString(); 1079 | } 1080 | 1081 | var rawEnumValue = enumInputValue is string enumAsText ? fieldDescriptor.EnumType.FindValueByName(enumAsText).Number : Convert.ToInt32(enumInputValue); 1082 | repeated.Add(rawEnumValue); 1083 | } 1084 | fieldSize = repeated.CalculateSize(FieldCodec.ForEnum(tag, o => o, i => i, 0)); 1085 | } 1086 | break; 1087 | default: 1088 | throw new ArgumentOutOfRangeException(); 1089 | } 1090 | 1091 | fieldSize += CodedOutputStream.ComputeRawVarint32Size(tag); 1092 | } 1093 | else 1094 | { 1095 | fieldSize = ComputeSimpleFieldSize(tag, parentDescriptor, fieldDescriptor, key, value, context); 1096 | fieldSize += CodedOutputStream.ComputeRawVarint32Size(tag); 1097 | } 1098 | 1099 | return fieldSize; 1100 | } 1101 | 1102 | private int ComputeSimpleFieldSize(uint tag, MessageDescriptor parentDescriptor, FieldDescriptor fieldDescriptor, string key, object value, DynamicGrpcClientContext context) 1103 | { 1104 | int fieldSize; 1105 | switch (fieldDescriptor.FieldType) 1106 | { 1107 | case FieldType.Double: 1108 | fieldSize = CodedOutputStream.ComputeDoubleSize(Convert.ToDouble(value)); 1109 | break; 1110 | case FieldType.Float: 1111 | fieldSize = CodedOutputStream.ComputeFloatSize(Convert.ToSingle(value)); 1112 | break; 1113 | case FieldType.Int64: 1114 | fieldSize = CodedOutputStream.ComputeInt64Size(Convert.ToInt64(value)); 1115 | break; 1116 | case FieldType.UInt64: 1117 | fieldSize = CodedOutputStream.ComputeUInt64Size(Convert.ToUInt64(value)); 1118 | break; 1119 | case FieldType.Int32: 1120 | fieldSize = CodedOutputStream.ComputeInt32Size(Convert.ToInt32(value)); 1121 | break; 1122 | case FieldType.Fixed64: 1123 | fieldSize = CodedOutputStream.ComputeFixed64Size(Convert.ToUInt64(value)); 1124 | break; 1125 | case FieldType.Fixed32: 1126 | fieldSize = CodedOutputStream.ComputeFixed32Size(Convert.ToUInt32(value)); 1127 | break; 1128 | case FieldType.Bool: 1129 | fieldSize = CodedOutputStream.ComputeBoolSize(Convert.ToBoolean(value)); 1130 | break; 1131 | case FieldType.String: 1132 | fieldSize = CodedOutputStream.ComputeStringSize(Convert.ToString(value)); 1133 | break; 1134 | case FieldType.Group: 1135 | { 1136 | var descriptor = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 1137 | var messageSize = descriptor.ComputeSize((IDictionary)value, context); 1138 | fieldSize = CodedOutputStream.ComputeLengthSize(messageSize) + messageSize; 1139 | fieldSize += CodedOutputStream.ComputeRawVarint32Size(GetGroupEndTag(tag)); 1140 | } 1141 | break; 1142 | case FieldType.Message: 1143 | { 1144 | var descriptor = GetSafeDescriptor(fieldDescriptor.MessageType.FullName); 1145 | var messageSize = descriptor.ComputeSize((IDictionary)value, context); 1146 | fieldSize = CodedOutputStream.ComputeLengthSize(messageSize) + messageSize; 1147 | } 1148 | break; 1149 | case FieldType.Bytes: 1150 | var bytes = (byte[])value; 1151 | fieldSize = CodedOutputStream.ComputeLengthSize(bytes.Length) + bytes.Length; 1152 | break; 1153 | case FieldType.UInt32: 1154 | fieldSize = CodedOutputStream.ComputeUInt32Size(Convert.ToUInt32(value)); 1155 | break; 1156 | case FieldType.SFixed32: 1157 | fieldSize = CodedOutputStream.ComputeSFixed32Size(Convert.ToInt32(value)); 1158 | break; 1159 | case FieldType.SFixed64: 1160 | fieldSize = CodedOutputStream.ComputeSFixed64Size(Convert.ToInt64(value)); 1161 | break; 1162 | case FieldType.SInt32: 1163 | fieldSize = CodedOutputStream.ComputeSInt32Size(Convert.ToInt32(value)); 1164 | break; 1165 | case FieldType.SInt64: 1166 | fieldSize = CodedOutputStream.ComputeSInt64Size(Convert.ToInt64(value)); 1167 | break; 1168 | case FieldType.Enum: 1169 | fieldSize = CodedOutputStream.ComputeEnumSize(Convert.ToInt32(value)); 1170 | break; 1171 | default: 1172 | throw new ArgumentOutOfRangeException($"Unsupported field type `{fieldDescriptor.FieldType}`= {(int)fieldDescriptor.FieldType} in message type `{parentDescriptor.FullName}`."); 1173 | } 1174 | 1175 | return fieldSize; 1176 | } 1177 | 1178 | private static WireFormat.WireType GetWireType(FieldType type, bool isPackedRepeated) 1179 | { 1180 | if (isPackedRepeated) return WireFormat.WireType.LengthDelimited; 1181 | 1182 | switch (type) 1183 | { 1184 | case FieldType.SFixed64: 1185 | case FieldType.Fixed64: 1186 | case FieldType.Double: 1187 | return WireFormat.WireType.Fixed64; 1188 | case FieldType.SFixed32: 1189 | case FieldType.Fixed32: 1190 | case FieldType.Float: 1191 | return WireFormat.WireType.Fixed32; 1192 | case FieldType.Enum: 1193 | case FieldType.Bool: 1194 | case FieldType.SInt32: 1195 | case FieldType.UInt32: 1196 | case FieldType.Int32: 1197 | case FieldType.Int64: 1198 | case FieldType.SInt64: 1199 | case FieldType.UInt64: 1200 | return WireFormat.WireType.Varint; 1201 | case FieldType.String: 1202 | case FieldType.Message: 1203 | case FieldType.Bytes: 1204 | return WireFormat.WireType.LengthDelimited; 1205 | case FieldType.Group: 1206 | return WireFormat.WireType.StartGroup; 1207 | default: 1208 | throw new ArgumentOutOfRangeException(nameof(type), type, null); 1209 | } 1210 | } 1211 | 1212 | 1213 | private DynamicMessageSerializer GetSafeDescriptor(string typeName) 1214 | { 1215 | if (!DescriptorSet.TryFindMessageDescriptorProto(typeName, out var descriptor)) 1216 | { 1217 | throw new InvalidOperationException($"Cannot find type {typeName}"); 1218 | } 1219 | 1220 | return descriptor; 1221 | } 1222 | 1223 | private class DynamicMessage : IMessage, IBufferMessage 1224 | { 1225 | public DynamicMessage(DynamicMessageSerializer dynamicMessageSerializer, IDictionary value, DynamicGrpcClientContext context) 1226 | { 1227 | DynamicMessageSerializer = dynamicMessageSerializer; 1228 | Value = value; 1229 | Context = context; 1230 | } 1231 | 1232 | private DynamicMessageSerializer DynamicMessageSerializer { get; } 1233 | 1234 | public IDictionary Value { get; private set; } 1235 | 1236 | private DynamicGrpcClientContext Context { get; } 1237 | 1238 | public void InternalMergeFrom(ref ParseContext ctx) 1239 | { 1240 | Value = DynamicMessageSerializer.ReadFrom(ref ctx, Context); 1241 | } 1242 | 1243 | public void InternalWriteTo(ref WriteContext ctx) 1244 | { 1245 | DynamicMessageSerializer.WriteTo(Value, ref ctx, Context); 1246 | } 1247 | 1248 | public void MergeFrom(DynamicMessage message) 1249 | { 1250 | foreach (var keyPair in message.Value) 1251 | { 1252 | Value[keyPair.Key] = keyPair.Value; 1253 | } 1254 | } 1255 | 1256 | public void MergeFrom(CodedInputStream input) 1257 | { 1258 | throw new NotImplementedException(); 1259 | } 1260 | 1261 | public void WriteTo(CodedOutputStream output) 1262 | { 1263 | throw new NotImplementedException(); 1264 | } 1265 | 1266 | public int CalculateSize() 1267 | { 1268 | return DynamicMessageSerializer.ComputeSize(Value, Context); 1269 | } 1270 | 1271 | public MessageDescriptor Descriptor => DynamicMessageSerializer.Descriptor; 1272 | 1273 | public bool Equals(DynamicMessage? other) 1274 | { 1275 | return false; 1276 | } 1277 | 1278 | public DynamicMessage Clone() 1279 | { 1280 | var newValue = Context.Factory(); 1281 | foreach (var keyPair in Value) 1282 | { 1283 | newValue.Add(keyPair); 1284 | } 1285 | 1286 | return new DynamicMessage(DynamicMessageSerializer, newValue, Context); 1287 | } 1288 | } 1289 | 1290 | /// 1291 | /// https://developers.google.com/protocol-buffers/docs/proto3#default 1292 | /// 1293 | private static class DefaultValueHelper 1294 | { 1295 | public static object? GetDefaultValue(FieldType type) 1296 | { 1297 | // For strings, the default value is the empty string. 1298 | // For bytes, the default value is empty bytes. 1299 | // For bools, the default value is false. 1300 | // For numeric types, the default value is zero. 1301 | // For enums, the default value is the first defined enum value, which must be 0. 1302 | switch (type) 1303 | { 1304 | case FieldType.Double: 1305 | return DefaultDouble; 1306 | case FieldType.Float: 1307 | return DefaultFloat; 1308 | case FieldType.Int64: 1309 | return DefaultInt64; 1310 | case FieldType.UInt64: 1311 | return DefaultUInt64; 1312 | case FieldType.Int32: 1313 | return DefaultInt32; 1314 | case FieldType.Fixed64: 1315 | return DefaultFixed64; 1316 | case FieldType.Fixed32: 1317 | return DefaultFixed32; 1318 | case FieldType.Bool: 1319 | return DefaultBool; 1320 | case FieldType.UInt32: 1321 | return DefaultUInt32; 1322 | case FieldType.SFixed32: 1323 | return DefaultSFixed32; 1324 | case FieldType.SFixed64: 1325 | return DefaultSFixed64; 1326 | case FieldType.SInt32: 1327 | return DefaultSInt32; 1328 | case FieldType.SInt64: 1329 | return DefaultSInt64; 1330 | case FieldType.Enum: 1331 | return DefaultEnum; 1332 | case FieldType.String: 1333 | return DefaultString; 1334 | case FieldType.Bytes: 1335 | return DefaultBytes; 1336 | default: 1337 | return null; 1338 | } 1339 | } 1340 | 1341 | public static bool IsDefaultValue(FieldType type, object? value) 1342 | { 1343 | var defaultValue = GetDefaultValue(type); 1344 | return defaultValue != null && defaultValue.Equals(value); 1345 | } 1346 | 1347 | private static readonly object DefaultDouble = 0.0; 1348 | private static readonly object DefaultFloat = 0.0f; 1349 | private static readonly object DefaultInt64 = 0L; 1350 | private static readonly object DefaultUInt64 = 0UL; 1351 | private static readonly object DefaultInt32 = 0; 1352 | private static readonly object DefaultFixed64 = 0UL; 1353 | private static readonly object DefaultFixed32 = 0U; 1354 | private static readonly object DefaultBool = false; 1355 | private static readonly object DefaultUInt32 = 0U; 1356 | private static readonly object DefaultSFixed32 = 0; 1357 | private static readonly object DefaultSFixed64 = 0L; 1358 | private static readonly object DefaultSInt32 = 0; 1359 | private static readonly object DefaultSInt64 = 0L; 1360 | private static readonly object DefaultEnum = 0; 1361 | private static readonly object DefaultString = string.Empty; 1362 | private static readonly object DefaultBytes = Array.Empty(); 1363 | } 1364 | } -------------------------------------------------------------------------------- /src/DynamicGrpc/DynamicServiceDescriptor.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf.Reflection; 2 | 3 | namespace DynamicGrpc; 4 | 5 | /// 6 | /// Internal class used to map a method to a method descriptor. 7 | /// 8 | internal sealed class DynamicServiceDescriptor : Dictionary 9 | { 10 | public DynamicServiceDescriptor(ServiceDescriptor proto) 11 | { 12 | Proto = proto; 13 | foreach (var method in proto.Methods) 14 | { 15 | this[method.Name] = method; 16 | } 17 | } 18 | 19 | public ServiceDescriptor Proto { get; } 20 | } -------------------------------------------------------------------------------- /src/GrpcCurl.Tests/BasicTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using DynamicGrpc; 9 | using Grpc.Net.Client; 10 | using GrpcCurl.Tests.Proto; 11 | using NUnit.Framework; 12 | 13 | namespace GrpcCurl.Tests 14 | { 15 | public class BasicTests : GrpcTestBase 16 | { 17 | [Test] 18 | [Ignore("Local only")] 19 | public async Task TestStarlink() 20 | { 21 | using var channel = GrpcChannel.ForAddress("http://192.168.100.1:9200"); 22 | var client = await DynamicGrpcClient.FromServerReflection(channel); 23 | 24 | var result = await client.AsyncUnaryCall("SpaceX.API.Device.Device", "Handle", new Dictionary() 25 | { 26 | { "get_status", new Dictionary() } 27 | }); 28 | 29 | var text= JsonSerializer.Serialize(result, new JsonSerializerOptions() { WriteIndented = true, NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals}); 30 | Console.WriteLine(text); 31 | } 32 | 33 | [Test] 34 | public async Task TestBasicUnaryCall() 35 | { 36 | var client = await DynamicGrpcClient.FromServerReflection(TestGrpcChannel); 37 | 38 | var result = await client.AsyncUnaryCall("greet.Greeter", "SayHello", new Dictionary() 39 | { 40 | { "name", "Hello GrpcCurl!" } 41 | }); 42 | 43 | Assert.That(result, Contains.Key("message")); 44 | Assert.AreEqual("Hello from server with input name: Hello GrpcCurl!.", result["message"]); 45 | 46 | dynamic dynamicResult = result; 47 | 48 | Assert.AreEqual(result["message"], dynamicResult.message); 49 | } 50 | 51 | [Test] 52 | [Ignore("Group is not correctly mapped by server reflection. Instead it is backed into Message.")] 53 | public async Task TestGroup() 54 | { 55 | // TODO: log an issue on server reflection 56 | var client = await DynamicGrpcClient.FromServerReflection(TestGrpcChannel); 57 | 58 | var input = new Dictionary() 59 | { 60 | { "url", "http://localhost" }, 61 | { "title", "This is a title" }, 62 | { "snippets", "This is a snippet" } 63 | }; 64 | var result = await client.AsyncUnaryCall("greet.GreeterProto2", "SayGroup", new Dictionary() 65 | { 66 | { "result", input } 67 | }); 68 | 69 | Assert.That(result, Contains.Key("result")); 70 | dynamic dynamicResult = result; 71 | Assert.AreEqual($"{input["url"]} - yes", dynamicResult.result.url); 72 | Assert.AreEqual($"{input["title"]} - yes", dynamicResult.result.title); 73 | Assert.AreEqual($"{input["snippets"]} - yes", dynamicResult.result.sessions); 74 | } 75 | 76 | [Test] 77 | public async Task TestServerStreamingCall() 78 | { 79 | var client = await DynamicGrpcClient.FromServerReflection(TestGrpcChannel); 80 | 81 | var call = client.AsyncServerStreamingCall("greet.Greeter", "SayHellos", new Dictionary() 82 | { 83 | { "name", "Hello GrpcCurl!" } 84 | }); 85 | 86 | var results = new List>(); 87 | while (await call.ResponseStream.MoveNext(CancellationToken.None)) 88 | { 89 | results.Add(call.ResponseStream.Current); 90 | } 91 | 92 | Assert.AreEqual(GreeterServiceImpl.StreamingCount, results.Count); 93 | 94 | for(int i = 0; i < results.Count; i++) 95 | { 96 | var result = results[i]; 97 | 98 | Assert.That(result, Contains.Key("message")); 99 | Assert.AreEqual($"Streaming Hello {i}/{GreeterServiceImpl.StreamingCount} from server with input name: Hello GrpcCurl!.", result["message"]); 100 | 101 | dynamic dynamicResult = result; 102 | 103 | Assert.AreEqual(result["message"], dynamicResult.message); 104 | } 105 | } 106 | 107 | [TestCase("double", 1.0, 2.0)] 108 | [TestCase("float", 1.0f, 2.0f)] 109 | [TestCase("int32", -1, 0)] // testing default value 110 | [TestCase("int32", 1, 2)] 111 | [TestCase("int64", 1L, 2L)] 112 | [TestCase("uint32", 1U, 2U)] 113 | [TestCase("uint64", 1UL, 2UL)] 114 | [TestCase("sint32", 3, 4)] 115 | [TestCase("sint64", 5L, 6L)] 116 | [TestCase("fixed32", 3U, 4U)] 117 | [TestCase("fixed64", 5UL, 6UL)] 118 | [TestCase("sfixed32", 1, 2)] 119 | [TestCase("sfixed64", 1L, 2L)] 120 | [TestCase("bool", true, false)] 121 | [TestCase("string", "hello", "hello1")] 122 | [TestCase("bytes", null, null)] 123 | [TestCase("enum_type", 0, "WEB")] 124 | [TestCase("enum_type", -1, "UNIVERSAL")] // testing default value 125 | [TestCase("enum_type", "IMAGES", "LOCAL")] 126 | public async Task TestPrimitives(string name, object input, object expectedOutput) 127 | { 128 | var client = await DynamicGrpcClient.FromServerReflection(TestGrpcChannel); 129 | 130 | if (name == "bytes") 131 | { 132 | input = new byte[] { 1, 2, 3 }; 133 | expectedOutput = input; 134 | } 135 | 136 | // Request with a simple field 137 | var result = await client.AsyncUnaryCall("Primitives.PrimitiveService", $"Request_{name}", new Dictionary() 138 | { 139 | { "value", input } 140 | }); 141 | ValidateResult(name, expectedOutput, result); 142 | 143 | // Request with repeated fields 144 | var inputList = new List 145 | { 146 | input, 147 | input, 148 | input, 149 | input 150 | }; 151 | result = await client.AsyncUnaryCall("Primitives.PrimitiveService", $"Request_with_repeated_{name}", new Dictionary() 152 | { 153 | { "values", inputList } 154 | }); 155 | Assert.AreEqual(1, result.Count); 156 | Assert.That(result, Contains.Key("values")); 157 | Assert.IsInstanceOf(result["values"]); 158 | int count = 0; 159 | foreach (var itemResult in (IEnumerable)result["values"]) 160 | { 161 | Assert.AreEqual(expectedOutput, itemResult); 162 | count++; 163 | } 164 | Assert.AreEqual(inputList.Count, count); 165 | 166 | static void ValidateResult(string name, object expectedOutput, IDictionary result) 167 | { 168 | Assert.That(result, Contains.Key("value")); 169 | var output = result["value"]; 170 | Assert.AreEqual(expectedOutput, output); 171 | } 172 | } 173 | 174 | 175 | [TestCase("int32", 1)] 176 | [TestCase("int64", 1L)] 177 | [TestCase("uint32", 1U)] 178 | [TestCase("uint64", 1UL)] 179 | [TestCase("sint32", 3)] 180 | [TestCase("sint64", 5L)] 181 | [TestCase("fixed32", 3U)] 182 | [TestCase("fixed64", 5UL)] 183 | [TestCase("sfixed32", 1)] 184 | [TestCase("sfixed64", 1L)] 185 | [TestCase("bool", true)] 186 | [TestCase("string", "hello")] 187 | public async Task TestMaps(string name, object input) 188 | { 189 | var client = await DynamicGrpcClient.FromServerReflection(TestGrpcChannel); 190 | 191 | var map_key = $"map_key_{name}_values"; 192 | var result = await client.AsyncUnaryCall("Primitives.PrimitiveService", $"Request_map_type", new Dictionary() 193 | { 194 | { "value", new Dictionary() 195 | { 196 | {map_key, new Dictionary() 197 | { 198 | {input, "test0"} // Creates a default value for the key 199 | } 200 | } 201 | } 202 | } 203 | }); 204 | 205 | Assert.That(result, Contains.Key("value")); 206 | var subValue = result["value"]; 207 | Assert.IsInstanceOf>(subValue); 208 | var dict = (IDictionary)subValue; 209 | Assert.That(dict, Contains.Key(map_key)); 210 | subValue = dict[map_key]; 211 | Assert.IsInstanceOf>(subValue); 212 | var subDict = (IDictionary)subValue; 213 | Assert.AreEqual(2, subDict.Count); 214 | Assert.That(subDict, Contains.Key(input)); 215 | Assert.That(subDict, Contains.Value("test0")); 216 | Assert.That(subDict, Contains.Value("test10")); 217 | } 218 | 219 | [Test] 220 | public async Task TestAny() 221 | { 222 | var client = await DynamicGrpcClient.FromServerReflection(TestGrpcChannel); 223 | 224 | var result = await client.AsyncUnaryCall("Primitives.PrimitiveService", $"Request_any_type", new Dictionary() 225 | { 226 | { "value", new Dictionary() 227 | { 228 | {"instrument", new Dictionary() 229 | { 230 | {"currency_message", "this is a currency"} 231 | }.WithAny("Primitives.Currency") 232 | } 233 | } 234 | } 235 | }); 236 | 237 | Assert.That(result, Contains.Key("value")); 238 | var subValue = result["value"]; 239 | Assert.IsInstanceOf>(subValue); 240 | var dict = (IDictionary)subValue; 241 | Assert.That(dict, Contains.Key("instrument")); 242 | subValue = dict["instrument"]; 243 | Assert.IsInstanceOf>(subValue); 244 | var subDict = (IDictionary)subValue; 245 | Assert.AreEqual(2, subDict.Count, $"Invalid number of key/values. {string.Join(", ", subDict.Keys)}"); 246 | Assert.That(subDict, Contains.Key("stock_message")); 247 | Assert.That(subDict, Contains.Key("@type")); 248 | Assert.AreEqual("From currency: this is a currency", subDict["stock_message"]); 249 | Assert.AreEqual("type.googleapis.com/Primitives.Stock", subDict["@type"]); 250 | } 251 | 252 | [Test] 253 | public async Task TestDefaults() 254 | { 255 | var client = await DynamicGrpcClient.FromServerReflection(TestGrpcChannel); 256 | 257 | var result = await client.AsyncUnaryCall("Primitives.PrimitiveService", $"Request_defaults_type", new Dictionary()); 258 | 259 | Assert.That(result, Contains.Key("value")); 260 | var subValue = result["value"]; 261 | Assert.IsInstanceOf>(subValue); 262 | dynamic dict = subValue; 263 | 264 | Assert.AreEqual(0, dict.field_int32); 265 | Assert.AreEqual(0L, dict.field_int64); 266 | Assert.AreEqual(0U, dict.field_uint32); 267 | Assert.AreEqual(0UL, dict.field_uint64); 268 | Assert.AreEqual(0, dict.field_sint32); 269 | Assert.AreEqual(0L, dict.field_sint64); 270 | Assert.AreEqual(0U, dict.field_fixed32); 271 | Assert.AreEqual(0UL, dict.field_fixed64); 272 | Assert.AreEqual(0, dict.field_sfixed32); 273 | Assert.AreEqual(0L, dict.field_sfixed64); 274 | Assert.AreEqual(false, dict.field_bool); 275 | Assert.AreEqual("", dict.field_string); 276 | Assert.AreEqual(Array.Empty(), dict.field_bytes); 277 | Assert.AreEqual("UNIVERSAL", dict.field_enum_type); 278 | } 279 | } 280 | } -------------------------------------------------------------------------------- /src/GrpcCurl.Tests/CurlTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using GrpcCurl.Tests.Proto; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Server.Kestrel.Core; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using NUnit.Framework; 12 | 13 | namespace GrpcCurl.Tests; 14 | 15 | public class CurlTests 16 | { 17 | [Test] 18 | public async Task Curl() 19 | { 20 | const int port = 9874; 21 | string host = $"http://localhost:{port}"; 22 | var builder = WebApplication.CreateBuilder(new WebApplicationOptions() 23 | { 24 | EnvironmentName = Environments.Development, 25 | }); 26 | builder.Services.AddGrpc(); 27 | builder.Services.AddGrpcReflection(); 28 | builder.WebHost.UseUrls(host); 29 | builder.WebHost.ConfigureKestrel((context, options) => 30 | { 31 | options.Listen(IPAddress.Loopback, port, listenOptions => 32 | { 33 | listenOptions.Protocols = HttpProtocols.Http2; 34 | }); 35 | }); 36 | 37 | await using var app = builder.Build(); 38 | app.UseRouting(); 39 | 40 | app.UseEndpoints(endpoints => 41 | { 42 | endpoints.MapGrpcService(); 43 | endpoints.MapGrpcReflectionService(); 44 | }); 45 | 46 | await app.StartAsync(); 47 | 48 | var savedOutput = Console.Out; 49 | try 50 | { 51 | var stringWriter = new StringWriter(); 52 | Console.SetOut(stringWriter); 53 | var code = await GrpcCurlApp.Run(new string[] { "-d", "{ \"name\": \"Hello grpc-curl!\"}", host, "greet.Greeter/SayHello" }); 54 | Assert.AreEqual(0, code); 55 | var result = stringWriter.ToString(); 56 | StringAssert.Contains(@"""message"": ""Hello from server with input name: Hello grpc-curl!.""", result); 57 | } 58 | finally 59 | { 60 | Console.SetOut(savedOutput); 61 | await app.StopAsync(); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/GrpcCurl.Tests/GrpcCurl.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | True 19 | True 20 | Primitives.tt 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | all 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | 36 | 37 | 38 | 39 | 40 | 41 | all 42 | runtime; build; native; contentfiles; analyzers; buildtransitive 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | TextTemplatingFileGenerator 58 | Primitives.proto 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/GrpcCurl.Tests/GrpcTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Grpc.Net.Client; 4 | using GrpcCurl.Tests.Proto; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.TestHost; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using NUnit.Framework; 10 | 11 | namespace GrpcCurl.Tests; 12 | 13 | public abstract class GrpcTestBase 14 | { 15 | private WebApplication? _app; 16 | private GrpcChannel? _testGrpcChannel; 17 | 18 | [OneTimeSetUp] 19 | public async Task Setup() 20 | { 21 | _app = CreateWebApplicationTest(); 22 | await _app.StartAsync(); 23 | var client = _app.GetTestClient(); 24 | _testGrpcChannel = GrpcChannel.ForAddress(client.BaseAddress ?? throw new InvalidOperationException("HttpClient.BaseAddress cannot be null"), new GrpcChannelOptions() 25 | { 26 | HttpClient = client 27 | }); 28 | } 29 | 30 | public GrpcChannel TestGrpcChannel => _testGrpcChannel!; 31 | 32 | [OneTimeTearDown] 33 | public async Task TearDown() 34 | { 35 | if (_testGrpcChannel != null) 36 | { 37 | await _testGrpcChannel.ShutdownAsync(); 38 | _testGrpcChannel.Dispose(); 39 | } 40 | 41 | if (_app != null) 42 | { 43 | await _app.DisposeAsync(); 44 | } 45 | } 46 | 47 | private static WebApplication CreateWebApplicationTest() 48 | { 49 | var builder = WebApplication.CreateBuilder(new WebApplicationOptions() 50 | { 51 | EnvironmentName = Environments.Development, 52 | }); 53 | builder.Services.AddGrpc(); 54 | builder.Services.AddGrpcReflection(); 55 | builder.WebHost.UseTestServer(); 56 | 57 | var app = builder.Build(); 58 | app.UseRouting(); 59 | app.UseEndpoints(endpoints => 60 | { 61 | endpoints.MapGrpcService(); 62 | endpoints.MapGrpcService(); 63 | endpoints.MapGrpcService(); 64 | endpoints.MapGrpcReflectionService(); 65 | }); 66 | 67 | return app; 68 | } 69 | } -------------------------------------------------------------------------------- /src/GrpcCurl.Tests/Proto/GreeterProto2Impl.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Greet; 3 | using Grpc.Core; 4 | 5 | namespace GrpcCurl.Tests.Proto; 6 | 7 | public class GreeterProto2Impl : Greet.GreeterProto2.GreeterProto2Base 8 | { 9 | public override Task SayGroup(TryGroup request, ServerCallContext context) 10 | { 11 | var response = new TryGroup() 12 | { 13 | Result = new TryGroup.Types.Result() 14 | { 15 | Url = request.Result.Url + " - yes", 16 | Snippets = request.Result.Snippets + " - yes", 17 | Title = request.Result.Title + " - yes", 18 | } 19 | }; 20 | return Task.FromResult(response); 21 | } 22 | } -------------------------------------------------------------------------------- /src/GrpcCurl.Tests/Proto/GreeterServiceImpl.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Greet; 3 | using Grpc.Core; 4 | 5 | namespace GrpcCurl.Tests.Proto; 6 | 7 | public class GreeterServiceImpl : Greet.Greeter.GreeterBase 8 | { 9 | public override Task SayHello(HelloRequest request, ServerCallContext context) 10 | { 11 | return Task.FromResult(new HelloReply() { Message = $"Hello from server with input name: {request.Name}." }); 12 | } 13 | 14 | public const int StreamingCount = 10; 15 | 16 | public override async Task SayHellos(HelloRequest request, IServerStreamWriter responseStream, ServerCallContext context) 17 | { 18 | for (int i = 0; i < StreamingCount; i++) 19 | { 20 | await responseStream.WriteAsync(new HelloReply() 21 | { 22 | Message = $"Streaming Hello {i}/{StreamingCount} from server with input name: {request.Name}." 23 | }); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/GrpcCurl.Tests/Proto/PrimitiveServiceImpl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Grpc.Core; 4 | using Primitives; 5 | 6 | namespace GrpcCurl.Tests.Proto; 7 | 8 | public class PrimitiveServiceImpl : PrimitiveService.PrimitiveServiceBase 9 | { 10 | public override Task Request_double(double_InOut request, ServerCallContext context) => Task.FromResult(new double_InOut() { Value = request.Value + 1 }); 11 | public override Task Request_float(float_InOut request, ServerCallContext context) => Task.FromResult(new float_InOut() { Value = request.Value + 1 }); 12 | public override Task Request_int32(int32_InOut request, ServerCallContext context) => Task.FromResult(new int32_InOut() { Value = request.Value + 1 }); 13 | public override Task Request_int64(int64_InOut request, ServerCallContext context) => Task.FromResult(new int64_InOut() { Value = request.Value + 1 }); 14 | public override Task Request_uint32(uint32_InOut request, ServerCallContext context) => Task.FromResult(new uint32_InOut() { Value = request.Value + 1 }); 15 | public override Task Request_uint64(uint64_InOut request, ServerCallContext context) => Task.FromResult(new uint64_InOut() { Value = request.Value + 1 }); 16 | public override Task Request_sint32(sint32_InOut request, ServerCallContext context) => Task.FromResult(new sint32_InOut() { Value = request.Value + 1 }); 17 | public override Task Request_sint64(sint64_InOut request, ServerCallContext context) => Task.FromResult(new sint64_InOut() { Value = request.Value + 1 }); 18 | public override Task Request_fixed32(fixed32_InOut request, ServerCallContext context) => Task.FromResult(new fixed32_InOut() { Value = request.Value + 1 }); 19 | public override Task Request_fixed64(fixed64_InOut request, ServerCallContext context) => Task.FromResult(new fixed64_InOut() { Value = request.Value + 1 }); 20 | public override Task Request_sfixed32(sfixed32_InOut request, ServerCallContext context) => Task.FromResult(new sfixed32_InOut() { Value = request.Value + 1 }); 21 | public override Task Request_sfixed64(sfixed64_InOut request, ServerCallContext context) => Task.FromResult(new sfixed64_InOut() { Value = request.Value + 1 }); 22 | public override Task Request_bool(bool_InOut request, ServerCallContext context) 23 | { 24 | return Task.FromResult(new bool_InOut() { Value = !request.Value }); 25 | } 26 | 27 | public override Task Request_string(string_InOut request, ServerCallContext context) => Task.FromResult(new string_InOut() { Value = request.Value + 1 }); 28 | public override Task Request_bytes(bytes_InOut request, ServerCallContext context) => Task.FromResult(new bytes_InOut() { Value = request.Value }); 29 | 30 | public override Task Request_enum_type(enum_type_InOut request, ServerCallContext context) => Task.FromResult(new enum_type_InOut() { Value = (enum_type)((int)request.Value + 1) }); 31 | 32 | public override Task Request_with_repeated_double(double_repeated_InOut request, ServerCallContext context) 33 | { 34 | var values = request.Values; 35 | for (int i = 0; i < values.Count; i++) 36 | { 37 | values[i]++; 38 | } 39 | return Task.FromResult(request); 40 | } 41 | 42 | public override Task Request_with_repeated_float(float_repeated_InOut request, ServerCallContext context) 43 | { 44 | var values = request.Values; 45 | for (int i = 0; i < values.Count; i++) 46 | { 47 | values[i]++; 48 | } 49 | return Task.FromResult(request); 50 | } 51 | 52 | public override Task Request_with_repeated_int32(int32_repeated_InOut request, ServerCallContext context) 53 | { 54 | var values = request.Values; 55 | for (int i = 0; i < values.Count; i++) 56 | { 57 | values[i]++; 58 | } 59 | return Task.FromResult(request); 60 | } 61 | 62 | public override Task Request_with_repeated_int64(int64_repeated_InOut request, ServerCallContext context) 63 | { 64 | var values = request.Values; 65 | for (int i = 0; i < values.Count; i++) 66 | { 67 | values[i]++; 68 | } 69 | return Task.FromResult(request); 70 | } 71 | 72 | public override Task Request_with_repeated_uint32(uint32_repeated_InOut request, ServerCallContext context) 73 | { 74 | var values = request.Values; 75 | for (int i = 0; i < values.Count; i++) 76 | { 77 | values[i]++; 78 | } 79 | return Task.FromResult(request); 80 | } 81 | 82 | public override Task Request_with_repeated_uint64(uint64_repeated_InOut request, ServerCallContext context) 83 | { 84 | var values = request.Values; 85 | for (int i = 0; i < values.Count; i++) 86 | { 87 | values[i]++; 88 | } 89 | return Task.FromResult(request); 90 | } 91 | 92 | public override Task Request_with_repeated_sint32(sint32_repeated_InOut request, ServerCallContext context) 93 | { 94 | var values = request.Values; 95 | for (int i = 0; i < values.Count; i++) 96 | { 97 | values[i]++; 98 | } 99 | return Task.FromResult(request); 100 | } 101 | 102 | public override Task Request_with_repeated_sint64(sint64_repeated_InOut request, ServerCallContext context) 103 | { 104 | var values = request.Values; 105 | for (int i = 0; i < values.Count; i++) 106 | { 107 | values[i]++; 108 | } 109 | return Task.FromResult(request); 110 | } 111 | 112 | public override Task Request_with_repeated_fixed32(fixed32_repeated_InOut request, ServerCallContext context) 113 | { 114 | var values = request.Values; 115 | for (int i = 0; i < values.Count; i++) 116 | { 117 | values[i]++; 118 | } 119 | return Task.FromResult(request); 120 | } 121 | 122 | public override Task Request_with_repeated_fixed64(fixed64_repeated_InOut request, ServerCallContext context) 123 | { 124 | var values = request.Values; 125 | for (int i = 0; i < values.Count; i++) 126 | { 127 | values[i]++; 128 | } 129 | return Task.FromResult(request); 130 | } 131 | 132 | public override Task Request_with_repeated_sfixed32(sfixed32_repeated_InOut request, ServerCallContext context) 133 | { 134 | var values = request.Values; 135 | for (int i = 0; i < values.Count; i++) 136 | { 137 | values[i]++; 138 | } 139 | return Task.FromResult(request); 140 | } 141 | 142 | public override Task Request_with_repeated_sfixed64(sfixed64_repeated_InOut request, ServerCallContext context) 143 | { 144 | var values = request.Values; 145 | for (int i = 0; i < values.Count; i++) 146 | { 147 | values[i]++; 148 | } 149 | return Task.FromResult(request); 150 | } 151 | 152 | public override Task Request_with_repeated_bool(bool_repeated_InOut request, ServerCallContext context) 153 | { 154 | var values = request.Values; 155 | for (int i = 0; i < values.Count; i++) 156 | { 157 | values[i] = !values[i]; 158 | } 159 | return Task.FromResult(request); 160 | } 161 | 162 | public override Task Request_with_repeated_string(string_repeated_InOut request, ServerCallContext context) 163 | { 164 | var values = request.Values; 165 | for (int i = 0; i < values.Count; i++) 166 | { 167 | values[i] = values[i] + 1; 168 | } 169 | return Task.FromResult(request); 170 | } 171 | 172 | 173 | public override Task Request_with_repeated_bytes(bytes_repeated_InOut request, ServerCallContext context) 174 | { 175 | var values = request.Values; 176 | return Task.FromResult(request); 177 | } 178 | 179 | public override Task Request_with_repeated_enum_type(enum_type_repeated_InOut request, ServerCallContext context) 180 | { 181 | var values = request.Values; 182 | for (int i = 0; i < values.Count; i++) 183 | { 184 | values[i]++; 185 | } 186 | return Task.FromResult(request); 187 | } 188 | 189 | public override Task Request_map_type(map_type_InOut request, ServerCallContext context) 190 | { 191 | request.Value.MapKeyInt32Values.Add(10, "test10"); 192 | request.Value.MapKeyInt64Values.Add(10, "test10"); 193 | request.Value.MapKeyUint32Values.Add(10, "test10"); 194 | request.Value.MapKeyUint64Values.Add(10, "test10"); 195 | request.Value.MapKeySint32Values.Add(10, "test10"); 196 | request.Value.MapKeySint64Values.Add(10, "test10"); 197 | request.Value.MapKeyFixed32Values.Add(10, "test10"); 198 | request.Value.MapKeyFixed64Values.Add(10, "test10"); 199 | request.Value.MapKeySfixed32Values.Add(10, "test10"); 200 | request.Value.MapKeySfixed64Values.Add(10, "test10"); 201 | request.Value.MapKeyBoolValues.Add(false, "test10"); 202 | request.Value.MapKeyStringValues.Add("hello10", "test10"); 203 | return Task.FromResult(request); 204 | } 205 | 206 | public override Task Request_with_repeated_map_type(map_type_repeated_InOut request, ServerCallContext context) 207 | { 208 | throw new NotImplementedException(); 209 | } 210 | 211 | public override Task Request_any_type(any_type_InOut request, ServerCallContext context) 212 | { 213 | if (request.Value.Instrument.Is(Currency.Descriptor)) 214 | { 215 | var message = request.Value.Instrument.Unpack().CurrencyMessage; 216 | request.Value.Instrument = Google.Protobuf.WellKnownTypes.Any.Pack(new Stock() { StockMessage = $"From currency: {message}" }); 217 | } 218 | else if (request.Value.Instrument.Is(Stock.Descriptor)) 219 | { 220 | var message = request.Value.Instrument.Unpack().StockMessage; 221 | request.Value.Instrument = Google.Protobuf.WellKnownTypes.Any.Pack(new Currency() { CurrencyMessage = $"From stock: {message}" }); 222 | } 223 | 224 | return Task.FromResult(request); 225 | } 226 | 227 | public override Task Request_with_repeated_any_type(any_type_repeated_InOut request, ServerCallContext context) 228 | { 229 | throw new NotImplementedException(); 230 | } 231 | 232 | public override Task Request_defaults_type(defaults_type_InOut request, ServerCallContext context) 233 | { 234 | return Task.FromResult(new defaults_type_InOut() { Value = new defaults_type() }); 235 | } 236 | } -------------------------------------------------------------------------------- /src/GrpcCurl.Tests/Proto/Primitives.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/any.proto"; 4 | 5 | package Primitives; 6 | 7 | service PrimitiveService { 8 | rpc Request_int32(int32_InOut) returns (int32_InOut); 9 | rpc Request_with_repeated_int32(int32_repeated_InOut) returns (int32_repeated_InOut); 10 | rpc Request_int64(int64_InOut) returns (int64_InOut); 11 | rpc Request_with_repeated_int64(int64_repeated_InOut) returns (int64_repeated_InOut); 12 | rpc Request_uint32(uint32_InOut) returns (uint32_InOut); 13 | rpc Request_with_repeated_uint32(uint32_repeated_InOut) returns (uint32_repeated_InOut); 14 | rpc Request_uint64(uint64_InOut) returns (uint64_InOut); 15 | rpc Request_with_repeated_uint64(uint64_repeated_InOut) returns (uint64_repeated_InOut); 16 | rpc Request_sint32(sint32_InOut) returns (sint32_InOut); 17 | rpc Request_with_repeated_sint32(sint32_repeated_InOut) returns (sint32_repeated_InOut); 18 | rpc Request_sint64(sint64_InOut) returns (sint64_InOut); 19 | rpc Request_with_repeated_sint64(sint64_repeated_InOut) returns (sint64_repeated_InOut); 20 | rpc Request_fixed32(fixed32_InOut) returns (fixed32_InOut); 21 | rpc Request_with_repeated_fixed32(fixed32_repeated_InOut) returns (fixed32_repeated_InOut); 22 | rpc Request_fixed64(fixed64_InOut) returns (fixed64_InOut); 23 | rpc Request_with_repeated_fixed64(fixed64_repeated_InOut) returns (fixed64_repeated_InOut); 24 | rpc Request_sfixed32(sfixed32_InOut) returns (sfixed32_InOut); 25 | rpc Request_with_repeated_sfixed32(sfixed32_repeated_InOut) returns (sfixed32_repeated_InOut); 26 | rpc Request_sfixed64(sfixed64_InOut) returns (sfixed64_InOut); 27 | rpc Request_with_repeated_sfixed64(sfixed64_repeated_InOut) returns (sfixed64_repeated_InOut); 28 | rpc Request_bool(bool_InOut) returns (bool_InOut); 29 | rpc Request_with_repeated_bool(bool_repeated_InOut) returns (bool_repeated_InOut); 30 | rpc Request_string(string_InOut) returns (string_InOut); 31 | rpc Request_with_repeated_string(string_repeated_InOut) returns (string_repeated_InOut); 32 | rpc Request_double(double_InOut) returns (double_InOut); 33 | rpc Request_with_repeated_double(double_repeated_InOut) returns (double_repeated_InOut); 34 | rpc Request_float(float_InOut) returns (float_InOut); 35 | rpc Request_with_repeated_float(float_repeated_InOut) returns (float_repeated_InOut); 36 | rpc Request_bytes(bytes_InOut) returns (bytes_InOut); 37 | rpc Request_with_repeated_bytes(bytes_repeated_InOut) returns (bytes_repeated_InOut); 38 | rpc Request_map_type(map_type_InOut) returns (map_type_InOut); 39 | rpc Request_with_repeated_map_type(map_type_repeated_InOut) returns (map_type_repeated_InOut); 40 | rpc Request_enum_type(enum_type_InOut) returns (enum_type_InOut); 41 | rpc Request_with_repeated_enum_type(enum_type_repeated_InOut) returns (enum_type_repeated_InOut); 42 | rpc Request_any_type(any_type_InOut) returns (any_type_InOut); 43 | rpc Request_with_repeated_any_type(any_type_repeated_InOut) returns (any_type_repeated_InOut); 44 | rpc Request_defaults_type(defaults_type_InOut) returns (defaults_type_InOut); 45 | rpc Request_with_repeated_defaults_type(defaults_type_repeated_InOut) returns (defaults_type_repeated_InOut); 46 | } 47 | 48 | message int32_InOut { 49 | int32 value = 1; 50 | } 51 | 52 | message int32_repeated_InOut { 53 | repeated int32 values = 1; 54 | } 55 | message int64_InOut { 56 | int64 value = 1; 57 | } 58 | 59 | message int64_repeated_InOut { 60 | repeated int64 values = 1; 61 | } 62 | message uint32_InOut { 63 | uint32 value = 1; 64 | } 65 | 66 | message uint32_repeated_InOut { 67 | repeated uint32 values = 1; 68 | } 69 | message uint64_InOut { 70 | uint64 value = 1; 71 | } 72 | 73 | message uint64_repeated_InOut { 74 | repeated uint64 values = 1; 75 | } 76 | message sint32_InOut { 77 | sint32 value = 1; 78 | } 79 | 80 | message sint32_repeated_InOut { 81 | repeated sint32 values = 1; 82 | } 83 | message sint64_InOut { 84 | sint64 value = 1; 85 | } 86 | 87 | message sint64_repeated_InOut { 88 | repeated sint64 values = 1; 89 | } 90 | message fixed32_InOut { 91 | fixed32 value = 1; 92 | } 93 | 94 | message fixed32_repeated_InOut { 95 | repeated fixed32 values = 1; 96 | } 97 | message fixed64_InOut { 98 | fixed64 value = 1; 99 | } 100 | 101 | message fixed64_repeated_InOut { 102 | repeated fixed64 values = 1; 103 | } 104 | message sfixed32_InOut { 105 | sfixed32 value = 1; 106 | } 107 | 108 | message sfixed32_repeated_InOut { 109 | repeated sfixed32 values = 1; 110 | } 111 | message sfixed64_InOut { 112 | sfixed64 value = 1; 113 | } 114 | 115 | message sfixed64_repeated_InOut { 116 | repeated sfixed64 values = 1; 117 | } 118 | message bool_InOut { 119 | bool value = 1; 120 | } 121 | 122 | message bool_repeated_InOut { 123 | repeated bool values = 1; 124 | } 125 | message string_InOut { 126 | string value = 1; 127 | } 128 | 129 | message string_repeated_InOut { 130 | repeated string values = 1; 131 | } 132 | message double_InOut { 133 | double value = 1; 134 | } 135 | 136 | message double_repeated_InOut { 137 | repeated double values = 1; 138 | } 139 | message float_InOut { 140 | float value = 1; 141 | } 142 | 143 | message float_repeated_InOut { 144 | repeated float values = 1; 145 | } 146 | message bytes_InOut { 147 | bytes value = 1; 148 | } 149 | 150 | message bytes_repeated_InOut { 151 | repeated bytes values = 1; 152 | } 153 | message map_type_InOut { 154 | map_type value = 1; 155 | } 156 | 157 | message map_type_repeated_InOut { 158 | repeated map_type values = 1; 159 | } 160 | message enum_type_InOut { 161 | enum_type value = 1; 162 | } 163 | 164 | message enum_type_repeated_InOut { 165 | repeated enum_type values = 1; 166 | } 167 | message any_type_InOut { 168 | any_type value = 1; 169 | } 170 | 171 | message any_type_repeated_InOut { 172 | repeated any_type values = 1; 173 | } 174 | message defaults_type_InOut { 175 | defaults_type value = 1; 176 | } 177 | 178 | message defaults_type_repeated_InOut { 179 | repeated defaults_type values = 1; 180 | } 181 | 182 | enum enum_type { 183 | UNIVERSAL = 0; 184 | WEB = 1; 185 | IMAGES = 2; 186 | LOCAL = 3; 187 | NEWS = 4; 188 | PRODUCTS = 5; 189 | VIDEO = 6; 190 | } 191 | 192 | message map_type { 193 | map map_key_int32_values = 1; 194 | map map_key_int64_values = 2; 195 | map map_key_uint32_values = 3; 196 | map map_key_uint64_values = 4; 197 | map map_key_sint32_values = 5; 198 | map map_key_sint64_values = 6; 199 | map map_key_fixed32_values = 7; 200 | map map_key_fixed64_values = 8; 201 | map map_key_sfixed32_values = 9; 202 | map map_key_sfixed64_values = 10; 203 | map map_key_bool_values = 11; 204 | map map_key_string_values = 12; 205 | } 206 | 207 | message Stock { 208 | string stock_message = 1; 209 | } 210 | 211 | message Currency { 212 | string currency_message = 1; 213 | } 214 | 215 | message any_type { 216 | google.protobuf.Any instrument = 1; 217 | } 218 | 219 | message defaults_type { 220 | int32 field_int32 = 1; 221 | int64 field_int64 = 2; 222 | uint32 field_uint32 = 3; 223 | uint64 field_uint64 = 4; 224 | sint32 field_sint32 = 5; 225 | sint64 field_sint64 = 6; 226 | fixed32 field_fixed32 = 7; 227 | fixed64 field_fixed64 = 8; 228 | sfixed32 field_sfixed32 = 9; 229 | sfixed64 field_sfixed64 = 10; 230 | bool field_bool = 11; 231 | string field_string = 12; 232 | bytes field_bytes = 13; 233 | enum_type field_enum_type = 14; 234 | } -------------------------------------------------------------------------------- /src/GrpcCurl.Tests/Proto/Primitives.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core" #> 3 | <#@ import namespace="System.Linq" #> 4 | <#@ import namespace="System.Text" #> 5 | <#@ import namespace="System.Collections.Generic" #> 6 | <#@ output extension=".proto" #> 7 | <# 8 | var scalarTypesForMapKey = new List() 9 | { 10 | "int32", 11 | "int64", 12 | "uint32", 13 | "uint64", 14 | "sint32", 15 | "sint64", 16 | "fixed32", 17 | "fixed64", 18 | "sfixed32", 19 | "sfixed64", 20 | "bool", 21 | "string", 22 | }; 23 | 24 | 25 | var allTypes = new List(scalarTypesForMapKey) 26 | { 27 | "double", 28 | "float", 29 | "bytes", 30 | "map_type", 31 | "enum_type", 32 | "any_type", 33 | "defaults_type", 34 | }; 35 | #> 36 | syntax = "proto3"; 37 | 38 | import "google/protobuf/any.proto"; 39 | 40 | package Primitives; 41 | 42 | service PrimitiveService { 43 | <# 44 | foreach (var type in allTypes) 45 | { 46 | #> 47 | rpc Request_<#= type #>(<#= type #>_InOut) returns (<#= type #>_InOut); 48 | rpc Request_with_repeated_<#= type #>(<#= type #>_repeated_InOut) returns (<#= type #>_repeated_InOut); 49 | <# 50 | } 51 | #> 52 | } 53 | 54 | <# 55 | foreach (var type in allTypes) 56 | { 57 | #> 58 | message <#= type #>_InOut { 59 | <#= type #> value = 1; 60 | } 61 | 62 | message <#= type #>_repeated_InOut { 63 | repeated <#= type #> values = 1; 64 | } 65 | <# 66 | } 67 | #> 68 | 69 | enum enum_type { 70 | UNIVERSAL = 0; 71 | WEB = 1; 72 | IMAGES = 2; 73 | LOCAL = 3; 74 | NEWS = 4; 75 | PRODUCTS = 5; 76 | VIDEO = 6; 77 | } 78 | 79 | message map_type { 80 | <# 81 | int number = 0; 82 | foreach (var type in scalarTypesForMapKey) 83 | { 84 | number++; 85 | #> 86 | map<<#= type #>, string> map_key_<#= type #>_values = <#= number #>; 87 | <# 88 | } 89 | #> 90 | } 91 | 92 | message Stock { 93 | string stock_message = 1; 94 | } 95 | 96 | message Currency { 97 | string currency_message = 1; 98 | } 99 | 100 | message any_type { 101 | google.protobuf.Any instrument = 1; 102 | } 103 | 104 | message defaults_type { 105 | int32 field_int32 = 1; 106 | int64 field_int64 = 2; 107 | uint32 field_uint32 = 3; 108 | uint64 field_uint64 = 4; 109 | sint32 field_sint32 = 5; 110 | sint64 field_sint64 = 6; 111 | fixed32 field_fixed32 = 7; 112 | fixed64 field_fixed64 = 8; 113 | sfixed32 field_sfixed32 = 9; 114 | sfixed64 field_sfixed64 = 10; 115 | bool field_bool = 11; 116 | string field_string = 12; 117 | bytes field_bytes = 13; 118 | enum_type field_enum_type = 14; 119 | } -------------------------------------------------------------------------------- /src/GrpcCurl.Tests/Proto/greet.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package greet; 4 | 5 | service Greeter { 6 | rpc SayHello (HelloRequest) returns (HelloReply); 7 | rpc SayHellos (HelloRequest) returns (stream HelloReply); 8 | } 9 | 10 | service SecondGreeter { 11 | rpc SayHello (HelloRequest) returns (HelloReply); 12 | rpc SayHellos (HelloRequest) returns (stream HelloReply); 13 | } 14 | 15 | message HelloRequest { 16 | string name = 1; 17 | } 18 | 19 | message HelloReply { 20 | string message = 1; 21 | } 22 | 23 | message DataMessage { 24 | bytes data = 1; 25 | } -------------------------------------------------------------------------------- /src/GrpcCurl.Tests/Proto/greet_proto2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package greet; 4 | 5 | service GreeterProto2 { 6 | rpc SayGroup (TryGroup) returns (TryGroup); 7 | } 8 | 9 | message TryGroup { 10 | required group Result = 1 { 11 | required string url = 2; 12 | required string title = 3; 13 | required string snippets = 4; 14 | } 15 | } -------------------------------------------------------------------------------- /src/GrpcCurl/GrpcCurl.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | net8.0 5 | enable 6 | enable 7 | grpc-curl 8 | $(AssemblyName) 9 | $(AssemblyName) 10 | ..\..\img\grpc-curl.ico 11 | grpc-curl is a command line tool for interacting with gRPC servers. 12 | true 13 | 14 | 15 | 16 | 17 | readme.md 18 | PreserveNewest 19 | 20 | 21 | 22 | license.txt 23 | PreserveNewest 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/GrpcCurl/GrpcCurlApp.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Diagnostics; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Text.Json.Nodes; 8 | using System.Text.Json.Serialization; 9 | using DynamicGrpc; 10 | using Grpc.Net.Client; 11 | using McMaster.Extensions.CommandLineUtils; 12 | namespace GrpcCurl; 13 | 14 | public class GrpcCurlApp 15 | { 16 | public static async Task Run(string[] args) 17 | { 18 | var exeName = "grpc-curl"; 19 | var version = typeof(Program).Assembly.GetCustomAttribute()?.InformationalVersion ?? typeof(Program).Assembly.GetCustomAttribute()?.Version ?? "?.?.?"; 20 | 21 | var app = new CommandLineApplication 22 | { 23 | Name = exeName, 24 | }; 25 | 26 | var options = new GrpcCurlOptions(); 27 | 28 | app.VersionOption("--version", $"{app.Name} {version} - {DateTime.Now.Year} (c) Copyright Alexandre Mutel", version); 29 | app.HelpOption(inherited: true); 30 | 31 | var addressArgument = app.Argument("address:port", @"A http/https URL or a simple host:address. If only host:address is used, HTTPS is used by default unless the options --http is passed.").IsRequired(); 32 | var serviceArgument = app.Argument("service/method", @"The service/method that will be called."); 33 | 34 | var dataOption = app.Option("-d|--data ", "JSON string to send as a message.", CommandOptionType.SingleValue); 35 | var httpOption = app.Option("--http", "Use HTTP instead of HTTPS unless the protocol is specified directly on the address.", CommandOptionType.NoValue); 36 | var jsonOption = app.Option("--json", "Use JSON naming for input and output.", CommandOptionType.NoValue); 37 | var describeOption = app.Option("--describe", "Describe the service or dump all services available.", CommandOptionType.NoValue); 38 | 39 | app.OnExecuteAsync(async (token) => 40 | { 41 | options.Address = addressArgument.Value!; 42 | options.ForceHttp = httpOption.ParsedValue; 43 | options.UseJsonNaming = jsonOption.ParsedValue; 44 | options.Describe = describeOption.ParsedValue; 45 | if (!options.Describe) 46 | options.Data = ParseJson(dataOption.ParsedValue); 47 | var serviceMethod = serviceArgument.Value; 48 | 49 | if (serviceMethod != null) 50 | { 51 | var indexOfSlash = serviceMethod.IndexOf('/'); 52 | if (!options.Describe && indexOfSlash < 0) throw new GrpcCurlException("Invalid symbol. The symbol must contain a slash (/) to separate the service from the method (serviceName/methodName)"); 53 | 54 | options.Service = indexOfSlash < 0 ? serviceMethod : serviceMethod.Substring(0, indexOfSlash); 55 | options.Method = indexOfSlash < 0 ? null : serviceMethod.Substring(indexOfSlash + 1); 56 | } 57 | 58 | return await Run(options); 59 | }); 60 | 61 | int result = 0; 62 | try 63 | { 64 | result = await app.ExecuteAsync(args); 65 | } 66 | catch (Exception exception) 67 | { 68 | string text = (exception is UnrecognizedCommandParsingException unrecognizedCommandParsingException) 69 | ? $"{unrecognizedCommandParsingException.Message} for command {unrecognizedCommandParsingException.Command.Name}" 70 | : $"Unexpected error {exception}"; 71 | await WriteLineError(text); 72 | result = 1; 73 | } 74 | 75 | return result; 76 | } 77 | 78 | private static async Task WriteLineError(string text) 79 | { 80 | var backColor = Console.ForegroundColor; 81 | Console.ForegroundColor = ConsoleColor.Red; 82 | await Console.Error.WriteLineAsync(text); 83 | Console.ForegroundColor = backColor; 84 | } 85 | 86 | public static async Task Run(GrpcCurlOptions options) 87 | { 88 | var httpAddress = options.Address.StartsWith("http") ? options.Address : $"{(options.ForceHttp?"http":"https")}://{options.Address}"; 89 | var channel = GrpcChannel.ForAddress(httpAddress); 90 | 91 | var client = await DynamicGrpcClient.FromServerReflection(channel, new DynamicGrpcClientOptions() 92 | { 93 | UseJsonNaming = options.UseJsonNaming 94 | }); 95 | 96 | // Describe 97 | if (options.Describe) 98 | { 99 | if (options.Service is null) 100 | { 101 | foreach (var file in client.Files) 102 | { 103 | file.ToProtoString(options.Writer, new DynamicGrpcPrinterOptions() { AddMetaComments = true }); 104 | await options.Writer.WriteLineAsync(); 105 | } 106 | } 107 | else 108 | { 109 | foreach (var file in client.Files) 110 | { 111 | var service = file.Services.FirstOrDefault(x => x.FullName == options.Service); 112 | if (service is not null) 113 | { 114 | service.ToProtoString(options.Writer, new DynamicGrpcPrinterOptions() { AddMetaComments = true }); 115 | await options.Writer.WriteLineAsync(); 116 | } 117 | } 118 | } 119 | 120 | return 0; 121 | } 122 | 123 | // Parse input from stdin if data was not passed by command line 124 | var data = options.Data; 125 | if (data is null) 126 | { 127 | if (Console.IsInputRedirected) 128 | { 129 | data = ParseJson(await Console.In.ReadToEndAsync()); 130 | } 131 | } 132 | data ??= new Dictionary(); 133 | 134 | 135 | Debug.Assert(options.Service is not null); 136 | Debug.Assert(options.Method is not null); 137 | if (!client.TryFindMethod(options.Service!, options.Method!, out var methodDescriptor)) 138 | { 139 | throw new GrpcCurlException($"Unable to find the method `{options.Service}/{options.Method}`"); 140 | } 141 | 142 | // Parse Input 143 | var input = new List>(); 144 | 145 | if (data is IEnumerable it) 146 | { 147 | int index = 0; 148 | foreach (var item in it) 149 | { 150 | 151 | if (item is IDictionary dict) 152 | { 153 | input.Add(dict); 154 | } 155 | else 156 | { 157 | throw new GrpcCurlException($"Invalid type `{item?.GetType()?.FullName}` from the input array at index [{index}]. Expecting an object."); 158 | } 159 | index++; 160 | } 161 | } 162 | else if (data is IDictionary dict) 163 | { 164 | input.Add(dict); 165 | } 166 | else 167 | { 168 | throw new GrpcCurlException($"Invalid type `{data?.GetType()?.FullName}` from the input. Expecting an object."); 169 | } 170 | 171 | // Perform the async call 172 | await foreach (var result in client.AsyncDynamicCall(options.Service, options.Method, ToAsync(input))) 173 | { 174 | OutputResult(options.Writer, result); 175 | } 176 | return 0; 177 | } 178 | 179 | private static async IAsyncEnumerable> ToAsync(IEnumerable> input) 180 | { 181 | foreach (var item in input) 182 | { 183 | yield return await ValueTask.FromResult(item); 184 | } 185 | } 186 | 187 | private static void OutputResult(TextWriter output, IDictionary result) 188 | { 189 | // Serialize the result back to the output 190 | var json = ToJson(result)!; 191 | var stream = new MemoryStream(); 192 | var utf8Writer = new Utf8JsonWriter(stream, new JsonWriterOptions() 193 | { 194 | SkipValidation = true, 195 | Indented = true 196 | }); 197 | json.WriteTo(utf8Writer, new JsonSerializerOptions() 198 | { 199 | NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, 200 | WriteIndented = true 201 | }); 202 | utf8Writer.Flush(); 203 | var jsonString = Encoding.UTF8.GetString(stream.ToArray()); 204 | output.WriteLine(jsonString); 205 | } 206 | 207 | 208 | private static object? ParseJson(string data) 209 | { 210 | try 211 | { 212 | var json = JsonNode.Parse(data); 213 | return ToApiRequest(json); 214 | } 215 | catch (Exception ex) 216 | { 217 | throw new GrpcCurlException($"Failing to deserialize JSON data. Reason: {ex.Message}."); 218 | } 219 | } 220 | 221 | private static object? ToApiRequest(JsonNode? requestObject) 222 | { 223 | switch (requestObject) 224 | { 225 | case JsonObject jObject: // objects become Dictionary 226 | return jObject.ToDictionary(j => j.Key, j => ToApiRequest(j.Value)); 227 | case JsonArray jArray: // arrays become List 228 | return jArray.Select(ToApiRequest).ToList(); 229 | case JsonValue jValue: // values just become the value 230 | return jValue.GetValue(); 231 | case null: 232 | return null; 233 | default: // don't know what to do here 234 | throw new Exception($"Unsupported type: {requestObject.GetType()}"); 235 | } 236 | } 237 | 238 | private static JsonNode? ToJson(object? requestObject) 239 | { 240 | switch (requestObject) 241 | { 242 | case int i32: return JsonValue.Create(i32); 243 | case uint u32: return JsonValue.Create(u32); 244 | case long i64: return JsonValue.Create(i64); 245 | case ulong u64: return JsonValue.Create(u64); 246 | case float f32: return JsonValue.Create(f32); 247 | case double f64: return JsonValue.Create(f64); 248 | case short i16: return JsonValue.Create(i16); 249 | case ushort u16: return JsonValue.Create(u16); 250 | case sbyte i8: return JsonValue.Create(i8); 251 | case byte u8: return JsonValue.Create(u8); 252 | case bool b: return JsonValue.Create(b); 253 | case string str: 254 | return JsonValue.Create(str); 255 | case IDictionary obj: // objects become Dictionary 256 | var jsonObject = new JsonObject(); 257 | foreach (var kp in obj) 258 | { 259 | jsonObject.Add(kp.Key, ToJson(kp.Value)); 260 | } 261 | return jsonObject; 262 | case IEnumerable array: // arrays become List 263 | var jsonArray = new JsonArray(); 264 | foreach (var o in array) 265 | { 266 | jsonArray.Add(ToJson(o)); 267 | } 268 | 269 | return jsonArray; 270 | default: // don't know what to do here 271 | return null; 272 | } 273 | } 274 | 275 | private class GrpcCurlException : Exception 276 | { 277 | public GrpcCurlException(string? message) : base(message) 278 | { 279 | } 280 | 281 | public string? AdditionalText { get; set; } 282 | } 283 | } -------------------------------------------------------------------------------- /src/GrpcCurl/GrpcCurlOptions.cs: -------------------------------------------------------------------------------- 1 | namespace GrpcCurl; 2 | 3 | public class GrpcCurlOptions 4 | { 5 | public GrpcCurlOptions() 6 | { 7 | Address = string.Empty; 8 | Writer = Console.Out; 9 | } 10 | 11 | public string Address { get; set; } 12 | 13 | public string? Service { get; set; } 14 | 15 | public string? Method { get; set; } 16 | 17 | public bool UseJsonNaming { get; set; } 18 | 19 | public bool Describe{ get; set; } 20 | 21 | public bool ForceHttp { get; set; } 22 | 23 | public object? Data { get; set; } 24 | 25 | public bool Verbose { get; set; } 26 | 27 | public TextWriter Writer { get; set; } 28 | } -------------------------------------------------------------------------------- /src/GrpcCurl/Program.cs: -------------------------------------------------------------------------------- 1 | using GrpcCurl; 2 | return await GrpcCurlApp.Run(args); -------------------------------------------------------------------------------- /src/common.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Alexandre Mutel 4 | en-US 5 | Alexandre Mutel 6 | gRPC;RPC;HTTP/2;tool 7 | readme.md 8 | https://github.com/xoofx/grpc-curl/blob/master/changelog.md 9 | grpc-curl.png 10 | https://github.com/xoofx/grpc-curl 11 | BSD-2-Clause 12 | true 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/dotnet-releaser.toml: -------------------------------------------------------------------------------- 1 | # config for dotnet-releaser 2 | [msbuild] 3 | project = "grpc-curl.sln" 4 | # Make the tool faster by compiling all references together 5 | properties.PublishReadyToRunComposite = true 6 | [github] 7 | user = "xoofx" 8 | repo = "grpc-curl" -------------------------------------------------------------------------------- /src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.100", 4 | "rollForward": "latestMinor", 5 | "allowPrerelease": false 6 | } 7 | } -------------------------------------------------------------------------------- /src/grpc-curl.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GrpcCurl", "GrpcCurl\GrpcCurl.csproj", "{5C55A0B4-D5EF-4CEA-A811-FC961242927D}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicGrpc", "DynamicGrpc\DynamicGrpc.csproj", "{3262C8EF-9D6A-44C5-867A-29ED36617D66}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GrpcCurl.Tests", "GrpcCurl.Tests\GrpcCurl.Tests.csproj", "{E7B5849C-09EA-4DCE-9E4A-76F984B7D408}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D6EF29F0-F4FB-4CE1-A930-512B34C6D3B1}" 13 | ProjectSection(SolutionItems) = preProject 14 | ..\changelog.md = ..\changelog.md 15 | ..\.github\workflows\ci.yml = ..\.github\workflows\ci.yml 16 | Directory.Build.props = Directory.Build.props 17 | Directory.Build.targets = Directory.Build.targets 18 | dotnet-releaser.toml = dotnet-releaser.toml 19 | global.json = global.json 20 | ..\license.txt = ..\license.txt 21 | ..\readme.md = ..\readme.md 22 | EndProjectSection 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {5C55A0B4-D5EF-4CEA-A811-FC961242927D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {5C55A0B4-D5EF-4CEA-A811-FC961242927D}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {5C55A0B4-D5EF-4CEA-A811-FC961242927D}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {5C55A0B4-D5EF-4CEA-A811-FC961242927D}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {3262C8EF-9D6A-44C5-867A-29ED36617D66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {3262C8EF-9D6A-44C5-867A-29ED36617D66}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {3262C8EF-9D6A-44C5-867A-29ED36617D66}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {3262C8EF-9D6A-44C5-867A-29ED36617D66}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {E7B5849C-09EA-4DCE-9E4A-76F984B7D408}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {E7B5849C-09EA-4DCE-9E4A-76F984B7D408}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {E7B5849C-09EA-4DCE-9E4A-76F984B7D408}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {E7B5849C-09EA-4DCE-9E4A-76F984B7D408}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {E827D658-0571-4F37-9826-377523B423D4} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /src/grpc-curl.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True --------------------------------------------------------------------------------