├── .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 |
--------------------------------------------------------------------------------
/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 [](https://github.com/xoofx/grpc-curl/actions) [](https://coveralls.io/github/xoofx/grpc-curl?branch=main) [](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