diff --git a/Directory.Build.props b/Directory.Build.props
index 0fb0ae78c..0434644c6 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -18,7 +18,7 @@
$(WarningsNotAsErrors);CS1591
true
- 8.0
+ 9.0
enable
diff --git a/examples/Retrier/Client/Client.csproj b/examples/Retrier/Client/Client.csproj
new file mode 100644
index 000000000..0d1dea897
--- /dev/null
+++ b/examples/Retrier/Client/Client.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/Retrier/Client/Program.cs b/examples/Retrier/Client/Program.cs
new file mode 100644
index 000000000..2477ee3f9
--- /dev/null
+++ b/examples/Retrier/Client/Program.cs
@@ -0,0 +1,195 @@
+#region Copyright notice and license
+
+// Copyright 2019 The gRPC Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Google.Protobuf.WellKnownTypes;
+using Grpc.Core;
+using Grpc.Net.Client;
+using Grpc.Net.Client.Configuration;
+using Retry;
+
+namespace Client
+{
+ public class Program
+ {
+ static async Task Main(string[] args)
+ {
+ using var channel = CreateChannel();
+ var client = new Retrier.RetrierClient(channel);
+
+ //await UnaryRetry(client);
+ await StreamingRetry(client);
+
+ Console.WriteLine("Shutting down");
+ Console.WriteLine("Press any key to exit...");
+ Console.ReadKey();
+ }
+
+ private static async Task UnaryRetry(Retrier.RetrierClient client)
+ {
+ Console.WriteLine("Delivering packages...");
+ foreach (var product in Products)
+ {
+ try
+ {
+ var package = new Package { Name = product };
+ var call = client.DeliverPackageAsync(package);
+ var response = await call;
+
+ #region Print success
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.Write(response.Message);
+ Console.ResetColor();
+ Console.Write(" " + await GetRetryCount(call.ResponseHeadersAsync));
+ Console.WriteLine();
+ #endregion
+ }
+ catch (RpcException ex)
+ {
+ #region Print failure
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine(ex.Status.Detail);
+ Console.ResetColor();
+ #endregion
+ }
+
+ await Task.Delay(TimeSpan.FromSeconds(0.2));
+ }
+ }
+
+ private static async Task StreamingRetry(Retrier.RetrierClient client)
+ {
+ var call = client.MessageUpload();
+
+ try
+ {
+ var lines = ImportantMessage.Split(Environment.NewLine);
+ for (var i = 0; i < lines.Length; i++)
+ {
+ #region Print percentage
+ if (i % 2 == 0)
+ {
+ Console.WriteLine((int)((i + 1D) / lines.Length * 100) + "% uploaded");
+ }
+ #endregion
+
+ await call.RequestStream.WriteAsync(new StringValue { Value = lines[i] });
+ await Task.Delay(TimeSpan.FromSeconds(0.1));
+ }
+ await call.RequestStream.CompleteAsync();
+
+ Console.WriteLine("Upload complete");
+ Console.WriteLine("Press any key to download important message...");
+ Console.ReadKey();
+
+ #region Print success
+ var count = 0;
+ await foreach (var line in call.ResponseStream.ReadAllAsync())
+ {
+ Console.ForegroundColor = (ConsoleColor)(count % 7) + 9;
+ Console.WriteLine(line.Value);
+ await Task.Delay(TimeSpan.FromSeconds(0.2));
+ count++;
+ }
+ Console.ResetColor();
+ Console.Write(" " + await GetRetryCount(call.ResponseHeadersAsync));
+ Console.WriteLine();
+ #endregion
+ }
+ catch (RpcException ex)
+ {
+ #region Print failure
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine(ex.Status.Detail);
+ Console.ResetColor();
+ #endregion
+ }
+ }
+
+ private static GrpcChannel CreateChannel()
+ {
+ var options = new GrpcChannelOptions
+ {
+ ServiceConfig = new ServiceConfig
+ {
+ MethodConfigs =
+ {
+ new MethodConfig
+ {
+ Names = { MethodName.Default },
+ }
+ }
+ }
+ };
+ return GrpcChannel.ForAddress("http://localhost:5000", options);
+ }
+
+ private static async Task GetRetryCount(Task responseHeadersTask)
+ {
+ var headers = await responseHeadersTask;
+ var previousAttemptCount = headers.GetValue("grpc-previous-rpc-attempts");
+ return previousAttemptCount != null ? $"(retry count: {previousAttemptCount})" : string.Empty;
+ }
+
+ private static readonly IList Products = new List
+ {
+ "Secrets of Silicon Valley",
+ "The Busy Executive's Database Guide",
+ "Emotional Security: A New Algorithm",
+ "Prolonged Data Deprivation: Four Case Studies",
+ "Cooking with Computers: Surreptitious Balance Sheets",
+ "Silicon Valley Gastronomic Treats",
+ "Sushi, Anyone?",
+ "Fifty Years in Buckingham Palace Kitchens",
+ "But Is It User Friendly?",
+ "You Can Combat Computer Stress!",
+ "Is Anger the Enemy?",
+ "Life Without Fear",
+ "The Gourmet Microwave",
+ "Onions, Leeks, and Garlic: Cooking Secrets of the Mediterranean",
+ "The Psychology of Computer Cooking",
+ "Straight Talk About Computers",
+ "Computer Phobic AND Non-Phobic Individuals: Behavior Variations",
+ "Net Etiquette"
+ };
+
+ private static readonly string ImportantMessage =@"
+ _____ _____ _____
+ | __ \| __ \ / ____|
+ __ _| |__) | |__) | |
+ / _` | _ /| ___/| |
+ | (_| | | \ \| | | |____
+ \__, |_| \_\_| \_____|
+ __/ |
+ |___/
+ _
+ (_)
+ _ ___
+ | / __|
+ | \__ \ _
+ |_|___/ | |
+ ___ ___ ___ | |
+ / __/ _ \ / _ \| |
+ | (_| (_) | (_) | |
+ \___\___/ \___/|_|
+
+ ";
+ }
+}
diff --git a/examples/Retrier/Proto/retry.proto b/examples/Retrier/Proto/retry.proto
new file mode 100644
index 000000000..2ba421457
--- /dev/null
+++ b/examples/Retrier/Proto/retry.proto
@@ -0,0 +1,32 @@
+// Copyright 2019 The gRPC Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+import "google/protobuf/wrappers.proto";
+
+package retry;
+
+service Retrier {
+ rpc DeliverPackage (Package) returns (Response);
+ rpc MessageUpload (stream google.protobuf.StringValue) returns (stream google.protobuf.StringValue);
+}
+
+message Package {
+ string name = 1;
+}
+
+message Response {
+ string message = 1;
+}
diff --git a/examples/Retrier/Retrier.sln b/examples/Retrier/Retrier.sln
new file mode 100644
index 000000000..fd620020c
--- /dev/null
+++ b/examples/Retrier/Retrier.sln
@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29230.61
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "Server\Server.csproj", "{534AC5F8-2DF2-40BD-87A5-B3D8310118C4}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{48A1D3BC-A14B-436A-8822-6DE2BEF8B747}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Net.Client", "..\..\src\Grpc.Net.Client\Grpc.Net.Client.csproj", "{F001F7FD-21F7-42E5-BFB6-D0136ACA8869}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Net.Common", "..\..\src\Grpc.Net.Common\Grpc.Net.Common.csproj", "{EB47A4E0-1AED-4D44-8BF6-BC7AE00D4058}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F001F7FD-21F7-42E5-BFB6-D0136ACA8869}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F001F7FD-21F7-42E5-BFB6-D0136ACA8869}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F001F7FD-21F7-42E5-BFB6-D0136ACA8869}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F001F7FD-21F7-42E5-BFB6-D0136ACA8869}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EB47A4E0-1AED-4D44-8BF6-BC7AE00D4058}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB47A4E0-1AED-4D44-8BF6-BC7AE00D4058}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EB47A4E0-1AED-4D44-8BF6-BC7AE00D4058}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EB47A4E0-1AED-4D44-8BF6-BC7AE00D4058}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {D22B3129-3BFB-41FA-9FCE-E45EBEF8C2DD}
+ EndGlobalSection
+EndGlobal
diff --git a/examples/Retrier/Server/Program.cs b/examples/Retrier/Server/Program.cs
new file mode 100644
index 000000000..8ec497bbf
--- /dev/null
+++ b/examples/Retrier/Server/Program.cs
@@ -0,0 +1,38 @@
+#region Copyright notice and license
+
+// Copyright 2019 The gRPC Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+
+namespace Server
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/examples/Retrier/Server/Server.csproj b/examples/Retrier/Server/Server.csproj
new file mode 100644
index 000000000..63e9d3525
--- /dev/null
+++ b/examples/Retrier/Server/Server.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+
+
diff --git a/examples/Retrier/Server/Services/RetrierService.cs b/examples/Retrier/Server/Services/RetrierService.cs
new file mode 100644
index 000000000..bb34a8011
--- /dev/null
+++ b/examples/Retrier/Server/Services/RetrierService.cs
@@ -0,0 +1,75 @@
+#region Copyright notice and license
+
+// Copyright 2019 The gRPC Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Google.Protobuf.WellKnownTypes;
+using Grpc.Core;
+using Retry;
+
+namespace Server
+{
+ public class RetrierService : Retrier.RetrierBase
+ {
+ private readonly Random _random = new Random();
+
+ public override Task DeliverPackage(Package request, ServerCallContext context)
+ {
+ const double deliveryChance = 0.5;
+ if (_random.NextDouble() > deliveryChance)
+ {
+ throw new RpcException(new Status(StatusCode.Unavailable, $"- {request.Name}"));
+ }
+
+ return Task.FromResult(new Response
+ {
+ Message = $"+ {request.Name}"
+ });
+ }
+
+ public override async Task MessageUpload(
+ IAsyncStreamReader requestStream,
+ IServerStreamWriter responseStream,
+ ServerCallContext context)
+ {
+ const double deliveryChance = 0.95;
+
+ // Receive chunks
+ var chunks = new List();
+ await foreach (var chunk in requestStream.ReadAllAsync())
+ {
+ if (_random.NextDouble() > deliveryChance)
+ {
+ throw new RpcException(new Status(StatusCode.Unavailable, $"Message chunk not delivered."));
+ }
+
+ chunks.Add(chunk.Value);
+ }
+
+ // Write chunks
+ foreach (var chunk in chunks)
+ {
+ await responseStream.WriteAsync(new StringValue
+ {
+ Value = chunk
+ });
+ }
+ }
+ }
+}
diff --git a/examples/Retrier/Server/Startup.cs b/examples/Retrier/Server/Startup.cs
new file mode 100644
index 000000000..739f2fbf0
--- /dev/null
+++ b/examples/Retrier/Server/Startup.cs
@@ -0,0 +1,48 @@
+#region Copyright notice and license
+
+// Copyright 2019 The gRPC Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Server
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddGrpc();
+ }
+
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+
+ app.UseRouting();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapGrpcService();
+ });
+ }
+ }
+}
diff --git a/examples/Retrier/Server/appsettings.Development.json b/examples/Retrier/Server/appsettings.Development.json
new file mode 100644
index 000000000..fe20c40cc
--- /dev/null
+++ b/examples/Retrier/Server/appsettings.Development.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Grpc": "Information",
+ "Microsoft": "Information"
+ }
+ }
+}
diff --git a/examples/Retrier/Server/appsettings.json b/examples/Retrier/Server/appsettings.json
new file mode 100644
index 000000000..f5f63744b
--- /dev/null
+++ b/examples/Retrier/Server/appsettings.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information"
+ }
+ },
+ "AllowedHosts": "*",
+ "Kestrel": {
+ "EndpointDefaults": {
+ "Protocols": "Http2"
+ }
+ }
+}
diff --git a/src/Grpc.AspNetCore.Server/InterceptorRegistration.cs b/src/Grpc.AspNetCore.Server/InterceptorRegistration.cs
index 828c032a2..51ee404d9 100644
--- a/src/Grpc.AspNetCore.Server/InterceptorRegistration.cs
+++ b/src/Grpc.AspNetCore.Server/InterceptorRegistration.cs
@@ -50,7 +50,7 @@ internal InterceptorRegistration(
{
throw new ArgumentNullException(nameof(arguments));
}
- for (int i = 0; i < arguments.Length; i++)
+ for (var i = 0; i < arguments.Length; i++)
{
if (arguments[i] == null)
{
diff --git a/src/Grpc.AspNetCore.Web/Internal/GrpcWebProtocolHelpers.cs b/src/Grpc.AspNetCore.Web/Internal/GrpcWebProtocolHelpers.cs
index 1950c5469..00d28233c 100644
--- a/src/Grpc.AspNetCore.Web/Internal/GrpcWebProtocolHelpers.cs
+++ b/src/Grpc.AspNetCore.Web/Internal/GrpcWebProtocolHelpers.cs
@@ -121,7 +121,7 @@ private static void WriteTrailersContent(Span buffer, IHeaderDictionary tr
// gRPC-Web protocol says that names should be lower-case and grpc-web JS client
// will check for 'grpc-status' and 'grpc-message' in trailers with lower-case key.
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2
- for (int i = 0; i < kv.Key.Length; i++)
+ for (var i = 0; i < kv.Key.Length; i++)
{
char c = kv.Key[i];
currentBuffer[i] = (byte)((uint)(c - 'A') <= ('Z' - 'A') ? c | 0x20 : c);
diff --git a/src/Grpc.Net.Client/Configuration/ConfigObject.cs b/src/Grpc.Net.Client/Configuration/ConfigObject.cs
new file mode 100644
index 000000000..297494b67
--- /dev/null
+++ b/src/Grpc.Net.Client/Configuration/ConfigObject.cs
@@ -0,0 +1,68 @@
+#region Copyright notice and license
+
+// Copyright 2019 The gRPC Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+using System.Collections;
+using System.Collections.Generic;
+using Grpc.Net.Client.Internal.Configuration;
+
+namespace Grpc.Net.Client.Configuration
+{
+ ///
+ /// Represents a configuration object. Implementations provide strongly typed wrappers over
+ /// collections of untyped values.
+ ///
+ public abstract class ConfigObject : IConfigValue
+ {
+ ///
+ /// Gets the underlying configuration values.
+ ///
+ public IDictionary Inner { get; }
+
+ internal ConfigObject() : this(new Dictionary())
+ {
+ }
+
+ internal ConfigObject(IDictionary inner)
+ {
+ Inner = inner;
+ }
+
+ object IConfigValue.Inner => Inner;
+
+ internal T? GetValue(string key)
+ {
+ if (Inner.TryGetValue(key, out var value))
+ {
+ return (T?)value;
+ }
+ return default;
+ }
+
+ internal void SetValue(string key, T? value)
+ {
+ if (value == null)
+ {
+ Inner.Remove(key);
+ }
+ else
+ {
+ Inner[key] = value;
+ }
+ }
+ }
+}
diff --git a/src/Grpc.Net.Client/Configuration/HedgingPolicy.cs b/src/Grpc.Net.Client/Configuration/HedgingPolicy.cs
new file mode 100644
index 000000000..7832dda16
--- /dev/null
+++ b/src/Grpc.Net.Client/Configuration/HedgingPolicy.cs
@@ -0,0 +1,84 @@
+#region Copyright notice and license
+
+// Copyright 2019 The gRPC Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using Grpc.Core;
+using Grpc.Net.Client.Internal.Configuration;
+
+namespace Grpc.Net.Client.Configuration
+{
+ ///
+ /// The hedging policy for outgoing calls. Hedged calls may execute more than
+ /// once on the server, so only idempotent methods should specify a hedging
+ /// policy.
+ ///
+ public sealed class HedgingPolicy : ConfigObject
+ {
+ internal const string MaxAttemptsPropertyName = "maxAttempts";
+ internal const string HedgingDelayPropertyName = "hedgingDelay";
+ internal const string NonFatalStatusCodesPropertyName = "nonFatalStatusCodes";
+
+ private ConfigProperty, IList