├── src
├── GrpcCurl
│ ├── Program.cs
│ ├── GrpcCurlOptions.cs
│ ├── GrpcCurl.csproj
│ └── GrpcCurlApp.cs
├── global.json
├── dotnet-releaser.toml
├── Directory.Build.targets
├── GrpcCurl.Tests
│ ├── Proto
│ │ ├── greet_proto2.proto
│ │ ├── greet.proto
│ │ ├── GreeterProto2Impl.cs
│ │ ├── GreeterServiceImpl.cs
│ │ ├── Primitives.tt
│ │ ├── Primitives.proto
│ │ └── PrimitiveServiceImpl.cs
│ ├── CurlTests.cs
│ ├── GrpcTestBase.cs
│ ├── GrpcCurl.Tests.csproj
│ └── BasicTests.cs
├── DynamicGrpc
│ ├── DynamicGrpcPrinterOptions.cs
│ ├── DynamicServiceDescriptor.cs
│ ├── DynamicGrpcClientException.cs
│ ├── DynamicGrpc.csproj
│ ├── DynamicAny.cs
│ ├── DynamicGrpcClientOptions.cs
│ ├── DynamicGrpcClientContext.cs
│ ├── DynamicFileDescriptorSet.cs
│ ├── DynamicGrpcClient.cs
│ ├── DynamicGrpcPrinter.cs
│ └── DynamicMessageSerializer.cs
├── grpc-curl.sln.DotSettings
├── Directory.Build.props
├── common.props
└── grpc-curl.sln
├── img
├── grpc-curl.ico
├── grpc-curl.png
├── github-banner.png
└── grpc-curl.svg
├── .gitattributes
├── .github
├── workflows
│ ├── nuget_org_only.config
│ └── ci.yml
└── FUNDING.yml
├── license.txt
├── changelog.md
├── readme.md
└── .gitignore
/src/GrpcCurl/Program.cs:
--------------------------------------------------------------------------------
1 | using GrpcCurl;
2 | return await GrpcCurlApp.Run(args);
--------------------------------------------------------------------------------
/img/grpc-curl.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xoofx/grpc-curl/HEAD/img/grpc-curl.ico
--------------------------------------------------------------------------------
/img/grpc-curl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xoofx/grpc-curl/HEAD/img/grpc-curl.png
--------------------------------------------------------------------------------
/img/github-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xoofx/grpc-curl/HEAD/img/github-banner.png
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto
3 | *.sh text eol=lf
--------------------------------------------------------------------------------
/src/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "8.0.100",
4 | "rollForward": "latestMinor",
5 | "allowPrerelease": false
6 | }
7 | }
--------------------------------------------------------------------------------
/.github/workflows/nuget_org_only.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/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/Directory.Build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | all
5 | runtime; build; native; contentfiles; analyzers
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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/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/grpc-curl.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
3 | True
--------------------------------------------------------------------------------
/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/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/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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 | }
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/img/grpc-curl.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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/
--------------------------------------------------------------------------------
/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/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/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