├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── documentation.md ├── screenshots.md └── screenshots │ ├── client_streaming.gif │ ├── codefirst_docs.png │ ├── duplex_streaming.gif │ ├── fx_service.gif │ ├── overview.png │ ├── protofirst_docs.png │ ├── server_streaming.gif │ └── unary.gif └── src ├── GrpcBrowser.sln ├── GrpcBrowser ├── App.razor ├── App.razor.cs ├── CommentProvider.cs ├── Components │ ├── AppTitleBar.razor │ ├── ClientStreamingOperationView.razor │ ├── ClientStreamingOperationView.razor.cs │ ├── DuplexOperationView.razor │ ├── DuplexOperationView.razor.cs │ ├── HeaderViewModel.cs │ ├── Pagination.razor │ ├── RequestResponseTypes.razor │ ├── ServerStreamingOperationView.razor │ ├── ServerStreamingOperationView.razor.cs │ ├── ServiceView.razor │ ├── ServiceView.razor.cs │ ├── UnaryOperationView.razor │ └── UnaryOperationView.razor.cs ├── Configuration │ ├── ConfiguredGrpcServices.cs │ └── StartupExtensions.cs ├── GrpcBrowser.csproj ├── Infrastructure │ ├── GrpcChannelUrlFinder.cs │ ├── GrpcClientHelpers.cs │ └── IsExternalInit.cs ├── Pages │ ├── Index.razor │ ├── Index.razor.cs │ └── _Host.cshtml ├── Properties │ └── launchSettings.json ├── Shared │ └── MainLayout.razor ├── Store │ ├── Requests │ │ ├── Actions.cs │ │ ├── Effects │ │ │ ├── ClientStreamingOperationEffects.cs │ │ │ ├── DuplexOperationEffects.cs │ │ │ ├── GrpcUtils.cs │ │ │ ├── ServerStreamingOperationEffects.cs │ │ │ └── UnaryOperationEffects.cs │ │ ├── Reducers.cs │ │ └── State.cs │ └── Services │ │ ├── Actions.cs │ │ ├── Model.cs │ │ ├── Reducers.cs │ │ └── State.cs ├── _Imports.razor ├── appsettings.Development.json ├── appsettings.json ├── icon.png └── wwwroot │ ├── css │ └── site.css │ └── favicon.ico └── Samples ├── FxService ├── Api │ ├── Account │ │ ├── AccountApi.cs │ │ └── account.proto │ └── Fx │ │ ├── Dto.cs │ │ └── FxApi.cs ├── Data │ ├── AccountRepository.cs │ └── FxRepository.cs ├── FxService.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json └── appsettings.json ├── SampleGrpcService.net50 ├── Program.cs ├── Properties │ └── launchSettings.json ├── Protos │ └── sample.proto ├── SampleGrpcService.net50.csproj ├── Services │ ├── CodeFirst │ │ └── CodeFirstService.cs │ └── ProtoFirst │ │ └── ProtoFirstSampleService.cs ├── Startup.cs ├── appsettings.Development.json └── appsettings.json ├── SampleGrpcService.net60 ├── Program.cs ├── Properties │ └── launchSettings.json ├── Protos │ └── sample.proto ├── SampleGrpcService.net60.csproj ├── Services │ ├── CodeFirst │ │ └── CodeFirstService.cs │ └── ProtoFirst │ │ └── ProtoFirstSampleService.cs ├── appsettings.Development.json └── appsettings.json └── SampleGrpcService.netcore31 ├── Program.cs ├── Properties └── launchSettings.json ├── Protos └── sample.proto ├── SampleGrpcService.netcore31.csproj ├── Services ├── CodeFirst │ └── CodeFirstService.cs └── ProtoFirst │ └── ProtoFirstSampleService.cs ├── Startup.cs ├── appsettings.Development.json └── appsettings.json /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | 12 | build: 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | working-directory: ./src 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v1 21 | with: 22 | dotnet-version: 6.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore 27 | - name: Test 28 | run: dotnet test --no-build --verbosity normal 29 | - name: Package 30 | if: github.ref == 'refs/heads/master' 31 | run: dotnet pack -c Release -o . GrpcBrowser/GrpcBrowser.csproj 32 | - name: Publish 33 | if: github.ref == 'refs/heads/master' 34 | run: dotnet nuget push *.nupkg -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json 35 | 36 | 37 | -------------------------------------------------------------------------------- /.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 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Nuget personal access tokens and Credentials 210 | # nuget.config 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio LightSwitch build output 301 | **/*.HTMLClient/GeneratedArtifacts 302 | **/*.DesktopClient/GeneratedArtifacts 303 | **/*.DesktopClient/ModelManifest.xml 304 | **/*.Server/GeneratedArtifacts 305 | **/*.Server/ModelManifest.xml 306 | _Pvt_Extensions 307 | 308 | # Paket dependency manager 309 | .paket/paket.exe 310 | paket-files/ 311 | 312 | # FAKE - F# Make 313 | .fake/ 314 | 315 | # CodeRush personal settings 316 | .cr/personal 317 | 318 | # Python Tools for Visual Studio (PTVS) 319 | __pycache__/ 320 | *.pyc 321 | 322 | # Cake - Uncomment if you are using it 323 | # tools/** 324 | # !tools/packages.config 325 | 326 | # Tabs Studio 327 | *.tss 328 | 329 | # Telerik's JustMock configuration file 330 | *.jmconfig 331 | 332 | # BizTalk build output 333 | *.btp.cs 334 | *.btm.cs 335 | *.odx.cs 336 | *.xsd.cs 337 | 338 | # OpenCover UI analysis results 339 | OpenCover/ 340 | 341 | # Azure Stream Analytics local run output 342 | ASALocalRun/ 343 | 344 | # MSBuild Binary and Structured Log 345 | *.binlog 346 | 347 | # NVidia Nsight GPU debugger configuration file 348 | *.nvuser 349 | 350 | # MFractors (Xamarin productivity tool) working folder 351 | .mfractor/ 352 | 353 | # Local History for Visual Studio 354 | .localhistory/ 355 | 356 | # BeatPulse healthcheck temp database 357 | healthchecksdb 358 | 359 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 360 | MigrationBackup/ 361 | 362 | # Ionide (cross platform F# VS Code tools) working folder 363 | .ionide/ 364 | 365 | # Fody - auto-generated XML schema 366 | FodyWeavers.xsd 367 | 368 | # VS Code files for those working on multiple tools 369 | .vscode/* 370 | !.vscode/settings.json 371 | !.vscode/tasks.json 372 | !.vscode/launch.json 373 | !.vscode/extensions.json 374 | *.code-workspace 375 | 376 | # Local History for Visual Studio Code 377 | .history/ 378 | 379 | # Windows Installer files from build outputs 380 | *.cab 381 | *.msi 382 | *.msix 383 | *.msm 384 | *.msp 385 | 386 | # JetBrains Rider 387 | .idea/ 388 | *.sln.iml 389 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gRPC Browser 2 | 3 | This project allows you to add a web-based gRPC Browser for debugging purposes to your .NET application. 4 | 5 | ![Example](docs/screenshots/fx_service.gif) 6 | 7 | [Click here to view more screenshots](docs/screenshots.md) 8 | 9 | ## Features 10 | 1. Allows you to view and execute gRPC services in your .NET application 11 | 1. Supports both code-first gRPC and proto-first gRPC (with `.proto` files) 12 | 2. Does not use gRPC reflection or gRPC web 13 | 3. Support for all types of gRPC operation - Unary, Server Streaming, Client Streaming and Duplex 14 | 4. Support for Metadata 15 | 5. Support for [displaying documentation](docs/documentation.md) 16 | 17 | ### Future Roadmap 18 | 1. OAuth 2.0 support 19 | 20 | ## Usage 21 | 1. Add the package `GrpcBrowser` from NuGet to your project. Your project SDK must be Microsoft.NET.Sdk.Web (see Troubleshooting for further details). 22 | 2. In the configure method of your Startup class, add `app.UseGrpcBrowser();` 23 | 3. In the configure method of your Startup class, where you call `endpoints.MapGrpcService()`, add the following: 24 | 1. For Code-First GRPC Services: `.AddToGrpcBrowserWithService()` 25 | 2. For Proto-First (where you have defined a `.proto` file): `.AddToGrpcBrowserWithClient()` 26 | 4. In the `UseEndpoints()` setup, add `endpoints.MapGrpcBrowser()` 27 | 28 | For example, the `Configure` method of a service with one proto-first and one code-first GRPC service could look like this: 29 | ```csharp 30 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 31 | { 32 | if (env.IsDevelopment()) 33 | { 34 | app.UseDeveloperExceptionPage(); 35 | } 36 | 37 | app.UseRouting(); 38 | 39 | app.UseGrpcBrowser(); 40 | 41 | app.UseEndpoints(endpoints => 42 | { 43 | endpoints.MapGrpcService().AddToGrpcBrowserWithClient(); 44 | endpoints.MapGrpcService().AddToGrpcBrowserWithService(); 45 | 46 | endpoints.MapGrpcBrowser(); 47 | }); 48 | } 49 | ``` 50 | 51 | 5. In the `ConfigureServices` method of your Startup class, add `services.AddGrpcBrowser()` 52 | 53 | 6. Start your service, and navigate to `/grpc` in your browser. 54 | 55 | [Click here for information on displaying documentation](docs/documentation.md) 56 | 57 | ## Troubleshooting 58 | 59 | Example projects for .NET Core 3.1, .NET 5.0 and .NET 6.0 can be found in the `/src` folder. 60 | 61 | ### Error when navigating to `/grpc` 62 | Make sure that your service SDK type is Microsoft.NET.Sdk.Web. This is required because GrpcBrowser adds server-side Blazor to your project. In the `.csproj` file, set the Project Sdk like this: `` 63 | 64 | ## Implementation Detail 65 | The service reflects over the types provided to `AddToGrpcBrowserWithClient` or `AddToGrpcBrowserWithClient` in order to determine the available operations, meaning that gRPC reflection is not required. Because we use server-side Blazor, the request execution is done from the server, meaning that gRPC web is not required. -------------------------------------------------------------------------------- /docs/documentation.md: -------------------------------------------------------------------------------- 1 | # Displaying Documentation 2 | Both code-first and proto-first services support documentation. To enable documentation, your project will need to be setup to output documentation as XML. This can be done by adding this to the project file: 3 | ```xml 4 | 5 | true 6 | 7 | ``` 8 | 9 | Once XML documentation has been generated, gRPC Browser will pick it up and display it. 10 | 11 | ## Examples 12 | 13 | ### Code-First 14 | 15 | A service interface with the following summary comments on the interface and method: 16 | 17 | ```csharp 18 | /// 19 | /// Place FX trade orders, stream and update FX rates. 20 | /// 21 | [Service] 22 | public interface IFxApi 23 | { 24 | /// 25 | /// Make an FX trade 26 | /// 27 | [Operation] 28 | Task PlaceFxOrder(FxOrder request, CallContext context = default); 29 | ``` 30 | 31 | Gets displayed like this: 32 | ![codefirst_docs](screenshots/codefirst_docs.png) 33 | 34 | ### Proto-First 35 | 36 | A proto file with the following comments above the service and operation: 37 | 38 | ```proto 39 | // Keeps track of the currency account balances 40 | service AccountService { 41 | // Streams out the state of the currency account balances after a change. Make a trade to receive an update message 42 | rpc SubscribeAccountBalances (AccountBalanceRequest) returns (stream AccountBalanceUpdate); 43 | } 44 | ``` 45 | 46 | Gets displayed like this: 47 | ![protofirst_docs](screenshots/protofirst_docs.png) -------------------------------------------------------------------------------- /docs/screenshots.md: -------------------------------------------------------------------------------- 1 | # gRPC Browser Screenshots 2 | 3 | ## Overview 4 | This is how gRPC Browser looks for the [example FxService](../src/Samples/FxService/). 5 | 6 | Each registered gRPC service is listed, and when expanded, each operation in that service is shown with the operation type and its name. 7 | 8 | ![Overview](docs/../screenshots/overview.png) 9 | 10 | Below are examples of how each operation type can be used in gRPC Browser. 11 | 12 | ## Unary 13 | ![Unary](screenshots/unary.gif) 14 | 15 | For a unary operation, the request body is shown and can be modified, and pressing execute will display the response body below. 16 | 17 | ## Server Streaming 18 | ![Server Streaming](screenshots/server_streaming.gif) 19 | 20 | For a server streaming operation, an initial request body is sent, and then a stream of responses are received from the server. Note that the first response may not necessarily come immediately, in which case no response body will be shown. 21 | 22 | ## Client Streaming 23 | 24 | ![Client Streaming](screenshots/client_streaming.gif) 25 | 26 | For a client streaming operation, the client (gRPC browser in this instance) sends a stream of messages to the service. A response from the service will only be received on disconnection, and in this example is an empty response. 27 | 28 | ## Duplex 29 | 30 | ![Duplex](screenshots/duplex_streaming.gif) 31 | 32 | For a duplex operation, both the client and the server stream messages to each other. Once connected, gRPC browser allows you to send messages to the service, and will display the latest streamed message from the service. -------------------------------------------------------------------------------- /docs/screenshots/client_streaming.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaswormald/grpc-browser/b521d35f24e05a1e3749af5b52e372a48d29d068/docs/screenshots/client_streaming.gif -------------------------------------------------------------------------------- /docs/screenshots/codefirst_docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaswormald/grpc-browser/b521d35f24e05a1e3749af5b52e372a48d29d068/docs/screenshots/codefirst_docs.png -------------------------------------------------------------------------------- /docs/screenshots/duplex_streaming.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaswormald/grpc-browser/b521d35f24e05a1e3749af5b52e372a48d29d068/docs/screenshots/duplex_streaming.gif -------------------------------------------------------------------------------- /docs/screenshots/fx_service.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaswormald/grpc-browser/b521d35f24e05a1e3749af5b52e372a48d29d068/docs/screenshots/fx_service.gif -------------------------------------------------------------------------------- /docs/screenshots/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaswormald/grpc-browser/b521d35f24e05a1e3749af5b52e372a48d29d068/docs/screenshots/overview.png -------------------------------------------------------------------------------- /docs/screenshots/protofirst_docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaswormald/grpc-browser/b521d35f24e05a1e3749af5b52e372a48d29d068/docs/screenshots/protofirst_docs.png -------------------------------------------------------------------------------- /docs/screenshots/server_streaming.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaswormald/grpc-browser/b521d35f24e05a1e3749af5b52e372a48d29d068/docs/screenshots/server_streaming.gif -------------------------------------------------------------------------------- /docs/screenshots/unary.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaswormald/grpc-browser/b521d35f24e05a1e3749af5b52e372a48d29d068/docs/screenshots/unary.gif -------------------------------------------------------------------------------- /src/GrpcBrowser.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31912.275 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleGrpcService.net50", "Samples\SampleGrpcService.net50\SampleGrpcService.net50.csproj", "{0A9F724C-C944-48EE-AD40-59792C41B3B7}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleGrpcService.netcore31", "Samples\SampleGrpcService.netcore31\SampleGrpcService.netcore31.csproj", "{968CC8A8-3989-4697-BB50-E47010FC8825}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GrpcBrowser", "GrpcBrowser\GrpcBrowser.csproj", "{4E304F54-626F-48A5-BAFF-ED169B6410E1}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleGrpcService.net60", "Samples\SampleGrpcService.net60\SampleGrpcService.net60.csproj", "{E2913EAD-E188-4F09-826C-D7A01462E15F}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FxService", "Samples\FxService\FxService.csproj", "{55AF171D-7551-405D-8D2A-DD5A11281FCD}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{9C2D266F-7FA6-47E1-9259-8562777CE962}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {0A9F724C-C944-48EE-AD40-59792C41B3B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {0A9F724C-C944-48EE-AD40-59792C41B3B7}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {0A9F724C-C944-48EE-AD40-59792C41B3B7}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {0A9F724C-C944-48EE-AD40-59792C41B3B7}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {968CC8A8-3989-4697-BB50-E47010FC8825}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {968CC8A8-3989-4697-BB50-E47010FC8825}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {968CC8A8-3989-4697-BB50-E47010FC8825}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {968CC8A8-3989-4697-BB50-E47010FC8825}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {4E304F54-626F-48A5-BAFF-ED169B6410E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {4E304F54-626F-48A5-BAFF-ED169B6410E1}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {4E304F54-626F-48A5-BAFF-ED169B6410E1}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {4E304F54-626F-48A5-BAFF-ED169B6410E1}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {E2913EAD-E188-4F09-826C-D7A01462E15F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {E2913EAD-E188-4F09-826C-D7A01462E15F}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {E2913EAD-E188-4F09-826C-D7A01462E15F}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {E2913EAD-E188-4F09-826C-D7A01462E15F}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {55AF171D-7551-405D-8D2A-DD5A11281FCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {55AF171D-7551-405D-8D2A-DD5A11281FCD}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {55AF171D-7551-405D-8D2A-DD5A11281FCD}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {55AF171D-7551-405D-8D2A-DD5A11281FCD}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(NestedProjects) = preSolution 49 | {0A9F724C-C944-48EE-AD40-59792C41B3B7} = {9C2D266F-7FA6-47E1-9259-8562777CE962} 50 | {968CC8A8-3989-4697-BB50-E47010FC8825} = {9C2D266F-7FA6-47E1-9259-8562777CE962} 51 | {E2913EAD-E188-4F09-826C-D7A01462E15F} = {9C2D266F-7FA6-47E1-9259-8562777CE962} 52 | {55AF171D-7551-405D-8D2A-DD5A11281FCD} = {9C2D266F-7FA6-47E1-9259-8562777CE962} 53 | EndGlobalSection 54 | GlobalSection(ExtensibilityGlobals) = postSolution 55 | SolutionGuid = {D3F6AC12-10F1-4985-BBB6-9B78DB7CEA5E} 56 | EndGlobalSection 57 | EndGlobal 58 | -------------------------------------------------------------------------------- /src/GrpcBrowser/App.razor: -------------------------------------------------------------------------------- 1 | @using GrpcBrowser.Configuration 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/GrpcBrowser/App.razor.cs: -------------------------------------------------------------------------------- 1 | namespace GrpcBrowser 2 | { 3 | public partial class App 4 | { 5 | public const int MaxTextFieldLines = 35; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/GrpcBrowser/CommentProvider.cs: -------------------------------------------------------------------------------- 1 | using GrpcBrowser.Store.Services; 2 | using System; 3 | using Namotion.Reflection; 4 | 5 | using System.Reflection; 6 | 7 | namespace GrpcBrowser 8 | { 9 | public static class CommentProvider 10 | { 11 | public static string GetServiceDescription(GrpcService service) 12 | { 13 | if (service.ImplementationType == GrpcServiceImplementationType.CodeFirst) 14 | { 15 | return service.ServiceType.GetXmlDocsSummary(); 16 | } 17 | else if (service.ImplementationType == GrpcServiceImplementationType.ProtoFile) 18 | { 19 | // The comment for the service gets put on the parent class of the client 20 | return service.ServiceType.DeclaringType.GetXmlDocsSummary(); 21 | } 22 | else 23 | { 24 | return string.Empty; 25 | } 26 | } 27 | 28 | public static string GetOperationDescription(GrpcOperation operation, GrpcService service) 29 | { 30 | MethodInfo? methodInfo = null; 31 | 32 | if (service.ImplementationType == GrpcServiceImplementationType.CodeFirst) 33 | { 34 | try 35 | { 36 | methodInfo = service.ServiceType.GetMethod(operation.Name); 37 | } 38 | catch (AmbiguousMatchException) 39 | { 40 | // ToDo handle this when multiple methods with the same name exist 41 | } 42 | } 43 | else if (service.ImplementationType == GrpcServiceImplementationType.ProtoFile) 44 | { 45 | methodInfo = 46 | service.ServiceType.GetMethod(operation.Name, new Type[] { typeof(Grpc.Core.CallOptions) }) 47 | ?? service.ServiceType.GetMethod(operation.Name, new Type[] { operation.RequestType, typeof(Grpc.Core.CallOptions) }); 48 | } 49 | 50 | if (methodInfo is null) 51 | { 52 | return string.Empty; 53 | } 54 | 55 | var docs = methodInfo.GetXmlDocsSummary(); 56 | 57 | return docs; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/AppTitleBar.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | gRPC Browser 4 | 5 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/ClientStreamingOperationView.razor: -------------------------------------------------------------------------------- 1 | @inherits Fluxor.Blazor.Web.Components.FluxorComponent 2 | 3 | 20 | 21 | @if (Operation is not null) 22 | { 23 |
24 | @CommentProvider.GetOperationDescription(Operation, Service) 25 |
26 |
27 | 28 |
29 | 30 |
31 |
Request Body
32 |
33 | 34 |
35 |
36 | 37 | 38 |
39 | @foreach (var header in _headers) 40 | { 41 |
42 | 43 | 44 | 45 |
46 | } 47 |
48 | 49 |
50 | Add Header 51 | @if (ConnectionState?.Connected ?? false) 52 | { 53 | Disconnect 54 | } 55 | 56 | Send Message 57 |
58 | 59 |
60 | @if (ConnectionState?.Connected ?? false) 61 | { 62 |
CONNECTED
63 | } 64 |
65 | 66 | @if (Response is not null) 67 | { 68 |
69 |
Response Body
70 | 71 | Download Response 72 | 73 |
74 | 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/ClientStreamingOperationView.razor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using AutoFixture; 8 | using AutoFixture.Kernel; 9 | using BlazorDownloadFile; 10 | using Fluxor; 11 | using GrpcBrowser.Store.Requests; 12 | using GrpcBrowser.Store.Services; 13 | using Microsoft.AspNetCore.Components; 14 | using Newtonsoft.Json; 15 | 16 | namespace GrpcBrowser.Components 17 | { 18 | public partial class ClientStreamingOperationView 19 | { 20 | [Inject] public IDispatcher? Dispatcher { get; set; } 21 | [Parameter] public GrpcService? Service { get; set; } 22 | [Parameter] public GrpcOperation? Operation { get; set; } 23 | [Inject] public IState? RequestState { get; set; } 24 | [Inject] IBlazorDownloadFileService BlazorDownloadFileService { get; set; } 25 | 26 | private string _requestJson = ""; 27 | private int _requestTextFieldLines = 5; 28 | private GrpcRequestId? _requestId = null; 29 | private ImmutableList _headers = ImmutableList.Empty; 30 | private int DisplayedRequestNumber 31 | { 32 | get => _displayedRequestNumber; 33 | set 34 | { 35 | _displayedRequestNumber = value; 36 | _requestJson = 37 | ConnectionState?.Requests[_displayedRequestNumber - 1] is null 38 | ? _requestJson 39 | : JsonConvert.SerializeObject(ConnectionState?.Requests[_displayedRequestNumber - 1].RequestBody, Formatting.Indented); 40 | StateHasChanged(); 41 | } 42 | } 43 | private int _displayedRequestNumber = 1; 44 | 45 | protected override void OnParametersSet() 46 | { 47 | var autoFixture = new Fixture(); 48 | autoFixture.Register(() => "string"); 49 | var randomInstanceOfRequestObject = 50 | autoFixture.Create(Operation.RequestType, new SpecimenContext(autoFixture)); 51 | 52 | _requestJson = JsonConvert.SerializeObject(randomInstanceOfRequestObject, Formatting.Indented); 53 | _requestTextFieldLines = Math.Min(_requestJson.Split('\n').Length, App.MaxTextFieldLines); 54 | 55 | base.OnParametersSet(); 56 | } 57 | 58 | private void SendMessage() 59 | { 60 | _requestId ??= new GrpcRequestId(Guid.NewGuid()); 61 | 62 | if (ConnectionState?.Connected ?? false) 63 | { 64 | Dispatcher?.Dispatch(new SendMessageToConnectedClientStreamingOperation(_requestId, Service, Operation, _requestJson, DateTimeOffset.Now)); 65 | } 66 | else 67 | { 68 | Dispatcher?.Dispatch(new CallClientStreamingOperation(Service, Operation, _requestJson, _requestId, new GrpcRequestHeaders(_headers.ToImmutableDictionary(h => h.Key, h => h.Value)), DateTimeOffset.Now)); 69 | } 70 | } 71 | 72 | private GrpcResponse? Response => ConnectionState?.Response; 73 | 74 | // This is a hack so that I can use the MudTextField to display the response 75 | private string? SerializedResponse 76 | { 77 | get => JsonConvert.SerializeObject(Response?.ResponseBody, Formatting.Indented); 78 | set { } 79 | } 80 | 81 | private int ResponseTextFieldLines 82 | { 83 | get => Math.Min(SerializedResponse?.Split('\n').Length ?? 5, App.MaxTextFieldLines); 84 | } 85 | 86 | private void AddHeader() 87 | { 88 | _headers = _headers.Add(new HeaderViewModel()); 89 | } 90 | 91 | private void RemoveHeader(HeaderViewModel header) 92 | { 93 | _headers = _headers.Remove(header); 94 | } 95 | 96 | private void Disconnect() 97 | { 98 | Dispatcher!.Dispatch(new StopClientStreamingOperation(_requestId!, Service, Operation)); 99 | } 100 | 101 | private ClientStreamingConnectionState? ConnectionState => 102 | _requestId is not null && 103 | RequestState!.Value.ClientStreamingRequests.TryGetValue(_requestId, out var streamingState) 104 | ? streamingState 105 | : null; 106 | 107 | record DownloadedClientStreamingOperationInformation(string Service, string Operation); 108 | record DownloadedClientStreamingConnectionInformation(ImmutableDictionary Headers); 109 | record DownloadedClientStreamingRequest(DateTimeOffset Timestamp, object Body); 110 | record DownloadedClientStreamingResponse(DateTimeOffset TimeStamp, object Body); 111 | record DownloadedClientStreamingDocument(DownloadedClientStreamingOperationInformation Operation, DownloadedClientStreamingConnectionInformation Connection, ImmutableList Requests, DownloadedClientStreamingResponse Response); 112 | 113 | private async Task Download() 114 | { 115 | var operation = new DownloadedClientStreamingOperationInformation( 116 | Service.ServiceType.Name, 117 | Operation.Name); 118 | 119 | var connection = new DownloadedClientStreamingConnectionInformation(ConnectionState.Headers.Values); 120 | 121 | var requests = 122 | ConnectionState.Requests.Select(request => new DownloadedClientStreamingRequest(request.TimeStamp, request.RequestBody)).OrderBy(r => r.Timestamp).ToImmutableList(); 123 | 124 | var response = 125 | ConnectionState.Response is not null 126 | ? new DownloadedClientStreamingResponse(ConnectionState.Response.TimeStamp, ConnectionState.Response.ResponseBody) 127 | : null; 128 | 129 | var document = new DownloadedClientStreamingDocument(operation, connection, requests, response); 130 | 131 | var documentJson = JsonConvert.SerializeObject(document, Formatting.Indented); 132 | 133 | await BlazorDownloadFileService.DownloadFileFromText($"{Operation.Name}-{DateTimeOffset.Now.Ticks}.json", documentJson, Encoding.UTF8, "text/plain", false); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/DuplexOperationView.razor: -------------------------------------------------------------------------------- 1 | @inherits Fluxor.Blazor.Web.Components.FluxorComponent 2 | 3 | 20 | 21 | @if (Operation is not null) 22 | { 23 |
24 | @CommentProvider.GetOperationDescription(Operation, Service) 25 |
26 |
27 | 28 |
29 | 30 |
31 |
Request Body
32 |
33 | 34 |
35 |
36 | 37 | 38 |
39 | @foreach (var header in _headers) 40 | { 41 |
42 | 43 | 44 | 45 |
46 | } 47 |
48 | 49 |
50 | Add Header 51 | @if (ConnectionState?.Connected ?? false) 52 | { 53 | Disconnect 54 | Send Message 55 | } 56 | else 57 | { 58 | Connect 59 | } 60 |
61 | 62 |
63 | @if (ConnectionState?.Connected ?? false) 64 | { 65 |
CONNECTED
66 | } 67 |
68 | 69 | @if (Response is not null) 70 | { 71 |
72 |
Response Body
73 | 74 | Download Response 75 | 76 |
77 | 78 |
79 |
80 | 81 | } 82 | } -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/DuplexOperationView.razor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using AutoFixture; 7 | using AutoFixture.Kernel; 8 | using BlazorDownloadFile; 9 | using Fluxor; 10 | using GrpcBrowser.Store.Requests; 11 | using GrpcBrowser.Store.Services; 12 | using Microsoft.AspNetCore.Components; 13 | using Newtonsoft.Json; 14 | 15 | namespace GrpcBrowser.Components 16 | { 17 | public partial class DuplexOperationView 18 | { 19 | [Inject] public IDispatcher? Dispatcher { get; set; } 20 | [Parameter] public GrpcService? Service { get; set; } 21 | [Parameter] public GrpcOperation? Operation { get; set; } 22 | [Inject] public IState? RequestState { get; set; } 23 | [Inject] IBlazorDownloadFileService BlazorDownloadFileService { get; set; } 24 | 25 | private string _requestJson = ""; 26 | private int _requestTextFieldLines = 5; 27 | private GrpcRequestId? _requestId = null; 28 | private ImmutableList _headers = ImmutableList.Empty; 29 | private int _displayedResponseNumber = 1; 30 | 31 | private int DisplayedRequestNumber 32 | { 33 | get => _displayedRequestNumber; 34 | set 35 | { 36 | _displayedRequestNumber = value; 37 | _requestJson = 38 | ConnectionState?.Requests[_displayedRequestNumber - 1] is null 39 | ? _requestJson 40 | : JsonConvert.SerializeObject(ConnectionState?.Requests[_displayedRequestNumber - 1].RequestBody, Formatting.Indented); 41 | StateHasChanged(); 42 | } 43 | } 44 | private int _displayedRequestNumber = 1; 45 | 46 | protected override void OnParametersSet() 47 | { 48 | var autoFixture = new Fixture(); 49 | autoFixture.Register(() => "string"); 50 | var randomInstanceOfRequestObject = 51 | autoFixture.Create(Operation.RequestType, new SpecimenContext(autoFixture)); 52 | 53 | _requestJson = JsonConvert.SerializeObject(randomInstanceOfRequestObject, Formatting.Indented); 54 | _requestTextFieldLines = Math.Min(_requestJson.Split('\n').Length, App.MaxTextFieldLines); 55 | 56 | base.OnParametersSet(); 57 | } 58 | 59 | private void SendMessage() 60 | { 61 | Dispatcher?.Dispatch(new SendMessageToConnectedDuplexOperation(_requestId, Service, Operation, _requestJson, DateTimeOffset.Now)); 62 | } 63 | 64 | private GrpcResponse? Response => ConnectionState?.Responses.Count >= _displayedResponseNumber ? ConnectionState?.Responses[_displayedResponseNumber - 1] : null; 65 | 66 | // This is a hack so that I can use the MudTextField to display the response 67 | private string? SerializedResponse 68 | { 69 | get => JsonConvert.SerializeObject(Response?.ResponseBody, Formatting.Indented); 70 | set { } 71 | } 72 | 73 | private int ResponseTextFieldLines 74 | { 75 | get => Math.Min(SerializedResponse?.Split('\n').Length ?? 5, App.MaxTextFieldLines); 76 | } 77 | 78 | private void AddHeader() 79 | { 80 | _headers = _headers.Add(new HeaderViewModel()); 81 | } 82 | 83 | private void RemoveHeader(HeaderViewModel header) 84 | { 85 | _headers = _headers.Remove(header); 86 | } 87 | 88 | private void Disconnect() 89 | { 90 | Dispatcher!.Dispatch(new StopDuplexOperation(_requestId!, Service, Operation)); 91 | } 92 | 93 | private void Connect() 94 | { 95 | _requestId ??= new GrpcRequestId(Guid.NewGuid()); 96 | Dispatcher?.Dispatch(new OpenDuplexConnection(Service, Operation, _requestId, new GrpcRequestHeaders(_headers.ToImmutableDictionary(h => h.Key, h => h.Value)))); 97 | } 98 | 99 | private DuplexConnectionState? ConnectionState => 100 | _requestId is not null && 101 | RequestState!.Value.DuplexRequests.TryGetValue(_requestId, out var streamingState) 102 | ? streamingState 103 | : null; 104 | 105 | record DownloadedDuplexOperationInformation(string Service, string Operation); 106 | record DownloadedDuplexConnectionInformation(ImmutableDictionary Headers); 107 | record DownloadedDuplexMessage(DateTimeOffset Timestamp, string Direction, object Body); 108 | record DownloadedDuplexResponse(DateTimeOffset TimeStamp, object Body); 109 | record DownloadedDuplexDocument(DownloadedDuplexOperationInformation Operation, DownloadedDuplexConnectionInformation Connection, ImmutableList Messages); 110 | 111 | private async Task Download() 112 | { 113 | var operation = new DownloadedDuplexOperationInformation( 114 | Service.ServiceType.Name, 115 | Operation.Name); 116 | 117 | var connection = new DownloadedDuplexConnectionInformation(ConnectionState.Headers.Values); 118 | 119 | var requests = 120 | ConnectionState.Requests.Select(request => new DownloadedDuplexMessage(request.TimeStamp, "ClientToServer", request.RequestBody)); 121 | 122 | var responses = 123 | ConnectionState.Responses.Select(response => new DownloadedDuplexMessage(response.TimeStamp, "ServerToClient", response.ResponseBody)); 124 | 125 | var document = new DownloadedDuplexDocument(operation, connection, requests.Concat(responses).OrderBy(r => r.Timestamp).ToImmutableList()); 126 | 127 | var documentJson = JsonConvert.SerializeObject(document, Formatting.Indented); 128 | 129 | await BlazorDownloadFileService.DownloadFileFromText($"{Operation.Name}-{DateTimeOffset.Now.Ticks}.json", documentJson, Encoding.UTF8, "text/plain", false); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/HeaderViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace GrpcBrowser.Components 2 | { 3 | public class HeaderViewModel 4 | { 5 | public string Key { get; set; } = ""; 6 | public string Value { get; set; } = ""; 7 | } 8 | } -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/Pagination.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | @SelectedMessage / @NumberOfMessages 4 | 5 | 6 | 7 | @code { 8 | private int _lastNumberOfMessages = 0; 9 | [Parameter] public int NumberOfMessages { get; set; } = 0; 10 | [Parameter] public int SelectedMessage { get; set; } = 1; 11 | [Parameter] public EventCallback SelectedMessageChanged { get; set; } 12 | [Parameter] public bool ShowLatestOnNewMessage { get; set; } = false; 13 | private bool _showingLatestResponse = true; 14 | 15 | private bool PreviousMessageButtonDisabled => SelectedMessage <= 1; 16 | private bool NextMessageButtonDisabled => SelectedMessage >= NumberOfMessages; 17 | private bool LatestMessageButtonDisabled => _showingLatestResponse; 18 | private async Task ShowFirst() 19 | { 20 | SelectedMessage = 1; 21 | await SelectedMessageChanged.InvokeAsync(SelectedMessage); 22 | _showingLatestResponse = false; 23 | StateHasChanged(); 24 | } 25 | private async Task ShowLatest() 26 | { 27 | SelectedMessage = NumberOfMessages; 28 | await SelectedMessageChanged.InvokeAsync(SelectedMessage); 29 | _showingLatestResponse = true; 30 | StateHasChanged(); 31 | } 32 | private async Task ShowPrevious() 33 | { 34 | SelectedMessage--; 35 | await SelectedMessageChanged.InvokeAsync(SelectedMessage); 36 | _showingLatestResponse = false; 37 | StateHasChanged(); 38 | } 39 | private async Task ShowNext() 40 | { 41 | SelectedMessage++; 42 | await SelectedMessageChanged.InvokeAsync(SelectedMessage); 43 | _showingLatestResponse = false; 44 | StateHasChanged(); 45 | } 46 | 47 | protected override async Task OnParametersSetAsync() 48 | { 49 | if (_showingLatestResponse && SelectedMessage != NumberOfMessages) 50 | { 51 | SelectedMessage = NumberOfMessages; 52 | await SelectedMessageChanged.InvokeAsync(SelectedMessage); 53 | } 54 | 55 | if (ShowLatestOnNewMessage && NumberOfMessages > _lastNumberOfMessages) 56 | { 57 | SelectedMessage = NumberOfMessages; 58 | await SelectedMessageChanged.InvokeAsync(SelectedMessage); 59 | } 60 | 61 | _lastNumberOfMessages = NumberOfMessages; 62 | 63 | if (SelectedMessage > NumberOfMessages) 64 | { 65 | SelectedMessage = NumberOfMessages; 66 | } 67 | 68 | StateHasChanged(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/RequestResponseTypes.razor: -------------------------------------------------------------------------------- 1 | @using GrpcBrowser.Store.Services 2 | @using ProtoBuf.Grpc.Internal 3 | @using static System.Reflection.BindingFlags 4 | 5 | 13 | 14 |
15 |
Request Type:
16 | @if (Operation.RequestType == typeof(Empty)) 17 | { 18 |
(None)
19 | } 20 | else 21 | { 22 |
@Operation.RequestType
23 | } 24 |
25 |
26 |
Response Type:
27 |
@Operation.ResponseType
28 |
29 | 30 | @code 31 | { 32 | [Parameter] public GrpcOperation Operation { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/ServerStreamingOperationView.razor: -------------------------------------------------------------------------------- 1 | @inherits Fluxor.Blazor.Web.Components.FluxorComponent 2 | 3 | 20 | 21 | @if (Operation is not null) 22 | { 23 |
24 | @CommentProvider.GetOperationDescription(Operation, Service) 25 |
26 |
27 | 28 |
29 | 30 |
Request Body
31 | 32 | 33 |
34 | @foreach (var header in _headers) 35 | { 36 |
37 | 38 | 39 | 40 |
41 | } 42 |
43 | 44 |
45 | Add Header 46 | @if (ConnectionState?.Connected ?? false) 47 | { 48 | Disconnect 49 | } 50 | else 51 | { 52 | Connect 53 | } 54 |
55 | 56 |
57 | @if (ConnectionState?.Connected ?? false) 58 | { 59 |
CONNECTED
60 | } 61 |
62 | 63 | @if (Response is not null) 64 | { 65 |
66 |
Response Body
67 | 68 | Download Response 69 | 70 |
71 | 72 |
73 |
74 | 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/ServerStreamingOperationView.razor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using AutoFixture; 7 | using AutoFixture.Kernel; 8 | using BlazorDownloadFile; 9 | using Fluxor; 10 | using GrpcBrowser.Store.Requests; 11 | using GrpcBrowser.Store.Services; 12 | using Microsoft.AspNetCore.Components; 13 | using Newtonsoft.Json; 14 | 15 | namespace GrpcBrowser.Components 16 | { 17 | public partial class ServerStreamingOperationView 18 | { 19 | [Inject] public IDispatcher? Dispatcher { get; set; } 20 | [Parameter] public GrpcService? Service { get; set; } 21 | [Parameter] public GrpcOperation? Operation { get; set; } 22 | [Inject] public IState? RequestState { get; set; } 23 | [Inject] IBlazorDownloadFileService BlazorDownloadFileService { get; set; } 24 | 25 | private string _requestJson = ""; 26 | private int _requestTextFieldLines = 5; 27 | private GrpcRequestId? _requestId = null; 28 | private ImmutableList _headers = ImmutableList.Empty; 29 | private int _displayedResponseNumber = 1; 30 | 31 | protected override void OnParametersSet() 32 | { 33 | var autoFixture = new Fixture(); 34 | autoFixture.Register(() => "string"); 35 | var randomInstanceOfRequestObject = 36 | autoFixture.Create(Operation.RequestType, new SpecimenContext(autoFixture)); 37 | 38 | _requestJson = JsonConvert.SerializeObject(randomInstanceOfRequestObject, Formatting.Indented); 39 | _requestTextFieldLines = Math.Min(_requestJson.Split('\n').Length, App.MaxTextFieldLines); 40 | 41 | base.OnParametersSet(); 42 | } 43 | 44 | private void Connect() 45 | { 46 | _requestId ??= new GrpcRequestId(Guid.NewGuid()); 47 | Dispatcher?.Dispatch(new CallServerStreamingOperation(Service, Operation, _requestJson, _requestId, new GrpcRequestHeaders(_headers.ToImmutableDictionary(h => h.Key, h => h.Value)), DateTimeOffset.Now)); 48 | } 49 | 50 | private GrpcResponse? Response => ConnectionState?.Responses.Count >= _displayedResponseNumber ? ConnectionState?.Responses[_displayedResponseNumber - 1] : null; 51 | 52 | // This is a hack so that I can use the MudTextField to display the response 53 | private string? SerializedResponse 54 | { 55 | get => JsonConvert.SerializeObject(Response?.ResponseBody, Formatting.Indented); 56 | set { } 57 | } 58 | 59 | private int ResponseTextFieldLines 60 | { 61 | get => Math.Min(SerializedResponse?.Split('\n').Length ?? 5, App.MaxTextFieldLines); 62 | } 63 | 64 | private void AddHeader() 65 | { 66 | _headers = _headers.Add(new HeaderViewModel()); 67 | } 68 | 69 | private void RemoveHeader(HeaderViewModel header) 70 | { 71 | _headers = _headers.Remove(header); 72 | } 73 | 74 | private void Disconnect() 75 | { 76 | Dispatcher!.Dispatch(new StopServerStreamingConnection(_requestId!)); 77 | } 78 | 79 | private ServerStreamingConnectionState? ConnectionState => 80 | _requestId is not null && 81 | RequestState!.Value.ServerStreamingRequests.TryGetValue(_requestId, out var streamingState) 82 | ? streamingState 83 | : null; 84 | 85 | record DownloadedServerStreamingOperationInformation(string Service, string Operation); 86 | record DownloadedServerStreamingResponse(DateTimeOffset TimeStamp, object ResponseBody); 87 | record DownloadedServerStreamingRequest(DateTimeOffset Timestamp, ImmutableDictionary Headers, object Body); 88 | record DownloadedServerStreamingDocument(DownloadedServerStreamingOperationInformation Operation, DownloadedServerStreamingRequest Request, ImmutableList Responses); 89 | 90 | private async Task Download() 91 | { 92 | var operation = new DownloadedServerStreamingOperationInformation( 93 | ConnectionState.RequestAction.Service.ServiceType.Name, 94 | ConnectionState.RequestAction.Operation.Name); 95 | 96 | var request = new DownloadedServerStreamingRequest( 97 | ConnectionState.RequestAction.Timestamp, 98 | ConnectionState.RequestAction.Headers.Values, 99 | ConnectionState.Request); 100 | 101 | var responses = ConnectionState!.Responses.Select(response => new DownloadedServerStreamingResponse(response.TimeStamp, response.ResponseBody)).OrderBy(r => r.TimeStamp).ToImmutableList(); 102 | 103 | var document = new DownloadedServerStreamingDocument(operation, request, responses); 104 | 105 | 106 | var documentJson = JsonConvert.SerializeObject(document, Formatting.Indented); 107 | 108 | await BlazorDownloadFileService.DownloadFileFromText($"{ConnectionState.RequestAction.Operation.Name}-{ConnectionState.RequestAction.Timestamp.Ticks}.json", documentJson, Encoding.UTF8, "text/plain", false); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/ServiceView.razor: -------------------------------------------------------------------------------- 1 | @using GrpcBrowser.Store.Services 2 | @inherits Fluxor.Blazor.Web.Components.FluxorComponent 3 | 4 | 80 | 81 | @if (Service is not null) 82 | { 83 | 84 | 85 | @foreach (var endpoint in Service.Endpoints.OrderBy(e => e.Key)) 86 | { 87 | @if (endpoint.Value.Type == GrpcOperationType.Unary) 88 | { 89 | 90 | 91 |
92 |
UNARY
93 |
@endpoint.Value.Name
94 |
95 |
96 | 97 | 98 | 99 |
100 | } 101 | else if (endpoint.Value.Type == GrpcOperationType.ServerStreaming) 102 | { 103 | 104 | 105 |
106 |
SERVER STREAMING
107 |
@endpoint.Value.Name
108 |
109 |
110 | 111 | 112 | 113 |
114 | } 115 | else if (endpoint.Value.Type == GrpcOperationType.ClientStreaming) 116 | { 117 | 118 | 119 |
120 |
CLIENT STREAMING
121 |
@endpoint.Value.Name
122 |
123 |
124 | 125 | 126 | 127 |
128 | } 129 | else if (endpoint.Value.Type == GrpcOperationType.Duplex) 130 | { 131 | 132 | 133 |
134 |
DUPLEX
135 |
@endpoint.Value.Name
136 |
137 |
138 | 139 | 140 | 141 |
142 | } 143 | } 144 |
145 | } -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/ServiceView.razor.cs: -------------------------------------------------------------------------------- 1 | using GrpcBrowser.Store.Services; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace GrpcBrowser.Components 5 | { 6 | public partial class ServiceView 7 | { 8 | [Parameter] public GrpcService? Service { get; set; } 9 | 10 | // TODO For some reason when applied as a css class this is all ignored, but as a style it works fine 11 | public const string jsonTextBoxStyle = "font-family: monospace; font-size: 12px; color: white; background-color: rgba(51, 51, 51, 1)"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/UnaryOperationView.razor: -------------------------------------------------------------------------------- 1 | @using GrpcBrowser.Store.Services 2 | @using ProtoBuf.Grpc.Internal 3 | @inherits Fluxor.Blazor.Web.Components.FluxorComponent 4 | 5 | 12 | 13 | @if (Operation is not null) 14 | { 15 |
16 | @CommentProvider.GetOperationDescription(Operation, Service) 17 |
18 | 19 |
20 | 21 |
22 | 23 | @if (@Operation.RequestType != typeof(Empty)) 24 | { 25 |
Request Body
26 | 27 | } 28 | 29 |
30 | @foreach (var header in _headers) 31 | { 32 |
33 | 34 | 35 | 36 |
37 | } 38 |
39 | 40 |
41 | Add Header 42 | Execute 43 |
44 | @if (UnaryRequestState is not null) 45 | { 46 |
47 |
Response Body
48 | 49 | Download Response 50 | 51 |
52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Components/UnaryOperationView.razor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Text; 4 | using AutoFixture; 5 | using AutoFixture.Kernel; 6 | using Fluxor; 7 | using GrpcBrowser.Store.Requests; 8 | using GrpcBrowser.Store.Services; 9 | using Microsoft.AspNetCore.Components; 10 | using Newtonsoft.Json; 11 | using System.Threading.Tasks; 12 | using BlazorDownloadFile; 13 | using ProtoBuf.Grpc.Internal; 14 | 15 | namespace GrpcBrowser.Components 16 | { 17 | public partial class UnaryOperationView 18 | { 19 | [Inject] public IDispatcher? Dispatcher { get; set; } 20 | [Parameter] public GrpcService? Service { get; set; } 21 | [Parameter] public GrpcOperation? Operation { get; set; } 22 | [Inject] public IState? RequestState { get; set; } 23 | [Inject] IBlazorDownloadFileService BlazorDownloadFileService { get; set; } 24 | 25 | private string _requestJson = ""; 26 | private int _requestTextFieldLines = 5; 27 | private GrpcRequestId? _requestId = null; 28 | private ImmutableList _headers = ImmutableList.Empty; 29 | 30 | protected override void OnParametersSet() 31 | { 32 | var autoFixture = new Fixture(); 33 | autoFixture.Register(() => "string"); 34 | autoFixture.Register(() => null); 35 | var randomInstanceOfRequestObject = 36 | autoFixture.Create(Operation.RequestType, new SpecimenContext(autoFixture)); 37 | 38 | _requestJson = JsonConvert.SerializeObject(randomInstanceOfRequestObject, Formatting.Indented); 39 | _requestTextFieldLines = Math.Min(_requestJson.Split('\n').Length, App.MaxTextFieldLines); 40 | 41 | base.OnParametersSet(); 42 | } 43 | 44 | private void Execute() 45 | { 46 | _requestId ??= new GrpcRequestId(Guid.NewGuid()); 47 | Dispatcher?.Dispatch(new CallUnaryOperation(Service, Operation, _requestJson, _requestId, new GrpcRequestHeaders(_headers.ToImmutableDictionary(h => h.Key, h => h.Value)), DateTimeOffset.Now)); 48 | } 49 | 50 | private UnaryRequestState? UnaryRequestState => _requestId is not null && RequestState is not null && RequestState.Value.UnaryRequests.TryGetValue(_requestId, out var unaryRequest) ? unaryRequest : null; 51 | 52 | // This is a hack so that I can use the MudTextField to display the response 53 | private string? SerializedResponse 54 | { 55 | get => JsonConvert.SerializeObject(UnaryRequestState?.Response.ResponseBody, Formatting.Indented); 56 | set { } 57 | } 58 | 59 | private int ResponseTextFieldLines 60 | { 61 | get => Math.Min(SerializedResponse?.Split('\n').Length ?? 5, App.MaxTextFieldLines); 62 | } 63 | 64 | private void AddHeader() 65 | { 66 | _headers = _headers.Add(new HeaderViewModel()); 67 | } 68 | 69 | private void RemoveHeader(HeaderViewModel header) 70 | { 71 | _headers = _headers.Remove(header); 72 | } 73 | 74 | record DownloadedUnaryOperationInformation(string Service, string Operation); 75 | record DownloadedUnaryRequest(DateTimeOffset Timestamp, ImmutableDictionary Headers, object Body); 76 | record DownloadedUnaryResponse(DateTimeOffset Timestamp, object Response); 77 | record DownloadedUnaryDocument(DownloadedUnaryOperationInformation Operation, DownloadedUnaryRequest Request, DownloadedUnaryResponse Response); 78 | 79 | private async Task Download() 80 | { 81 | var operation = new DownloadedUnaryOperationInformation( 82 | UnaryRequestState.RequestAction.Service.ServiceType.Name, 83 | UnaryRequestState.RequestAction.Operation.Name); 84 | 85 | var request = new DownloadedUnaryRequest( 86 | UnaryRequestState.RequestAction.Timestamp, 87 | UnaryRequestState.RequestAction.Headers.Values, 88 | UnaryRequestState.Request); 89 | 90 | var response = new DownloadedUnaryResponse( 91 | UnaryRequestState.Response.TimeStamp, 92 | UnaryRequestState.Response.ResponseBody); 93 | 94 | var document = new DownloadedUnaryDocument(operation, request, response); 95 | 96 | var documentJson = JsonConvert.SerializeObject(document, Formatting.Indented); 97 | 98 | await BlazorDownloadFileService.DownloadFileFromText($"{UnaryRequestState.RequestAction.Operation.Name}-{UnaryRequestState.Response.TimeStamp.Ticks}.json", documentJson, Encoding.UTF8, "text/plain", false); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Configuration/ConfiguredGrpcServices.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ServiceModel; 4 | using ProtoBuf.Grpc.Configuration; 5 | 6 | namespace GrpcBrowser.Configuration 7 | { 8 | internal static class ConfiguredGrpcServices 9 | { 10 | public static List ProtoGrpcClients = new List(); 11 | public static List CodeFirstGrpcServiceInterfaces = new List(); 12 | } 13 | 14 | public static class GrpcBrowser 15 | { 16 | public static void AddProtoFirstService() where TClient : Grpc.Core.ClientBase 17 | { 18 | ConfiguredGrpcServices.ProtoGrpcClients.Add(typeof(TClient)); 19 | } 20 | 21 | public static void AddCodeFirstService() 22 | { 23 | if (!typeof(TServiceInterface).IsInterface) 24 | { 25 | throw new ArgumentException( 26 | $"To add your code-first service to GrpcBrowser, you must provide your GRPC service's interface. '{typeof(TServiceInterface).Name}' is not an interface"); 27 | } 28 | 29 | if (!typeof(TServiceInterface).IsDefined(typeof(ServiceAttribute), false) && 30 | !typeof(TServiceInterface).IsDefined(typeof(ServiceContractAttribute), false)) 31 | { 32 | throw new ArgumentException( 33 | $"To add your code-first service to GrpcBrowser, you must provide your GRPC service's interface. '{typeof(TServiceInterface).Name}' does not have the attribute '{nameof(ServiceAttribute)}' or '{nameof(ServiceContractAttribute)}', which is required for code-first GRPC services"); 34 | } 35 | 36 | ConfiguredGrpcServices.CodeFirstGrpcServiceInterfaces.Add(typeof(TServiceInterface)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Configuration/StartupExtensions.cs: -------------------------------------------------------------------------------- 1 | using Fluxor; 2 | using GrpcBrowser.Infrastructure; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using MudBlazor.Services; 6 | using ProtoBuf.Grpc.Configuration; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.ServiceModel; 10 | using Microsoft.AspNetCore.Routing; 11 | using BlazorDownloadFile; 12 | 13 | namespace GrpcBrowser.Configuration 14 | { 15 | public static class StartupExtensions 16 | { 17 | public static void AddGrpcBrowser(this IServiceCollection services) 18 | { 19 | services.AddRazorPages(); 20 | services.AddServerSideBlazor(); 21 | services.AddMudServices(); 22 | services.AddScoped(); 23 | services.AddFluxor(o => o 24 | .ScanAssemblies(typeof(StartupExtensions).Assembly) 25 | .UseReduxDevTools()); 26 | services.AddBlazorDownloadFile(ServiceLifetime.Scoped); 27 | } 28 | 29 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 30 | public static void UseGrpcBrowser(this IApplicationBuilder app) 31 | { 32 | app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/grpc"), tools => 33 | { 34 | app.UseStaticFiles("/grpc"); 35 | }); 36 | 37 | app.UseStaticFiles(); 38 | } 39 | 40 | public static void MapGrpcBrowser(this IEndpointRouteBuilder endpoints) 41 | { 42 | endpoints.MapBlazorHub("/grpc/_blazor"); 43 | endpoints.MapFallbackToPage("/grpc/{*path:nonfile}", "/_Host"); 44 | endpoints.MapFallbackToPage("/grpc", "/_Host"); 45 | } 46 | 47 | public static GrpcServiceEndpointConventionBuilder AddToGrpcBrowserWithClient(this GrpcServiceEndpointConventionBuilder builder) where TClient : Grpc.Core.ClientBase 48 | { 49 | GrpcBrowser.AddProtoFirstService(); 50 | 51 | return builder; 52 | } 53 | 54 | public static GrpcServiceEndpointConventionBuilder AddToGrpcBrowserWithService(this GrpcServiceEndpointConventionBuilder builder) 55 | { 56 | GrpcBrowser.AddCodeFirstService(); 57 | 58 | return builder; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/GrpcBrowser/GrpcBrowser.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | /grpc 6 | Library 7 | 10 8 | true 9 | 1.3.3 10 | Thomas Wormald 11 | Extends your application with a web UI for browsing and executing gRPC operations 12 | grpc;browser;rpc; 13 | MIT 14 | https://github.com/thomaswormald/grpc-browser 15 | icon.png 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Infrastructure/GrpcChannelUrlFinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace GrpcBrowser.Infrastructure 5 | { 6 | public class GrpcChannelUrlProvider 7 | { 8 | public string BaseUrl { get; private set; } 9 | 10 | public GrpcChannelUrlProvider(NavigationManager navManager) 11 | { 12 | BaseUrl = navManager.BaseUri; 13 | 14 | if (BaseUrl.EndsWith("/grpc/")) 15 | { 16 | BaseUrl = BaseUrl.Substring(0, BaseUrl.Length - "/grpc/".Length); 17 | } 18 | 19 | Console.WriteLine("Base URL: " + BaseUrl); 20 | 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Infrastructure/GrpcClientHelpers.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf.Grpc.Client; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using ProtoBuf.Grpc; 9 | using ProtoBuf.Grpc.Internal; 10 | 11 | namespace GrpcBrowser.Infrastructure 12 | { 13 | public static class GrpcClientHelpers 14 | { 15 | // This class is required because the GrpcClient methods that we need are generic, but we don't have the types available to call them 16 | 17 | public static Task UnaryAsync(this GrpcClient client, object request, string operationName, Type requestType, Type responseType, CallContext callContext = default) 18 | { 19 | var methodInfo = 20 | typeof(GrpcClient).GetMethods() 21 | .Where(method => method.Name == nameof(GrpcClient.UnaryAsync)) 22 | .Single(method => method.GetParameters()[1].ParameterType == typeof(string)); 23 | 24 | if (responseType == typeof(Task)) 25 | { 26 | // Handle case of Task with no return type 27 | responseType = typeof(Empty); 28 | } 29 | 30 | var genericMethodInfo = methodInfo.MakeGenericMethod(requestType, responseType); 31 | 32 | return genericMethodInfo.InvokeAsync(client, request, operationName, callContext); 33 | } 34 | 35 | public static IAsyncEnumerable ServerStreamingAsync(this GrpcClient client, object request, string operationName, Type requestType, Type responseType, CallContext callContext = default) 36 | { 37 | var methodInfo = 38 | typeof(GrpcClient).GetMethods() 39 | .Where(method => method.Name == nameof(GrpcClient.ServerStreamingAsync)) 40 | .Single(method => method.GetParameters()[1].ParameterType == typeof(string)); 41 | 42 | var genericMethodInfo = methodInfo.MakeGenericMethod(requestType, responseType); 43 | 44 | var result = genericMethodInfo.Invoke(client, new[] { request, operationName, callContext }); 45 | 46 | return (IAsyncEnumerable)result; 47 | } 48 | 49 | public static Task ClientStreamingAsync(this GrpcClient client, IAsyncEnumerable requestStream, string operationName, Type requestType, Type responseType, CallContext callContext = default) 50 | { 51 | var methodInfo = 52 | typeof(GrpcClient).GetMethods() 53 | .Where(method => method.Name == nameof(GrpcClient.ClientStreamingAsync)) 54 | .Single(method => method.GetParameters()[1].ParameterType == typeof(string)); 55 | 56 | var genericMethodInfo = methodInfo.MakeGenericMethod(requestType, responseType); 57 | 58 | var castingEnumerableType = typeof(CastingAsyncEnumerable<>).MakeGenericType(requestType); 59 | 60 | var requestStreamCorrectGeneric = Activator.CreateInstance(castingEnumerableType, requestStream, callContext.CancellationToken); 61 | 62 | return (Task)genericMethodInfo.Invoke(client, new object[] { requestStreamCorrectGeneric, operationName, callContext }); 63 | } 64 | 65 | public static IAsyncEnumerable DuplexAsync(this GrpcClient client, IAsyncEnumerable requestStream, string operationName, Type requestType, Type responseType, CallContext callContext = default) 66 | { 67 | var methodInfo = 68 | typeof(GrpcClient).GetMethods() 69 | .Where(method => method.Name == nameof(GrpcClient.DuplexStreamingAsync)) 70 | .Single(method => method.GetParameters()[1].ParameterType == typeof(string)); 71 | 72 | var genericMethodInfo = methodInfo.MakeGenericMethod(requestType, responseType); 73 | 74 | var castingEnumerableType = typeof(CastingAsyncEnumerable<>).MakeGenericType(requestType); 75 | 76 | var requestStreamCorrectGeneric = Activator.CreateInstance(castingEnumerableType, requestStream, callContext.CancellationToken); 77 | 78 | return (IAsyncEnumerable)genericMethodInfo.Invoke(client, new object[] { requestStreamCorrectGeneric, operationName, callContext }); 79 | } 80 | 81 | private static async Task InvokeAsync(this MethodInfo @this, object obj, params object[] parameters) 82 | { 83 | var task = (Task)@this.Invoke(obj, parameters); 84 | await task.ConfigureAwait(false); 85 | var resultProperty = task.GetType().GetProperty("Result"); 86 | return resultProperty.GetValue(task); 87 | } 88 | } 89 | 90 | class CastingAsyncEnumerable : IAsyncEnumerable 91 | { 92 | private readonly IAsyncEnumerable _inner; 93 | private readonly IAsyncEnumerator _enumerator; 94 | 95 | public CastingAsyncEnumerable(IAsyncEnumerable inner, CancellationToken cancellationToken) 96 | { 97 | _inner = inner; 98 | _enumerator = new CastingAsyncEnumerator(_inner.GetAsyncEnumerator(cancellationToken)); 99 | } 100 | 101 | public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) => _enumerator; 102 | } 103 | 104 | class CastingAsyncEnumerator : IAsyncEnumerator 105 | { 106 | private readonly IAsyncEnumerator _inner; 107 | 108 | public CastingAsyncEnumerator(IAsyncEnumerator inner) 109 | { 110 | _inner = inner; 111 | } 112 | 113 | public ValueTask DisposeAsync() => _inner.DisposeAsync(); 114 | 115 | public ValueTask MoveNextAsync() => _inner.MoveNextAsync(); 116 | 117 | public T Current => (T)_inner.Current; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Infrastructure/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | // This is required because I want to use record types, but also .NET Core 3.1 6 | 7 | #if NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 8 | 9 | using System.ComponentModel; 10 | 11 | namespace System.Runtime.CompilerServices 12 | { 13 | /// 14 | /// Reserved to be used by the compiler for tracking metadata. 15 | /// This class should not be used by developers in source code. 16 | /// 17 | [EditorBrowsable(EditorBrowsableState.Never)] 18 | internal static class IsExternalInit 19 | { 20 | } 21 | } 22 | 23 | #endif -------------------------------------------------------------------------------- /src/GrpcBrowser/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using System.Reflection 3 | @using GrpcBrowser.Store.Services 4 | @using GrpcBrowser.Components 5 | @using Namotion.Reflection 6 | @inherits Fluxor.Blazor.Web.Components.FluxorComponent 7 | 8 | 9 | 10 | 11 | 12 | @Assembly.GetEntryAssembly().GetName().Name 13 | 14 | 15 | 16 | 17 | 18 | @if (ServicesState is not null) 19 | { 20 | 21 | @foreach (var client in ServicesState.Value.Services.OrderBy(c => c.Key)) 22 | { 23 | 24 | 25 |
26 | 27 | @client.Value.ServiceType.Name 28 | 29 |
30 | @if (client.Value.ImplementationType == GrpcServiceImplementationType.ProtoFile) 31 | { 32 | PROTO-FIRST 33 | } 34 | else if (client.Value.ImplementationType == GrpcServiceImplementationType.CodeFirst) 35 | { 36 | CODE-FIRST 37 | } 38 |
39 |
40 | 41 | @CommentProvider.GetServiceDescription(client.Value) 42 | 43 |
44 | 45 | 46 | 47 |
48 | } 49 |
50 | } 51 |
52 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Pages/Index.razor.cs: -------------------------------------------------------------------------------- 1 | using Fluxor; 2 | using Grpc.Core; 3 | using GrpcBrowser.Configuration; 4 | using GrpcBrowser.Store.Services; 5 | using Microsoft.AspNetCore.Components; 6 | using ProtoBuf.Grpc; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Collections.Immutable; 10 | using System.Linq; 11 | using System.Reflection; 12 | using ProtoBuf.Grpc.Internal; 13 | using System.Threading; 14 | 15 | namespace GrpcBrowser.Pages 16 | { 17 | public partial class Index 18 | { 19 | [Inject] public IDispatcher? Dispatcher { get; set; } 20 | [Inject] public IState? ServicesState { get; set; } 21 | 22 | record GrpcOperationMethod(MethodInfo Method, GrpcOperationType OperationType, Type? RequestMessageType, Type ResponseMessageType); 23 | 24 | private static GrpcOperationMethod DetermineOperationTypeFromProtoFirstSeviceMethod(MethodInfo method) 25 | { 26 | // nameof(Generic) does not add the back-tick and number of generic arguments to the name, 27 | // unlike typeof(Generic).Name. But typeof is not a constant and can not be used in the below 28 | // switch statement 29 | const string duplexStreamingReturnTypeName = nameof(AsyncDuplexStreamingCall) + "`2"; 30 | const string clientStreamingReturnTypeName = nameof(AsyncClientStreamingCall) + "`2"; 31 | const string serverStreamingReturnTypeName = nameof(AsyncServerStreamingCall) + "`1"; 32 | 33 | var type = method.ReturnType.Name switch 34 | { 35 | duplexStreamingReturnTypeName => GrpcOperationType.Duplex, 36 | clientStreamingReturnTypeName => GrpcOperationType.ClientStreaming, 37 | serverStreamingReturnTypeName => GrpcOperationType.ServerStreaming, 38 | _ => GrpcOperationType.Unary, 39 | }; 40 | 41 | var (requestMessageType, responseMessageType) = type switch 42 | { 43 | GrpcOperationType.Duplex => (method.ReturnType.GetGenericArguments()[0], method.ReturnType.GetGenericArguments()[1]), 44 | GrpcOperationType.ClientStreaming => (method.ReturnType.GetGenericArguments()[0], method.ReturnType.GetGenericArguments()[1]), 45 | GrpcOperationType.ServerStreaming => (method.GetParameters()[0].ParameterType, method.ReturnType.GetGenericArguments()[0]), 46 | GrpcOperationType.Unary => (method.GetParameters()[0].ParameterType, method.ReturnType.IsGenericType ? method.ReturnType.GetGenericArguments()[0] : method.ReturnType), 47 | }; 48 | 49 | return new GrpcOperationMethod(method, type, requestMessageType, responseMessageType); 50 | } 51 | 52 | private static GrpcOperationMethod DetermineOperationFromCodeFirstService(MethodInfo method) 53 | { 54 | var returnsStream = method.ReturnType.Name == typeof(IAsyncEnumerable).Name; 55 | var streamParameter = method.GetParameters().Any(p => p.ParameterType.Name == typeof(IAsyncEnumerable).Name); 56 | 57 | var type = (returnsStream, streamParameter) switch 58 | { 59 | (true, true) => GrpcOperationType.Duplex, 60 | (true, false) => GrpcOperationType.ServerStreaming, 61 | (false, true) => GrpcOperationType.ClientStreaming, 62 | (false, false) => GrpcOperationType.Unary 63 | }; 64 | 65 | var (requestMessageType, responseMessageType) = type switch 66 | { 67 | GrpcOperationType.Duplex => (method.GetParameters()[0].ParameterType.GetGenericArguments()[0], method.ReturnType.GetGenericArguments()[0]), 68 | GrpcOperationType.ClientStreaming => (method.GetParameters()[0].ParameterType.GetGenericArguments()[0], method.ReturnType.GetGenericArguments()[0]), 69 | GrpcOperationType.ServerStreaming => (method.GetParameters()[0].ParameterType, method.ReturnType.GetGenericArguments()[0]), 70 | GrpcOperationType.Unary => (method.GetParameters().Length > 0 ? method.GetParameters()[0].ParameterType : typeof(Empty), method.ReturnType.IsGenericType ? method.ReturnType.GetGenericArguments()[0] : method.ReturnType), 71 | }; 72 | 73 | return new GrpcOperationMethod(method, type, requestMessageType, responseMessageType); 74 | } 75 | 76 | private GrpcService ToGrpcService(Type serviceType, GrpcServiceImplementationType implementationType) 77 | { 78 | // There's some magic going on which causes the Async suffix to be ignored in code-first services 79 | string RemoveAsyncFromCodeFirstMethodName(string methodName, GrpcServiceImplementationType implementationType) => implementationType == GrpcServiceImplementationType.CodeFirst && methodName.EndsWith("Async") ? methodName.Substring(0, methodName.Length - 5) : methodName; 80 | 81 | var methods = 82 | serviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) 83 | .Where(m => !m.IsConstructor) 84 | .Where(m => m.GetParameters().Length <= 2) 85 | .Where(m => m.GetParameters().Length == 0 || 86 | m.GetParameters()[0].ParameterType == typeof(CallOptions) || 87 | (m.GetParameters().Length == 2 && 88 | (m.GetParameters()[1].ParameterType == typeof(CallContext) || 89 | m.GetParameters()[1].ParameterType == typeof(CallOptions) || 90 | m.GetParameters()[1].ParameterType == typeof(CancellationToken)))) 91 | .Select(method => implementationType == GrpcServiceImplementationType.CodeFirst ? DetermineOperationFromCodeFirstService(method) : DetermineOperationTypeFromProtoFirstSeviceMethod(method)) 92 | .GroupBy(method => RemoveAsyncFromCodeFirstMethodName(method.Method.Name, implementationType)) 93 | .Select(method => method.First()) 94 | .Select(operationMethod => new GrpcOperation(RemoveAsyncFromCodeFirstMethodName(operationMethod.Method.Name, implementationType), operationMethod.RequestMessageType, operationMethod.ResponseMessageType, operationMethod.OperationType)) 95 | .ToImmutableDictionary(k => k.Name, v => v); 96 | 97 | return new GrpcService(serviceType, methods, implementationType); 98 | } 99 | 100 | protected override void OnParametersSet() 101 | { 102 | var protoFileGrpcServices = ConfiguredGrpcServices.ProtoGrpcClients.Select(service => ToGrpcService(service, GrpcServiceImplementationType.ProtoFile)); 103 | 104 | var codeFirstGrpcServices = ConfiguredGrpcServices.CodeFirstGrpcServiceInterfaces.Select(service => ToGrpcService(service, GrpcServiceImplementationType.CodeFirst)); 105 | 106 | Dispatcher?.Dispatch(new SetServices(protoFileGrpcServices.Concat(codeFirstGrpcServices).ToImmutableList())); 107 | 108 | base.OnParametersSet(); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/grpc" 2 | @namespace GrpcBrowser.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | GrpcBrowser 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | An error has occurred. This application may no longer respond until reloaded. 27 | 28 | 29 | An unhandled exception has occurred. See browser dev tools for details. 30 | 31 | Reload 32 | 🗙 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:17098", 7 | "sslPort": 44324 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "GrpcBrowser": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | @Body 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Requests/Actions.cs: -------------------------------------------------------------------------------- 1 | using GrpcBrowser.Store.Services; 2 | using System; 3 | 4 | namespace GrpcBrowser.Store.Requests 5 | { 6 | public record CallUnaryOperation(GrpcService Service, GrpcOperation Operation, string RequestParameterJson, GrpcRequestId RequestId, GrpcRequestHeaders Headers, DateTimeOffset Timestamp); 7 | public record UnaryResponseReceived(object RequestBody, CallUnaryOperation RequestAction, GrpcResponse Response); 8 | 9 | public record CallServerStreamingOperation(GrpcService Service, GrpcOperation Operation, string RequestParameterJson, GrpcRequestId RequestId, GrpcRequestHeaders Headers, DateTimeOffset Timestamp); 10 | public record ServerStreamingResponseReceived(object RequestBody, CallServerStreamingOperation RequestAction, GrpcResponse Response); 11 | public record StopServerStreamingConnection(GrpcRequestId RequestId); 12 | public record ServerStreamingConnectionStopped(GrpcRequestId RequestId); 13 | 14 | public record CallClientStreamingOperation(GrpcService Service, GrpcOperation Operation, string FirstRequestParameterJson, GrpcRequestId RequestId, GrpcRequestHeaders Headers, DateTimeOffset Timestamp); 15 | public record SendMessageToConnectedClientStreamingOperation(GrpcRequestId RequestId, GrpcService Service, GrpcOperation Operation, string RequestParameterJson, DateTimeOffset Timestamp); 16 | public record MessageSentToClientStreamingOperation(GrpcRequest Request); 17 | public record StopClientStreamingOperation(GrpcRequestId RequestId, GrpcService Service, GrpcOperation Operation); 18 | public record ClientStreamingResponseReceived(GrpcResponse Response); 19 | 20 | public record OpenDuplexConnection(GrpcService Service, GrpcOperation Operation, GrpcRequestId RequestId, GrpcRequestHeaders Headers); 21 | public record DuplexConnectionOpened(GrpcRequestId RequestId); 22 | public record SendMessageToConnectedDuplexOperation(GrpcRequestId RequestId, GrpcService Service, GrpcOperation Operation, string RequestParameterJson, DateTimeOffset Timestamp); 23 | public record MessageSentToDuplexOperation(GrpcRequest Request); 24 | public record StopDuplexOperation(GrpcRequestId RequestId, GrpcService Service, GrpcOperation Operation); 25 | public record DuplexResponseReceived(GrpcResponse Response); 26 | public record DuplexConnectionStopped(GrpcRequestId RequestId); 27 | } 28 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Requests/Effects/ClientStreamingOperationEffects.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Reactive.Subjects; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Fluxor; 9 | using Grpc.Core; 10 | using Grpc.Net.Client; 11 | using GrpcBrowser.Infrastructure; 12 | using GrpcBrowser.Store.Requests; 13 | using GrpcBrowser.Store.Services; 14 | using ProtoBuf.Grpc; 15 | 16 | namespace GrpcBrowser.Store.Requests.Effects 17 | { 18 | public class ClientStreamingOperationEffects 19 | { 20 | private readonly GrpcChannelUrlProvider _channelUrlProvider; 21 | // Stores the AsyncClientStreamingCall for open client streaming connections 22 | // Only used for proto-first service connections 23 | private ImmutableDictionary _openProtoFirstStreamingRequests = ImmutableDictionary.Empty; 24 | 25 | record CodeFirstOpenClientStreamingConnection(Subject RequestStream, Task ResultTask); 26 | private ImmutableDictionary _openCodeFirstStreamingRequests = ImmutableDictionary.Empty; 27 | 28 | public ClientStreamingOperationEffects(GrpcChannelUrlProvider channelUrlProvider) 29 | { 30 | _channelUrlProvider = channelUrlProvider; 31 | } 32 | 33 | private async Task WriteMessageToCodeFirstOperation(GrpcRequestId requestId, object request, DateTimeOffset timestamp, Type requestType, IDispatcher dispatcher) 34 | { 35 | if (_openCodeFirstStreamingRequests.TryGetValue(requestId, out var openConnection)) 36 | { 37 | openConnection.RequestStream.OnNext(request); 38 | 39 | dispatcher.Dispatch(new MessageSentToClientStreamingOperation(new GrpcRequest(timestamp, requestId, requestType, request))); 40 | } 41 | } 42 | 43 | private async Task InvokeCodeFirstService(GrpcChannel channel, CallClientStreamingOperation action, object requestParameter, CallOptions callOptions, IDispatcher dispatcher) 44 | { 45 | var client = channel.GetCodeFirstGrpcServiceClient(action.Service.ServiceType.Name); 46 | var context = new CallContext(callOptions); 47 | 48 | var subject = new Subject(); 49 | 50 | var resultTask = client.ClientStreamingAsync(subject.ToAsyncEnumerable(), action.Operation.Name, action.Operation.RequestType, action.Operation.ResponseType, context); 51 | 52 | _openCodeFirstStreamingRequests = _openCodeFirstStreamingRequests.SetItem(action.RequestId, new CodeFirstOpenClientStreamingConnection(subject, resultTask)); 53 | 54 | await WriteMessageToCodeFirstOperation(action.RequestId, requestParameter, action.Timestamp, action.Operation.RequestType, dispatcher); 55 | } 56 | 57 | private async Task WriteMessageToProtoFirstOperation(GrpcRequestId requestId, GrpcOperation operation, DateTimeOffset timestamp, Type requestType, object request, IDispatcher dispatcher) 58 | { 59 | if (_openProtoFirstStreamingRequests.TryGetValue(requestId, out var clientStreamingCall)) 60 | { 61 | var requestStream = clientStreamingCall.GetType().GetProperty(nameof(AsyncClientStreamingCall.RequestStream))?.GetValue(clientStreamingCall); 62 | 63 | var writeAsync = requestStream.GetType().GetMethod(nameof(IAsyncStreamWriter.WriteAsync), new[] { operation.RequestType }); 64 | 65 | await (Task)writeAsync.Invoke(requestStream, new[] { request }); 66 | 67 | dispatcher.Dispatch(new MessageSentToClientStreamingOperation(new GrpcRequest(timestamp, requestId, requestType, request))); 68 | } 69 | } 70 | 71 | private async Task InvokeProtoFileService(GrpcChannel channel, CallClientStreamingOperation action, object requestParameter, CallOptions callOptions, IDispatcher dispatcher) 72 | { 73 | var client = channel.GetProtoFileGrpcServiceClient(action.Service.ServiceType.Name); 74 | 75 | var method = client.GetType().GetMethod(action.Operation.Name, new[] { typeof(CallOptions) }); 76 | 77 | var result = method?.Invoke(client, new object[] { callOptions }); 78 | 79 | _openProtoFirstStreamingRequests = _openProtoFirstStreamingRequests.SetItem(action.RequestId, result); 80 | 81 | await WriteMessageToProtoFirstOperation(action.RequestId, action.Operation, action.Timestamp, action.Operation.RequestType, requestParameter, dispatcher); 82 | } 83 | 84 | private async Task StopOpenProtoFirstOperation(GrpcRequestId requestId, GrpcOperation operation, IDispatcher dispatcher) 85 | { 86 | if (_openProtoFirstStreamingRequests.TryGetValue(requestId, out var clientStreamingCall)) 87 | { 88 | var requestStream = clientStreamingCall.GetType() 89 | .GetProperty(nameof(AsyncClientStreamingCall.RequestStream))?.GetValue(clientStreamingCall); 90 | 91 | var completeAsync = requestStream.GetType().GetMethod(nameof(IClientStreamWriter.CompleteAsync)); 92 | 93 | await (Task)completeAsync.Invoke(requestStream, null); 94 | 95 | var responseTask = (Task)clientStreamingCall.GetType().GetProperty(nameof(AsyncClientStreamingCall.ResponseAsync))?.GetValue(clientStreamingCall); 96 | 97 | try 98 | { 99 | await responseTask; 100 | 101 | var response = responseTask.GetType().GetProperty("Result").GetValue(responseTask); 102 | 103 | dispatcher.Dispatch(new ClientStreamingResponseReceived(new GrpcResponse(DateTimeOffset.Now, requestId, operation.ResponseType, response))); 104 | } 105 | catch (Exception ex) 106 | { 107 | dispatcher.Dispatch(new ClientStreamingResponseReceived(new GrpcResponse(DateTimeOffset.Now, requestId, ex.GetType(), ex))); 108 | } 109 | finally 110 | { 111 | _openProtoFirstStreamingRequests = _openProtoFirstStreamingRequests.Remove(requestId); 112 | } 113 | } 114 | } 115 | 116 | private async Task StopOpenCodeFirstOperation(GrpcRequestId requestId, GrpcOperation operation, IDispatcher dispatcher) 117 | { 118 | if (_openCodeFirstStreamingRequests.TryGetValue(requestId, out var clientStreamingCall)) 119 | { 120 | clientStreamingCall.RequestStream.OnCompleted(); 121 | 122 | await clientStreamingCall.ResultTask; 123 | 124 | var response = clientStreamingCall.ResultTask.GetType().GetProperty("Result").GetValue(clientStreamingCall.ResultTask); 125 | 126 | dispatcher.Dispatch(new ClientStreamingResponseReceived(new GrpcResponse(DateTimeOffset.Now, requestId, operation.ResponseType, response))); 127 | } 128 | } 129 | 130 | [EffectMethod] 131 | public async Task Handle(CallClientStreamingOperation action, IDispatcher dispatcher) 132 | { 133 | var channel = GrpcChannel.ForAddress(_channelUrlProvider.BaseUrl); 134 | 135 | var callOptions = GrpcUtils.GetCallOptions(action.Headers, CancellationToken.None); 136 | 137 | var requestParameter = GrpcUtils.GetRequestParameter(action.FirstRequestParameterJson, action.Operation.RequestType); 138 | 139 | try 140 | { 141 | if (action.Service.ImplementationType == GrpcServiceImplementationType.CodeFirst) 142 | { 143 | await InvokeCodeFirstService(channel, action, requestParameter, callOptions, dispatcher); 144 | } 145 | else if (action.Service.ImplementationType == GrpcServiceImplementationType.ProtoFile) 146 | { 147 | await InvokeProtoFileService(channel, action, requestParameter, callOptions, dispatcher); 148 | } 149 | } 150 | catch (Exception ex) 151 | { 152 | dispatcher.Dispatch(new ClientStreamingResponseReceived(new GrpcResponse(DateTimeOffset.Now, action.RequestId, ex.GetType(), ex))); 153 | } 154 | } 155 | 156 | [EffectMethod] 157 | public async Task Handle(SendMessageToConnectedClientStreamingOperation action, IDispatcher dispatcher) 158 | { 159 | var requestParameter = GrpcUtils.GetRequestParameter(action.RequestParameterJson, action.Operation.RequestType); 160 | 161 | try 162 | { 163 | if (action.Service.ImplementationType == GrpcServiceImplementationType.ProtoFile) 164 | { 165 | await WriteMessageToProtoFirstOperation(action.RequestId, action.Operation, action.Timestamp, action.Operation.RequestType, requestParameter, dispatcher); 166 | } 167 | else if (action.Service.ImplementationType == GrpcServiceImplementationType.CodeFirst) 168 | { 169 | await WriteMessageToCodeFirstOperation(action.RequestId, requestParameter, action.Timestamp, action.Operation.RequestType, dispatcher); 170 | } 171 | } 172 | catch (Exception ex) 173 | { 174 | dispatcher.Dispatch(new ClientStreamingResponseReceived(new GrpcResponse(DateTimeOffset.Now, action.RequestId, ex.GetType(), ex))); 175 | } 176 | } 177 | 178 | [EffectMethod] 179 | public async Task Handle(StopClientStreamingOperation action, IDispatcher dispatcher) 180 | { 181 | if (action.Service.ImplementationType == GrpcServiceImplementationType.ProtoFile) 182 | { 183 | await StopOpenProtoFirstOperation(action.RequestId, action.Operation, dispatcher); 184 | } 185 | else if (action.Service.ImplementationType == GrpcServiceImplementationType.CodeFirst) 186 | { 187 | await StopOpenCodeFirstOperation(action.RequestId, action.Operation, dispatcher); 188 | } 189 | } 190 | 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Requests/Effects/DuplexOperationEffects.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Reactive.Subjects; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Fluxor; 9 | using Grpc.Core; 10 | using Grpc.Net.Client; 11 | using GrpcBrowser.Infrastructure; 12 | using GrpcBrowser.Store.Services; 13 | using ProtoBuf.Grpc; 14 | 15 | namespace GrpcBrowser.Store.Requests.Effects 16 | { 17 | public class DuplexOperationEffects 18 | { 19 | private readonly GrpcChannelUrlProvider _channelUrlProvider; 20 | record OpenProtoDuplexConnection(object DuplexResponse, CancellationTokenSource CancellationTokenSource); 21 | private ImmutableDictionary _openProtoConnections = ImmutableDictionary.Empty; 22 | 23 | record OpenCodeFirstDuplexConnection(Subject RequestStream, CancellationTokenSource CancellationTokenSource); 24 | private ImmutableDictionary _openCodeFirstConnections = ImmutableDictionary.Empty; 25 | 26 | public DuplexOperationEffects(GrpcChannelUrlProvider channelUrlProvider) 27 | { 28 | _channelUrlProvider = channelUrlProvider; 29 | } 30 | 31 | private async Task WriteMessageToProtoFirstOperation(GrpcRequestId requestId, GrpcOperation operation, object request, DateTimeOffset timestamp, Type requestType, IDispatcher dispatcher) 32 | { 33 | if (_openProtoConnections.TryGetValue(requestId, out var clientStreamingCall)) 34 | { 35 | var requestStream = clientStreamingCall.DuplexResponse.GetType().GetProperty(nameof(AsyncDuplexStreamingCall.RequestStream))?.GetValue(clientStreamingCall.DuplexResponse); 36 | 37 | var writeAsync = requestStream.GetType().GetMethod(nameof(IAsyncStreamWriter.WriteAsync), new[] { operation.RequestType }); 38 | 39 | await (Task)writeAsync.Invoke(requestStream, new[] { request }); 40 | 41 | dispatcher.Dispatch(new MessageSentToDuplexOperation(new GrpcRequest(timestamp, requestId, requestType, request))); 42 | } 43 | } 44 | 45 | private async Task InvokeProtoFileService(GrpcChannel channel, OpenDuplexConnection action, CallOptions callOptions, IDispatcher dispatcher, CancellationTokenSource cts) 46 | { 47 | var client = channel.GetProtoFileGrpcServiceClient(action.Service.ServiceType.Name); 48 | var method = client.GetType().GetMethod(action.Operation.Name, new[] { typeof(CallOptions) }); 49 | var result = method?.Invoke(client, new object[] { callOptions }); 50 | 51 | dispatcher.Dispatch(new DuplexConnectionOpened(action.RequestId)); 52 | 53 | var responseStream = result.GetType().GetProperty(nameof(AsyncDuplexStreamingCall.ResponseStream))?.GetValue(result); 54 | var moveNext = responseStream.GetType().GetMethod(nameof(AsyncDuplexStreamingCall.ResponseStream.MoveNext)); 55 | 56 | _openProtoConnections = _openProtoConnections.SetItem(action.RequestId, new OpenProtoDuplexConnection(result, cts)); 57 | 58 | var endOfStream = false; 59 | try 60 | { 61 | do 62 | { 63 | var moveNextTask = (Task)moveNext.Invoke(responseStream, new object[] { cts.Token }); 64 | 65 | await moveNextTask; 66 | 67 | var success = (bool)moveNextTask.GetType().GetProperty("Result").GetValue(moveNextTask); 68 | endOfStream = !success; 69 | 70 | if (!endOfStream) 71 | { 72 | var current = responseStream.GetType().GetProperty("Current")?.GetValue(responseStream); 73 | dispatcher.Dispatch(new DuplexResponseReceived(new GrpcResponse(DateTimeOffset.Now, action.RequestId, action.Operation.ResponseType, current))); 74 | } 75 | 76 | } while (!endOfStream && !cts.IsCancellationRequested); 77 | } 78 | catch (OperationCanceledException) { /* Don't care, this will throw when the cancellation token is activated */ } 79 | finally 80 | { 81 | dispatcher.Dispatch(new DuplexConnectionStopped(action.RequestId)); 82 | } 83 | } 84 | 85 | private async Task WriteMessageToCodeFirstOperation(GrpcRequestId requestId, object request, DateTimeOffset timestamp, Type requestType, IDispatcher dispatcher) 86 | { 87 | if (_openCodeFirstConnections.TryGetValue(requestId, out var openConnection)) 88 | { 89 | openConnection.RequestStream.OnNext(request); 90 | 91 | dispatcher.Dispatch(new MessageSentToDuplexOperation(new GrpcRequest(timestamp, requestId, requestType, request))); 92 | } 93 | } 94 | 95 | private async Task InvokeCodeFirstService(GrpcChannel channel, OpenDuplexConnection action, CallOptions callOptions, IDispatcher dispatcher, CancellationTokenSource cts) 96 | { 97 | var client = channel.GetCodeFirstGrpcServiceClient(action.Service.ServiceType.Name); 98 | var context = new CallContext(callOptions); 99 | 100 | var requestStreamSubject = new Subject(); 101 | 102 | try 103 | { 104 | var result = client.DuplexAsync(requestStreamSubject.ToAsyncEnumerable(), action.Operation.Name, action.Operation.RequestType, action.Operation.ResponseType, context); 105 | 106 | _openCodeFirstConnections = _openCodeFirstConnections.SetItem(action.RequestId, new OpenCodeFirstDuplexConnection(requestStreamSubject, cts)); 107 | dispatcher.Dispatch(new DuplexConnectionOpened(action.RequestId)); 108 | 109 | await foreach (var message in result) 110 | { 111 | dispatcher.Dispatch(new DuplexResponseReceived(new GrpcResponse(DateTimeOffset.Now, action.RequestId, action.Operation.ResponseType, message))); 112 | } 113 | } 114 | catch (OperationCanceledException) { /* Don't care, this will throw when the cancellation token is activated */ } 115 | finally 116 | { 117 | dispatcher.Dispatch(new DuplexConnectionStopped(action.RequestId)); 118 | } 119 | } 120 | 121 | [EffectMethod] 122 | public async Task Handle(OpenDuplexConnection action, IDispatcher dispatcher) 123 | { 124 | var channel = GrpcChannel.ForAddress(_channelUrlProvider.BaseUrl); 125 | 126 | var cts = new CancellationTokenSource(); 127 | var callOptions = GrpcUtils.GetCallOptions(action.Headers, cts.Token); 128 | 129 | try 130 | { 131 | if (action.Service.ImplementationType == GrpcServiceImplementationType.CodeFirst) 132 | { 133 | await InvokeCodeFirstService(channel, action, callOptions, dispatcher, cts); 134 | } 135 | else if (action.Service.ImplementationType == GrpcServiceImplementationType.ProtoFile) 136 | { 137 | await InvokeProtoFileService(channel, action, callOptions, dispatcher, cts); 138 | } 139 | } 140 | catch (Exception ex) 141 | { 142 | dispatcher.Dispatch(new DuplexResponseReceived(new GrpcResponse(DateTimeOffset.Now, action.RequestId, ex.GetType(), ex))); 143 | } 144 | } 145 | 146 | [EffectMethod] 147 | public async Task Handle(SendMessageToConnectedDuplexOperation action, IDispatcher dispatcher) 148 | { 149 | var requestParameter = GrpcUtils.GetRequestParameter(action.RequestParameterJson, action.Operation.RequestType); 150 | 151 | try 152 | { 153 | if (action.Service.ImplementationType == GrpcServiceImplementationType.CodeFirst) 154 | { 155 | await WriteMessageToCodeFirstOperation(action.RequestId, requestParameter, action.Timestamp, action.Operation.RequestType, dispatcher); 156 | } 157 | else if (action.Service.ImplementationType == GrpcServiceImplementationType.ProtoFile) 158 | { 159 | await WriteMessageToProtoFirstOperation(action.RequestId, action.Operation, requestParameter, action.Timestamp, action.Operation.RequestType, dispatcher); 160 | } 161 | } 162 | catch (Exception ex) 163 | { 164 | dispatcher.Dispatch(new DuplexResponseReceived(new GrpcResponse(DateTimeOffset.Now, action.RequestId, ex.GetType(), ex))); 165 | dispatcher.Dispatch(new DuplexConnectionStopped(action.RequestId)); 166 | } 167 | } 168 | 169 | [EffectMethod] 170 | public async Task Handle(StopDuplexOperation action, IDispatcher dispatcher) 171 | { 172 | if (action.Service.ImplementationType == GrpcServiceImplementationType.ProtoFile) 173 | { 174 | if (_openProtoConnections.TryGetValue(action.RequestId, out var openConnection)) 175 | { 176 | var requestStream = openConnection.DuplexResponse.GetType().GetProperty(nameof(AsyncDuplexStreamingCall.RequestStream))?.GetValue(openConnection.DuplexResponse); 177 | 178 | var completeAsync = requestStream.GetType().GetMethod(nameof(IClientStreamWriter.CompleteAsync)); 179 | 180 | await (Task)completeAsync.Invoke(requestStream, null); 181 | 182 | openConnection.CancellationTokenSource.Cancel(); 183 | _openProtoConnections = _openProtoConnections.Remove(action.RequestId); 184 | } 185 | } 186 | else if (action.Service.ImplementationType == GrpcServiceImplementationType.CodeFirst) 187 | { 188 | if (_openCodeFirstConnections.TryGetValue(action.RequestId, out var openConnection)) 189 | { 190 | openConnection.RequestStream.OnCompleted(); 191 | 192 | // There appears to be a race condition in Grpc.Net.Client which makes this delay necessary: https://issueexplorer.com/issue/grpc/grpc-dotnet/1394 193 | await Task.Delay(500); 194 | 195 | openConnection.CancellationTokenSource.Cancel(); 196 | _openCodeFirstConnections = _openCodeFirstConnections.Remove(action.RequestId); 197 | } 198 | } 199 | 200 | dispatcher.Dispatch(new DuplexConnectionStopped(action.RequestId)); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Requests/Effects/GrpcUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Grpc.Core; 7 | using Grpc.Net.Client; 8 | using GrpcBrowser.Configuration; 9 | using GrpcBrowser.Store.Services; 10 | using Newtonsoft.Json; 11 | using ProtoBuf.Grpc.Client; 12 | 13 | namespace GrpcBrowser.Store.Requests.Effects 14 | { 15 | internal static class GrpcUtils 16 | { 17 | internal static object? GetRequestParameter(string requestParamJson, Type requestType) => JsonConvert.DeserializeObject(requestParamJson, requestType); 18 | 19 | internal static CallOptions GetCallOptions(GrpcRequestHeaders requestHeaders, CancellationToken cancellationToken) 20 | { 21 | var headers = new Metadata(); 22 | 23 | foreach (var header in requestHeaders.Values) 24 | { 25 | headers.Add(header.Key, header.Value); 26 | } 27 | 28 | return new CallOptions(headers, cancellationToken: cancellationToken); 29 | } 30 | 31 | internal static GrpcClient GetCodeFirstGrpcServiceClient(this GrpcChannel channel, string serviceName) 32 | { 33 | var clientType = ConfiguredGrpcServices.CodeFirstGrpcServiceInterfaces.Single(c => c.Name == serviceName); 34 | 35 | return channel.CreateGrpcService(clientType); 36 | } 37 | 38 | internal static object? GetProtoFileGrpcServiceClient(this GrpcChannel channel, string serviceName) 39 | { 40 | var clientType = ConfiguredGrpcServices.ProtoGrpcClients.Single(c => c.Name == serviceName); 41 | return Activator.CreateInstance(clientType, channel); 42 | } 43 | 44 | internal static object? InvokeGrpcOperation(this object client, GrpcOperation operation, object requestParameter, CallOptions options) 45 | { 46 | var method = client.GetType().GetMethod(operation.Name, new[] { operation.RequestType, typeof(CallOptions) }); 47 | 48 | return method?.Invoke(client, new[] { requestParameter, options }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Requests/Effects/ServerStreamingOperationEffects.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Fluxor; 6 | using Grpc.Core; 7 | using Grpc.Net.Client; 8 | using GrpcBrowser.Infrastructure; 9 | using GrpcBrowser.Store.Services; 10 | using ProtoBuf.Grpc; 11 | 12 | namespace GrpcBrowser.Store.Requests.Effects 13 | { 14 | public class ServerStreamingOperationEffects 15 | { 16 | private readonly GrpcChannelUrlProvider _urlProvider; 17 | 18 | public ServerStreamingOperationEffects(GrpcChannelUrlProvider urlProvider) 19 | { 20 | _urlProvider = urlProvider; 21 | } 22 | private ImmutableDictionary _cancellationTokens = ImmutableDictionary.Empty; 23 | 24 | private static async Task InvokeCodeFirstService(GrpcChannel channel, CallServerStreamingOperation action, object requestParameter, CallOptions callOptions, IDispatcher dispatcher) 25 | { 26 | var client = channel.GetCodeFirstGrpcServiceClient(action.Service.ServiceType.Name); 27 | var context = new CallContext(callOptions); 28 | 29 | var result = client.ServerStreamingAsync(requestParameter, action.Operation.Name, action.Operation.RequestType, action.Operation.ResponseType, context); 30 | 31 | await foreach (var message in result) 32 | { 33 | dispatcher.Dispatch(new ServerStreamingResponseReceived(requestParameter, action, new GrpcResponse(DateTimeOffset.Now, action.RequestId, action.Operation.ResponseType, message))); 34 | } 35 | } 36 | 37 | private async Task InvokeProtoFileService(GrpcChannel channel, CallServerStreamingOperation action, object requestParameter, CallOptions callOptions, IDispatcher dispatcher) 38 | { 39 | var client = channel.GetProtoFileGrpcServiceClient(action.Service.ServiceType.Name); 40 | var result = client.InvokeGrpcOperation(action.Operation, requestParameter, callOptions); 41 | 42 | var responseStream = result.GetType().GetProperty("ResponseStream")?.GetValue(result); 43 | var moveNext = responseStream.GetType().GetMethod("MoveNext"); 44 | 45 | 46 | var endOfStream = false; 47 | 48 | do 49 | { 50 | var moveNextTask = (Task)moveNext.Invoke(responseStream, 51 | new object[] { _cancellationTokens[action.RequestId].Token }); 52 | 53 | await moveNextTask; 54 | 55 | var success = (bool)moveNextTask.GetType().GetProperty("Result").GetValue(moveNextTask); 56 | endOfStream = !success; 57 | 58 | if (!endOfStream) 59 | { 60 | var current = responseStream.GetType().GetProperty("Current")?.GetValue(responseStream); 61 | dispatcher.Dispatch(new ServerStreamingResponseReceived(requestParameter, action, new GrpcResponse(DateTimeOffset.Now, action.RequestId, action.Operation.ResponseType, current))); 62 | } 63 | 64 | } while (!endOfStream && !_cancellationTokens[action.RequestId].IsCancellationRequested); 65 | 66 | } 67 | 68 | [EffectMethod] 69 | public async Task Handle(CallServerStreamingOperation action, IDispatcher dispatcher) 70 | { 71 | var channel = GrpcChannel.ForAddress(_urlProvider.BaseUrl); 72 | 73 | var requestParameter = GrpcUtils.GetRequestParameter(action.RequestParameterJson, action.Operation.RequestType); 74 | 75 | var cts = new CancellationTokenSource(); 76 | _cancellationTokens = _cancellationTokens.SetItem(action.RequestId, cts); 77 | var callOptions = GrpcUtils.GetCallOptions(action.Headers, cts.Token); 78 | 79 | try 80 | { 81 | if (action.Service.ImplementationType == GrpcServiceImplementationType.CodeFirst) 82 | { 83 | await InvokeCodeFirstService(channel, action, requestParameter, callOptions, dispatcher); 84 | } 85 | else if (action.Service.ImplementationType == GrpcServiceImplementationType.ProtoFile) 86 | { 87 | await InvokeProtoFileService(channel, action, requestParameter, callOptions, dispatcher); 88 | } 89 | } 90 | catch (Exception ex) 91 | { 92 | dispatcher.Dispatch(new ServerStreamingResponseReceived(requestParameter, action, new GrpcResponse(DateTimeOffset.Now, action.RequestId, ex.GetType(), ex))); 93 | dispatcher.Dispatch(new ServerStreamingConnectionStopped(action.RequestId)); 94 | } 95 | } 96 | 97 | [EffectMethod] 98 | public async Task Handle(StopServerStreamingConnection action, IDispatcher dispatcher) 99 | { 100 | if (_cancellationTokens.TryGetValue(action.RequestId, out var cts)) 101 | { 102 | cts.Cancel(); 103 | _cancellationTokens = _cancellationTokens.Remove(action.RequestId); 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Requests/Effects/UnaryOperationEffects.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Fluxor; 5 | using Grpc.Core; 6 | using Grpc.Net.Client; 7 | using GrpcBrowser.Infrastructure; 8 | using GrpcBrowser.Store.Services; 9 | using ProtoBuf.Grpc; 10 | 11 | namespace GrpcBrowser.Store.Requests.Effects 12 | { 13 | public class UnaryOperationEffects 14 | { 15 | private readonly GrpcChannelUrlProvider _urlProvider; 16 | 17 | public UnaryOperationEffects(GrpcChannelUrlProvider urlProvider) 18 | { 19 | _urlProvider = urlProvider; 20 | } 21 | 22 | private static async Task InvokeCodeFirstService(GrpcChannel channel, CallUnaryOperation action, object requestParameter, CallOptions callOptions, IDispatcher dispatcher) 23 | { 24 | var client = channel.GetCodeFirstGrpcServiceClient(action.Service.ServiceType.Name); 25 | var context = new CallContext(callOptions); 26 | 27 | var result = await client.UnaryAsync(requestParameter, action.Operation.Name, action.Operation.RequestType, action.Operation.ResponseType, context); 28 | 29 | dispatcher.Dispatch(new UnaryResponseReceived(requestParameter, action, new GrpcResponse(DateTimeOffset.Now, action.RequestId, action.Operation.ResponseType, result))); 30 | } 31 | 32 | private static async Task InvokeProtoFileService(GrpcChannel channel, CallUnaryOperation action, object requestParameter, CallOptions callOptions, IDispatcher dispatcher) 33 | { 34 | var client = channel.GetProtoFileGrpcServiceClient(action.Service.ServiceType.Name); 35 | var result = client.InvokeGrpcOperation(action.Operation, requestParameter, callOptions); 36 | 37 | if (result.GetType().Name == "AsyncUnaryCall`1") 38 | { 39 | var resultTask = (Task)result.GetType().GetProperty("ResponseAsync").GetValue(result); 40 | 41 | await resultTask; 42 | var resultProperty = resultTask.GetType().GetProperty("Result"); 43 | result = resultProperty.GetValue(resultTask); 44 | } 45 | 46 | dispatcher.Dispatch(new UnaryResponseReceived(requestParameter, action, new GrpcResponse(DateTimeOffset.Now, action.RequestId, action.Operation.ResponseType, result))); 47 | } 48 | 49 | [EffectMethod] 50 | public async Task Handle(CallUnaryOperation action, IDispatcher dispatcher) 51 | { 52 | var channel = GrpcChannel.ForAddress(_urlProvider.BaseUrl); 53 | 54 | var requestParameter = GrpcUtils.GetRequestParameter(action.RequestParameterJson, action.Operation.RequestType); 55 | 56 | var callOptions = GrpcUtils.GetCallOptions(action.Headers, CancellationToken.None); 57 | 58 | 59 | try 60 | { 61 | if (action.Service.ImplementationType == GrpcServiceImplementationType.CodeFirst) 62 | { 63 | await InvokeCodeFirstService(channel, action, requestParameter, callOptions, dispatcher); 64 | } 65 | else if (action.Service.ImplementationType == GrpcServiceImplementationType.ProtoFile) 66 | { 67 | await InvokeProtoFileService(channel, action, requestParameter, callOptions, dispatcher); 68 | } 69 | } 70 | catch (Exception ex) 71 | { 72 | dispatcher.Dispatch(new UnaryResponseReceived(requestParameter, action, new GrpcResponse(DateTimeOffset.Now, action.RequestId, ex.GetType(), ex))); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Requests/Reducers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Fluxor; 3 | using GrpcBrowser.Store.Services; 4 | 5 | namespace GrpcBrowser.Store.Requests 6 | { 7 | public static class Reducers 8 | { 9 | [ReducerMethod] 10 | public static RequestState Reduce(RequestState state, UnaryResponseReceived action) => state with {UnaryRequests = state.UnaryRequests.SetItem(action.Response.RequestId, new UnaryRequestState(action.RequestBody, action.RequestAction, action.Response))}; 11 | 12 | [ReducerMethod] 13 | public static RequestState Reduce(RequestState state, ServerStreamingResponseReceived action) 14 | { 15 | var existingResponses = 16 | state.ServerStreamingRequests.TryGetValue(action.Response.RequestId, out var existing) 17 | ? existing 18 | : new ServerStreamingConnectionState(action.RequestBody, false, action.RequestAction, ImmutableList.Empty); 19 | 20 | var updated = existingResponses with { Responses = existingResponses.Responses.Add(action.Response), Connected = true }; 21 | 22 | return state with 23 | { 24 | ServerStreamingRequests = state.ServerStreamingRequests.SetItem(action.Response.RequestId, updated) 25 | }; 26 | } 27 | 28 | [ReducerMethod] 29 | public static RequestState Reduce(RequestState state, ServerStreamingConnectionStopped action) => 30 | state with 31 | { 32 | ServerStreamingRequests = state.ServerStreamingRequests.SetItem(action.RequestId, 33 | state.ServerStreamingRequests[action.RequestId] with { Connected = false }) 34 | }; 35 | 36 | [ReducerMethod] 37 | public static RequestState Reduce(RequestState state, CallClientStreamingOperation action) => 38 | state with 39 | { 40 | ClientStreamingRequests = state.ClientStreamingRequests.SetItem(action.RequestId, new ClientStreamingConnectionState(false, action.Headers, ImmutableList.Empty, null)) 41 | }; 42 | 43 | [ReducerMethod] 44 | public static RequestState Reduce(RequestState state, MessageSentToClientStreamingOperation action) => 45 | state with 46 | { 47 | ClientStreamingRequests = state.ClientStreamingRequests.TryGetValue(action.Request.RequestId, out var existing) 48 | ? state.ClientStreamingRequests.SetItem(action.Request.RequestId, existing with { Connected = true, Requests = existing.Requests.Add(action.Request) }) 49 | : state.ClientStreamingRequests.SetItem(action.Request.RequestId, new ClientStreamingConnectionState(true, new GrpcRequestHeaders(ImmutableDictionary.Empty), ImmutableList.Empty.Add(action.Request), null)) 50 | }; 51 | 52 | [ReducerMethod] 53 | public static RequestState Reduce(RequestState state, ClientStreamingResponseReceived action) => 54 | state with 55 | { 56 | ClientStreamingRequests = state.ClientStreamingRequests.SetItem(action.Response.RequestId, 57 | state.ClientStreamingRequests[action.Response.RequestId] with 58 | { 59 | Connected = false, Response = action.Response 60 | }) 61 | }; 62 | 63 | [ReducerMethod] 64 | public static RequestState Reduce(RequestState state, OpenDuplexConnection action) => 65 | state with 66 | { 67 | DuplexRequests = 68 | state.DuplexRequests.TryGetValue(action.RequestId, out var existing) 69 | ? state.DuplexRequests.SetItem(action.RequestId, existing with { Headers = action.Headers }) 70 | : state.DuplexRequests.SetItem(action.RequestId, new DuplexConnectionState(false, action.Headers, ImmutableList.Empty, ImmutableList.Empty)) 71 | }; 72 | 73 | [ReducerMethod] 74 | public static RequestState Reduce(RequestState state, DuplexConnectionOpened action) => 75 | state with 76 | { 77 | DuplexRequests = 78 | state.DuplexRequests.TryGetValue(action.RequestId, out var existing) 79 | ? state.DuplexRequests.SetItem(action.RequestId, existing with { Connected = true }) 80 | : state.DuplexRequests.SetItem(action.RequestId, new DuplexConnectionState(false, new GrpcRequestHeaders(ImmutableDictionary.Empty), ImmutableList.Empty, ImmutableList.Empty)) 81 | }; 82 | 83 | [ReducerMethod] 84 | public static RequestState Reduce(RequestState state, DuplexConnectionStopped action) => 85 | state with 86 | { 87 | DuplexRequests = state.DuplexRequests.SetItem(action.RequestId, state.DuplexRequests[action.RequestId] with { Connected = false }) 88 | }; 89 | 90 | [ReducerMethod] 91 | public static RequestState Reduce(RequestState state, MessageSentToDuplexOperation action) => 92 | state with 93 | { 94 | DuplexRequests = 95 | state.DuplexRequests.TryGetValue(action.Request.RequestId, out var existing) 96 | ? state.DuplexRequests.SetItem(action.Request.RequestId, existing with { Connected = true, Requests = existing.Requests.Add(action.Request) }) 97 | : state.DuplexRequests.SetItem(action.Request.RequestId, new DuplexConnectionState(true, new GrpcRequestHeaders(ImmutableDictionary.Empty), ImmutableList.Empty.Add(action.Request), ImmutableList.Empty)) 98 | }; 99 | 100 | [ReducerMethod] 101 | public static RequestState Reduce(RequestState state, DuplexResponseReceived action) => 102 | state with 103 | { 104 | DuplexRequests = 105 | state.DuplexRequests.TryGetValue(action.Response.RequestId, out var existing) 106 | ? state.DuplexRequests.SetItem(action.Response.RequestId, existing with { Connected = true, Responses = existing.Responses.Add(action.Response) }) 107 | : state.DuplexRequests.SetItem(action.Response.RequestId, new DuplexConnectionState(true, new GrpcRequestHeaders(ImmutableDictionary.Empty), ImmutableList.Empty, ImmutableList.Empty.Add(action.Response))) 108 | }; 109 | 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Requests/State.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Fluxor; 3 | using GrpcBrowser.Store.Services; 4 | 5 | namespace GrpcBrowser.Store.Requests 6 | { 7 | public record UnaryRequestState(object Request, CallUnaryOperation RequestAction, GrpcResponse Response); 8 | public record ServerStreamingConnectionState(object Request, bool Connected, CallServerStreamingOperation RequestAction, ImmutableList Responses); 9 | public record ClientStreamingConnectionState(bool Connected, GrpcRequestHeaders Headers, ImmutableList Requests, GrpcResponse? Response); 10 | public record DuplexConnectionState(bool Connected, GrpcRequestHeaders Headers, ImmutableList Requests, ImmutableList Responses); 11 | 12 | public record RequestState( 13 | ImmutableDictionary UnaryRequests, 14 | ImmutableDictionary ServerStreamingRequests, 15 | ImmutableDictionary ClientStreamingRequests, 16 | ImmutableDictionary DuplexRequests); 17 | 18 | public class Feature : Feature 19 | { 20 | public override string GetName() => "Requests"; 21 | 22 | protected override RequestState GetInitialState() => 23 | new RequestState( 24 | ImmutableDictionary.Empty, 25 | ImmutableDictionary.Empty, 26 | ImmutableDictionary.Empty, 27 | ImmutableDictionary.Empty); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Services/Actions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace GrpcBrowser.Store.Services 4 | { 5 | public record SetServices(ImmutableList Services); 6 | } 7 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Services/Model.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | 4 | namespace GrpcBrowser.Store.Services 5 | { 6 | public record GrpcRequestId(Guid Value); 7 | 8 | public record GrpcRequest(DateTimeOffset TimeStamp, GrpcRequestId RequestId, Type RequestType, object RequestBody); 9 | 10 | public record GrpcResponse(DateTimeOffset TimeStamp, GrpcRequestId RequestId, Type ResponseType, object ResponseBody); 11 | 12 | public enum GrpcOperationType { Unary, ServerStreaming, ClientStreaming, Duplex } 13 | 14 | public record GrpcOperation(string Name, Type RequestType, Type ResponseType, GrpcOperationType Type); 15 | 16 | public enum GrpcServiceImplementationType { CodeFirst, ProtoFile } 17 | 18 | public record GrpcService(Type ServiceType, ImmutableDictionary Endpoints, GrpcServiceImplementationType ImplementationType); 19 | 20 | public record GrpcRequestHeaders(ImmutableDictionary Values); 21 | 22 | public record GrpcResponseHeaders(ImmutableDictionary Values); 23 | } 24 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Services/Reducers.cs: -------------------------------------------------------------------------------- 1 | using Fluxor; 2 | using System.Collections.Immutable; 3 | 4 | namespace GrpcBrowser.Store.Services 5 | { 6 | public static class Reducers 7 | { 8 | [ReducerMethod] 9 | public static ServicesState Reduce(ServicesState state, SetServices action) => 10 | state with { Services = action.Services.ToImmutableDictionary(c => c.ServiceType.Name, c => c) }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/GrpcBrowser/Store/Services/State.cs: -------------------------------------------------------------------------------- 1 | using Fluxor; 2 | using System.Collections.Immutable; 3 | 4 | namespace GrpcBrowser.Store.Services 5 | { 6 | public record ServicesState(ImmutableDictionary Services); 7 | 8 | public class Feature : Feature 9 | { 10 | public override string GetName() => "Services"; 11 | 12 | protected override ServicesState GetInitialState() => new ServicesState(ImmutableDictionary.Empty); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/GrpcBrowser/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.JSInterop 8 | @using GrpcBrowser 9 | @using GrpcBrowser.Shared 10 | @using MudBlazor -------------------------------------------------------------------------------- /src/GrpcBrowser/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/GrpcBrowser/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/GrpcBrowser/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaswormald/grpc-browser/b521d35f24e05a1e3749af5b52e372a48d29d068/src/GrpcBrowser/icon.png -------------------------------------------------------------------------------- /src/GrpcBrowser/wwwroot/css/site.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaswormald/grpc-browser/b521d35f24e05a1e3749af5b52e372a48d29d068/src/GrpcBrowser/wwwroot/css/site.css -------------------------------------------------------------------------------- /src/GrpcBrowser/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaswormald/grpc-browser/b521d35f24e05a1e3749af5b52e372a48d29d068/src/GrpcBrowser/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Samples/FxService/Api/Account/AccountApi.cs: -------------------------------------------------------------------------------- 1 | using FxService.Api.Account; 2 | using Grpc.Core; 3 | using System.Reactive.Linq; 4 | using System.Reactive.Threading.Tasks; 5 | 6 | public class AccountApi: AccountService.AccountServiceBase 7 | { 8 | private readonly IAccountRepository _accountRepository; 9 | 10 | public AccountApi(IAccountRepository accountRepository) 11 | { 12 | _accountRepository = accountRepository; 13 | } 14 | 15 | public override Task SubscribeAccountBalances(AccountBalanceRequest request, IServerStreamWriter responseStream, ServerCallContext context) 16 | { 17 | return _accountRepository.BalanceChanges 18 | .Do(async balance => 19 | { 20 | var update = new AccountBalanceUpdate(); 21 | 22 | foreach (var kvp in balance) 23 | { 24 | update.CurrencyBalances.Add(kvp.Key, (double)kvp.Value); 25 | } 26 | 27 | update.LastUpdate = DateTimeOffset.Now.ToString("o"); 28 | 29 | await responseStream.WriteAsync(update); 30 | }) 31 | .ToTask(); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Samples/FxService/Api/Account/account.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option csharp_namespace = "FxService.Api.Account"; 4 | 5 | package account; 6 | 7 | message AccountBalanceRequest { 8 | bool allCurrencies = 1; 9 | repeated string currencies = 2; 10 | } 11 | 12 | message AccountBalanceUpdate { 13 | map currencyBalances = 1; 14 | string lastUpdate = 2; 15 | } 16 | 17 | // Keeps track of the currency account balances 18 | service AccountService { 19 | // Streams out the state of the currency account balances after a change. Make a trade to receive an update message 20 | rpc SubscribeAccountBalances (AccountBalanceRequest) returns (stream AccountBalanceUpdate); 21 | } 22 | -------------------------------------------------------------------------------- /src/Samples/FxService/Api/Fx/Dto.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Runtime.Serialization; 3 | 4 | [DataContract] 5 | public class FxRateRequest 6 | { 7 | [DataMember(Order = 1)] 8 | public string FromCurrency { get; set; } 9 | 10 | [DataMember(Order = 2)] 11 | public string ToCurrency { get; set; } 12 | 13 | [DataMember(Order = 3)] 14 | public bool Show { get; set; } 15 | } 16 | 17 | [DataContract] 18 | public class FxRateUpdate 19 | { 20 | [DataMember(Order = 1)] 21 | public string FromCurrency { get; set; } 22 | 23 | [DataMember(Order = 2)] 24 | public string ToCurrency { get; set; } 25 | 26 | [DataMember(Order = 3)] 27 | public decimal ConversionRate { get; set; } 28 | 29 | [DataMember(Order = 4)] 30 | public string Timestamp { get; set; } 31 | } 32 | 33 | [DataContract] 34 | public class FxOrder 35 | { 36 | [DataMember(Order = 1)] 37 | public string FromCurrency { get; set; } 38 | 39 | [DataMember(Order = 2)] 40 | public string ToCurrency { get; set; } 41 | 42 | [DataMember(Order = 3)] 43 | public decimal MinimumAcceptableConversionRate { get; set; } 44 | 45 | [DataMember(Order = 4)] 46 | public decimal AmountInFromCurrency { get; set; } 47 | } 48 | 49 | [DataContract] 50 | public class FxOrderResult 51 | { 52 | [DataMember(Order = 1)] 53 | public bool Success { get; set; } 54 | 55 | [DataMember(Order = 2)] 56 | public string? ErrorMessage { get; set; } 57 | 58 | [DataMember(Order = 3)] 59 | public Guid? OrderId { get; set; } 60 | } 61 | 62 | [DataContract] 63 | public class SetFxRateRequest 64 | { 65 | [DataMember(Order = 1)] 66 | public string FromCurrency { get; set; } 67 | 68 | [DataMember(Order = 2)] 69 | public string ToCurrency { get; set; } 70 | 71 | [DataMember(Order = 3)] 72 | public decimal ConversionRate { get; set; } 73 | } 74 | 75 | [DataContract] 76 | public class SetFxRateResult 77 | { 78 | public ImmutableDictionary<(string, string), decimal> FxRates { get; set; } 79 | } -------------------------------------------------------------------------------- /src/Samples/FxService/Api/Fx/FxApi.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf.Grpc; 2 | using ProtoBuf.Grpc.Configuration; 3 | using System.Reactive.Linq; 4 | 5 | /// 6 | /// Place FX trade orders, stream and update FX rates. 7 | /// 8 | [Service] 9 | public interface IFxApi 10 | { 11 | /// 12 | /// Make an FX trade. Supported currencies are GBP, USD and EUR 13 | /// 14 | [Operation] 15 | Task PlaceFxOrder(FxOrder request, CallContext context = default); 16 | 17 | /// 18 | /// Stream out individual FX rate changes 19 | /// 20 | [Operation] 21 | IAsyncEnumerable StreamFxRates(IAsyncEnumerable request, CallContext context = default); 22 | 23 | /// 24 | /// Set the FX rate between two currencies 25 | /// 26 | [Operation] 27 | Task SetFxRates(IAsyncEnumerable requestStream, CallContext context = default); 28 | } 29 | 30 | public class FxApi : IFxApi 31 | { 32 | private readonly IFxRepository _fxRepository; 33 | private readonly IAccountRepository _accountRepository; 34 | 35 | public FxApi(IFxRepository fxRepository, IAccountRepository accountRepository) 36 | { 37 | _fxRepository = fxRepository; 38 | _accountRepository = accountRepository; 39 | } 40 | 41 | public async Task PlaceFxOrder(FxOrder request, CallContext context = default) 42 | { 43 | // Good job I don't work for a bank, this is not safe at all. Bank repository needs to withdraw and deposit in one transaction 44 | 45 | var fxRate = await _fxRepository.GetFxRate(request.FromCurrency, request.ToCurrency); 46 | 47 | if (fxRate is null) 48 | { 49 | return new FxOrderResult 50 | { 51 | Success = false, 52 | ErrorMessage = $"Unable to find FX Rate from {request.FromCurrency} to {request.ToCurrency}" 53 | }; 54 | } 55 | 56 | var sufficientFunds = await _accountRepository.Withdraw(request.FromCurrency, request.AmountInFromCurrency); 57 | 58 | if (!sufficientFunds) 59 | { 60 | return new FxOrderResult 61 | { 62 | Success = false, 63 | ErrorMessage = $"Insufficient funds to withdraw {request.FromCurrency} {request.AmountInFromCurrency}" 64 | }; 65 | } 66 | 67 | var convertedAmount = fxRate.Rate * request.AmountInFromCurrency; 68 | 69 | await _accountRepository.Deposit(request.ToCurrency, convertedAmount); 70 | 71 | return new FxOrderResult 72 | { 73 | Success = true, 74 | OrderId = Guid.NewGuid() 75 | }; 76 | } 77 | 78 | public async Task SetFxRates(IAsyncEnumerable requestStream, CallContext context = default) 79 | { 80 | await foreach (var fxRate in requestStream) 81 | { 82 | await _fxRepository.SetFxRate(new FxRate(fxRate.FromCurrency, fxRate.ToCurrency, fxRate.ConversionRate)); 83 | } 84 | 85 | return new SetFxRateResult 86 | { 87 | FxRates = await _fxRepository.GetAllFxRates() 88 | }; 89 | } 90 | 91 | public IAsyncEnumerable StreamFxRates(IAsyncEnumerable request, CallContext context = default) 92 | { 93 | var currencies = new HashSet<(string, string)>(new[] {("USD", "EUR"), ("USD", "GBP"), ("GBP", "EUR"), ("GBP", "USD"), ("EUR", "GBP"), ("EUR", "USD")}); 94 | 95 | request.ToObservable().Subscribe(currencyUpdate => 96 | { 97 | if (currencyUpdate.Show) 98 | { 99 | currencies.Add((currencyUpdate.FromCurrency, currencyUpdate.ToCurrency)); 100 | } 101 | else 102 | { 103 | currencies.Remove((currencyUpdate.FromCurrency, currencyUpdate.ToCurrency)); 104 | } 105 | }); 106 | 107 | return 108 | _fxRepository.FxRateObservable 109 | .Where(update => currencies.Contains((update.FromCurrency, update.ToCurrency))) 110 | .Select(update => new FxRateUpdate 111 | { 112 | FromCurrency = update.FromCurrency, 113 | ToCurrency = update.ToCurrency, 114 | ConversionRate = update.Rate, 115 | Timestamp = DateTimeOffset.Now.ToString("o") 116 | }) 117 | .ToAsyncEnumerable(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Samples/FxService/Data/AccountRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Immutable; 3 | using System.Reactive.Subjects; 4 | 5 | public interface IAccountRepository 6 | { 7 | Task Withdraw(string currency, decimal amount); 8 | Task Deposit(string currency, decimal amount); 9 | IObservable> BalanceChanges { get; } 10 | } 11 | 12 | public class AccountRepository : IAccountRepository 13 | { 14 | private readonly ConcurrentDictionary _currencyBalances; 15 | private readonly Subject> _balanceStream; 16 | 17 | public AccountRepository() 18 | { 19 | _currencyBalances = new ConcurrentDictionary(); 20 | _balanceStream = new Subject>(); 21 | 22 | _currencyBalances["USD"] = 10_000; 23 | _currencyBalances["GBP"] = 20_000; 24 | _currencyBalances["EUR"] = 30_000; 25 | } 26 | 27 | private void UpdateBalanceStream() 28 | { 29 | _balanceStream.OnNext(_currencyBalances.ToImmutableDictionary()); 30 | } 31 | 32 | public Task Withdraw(string currency, decimal amount) 33 | { 34 | bool success = false; 35 | currency = currency.ToUpper(); 36 | 37 | _currencyBalances.AddOrUpdate(currency, _ => 0, (_, balance) => 38 | { 39 | if (balance >= amount) 40 | { 41 | success = true; 42 | return balance - amount; 43 | } 44 | 45 | return balance; 46 | }); 47 | 48 | UpdateBalanceStream(); 49 | 50 | return Task.FromResult(success); 51 | } 52 | 53 | public Task Deposit(string currency, decimal amount) 54 | { 55 | currency = currency.ToUpper(); 56 | 57 | _currencyBalances.AddOrUpdate(currency, amount, (_, balance) => balance + amount); 58 | 59 | UpdateBalanceStream(); 60 | 61 | return Task.CompletedTask; 62 | } 63 | 64 | public IObservable> BalanceChanges => _balanceStream; 65 | } 66 | -------------------------------------------------------------------------------- /src/Samples/FxService/Data/FxRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Reactive.Linq; 3 | using System.Reactive.Subjects; 4 | using System.Reactive.Threading.Tasks; 5 | 6 | public interface IFxRepository 7 | { 8 | IObservable FxRateObservable { get; } 9 | Task SetFxRate(FxRate rate); 10 | Task GetFxRate(string FromCurrency, string ToCurrency); 11 | Task> GetAllFxRates(); 12 | } 13 | 14 | public record FxRate(string FromCurrency, string ToCurrency, decimal Rate); 15 | 16 | public class FxRepository : IFxRepository 17 | { 18 | private readonly Subject _fxRateSubject; 19 | private ImmutableDictionary<(string, string), decimal> _rates; 20 | 21 | public FxRepository() 22 | { 23 | _rates = ImmutableDictionary<(string, string), decimal>.Empty; 24 | _fxRateSubject = new Subject(); 25 | 26 | _fxRateSubject.Subscribe(update => 27 | { 28 | _rates = _rates.SetItem((update.FromCurrency.ToUpper(), update.ToCurrency.ToUpper()), update.Rate); 29 | }); 30 | } 31 | 32 | public IObservable FxRateObservable => _fxRateSubject; 33 | 34 | public async Task SetFxRate(FxRate rate) 35 | { 36 | _fxRateSubject.OnNext(rate); 37 | } 38 | 39 | public Task GetFxRate(string FromCurrency, string ToCurrency) 40 | { 41 | return Task.FromResult( 42 | _rates.TryGetValue((FromCurrency.ToUpper(), ToCurrency.ToUpper()), out var result) 43 | ? new FxRate(FromCurrency, ToCurrency, result) 44 | : null); 45 | } 46 | 47 | public Task> GetAllFxRates() 48 | { 49 | return Task.FromResult(_rates); 50 | } 51 | } 52 | 53 | public class FxRateRandomiser : BackgroundService 54 | { 55 | private readonly IFxRepository _fxRepository; 56 | private readonly IDisposable _updater; 57 | 58 | public FxRateRandomiser(IFxRepository fxRepository) 59 | { 60 | _fxRepository = fxRepository; 61 | } 62 | 63 | protected override Task ExecuteAsync(CancellationToken stoppingToken) 64 | { 65 | var random = new Random(); 66 | 67 | var fxRates = new Dictionary<(string, string), decimal>(); 68 | 69 | fxRates[("USD", "EUR")] = 0.88m; 70 | fxRates[("EUR", "USD")] = 1.14m; 71 | fxRates[("USD", "GBP")] = 0.74m; 72 | fxRates[("GBP", "USD")] = 1.36m; 73 | fxRates[("GBP", "EUR")] = 1.19m; 74 | fxRates[("EUR", "GBP")] = 0.84m; 75 | 76 | return Observable.Interval(TimeSpan.FromSeconds(1)) 77 | .Do(_ => 78 | { 79 | var adjustment = (random.NextDouble() - 0.5) * 0.1; 80 | var rateToAdjust = fxRates.Keys.ToList()[random.Next(0, fxRates.Count - 1)]; 81 | var currentValue = fxRates[rateToAdjust]; 82 | var newValue = Math.Round(currentValue + (decimal)adjustment, 2); 83 | 84 | fxRates[rateToAdjust] = newValue; 85 | var (fromCurrency, toCurrency) = rateToAdjust; 86 | 87 | _fxRepository.SetFxRate(new FxRate(fromCurrency, toCurrency, newValue)); 88 | }) 89 | .ToTask(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Samples/FxService/FxService.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Samples/FxService/Program.cs: -------------------------------------------------------------------------------- 1 | using FxService.Api.Account; 2 | using GrpcBrowser.Configuration; 3 | using ProtoBuf.Grpc.Server; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | 7 | // Additional configuration is required to successfully run gRPC on macOS. 8 | // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 9 | 10 | // Add services to the container. 11 | builder.Services.AddGrpc(); 12 | builder.Services.AddCodeFirstGrpc(); 13 | builder.Services.AddGrpcBrowser(); 14 | 15 | builder.Services.AddSingleton(); 16 | builder.Services.AddSingleton(); 17 | builder.Services.AddHostedService(); 18 | 19 | var app = builder.Build(); 20 | 21 | app.UseGrpcBrowser(); 22 | 23 | // Configure the HTTP request pipeline. 24 | app.MapGrpcService().AddToGrpcBrowserWithClient(); 25 | app.MapGrpcService().AddToGrpcBrowserWithService(); 26 | 27 | app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); 28 | app.MapGrpcBrowser(); 29 | 30 | app.Run(); 31 | -------------------------------------------------------------------------------- /src/Samples/FxService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "FxService": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5068;https://localhost:7068", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Samples/FxService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Samples/FxService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "Kestrel": { 10 | "EndpointDefaults": { 11 | "Protocols": "Http2" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net50/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace SampleGrpcService.net50 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | CreateHostBuilder(args).Build().Run(); 16 | } 17 | 18 | // Additional configuration is required to successfully run gRPC on macOS. 19 | // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 20 | public static IHostBuilder CreateHostBuilder(string[] args) => 21 | Host.CreateDefaultBuilder(args) 22 | .ConfigureWebHostDefaults(webBuilder => 23 | { 24 | webBuilder.UseStartup(); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net50/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SampleGrpcService.net50": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": "true", 6 | "launchBrowser": true, 7 | "applicationUrl": "https://localhost:5001", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net50/Protos/sample.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option csharp_namespace = "SampleGrpcService.net50.Services.ProtoFirst"; 4 | 5 | package greet; 6 | 7 | message SampleProtoFirstRequest { 8 | string content = 1; 9 | } 10 | 11 | message SampleProtoFirstReply { 12 | string content = 1; 13 | } 14 | 15 | // This is a sample service that demonstrates all types of gRPC operations from a proto-first gRPC service 16 | service ProtoFirstGreeter { 17 | 18 | // A Unary operation takes a single request, and returns a single response 19 | rpc UnaryOperation (SampleProtoFirstRequest) returns (SampleProtoFirstReply); 20 | 21 | // A Server Streaming operation takes a single request, and returns a stream of zero or more responses 22 | rpc ServerStreamingOperation (SampleProtoFirstRequest) returns (stream SampleProtoFirstReply); 23 | 24 | // A Client Streaming operation takes a stream of one or more requests, and returns a single response when the request stream is closed 25 | rpc ClientStreamingOperation (stream SampleProtoFirstRequest) returns (SampleProtoFirstReply); 26 | 27 | // A Duplex operation take a stream of zero or more requests, and returns a stream of zero or more responses 28 | rpc DuplexStreamingOperation (stream SampleProtoFirstRequest) returns (stream SampleProtoFirstReply); 29 | } 30 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net50/SampleGrpcService.net50.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net50/Services/CodeFirst/CodeFirstService.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf.Grpc; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reactive.Linq; 6 | using System.Runtime.Serialization; 7 | using System.ServiceModel; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace SampleGrpcService.net50.Services.CodeFirst 13 | { 14 | [DataContract] 15 | public class SampleCodeFirstRequest 16 | { 17 | [DataMember(Order = 1)] 18 | public string Content { get; set; } 19 | } 20 | 21 | [DataContract] 22 | public class SampleCodeFirstReply 23 | { 24 | [DataMember(Order = 1)] 25 | public string Content { get; set; } 26 | } 27 | 28 | /// 29 | /// This is a sample service that demonstrates all types of gRPC operations from a code-first gRPC service 30 | /// 31 | [ServiceContract] 32 | public interface ICodeFirstGreeterService 33 | { 34 | /// 35 | /// A Unary parameterles operation takes no parameters, and returns a response 36 | /// 37 | /// 38 | [OperationContract] 39 | Task UnaryParameterlessOperation(); 40 | 41 | /// 42 | /// A Unary Void operation takes a single request, and does not return a response 43 | /// 44 | [OperationContract] 45 | Task UnaryVoidOperation(SampleCodeFirstRequest request, CallContext context = default); 46 | 47 | /// 48 | /// A Unary operation takes a single request, and returns a single response 49 | /// 50 | /// 51 | /// 52 | /// 53 | [OperationContract] 54 | Task UnaryOperation(SampleCodeFirstRequest request, CallContext context = default); 55 | 56 | /// 57 | /// A Unary operation takes a single request and a cancellation token, and returns a single response 58 | /// 59 | /// 60 | /// 61 | /// 62 | [OperationContract] 63 | Task UnaryOperationWithCancellationToken(SampleCodeFirstRequest request, CancellationToken token = default); 64 | 65 | /// 66 | /// A Server Streaming operation takes a single request, and returns a stream of zero or more responses 67 | /// 68 | [OperationContract] 69 | IAsyncEnumerable ServerStreamingOperation(SampleCodeFirstRequest request, CallContext context = default); 70 | 71 | /// 72 | /// A Client Streaming operation takes a stream of one or more requests, and returns a single response when the request stream is closed 73 | /// 74 | [OperationContract] 75 | Task ClientStreamingOperation(IAsyncEnumerable request, CallContext context = default); 76 | 77 | /// 78 | /// A Duplex operation take a stream of zero or more requests, and returns a stream of zero or more responses 79 | /// 80 | [OperationContract] 81 | IAsyncEnumerable DuplexStreamingOperation(IAsyncEnumerable request, CallContext context = default); 82 | } 83 | 84 | public class CodeFirstGreeterService : ICodeFirstGreeterService 85 | { 86 | public Task UnaryVoidOperation(SampleCodeFirstRequest request, CallContext context = default) 87 | { 88 | return Task.CompletedTask; 89 | } 90 | 91 | public Task UnaryOperation(SampleCodeFirstRequest request, CallContext context = default) 92 | { 93 | return Task.FromResult(new SampleCodeFirstReply { Content = $"Your request content was '{request.Content}'" }); 94 | } 95 | 96 | public IAsyncEnumerable ServerStreamingOperation(SampleCodeFirstRequest request, CallContext context = default) 97 | { 98 | return Observable.Interval(TimeSpan.FromSeconds(1)) 99 | .Select(i => new SampleCodeFirstReply { Content = $"Streaming message #{i}. Your request content was '{request.Content}'" }) 100 | .ToAsyncEnumerable(); 101 | } 102 | 103 | public async Task ClientStreamingOperation(IAsyncEnumerable request, CallContext context = default) 104 | { 105 | var messageCount = 0; 106 | var contentBuilder = new StringBuilder(); 107 | 108 | await foreach (var message in request) 109 | { 110 | messageCount++; 111 | contentBuilder.AppendLine(message.Content); 112 | } 113 | 114 | return new SampleCodeFirstReply() 115 | { 116 | Content = $"You sent {messageCount} messages. The content of these messages was:\n{contentBuilder}" 117 | }; 118 | } 119 | 120 | public IAsyncEnumerable DuplexStreamingOperation(IAsyncEnumerable request, CallContext context = default) 121 | { 122 | return Observable.Create(async obs => 123 | { 124 | var messageCount = 0; 125 | await foreach (var message in request) 126 | { 127 | messageCount++; 128 | obs.OnNext(new SampleCodeFirstReply() { Content = $"You have sent {messageCount} requests so far. The most recent request content was {message.Content}" }); 129 | } 130 | }).ToAsyncEnumerable(); 131 | } 132 | 133 | public Task UnaryParameterlessOperation() 134 | { 135 | return Task.FromResult(new SampleCodeFirstReply { Content = $"Your request content was nothing, as there is no request parameter." }); 136 | 137 | } 138 | 139 | public Task UnaryOperationWithCancellationToken(SampleCodeFirstRequest request, CancellationToken token = default) 140 | { 141 | return Task.FromResult(new SampleCodeFirstReply { Content = $"Your request content was '{request.Content}'" }); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net50/Services/ProtoFirst/ProtoFirstSampleService.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | using System; 3 | using System.Linq; 4 | using System.Reactive.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Reactive.Threading.Tasks; 8 | 9 | namespace SampleGrpcService.net50.Services.ProtoFirst 10 | { 11 | public class ProtoFirstSampleService : ProtoFirstGreeter.ProtoFirstGreeterBase 12 | { 13 | public override Task UnaryOperation(SampleProtoFirstRequest request, ServerCallContext context) 14 | { 15 | return Task.FromResult(new SampleProtoFirstReply { Content = $"Your request content was '{request.Content}'" }); 16 | } 17 | 18 | public override Task ServerStreamingOperation(SampleProtoFirstRequest request, IServerStreamWriter responseStream, ServerCallContext context) 19 | { 20 | return Observable.Interval(TimeSpan.FromSeconds(1)) 21 | .Select(i => new SampleProtoFirstReply { Content = $"Streaming message #{i}. Your request content was '{request.Content}'" }) 22 | .Do(reply => responseStream.WriteAsync(reply)) 23 | .ToTask(); 24 | } 25 | 26 | public override async Task ClientStreamingOperation(IAsyncStreamReader requestStream, ServerCallContext context) 27 | { 28 | var messageCount = 0; 29 | var contentBuilder = new StringBuilder(); 30 | 31 | await foreach (var message in requestStream.ReadAllAsync()) 32 | { 33 | messageCount++; 34 | contentBuilder.AppendLine(message.Content); 35 | } 36 | 37 | return new SampleProtoFirstReply() 38 | { 39 | Content = $"You sent {messageCount} messages. The content of these messages was:\n{contentBuilder}" 40 | }; 41 | } 42 | 43 | public override async Task DuplexStreamingOperation(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) 44 | { 45 | var messageCount = 0; 46 | await foreach (var message in requestStream.ReadAllAsync()) 47 | { 48 | messageCount++; 49 | await responseStream.WriteAsync(new SampleProtoFirstReply() { Content = $"You have sent {messageCount} requests so far. The most recent request content was {message.Content}" }); 50 | } 51 | 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net50/Startup.cs: -------------------------------------------------------------------------------- 1 | using GrpcBrowser; 2 | using GrpcBrowser.Configuration; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using ProtoBuf.Grpc.Server; 9 | using SampleGrpcService.net50.Services.CodeFirst; 10 | using SampleGrpcService.net50.Services.ProtoFirst; 11 | using System.Threading.Tasks; 12 | 13 | namespace SampleGrpcService.net50 14 | { 15 | public class Startup 16 | { 17 | // This method gets called by the runtime. Use this method to add services to the container. 18 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 19 | public void ConfigureServices(IServiceCollection services) 20 | { 21 | services.AddGrpc(); 22 | services.AddCodeFirstGrpc(); 23 | services.AddGrpcBrowser(); 24 | } 25 | 26 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 27 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 28 | { 29 | if (env.IsDevelopment()) 30 | { 31 | app.UseDeveloperExceptionPage(); 32 | } 33 | 34 | app.UseRouting(); 35 | 36 | app.UseGrpcBrowser(); 37 | 38 | app.UseEndpoints(endpoints => 39 | { 40 | endpoints.MapGrpcService().AddToGrpcBrowserWithClient(); 41 | endpoints.MapGrpcService().AddToGrpcBrowserWithService(); 42 | endpoints.MapGrpcBrowser(); 43 | 44 | endpoints.MapGet("/", context => 45 | { 46 | context.Response.StatusCode = 302; 47 | context.Response.Headers.Add("Location", "https://localhost:5001/grpc"); 48 | return Task.CompletedTask; 49 | }); 50 | }); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net50/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Grpc": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net50/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Kestrel": { 11 | "EndpointDefaults": { 12 | "Protocols": "Http2" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net60/Program.cs: -------------------------------------------------------------------------------- 1 | using GrpcBrowser.Configuration; 2 | using ProtoBuf.Grpc.Server; 3 | using SampleGrpcService.net60.Services.CodeFirst; 4 | using SampleGrpcService.net60.Services.ProtoFirst; 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | 8 | // Additional configuration is required to successfully run gRPC on macOS. 9 | // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 10 | 11 | // Add services to the container. 12 | builder.Services.AddGrpc(); 13 | builder.Services.AddCodeFirstGrpc(); 14 | builder.Services.AddGrpcBrowser(); 15 | 16 | var app = builder.Build(); 17 | 18 | // Configure the HTTP request pipeline. 19 | app.MapGrpcService().AddToGrpcBrowserWithClient(); 20 | app.MapGrpcService().AddToGrpcBrowserWithService(); 21 | app.UseRouting(); 22 | app.UseGrpcBrowser(); 23 | app.MapGrpcBrowser(); 24 | app.MapGet("/", context => 25 | { 26 | context.Response.StatusCode = 302; 27 | context.Response.Headers.Add("Location", "https://localhost:7262/grpc"); 28 | return Task.CompletedTask; 29 | }); 30 | 31 | app.Run(); 32 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net60/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SampleGrpcService.net60": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": true, 7 | "applicationUrl": "https://localhost:7262", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net60/Protos/sample.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option csharp_namespace = "SampleGrpcService.net60.Services.ProtoFirst"; 4 | 5 | package greet; 6 | 7 | message SampleProtoFirstRequest { 8 | string content = 1; 9 | } 10 | 11 | message SampleProtoFirstReply { 12 | string content = 1; 13 | } 14 | 15 | // This is a sample service that demonstrates all types of gRPC operations from a proto-first gRPC service 16 | service ProtoFirstGreeter { 17 | 18 | // A Unary operation takes a single request, and returns a single response 19 | rpc UnaryOperation (SampleProtoFirstRequest) returns (SampleProtoFirstReply); 20 | 21 | // A Server Streaming operation takes a single request, and returns a stream of zero or more responses 22 | rpc ServerStreamingOperation (SampleProtoFirstRequest) returns (stream SampleProtoFirstReply); 23 | 24 | // A Client Streaming operation takes a stream of one or more requests, and returns a single response when the request stream is closed 25 | rpc ClientStreamingOperation (stream SampleProtoFirstRequest) returns (SampleProtoFirstReply); 26 | 27 | // A Duplex operation take a stream of zero or more requests, and returns a stream of zero or more responses 28 | rpc DuplexStreamingOperation (stream SampleProtoFirstRequest) returns (stream SampleProtoFirstReply); 29 | } 30 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net60/SampleGrpcService.net60.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net60/Services/CodeFirst/CodeFirstService.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf.Grpc; 2 | using System.Reactive.Linq; 3 | using System.Runtime.Serialization; 4 | using System.ServiceModel; 5 | using System.Text; 6 | 7 | namespace SampleGrpcService.net60.Services.CodeFirst; 8 | 9 | [DataContract] 10 | public class SampleCodeFirstRequest 11 | { 12 | [DataMember(Order = 1)] 13 | public string Content { get; set; } 14 | } 15 | 16 | [DataContract] 17 | public class SampleCodeFirstReply 18 | { 19 | [DataMember(Order = 1)] 20 | public string Content { get; set; } 21 | } 22 | 23 | /// 24 | /// This is a sample service that demonstrates all types of gRPC operations from a code-first gRPC service 25 | /// 26 | [ServiceContract] 27 | public interface ICodeFirstGreeterService 28 | { 29 | /// 30 | /// A Unary parameterles operation takes no parameters, and returns a response 31 | /// 32 | /// 33 | [OperationContract] 34 | Task UnaryParameterlessOperation(); 35 | 36 | /// 37 | /// A Unary Void operation takes a single request, and does not return a response 38 | /// 39 | [OperationContract] 40 | Task UnaryVoidOperation(SampleCodeFirstRequest request, CallContext context = default); 41 | 42 | /// 43 | /// A Unary operation takes a single request, and returns a single response 44 | /// 45 | /// 46 | /// 47 | /// 48 | [OperationContract] 49 | Task UnaryOperation(SampleCodeFirstRequest request, CallContext context = default); 50 | 51 | /// 52 | /// A Unary operation takes a single request and a cancellation token, and returns a single response 53 | /// 54 | /// 55 | /// 56 | /// 57 | [OperationContract] 58 | Task UnaryOperationWithCancellationToken(SampleCodeFirstRequest request, CancellationToken token = default); 59 | 60 | 61 | /// 62 | /// A Server Streaming operation takes a single request, and returns a stream of zero or more responses 63 | /// 64 | [OperationContract] 65 | IAsyncEnumerable ServerStreamingOperation(SampleCodeFirstRequest request, CallContext context = default); 66 | 67 | /// 68 | /// A Client Streaming operation takes a stream of one or more requests, and returns a single response when the request stream is closed 69 | /// 70 | [OperationContract] 71 | Task ClientStreamingOperation(IAsyncEnumerable request, CallContext context = default); 72 | 73 | /// 74 | /// A Duplex operation take a stream of zero or more requests, and returns a stream of zero or more responses 75 | /// 76 | [OperationContract] 77 | IAsyncEnumerable DuplexStreamingOperation(IAsyncEnumerable request, CallContext context = default); 78 | } 79 | 80 | public class CodeFirstGreeterService : ICodeFirstGreeterService 81 | { 82 | public Task UnaryVoidOperation(SampleCodeFirstRequest request, CallContext context = default) 83 | { 84 | return Task.CompletedTask; 85 | } 86 | 87 | public Task UnaryOperation(SampleCodeFirstRequest request, CallContext context = default) 88 | { 89 | return Task.FromResult(new SampleCodeFirstReply { Content = $"Your request content was '{request.Content}'" }); 90 | } 91 | 92 | public IAsyncEnumerable ServerStreamingOperation(SampleCodeFirstRequest request, CallContext context = default) 93 | { 94 | return Observable.Interval(TimeSpan.FromSeconds(1)) 95 | .Select(i => new SampleCodeFirstReply { Content = $"Streaming message #{i}. Your request content was '{request.Content}'" }) 96 | .ToAsyncEnumerable(); 97 | } 98 | 99 | public async Task ClientStreamingOperation(IAsyncEnumerable request, CallContext context = default) 100 | { 101 | var messageCount = 0; 102 | var contentBuilder = new StringBuilder(); 103 | 104 | await foreach (var message in request) 105 | { 106 | messageCount++; 107 | contentBuilder.AppendLine(message.Content); 108 | } 109 | 110 | return new SampleCodeFirstReply() 111 | { 112 | Content = $"You sent {messageCount} messages. The content of these messages was:\n{contentBuilder}" 113 | }; 114 | } 115 | 116 | public IAsyncEnumerable DuplexStreamingOperation(IAsyncEnumerable request, CallContext context = default) 117 | { 118 | return Observable.Create(async obs => 119 | { 120 | var messageCount = 0; 121 | await foreach (var message in request) 122 | { 123 | messageCount++; 124 | obs.OnNext(new SampleCodeFirstReply() { Content = $"You have sent {messageCount} requests so far. The most recent request content was {message.Content}" }); 125 | } 126 | }).ToAsyncEnumerable(); 127 | } 128 | 129 | public Task UnaryParameterlessOperation() 130 | { 131 | return Task.FromResult(new SampleCodeFirstReply { Content = $"Your request content was nothing, as there is no request parameter." }); 132 | 133 | } 134 | 135 | public Task UnaryOperationWithCancellationToken(SampleCodeFirstRequest request, CancellationToken token = default) 136 | { 137 | return Task.FromResult(new SampleCodeFirstReply { Content = $"Your request content was '{request.Content}'" }); 138 | } 139 | 140 | } -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net60/Services/ProtoFirst/ProtoFirstSampleService.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | using System.Reactive.Linq; 3 | using System.Reactive.Threading.Tasks; 4 | using System.Text; 5 | 6 | namespace SampleGrpcService.net60.Services.ProtoFirst; 7 | 8 | public class ProtoFirstSampleService : ProtoFirstGreeter.ProtoFirstGreeterBase 9 | { 10 | public override Task UnaryOperation(SampleProtoFirstRequest request, ServerCallContext context) 11 | { 12 | return Task.FromResult(new SampleProtoFirstReply { Content = $"Your request content was '{request.Content}'" }); 13 | } 14 | 15 | public override Task ServerStreamingOperation(SampleProtoFirstRequest request, IServerStreamWriter responseStream, ServerCallContext context) 16 | { 17 | return Observable.Interval(TimeSpan.FromSeconds(1)) 18 | .Select(i => new SampleProtoFirstReply { Content = $"Streaming message #{i}. Your request content was '{request.Content}'" }) 19 | .Do(reply => responseStream.WriteAsync(reply)) 20 | .ToTask(); 21 | } 22 | 23 | public override async Task ClientStreamingOperation(IAsyncStreamReader requestStream, ServerCallContext context) 24 | { 25 | var messageCount = 0; 26 | var contentBuilder = new StringBuilder(); 27 | 28 | await foreach (var message in requestStream.ReadAllAsync()) 29 | { 30 | messageCount++; 31 | contentBuilder.AppendLine(message.Content); 32 | } 33 | 34 | return new SampleProtoFirstReply() 35 | { 36 | Content = $"You sent {messageCount} messages. The content of these messages was:\n{contentBuilder}" 37 | }; 38 | } 39 | 40 | public override async Task DuplexStreamingOperation(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) 41 | { 42 | var messageCount = 0; 43 | await foreach (var message in requestStream.ReadAllAsync()) 44 | { 45 | messageCount++; 46 | await responseStream.WriteAsync(new SampleProtoFirstReply() { Content = $"You have sent {messageCount} requests so far. The most recent request content was {message.Content}" }); 47 | } 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net60/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.net60/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "Kestrel": { 10 | "EndpointDefaults": { 11 | "Protocols": "Http2" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.netcore31/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace SampleGrpcService.netcore31 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | CreateHostBuilder(args).Build().Run(); 16 | } 17 | 18 | // Additional configuration is required to successfully run gRPC on macOS. 19 | // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 20 | public static IHostBuilder CreateHostBuilder(string[] args) => 21 | Host.CreateDefaultBuilder(args) 22 | .ConfigureWebHostDefaults(webBuilder => 23 | { 24 | webBuilder.UseStartup(); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.netcore31/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SampleGrpcService.netcore31": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "applicationUrl": "https://localhost:5001", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.netcore31/Protos/sample.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option csharp_namespace = "SampleGrpcService.netcore31.Services.ProtoFirst"; 4 | 5 | package greet; 6 | 7 | message SampleProtoFirstRequest { 8 | string content = 1; 9 | } 10 | 11 | message SampleProtoFirstReply { 12 | string content = 1; 13 | } 14 | 15 | // This is a sample service that demonstrates all types of gRPC operations from a proto-first gRPC service 16 | service ProtoFirstGreeter { 17 | 18 | // A Unary operation takes a single request, and returns a single response 19 | rpc UnaryOperation (SampleProtoFirstRequest) returns (SampleProtoFirstReply); 20 | 21 | // A Server Streaming operation takes a single request, and returns a stream of zero or more responses 22 | rpc ServerStreamingOperation (SampleProtoFirstRequest) returns (stream SampleProtoFirstReply); 23 | 24 | // A Client Streaming operation takes a stream of one or more requests, and returns a single response when the request stream is closed 25 | rpc ClientStreamingOperation (stream SampleProtoFirstRequest) returns (SampleProtoFirstReply); 26 | 27 | // A Duplex operation take a stream of zero or more requests, and returns a stream of zero or more responses 28 | rpc DuplexStreamingOperation (stream SampleProtoFirstRequest) returns (stream SampleProtoFirstReply); 29 | } 30 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.netcore31/SampleGrpcService.netcore31.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.netcore31/Services/CodeFirst/CodeFirstService.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf.Grpc; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reactive.Linq; 6 | using System.Runtime.Serialization; 7 | using System.ServiceModel; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace SampleGrpcService.netcore31.Services.CodeFirst 13 | { 14 | [DataContract] 15 | public class SampleCodeFirstRequest 16 | { 17 | [DataMember(Order = 1)] 18 | public string Content { get; set; } 19 | } 20 | 21 | [DataContract] 22 | public class SampleCodeFirstReply 23 | { 24 | [DataMember(Order = 1)] 25 | public string Content { get; set; } 26 | } 27 | 28 | /// 29 | /// This is a sample service that demonstrates all types of gRPC operations from a code-first gRPC service 30 | /// 31 | [ServiceContract] 32 | public interface ICodeFirstGreeterService 33 | { 34 | /// 35 | /// A Unary parameterles operation takes no parameters, and returns a response 36 | /// 37 | /// 38 | [OperationContract] 39 | Task UnaryParameterlessOperation(); 40 | 41 | /// 42 | /// A Unary Void operation takes a single request, and does not return a response 43 | /// 44 | [OperationContract] 45 | Task UnaryVoidOperation(SampleCodeFirstRequest request, CallContext context = default); 46 | 47 | /// 48 | /// A Unary operation takes a single request, and returns a single response 49 | /// 50 | /// 51 | /// 52 | /// 53 | [OperationContract] 54 | Task UnaryOperation(SampleCodeFirstRequest request, CallContext context = default); 55 | 56 | /// 57 | /// A Unary operation takes a single request and a cancellation token, and returns a single response 58 | /// 59 | /// 60 | /// 61 | /// 62 | [OperationContract] 63 | Task UnaryOperationWithCancellationToken(SampleCodeFirstRequest request, CancellationToken token = default); 64 | 65 | /// 66 | /// A Server Streaming operation takes a single request, and returns a stream of zero or more responses 67 | /// 68 | [OperationContract] 69 | IAsyncEnumerable ServerStreamingOperation(SampleCodeFirstRequest request, CallContext context = default); 70 | 71 | /// 72 | /// A Client Streaming operation takes a stream of one or more requests, and returns a single response when the request stream is closed 73 | /// 74 | [OperationContract] 75 | Task ClientStreamingOperation(IAsyncEnumerable request, CallContext context = default); 76 | 77 | /// 78 | /// A Duplex operation take a stream of zero or more requests, and returns a stream of zero or more responses 79 | /// 80 | [OperationContract] 81 | IAsyncEnumerable DuplexStreamingOperation(IAsyncEnumerable request, CallContext context = default); 82 | } 83 | 84 | public class CodeFirstGreeterService : ICodeFirstGreeterService 85 | { 86 | public Task UnaryVoidOperation(SampleCodeFirstRequest request, CallContext context = default) 87 | { 88 | return Task.CompletedTask; 89 | } 90 | 91 | public Task UnaryOperation(SampleCodeFirstRequest request, CallContext context = default) 92 | { 93 | return Task.FromResult(new SampleCodeFirstReply { Content = $"Your request content was '{request.Content}'" }); 94 | } 95 | 96 | public IAsyncEnumerable ServerStreamingOperation(SampleCodeFirstRequest request, CallContext context = default) 97 | { 98 | return Observable.Interval(TimeSpan.FromSeconds(1)) 99 | .Select(i => new SampleCodeFirstReply { Content = $"Streaming message #{i}. Your request content was '{request.Content}'" }) 100 | .ToAsyncEnumerable(); 101 | } 102 | 103 | public async Task ClientStreamingOperation(IAsyncEnumerable request, CallContext context = default) 104 | { 105 | var messageCount = 0; 106 | var contentBuilder = new StringBuilder(); 107 | 108 | await foreach (var message in request) 109 | { 110 | messageCount++; 111 | contentBuilder.AppendLine(message.Content); 112 | } 113 | 114 | return new SampleCodeFirstReply() 115 | { 116 | Content = $"You sent {messageCount} messages. The content of these messages was:\n{contentBuilder}" 117 | }; 118 | } 119 | 120 | public IAsyncEnumerable DuplexStreamingOperation(IAsyncEnumerable request, CallContext context = default) 121 | { 122 | return Observable.Create(async obs => 123 | { 124 | var messageCount = 0; 125 | await foreach (var message in request) 126 | { 127 | messageCount++; 128 | obs.OnNext(new SampleCodeFirstReply() { Content = $"You have sent {messageCount} requests so far. The most recent request content was {message.Content}" }); 129 | } 130 | }).ToAsyncEnumerable(); 131 | } 132 | 133 | public Task UnaryParameterlessOperation() 134 | { 135 | return Task.FromResult(new SampleCodeFirstReply { Content = $"Your request content was nothing, as there is no request parameter." }); 136 | 137 | } 138 | 139 | public Task UnaryOperationWithCancellationToken(SampleCodeFirstRequest request, CancellationToken token = default) 140 | { 141 | return Task.FromResult(new SampleCodeFirstReply { Content = $"Your request content was '{request.Content}'" }); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.netcore31/Services/ProtoFirst/ProtoFirstSampleService.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | using System; 3 | using System.Linq; 4 | using System.Reactive.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Reactive.Threading.Tasks; 8 | 9 | namespace SampleGrpcService.netcore31.Services.ProtoFirst 10 | { 11 | public class ProtoFirstSampleService : ProtoFirstGreeter.ProtoFirstGreeterBase 12 | { 13 | public override Task UnaryOperation(SampleProtoFirstRequest request, ServerCallContext context) 14 | { 15 | return Task.FromResult(new SampleProtoFirstReply { Content = $"Your request content was '{request.Content}'" }); 16 | } 17 | 18 | public override Task ServerStreamingOperation(SampleProtoFirstRequest request, IServerStreamWriter responseStream, ServerCallContext context) 19 | { 20 | return Observable.Interval(TimeSpan.FromSeconds(1)) 21 | .Select(i => new SampleProtoFirstReply { Content = $"Streaming message #{i}. Your request content was '{request.Content}'" }) 22 | .Do(reply => responseStream.WriteAsync(reply)) 23 | .ToTask(); 24 | } 25 | 26 | public override async Task ClientStreamingOperation(IAsyncStreamReader requestStream, ServerCallContext context) 27 | { 28 | var messageCount = 0; 29 | var contentBuilder = new StringBuilder(); 30 | 31 | await foreach (var message in requestStream.ReadAllAsync()) 32 | { 33 | messageCount++; 34 | contentBuilder.AppendLine(message.Content); 35 | } 36 | 37 | return new SampleProtoFirstReply() 38 | { 39 | Content = $"You sent {messageCount} messages. The content of these messages was:\n{contentBuilder}" 40 | }; 41 | } 42 | 43 | public override async Task DuplexStreamingOperation(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) 44 | { 45 | var messageCount = 0; 46 | await foreach (var message in requestStream.ReadAllAsync()) 47 | { 48 | messageCount++; 49 | await responseStream.WriteAsync(new SampleProtoFirstReply() { Content = $"You have sent {messageCount} requests so far. The most recent request content was {message.Content}" }); 50 | } 51 | 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.netcore31/Startup.cs: -------------------------------------------------------------------------------- 1 | using GrpcBrowser; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using ProtoBuf.Grpc.Server; 8 | using SampleGrpcService.netcore31.Services.ProtoFirst; 9 | using SampleGrpcService.netcore31.Services.CodeFirst; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Linq; 13 | using System.Threading.Tasks; 14 | using GrpcBrowser.Configuration; 15 | 16 | namespace SampleGrpcService.netcore31 17 | { 18 | public class Startup 19 | { 20 | // This method gets called by the runtime. Use this method to add services to the container. 21 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddGrpc(); 25 | services.AddCodeFirstGrpc(); 26 | services.AddGrpcBrowser(); 27 | } 28 | 29 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 30 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 31 | { 32 | if (env.IsDevelopment()) 33 | { 34 | app.UseDeveloperExceptionPage(); 35 | } 36 | 37 | app.UseRouting(); 38 | 39 | app.UseGrpcBrowser(); 40 | 41 | app.UseEndpoints(endpoints => 42 | { 43 | endpoints.MapGrpcService().AddToGrpcBrowserWithClient(); 44 | endpoints.MapGrpcService().AddToGrpcBrowserWithService(); 45 | endpoints.MapGrpcBrowser(); 46 | 47 | endpoints.MapGet("/", context => 48 | { 49 | context.Response.StatusCode = 302; 50 | context.Response.Headers.Add("Location", "https://localhost:5001/grpc"); 51 | return Task.CompletedTask; 52 | }); 53 | }); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.netcore31/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Grpc": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Samples/SampleGrpcService.netcore31/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Kestrel": { 11 | "EndpointDefaults": { 12 | "Protocols": "Http2" 13 | } 14 | } 15 | } 16 | --------------------------------------------------------------------------------