diff --git a/Directory.Packages.props b/Directory.Packages.props
index 096f72701..e5914c97a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -56,6 +56,7 @@
+
diff --git a/Grpc.DotNet.sln b/Grpc.DotNet.sln
index 24ff6e3dc..1e04a7f6a 100644
--- a/Grpc.DotNet.sln
+++ b/Grpc.DotNet.sln
@@ -140,6 +140,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.HealthCheck.Tests", "t
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Reflection.Tests", "test\Grpc.Reflection.Tests\Grpc.Reflection.Tests.csproj", "{857C5B4B-E2A8-4ACA-98FB-5E592E2224CC}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.StatusProto", "src\Grpc.StatusProto\Grpc.StatusProto.csproj", "{C01E4F44-9AB0-4478-A453-C88CCB49A4F1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.StatusProto.Tests", "test\Grpc.StatusProto.Tests\Grpc.StatusProto.Tests.csproj", "{E49FA5BF-4D67-4C95-9543-8E9FCEAF3609}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -302,6 +306,14 @@ Global
{857C5B4B-E2A8-4ACA-98FB-5E592E2224CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{857C5B4B-E2A8-4ACA-98FB-5E592E2224CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{857C5B4B-E2A8-4ACA-98FB-5E592E2224CC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C01E4F44-9AB0-4478-A453-C88CCB49A4F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C01E4F44-9AB0-4478-A453-C88CCB49A4F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C01E4F44-9AB0-4478-A453-C88CCB49A4F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C01E4F44-9AB0-4478-A453-C88CCB49A4F1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E49FA5BF-4D67-4C95-9543-8E9FCEAF3609}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E49FA5BF-4D67-4C95-9543-8E9FCEAF3609}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E49FA5BF-4D67-4C95-9543-8E9FCEAF3609}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E49FA5BF-4D67-4C95-9543-8E9FCEAF3609}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -355,6 +367,8 @@ Global
{B4153E7F-5CF3-4DFB-A9D1-5E77A2FB2C48} = {8C62055F-8CD7-4859-9001-634D544DF2AE}
{25544326-C145-4D05-A4C3-AC7D59E17196} = {CECC4AE8-9C4E-4727-939B-517CC2E58D65}
{857C5B4B-E2A8-4ACA-98FB-5E592E2224CC} = {CECC4AE8-9C4E-4727-939B-517CC2E58D65}
+ {C01E4F44-9AB0-4478-A453-C88CCB49A4F1} = {8C62055F-8CD7-4859-9001-634D544DF2AE}
+ {E49FA5BF-4D67-4C95-9543-8E9FCEAF3609} = {CECC4AE8-9C4E-4727-939B-517CC2E58D65}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CD5C2B19-49B4-480A-990C-36D98A719B07}
diff --git a/src/Grpc.StatusProto/ExceptionExtensions.cs b/src/Grpc.StatusProto/ExceptionExtensions.cs
new file mode 100644
index 000000000..b22397eb4
--- /dev/null
+++ b/src/Grpc.StatusProto/ExceptionExtensions.cs
@@ -0,0 +1,105 @@
+// Copyright 2023 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.
+
+using Google.Rpc;
+using Grpc.Shared;
+
+namespace Grpc.Core;
+
+///
+/// Extensions methods for
+///
+public static class ExceptionExtensions
+{
+ ///
+ /// Create a from an ,
+ /// populating the Message and StackTrace from the exception.
+ /// Note: experimental API that can change or be removed without any prior notice.
+ ///
+ ///
+ ///
+ /// For example:
+ ///
+ /// try { /* ... */
+ /// }
+ /// catch (Exception e) {
+ /// Google.Rpc.Status status = new() {
+ /// Code = (int)StatusCode.Internal,
+ /// Message = "Internal error",
+ /// Details = {
+ /// // populate debugInfo from the exception
+ /// Any.Pack(e.ToRpcDebugInfo())
+ /// }
+ /// };
+ /// // ...
+ /// }
+ ///
+ ///
+ ///
+ ///
+ /// Maximum number of inner exceptions to include in the StackTrace. Defaults
+ /// to not including any inner exceptions
+ ///
+ /// A new populated from the exception.
+ ///
+ public static DebugInfo ToRpcDebugInfo(this Exception exception, int innerDepth = 0)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(exception);
+
+ var debugInfo = new DebugInfo();
+
+ var message = exception.Message;
+ var name = exception.GetType().FullName;
+
+ // Populate the Detail from the exception type and message
+ debugInfo.Detail = message is null ? name : name + ": " + message;
+
+ // Populate the StackEntries from the exception StackTrace
+ if (exception.StackTrace is not null)
+ {
+ var sr = new StringReader(exception.StackTrace);
+ var entry = sr.ReadLine();
+ while (entry is not null)
+ {
+ debugInfo.StackEntries.Add(entry);
+ entry = sr.ReadLine();
+ }
+ }
+
+ // Add inner exceptions to the StackEntries
+ var inner = exception.InnerException;
+ while (innerDepth > 0 && inner is not null)
+ {
+ message = inner.Message;
+ name = inner.GetType().FullName;
+ debugInfo.StackEntries.Add("InnerException: " + (message is null ? name : name + ": " + message));
+
+ if (inner.StackTrace is not null)
+ {
+ var sr = new StringReader(inner.StackTrace);
+ var entry = sr.ReadLine();
+ while (entry is not null)
+ {
+ debugInfo.StackEntries.Add(entry);
+ entry = sr.ReadLine();
+ }
+ }
+
+ inner = inner.InnerException;
+ --innerDepth;
+ }
+
+ return debugInfo;
+ }
+}
diff --git a/src/Grpc.StatusProto/Grpc.StatusProto.csproj b/src/Grpc.StatusProto/Grpc.StatusProto.csproj
new file mode 100644
index 000000000..da239737e
--- /dev/null
+++ b/src/Grpc.StatusProto/Grpc.StatusProto.csproj
@@ -0,0 +1,29 @@
+
+
+
+ gRPC C# API for error handling using google/rpc/status.proto
+ gRPC RPC HTTP/2
+
+ true
+ true
+ net462;netstandard2.0;netstandard2.1
+ README.md
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Grpc.StatusProto/MetadataExtensions.cs b/src/Grpc.StatusProto/MetadataExtensions.cs
new file mode 100644
index 000000000..4cbe2b6a1
--- /dev/null
+++ b/src/Grpc.StatusProto/MetadataExtensions.cs
@@ -0,0 +1,83 @@
+// Copyright 2023 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.
+
+using Google.Protobuf;
+using Grpc.Shared;
+
+namespace Grpc.Core;
+
+///
+/// Extension methods for the Grpc.Core.Metadata
+///
+public static class MetadataExtensions
+{
+ ///
+ /// Name of key in the metadata for the binary encoding of
+ ///
+ ///
+ public const string StatusDetailsTrailerName = "grpc-status-details-bin";
+
+ ///
+ /// Get the from the metadata.
+ /// Note: experimental API that can change or be removed without any prior notice.
+ ///
+ ///
+ /// if true then null is returned on a parsing error,
+ /// otherwise
+ /// will be thrown if the metadata cannot be parsed.
+ ///
+ /// The found or null if it was
+ /// not present or could the data could not be parsed.
+ ///
+ public static Google.Rpc.Status? GetRpcStatus(this Metadata metadata, bool ignoreParseError = false)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(metadata);
+
+ var entry = metadata.Get(StatusDetailsTrailerName);
+ if (entry is null)
+ {
+ return null;
+ }
+ try
+ {
+ return Google.Rpc.Status.Parser.ParseFrom(entry.ValueBytes);
+ }
+ catch when (ignoreParseError)
+ {
+ // If the message is malformed just report there's no information.
+ return null;
+ }
+ }
+
+ ///
+ /// Add to the metadata.
+ /// Any existing status in the metadata will be overwritten.
+ /// Note: experimental API that can change or be removed without any prior notice.
+ ///
+ ///
+ /// Status to add
+ public static void SetRpcStatus(this Metadata metadata, Google.Rpc.Status status)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(metadata);
+ ArgumentNullThrowHelper.ThrowIfNull(status);
+
+ var entry = metadata.Get(StatusDetailsTrailerName);
+ while (entry is not null)
+ {
+ metadata.Remove(entry);
+ entry = metadata.Get(StatusDetailsTrailerName);
+ }
+ metadata.Add(StatusDetailsTrailerName, status.ToByteArray());
+ }
+}
diff --git a/src/Grpc.StatusProto/README.md b/src/Grpc.StatusProto/README.md
new file mode 100644
index 000000000..baef8e8f7
--- /dev/null
+++ b/src/Grpc.StatusProto/README.md
@@ -0,0 +1,301 @@
+# gRPC C# API for error handling with status.proto
+
+This is a protoype NuGet package providing C# and .NET client and server side support for the
+[gRPC richer error model](https://grpc.io/docs/guides/error/#richer-error-model).
+
+This feature is already available in many other implementations including C++,
+Go, Java and Python.
+
+This package has dependencies on these NuGet packages:
+* `Google.Api.CommonProtos` - to provide the proto implementations used by the richer error model
+* `Grpc.Core.Api` - for API classes such as `RpcException`
+
+## Error handling in gRPC
+
+The standard way for gRPC to report the success or failure of a gRPC call is for a
+status code to be returned. If a call completes successfully the server returns an `OK`
+status to the client, otherwise an error status code is returned with an optional string
+error message that provides further details about what happened. This is known as the
+_standard error model_ and is the official gRPC error model supported by all gRPC
+implementations.
+
+There is another error model known as the _richer error model_ that allows additional
+error details to be included by the server. These are expressed in protocol buffers
+messages, and a
+[set of standard error message types](https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto)
+is defined to cover most needs. The protobuf binary encoding of this extra error
+information is provided as trailing metadata in the response.
+
+For more information on the richer error model see the
+[gRPC documentation on error handling](https://grpc.io/docs/guides/error/),
+and the [Google APIs overview of the error model](https://cloud.google.com/apis/design/errors#error_model).
+
+## .NET implementation of the richer error model
+
+The error model is define by the protocol buffers files [status.proto](https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto)
+and [error_details.proto](https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto),
+and the `Google.Api.CommonProtos` NuGet package that provides the generated .NET classes
+from these proto files.
+
+The error is encapsulated by an instance of `Google.Rpc.Status` and
+returned in the trailing response metadata with well-known key `grpc-status-details-bin`.
+Setting and reading this metadata is handled
+for you when using the methods provided in this package.
+
+## Server Side
+
+The server side uses C#'s Object and Collection initializer syntax.
+
+The server returns the additional error information by throwing an `RpcException` that is
+created from a `Google.Rpc.Status` which contains the details of the error.
+
+To add messages to the `Details` repeated field in `Google.Rpc.Status`, wrap each one in `Any.Pack()` - see example below.
+
+The `Google.Rpc.Status` extension method `ToRpcException` creates the appropriate `RpcException` from the status.
+
+__Example__ - creating and throwing a `RpcException`:
+```C#
+public override Task SayHello(HelloRequest request, ServerCallContext context)
+{
+ ArgumentNotNullOrEmpty(request.Name);
+
+ return Task.FromResult(new HelloReply { Message = "Hello " + request.Name });
+}
+
+private static void ArgumentNotNullOrEmpty(string value, [CallerArgumentExpression(nameof(value))] string? paramName = null)
+{
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new Google.Rpc.Status
+ {
+ Code = (int)Code.InvalidArgument,
+ Message = "Bad request",
+ Details =
+ {
+ Any.Pack(new BadRequest
+ {
+ FieldViolations =
+ {
+ new BadRequest.Types.FieldViolation
+ {
+ Field = paramName,
+ Description = "Value is null or empty"
+ }
+ }
+ })
+ }
+ }.ToRpcException();
+ }
+}
+```
+
+### A note on error codes
+
+Both `Grpc.Core.StatusCode` and `Google.Rpc.Code` define enums for a common
+set of status codes such as `NotFound`, `PermissionDenied`, etc. They have the same values and are based on the codes defined
+in [grpc/status.h](https://github.com/grpc/grpc/blob/master/include/grpc/status.h).
+
+The recommendation is to use the values in `Google.Rpc.Code` as a convention.
+This is a must for Google APIs and strongly recommended for third party services.
+But users can use a different domain of values if they want and and as long as their
+services are mutually compatible, things will work fine.
+
+In the richer error model the `RpcException` will contain both a `Grpc.Core.Status` (for the
+standard error model) and a `Google.Rpc.Status` (for the richer error model), each with their
+own status code. While an application is free to set these to different values we recommend
+that they are set to the same value to avoid ambiguity.
+
+### Passing stack traces from the server to the client
+
+The richer error model defines a standard way of passing stack traces from the server to the
+client. The `DebugInfo` message can be populated with stack traces and then it can
+be included in the `Details` of the `Google.Rpc.Status`.
+
+This package includes the extension method `ToRpcDebugInfo` for `System.Exception` to help
+create the `DebugInfo` message with the details from the exception.
+
+Example:
+
+```C#
+try
+{
+ // ...
+}
+catch (Exception e)
+{
+ throw new Google.Rpc.Status
+ {
+ Code = (int)Google.Rpc.Code.Internal,
+ Message = "Internal error",
+ Details =
+ {
+ // populate debugInfo from the exception
+ Any.Pack(e.ToRpcDebugInfo()),
+ // Add any other messages to the details ...
+ }
+ }.ToRpcException();
+}
+```
+
+## Client Side
+
+There is an extension method to retrieve a `Google.Rpc.Status` from the metadata in
+an `RpcException`.
+
+Once the `Google.Rpc.Status` has been retrieved the messages in the `Details`
+can be unpacked. There are two ways of doing this:
+
+- calling `GetDetail()` with one of the expected message types
+- iterating over all the messages in the `Details` using `UnpackDetailMessage()`
+
+__Example__ - calling `GetDetail()`:
+
+```C#
+void PrintError(RpcException ex)
+{
+ // Get the status from the RpcException
+ Google.Rpc.Status? rpcStatus = ex.GetRpcStatus(); // Extension method
+
+ if (rpcStatus != null)
+ {
+ Console.WriteLine($"Google.Rpc Status: Code: {rpcStatus.Code}, Message: {rpcStatus.Message}");
+
+ // Try and get the ErrorInfo from the details
+ ErrorInfo? errorInfo = rpcStatus.GetDetail();
+ if (errorInfo != null)
+ {
+ Console.WriteLine($"\tErrorInfo: Reason: {errorInfo.Reason}, Domain: {errorInfo.Domain}");
+ foreach (var md in errorInfo.Metadata)
+ {
+ Console.WriteLine($"\tKey: {md.Key}, Value: {md.Value}");
+ }
+ }
+ // etc, for any other messages expected in the Details ...
+ }
+}
+```
+
+__Example__ - iterating over all the messages in the `Details`:
+
+```C#
+void PrintStatusDetails(RpcException ex)
+{
+ // Get the status from the RpcException
+ Google.Rpc.Status? rpcStatus = ex.GetRpcStatus(); // Extension method
+
+ if (rpcStatus != null)
+ {
+ // Decode each message item in the details in turn
+ foreach (var msg in rpcStatus.UnpackDetailMessages())
+ {
+ switch (msg)
+ {
+ case ErrorInfo errorInfo:
+ Console.WriteLine($"ErrorInfo: Reason: {errorInfo.Reason}, Domain: {errorInfo.Domain}");
+ foreach (var md in errorInfo.Metadata)
+ {
+ Console.WriteLine($"\tKey: {md.Key}, Value: {md.Value}");
+ }
+ break;
+
+ case BadRequest badRequest:
+ Console.WriteLine("BadRequest:");
+ foreach (BadRequest.Types.FieldViolation fv in badRequest.FieldViolations)
+ {
+ Console.WriteLine($"\tField: {fv.Field}, Description: {fv.Description}");
+ }
+ break;
+
+ // Other cases handled here ...
+ }
+ }
+ }
+
+```
+
+## Returning errors within gRPC streams
+
+The model described above allows you to return an error status when the gRPC call finishes.
+
+As an extension to the richer error model you may want to allow servers to send back
+multiple statuses when streaming responses without terminating the call.
+
+One way of doing this is to include a `google.rpc.Status` message in the definition
+of the response messages returned by the server. The client should also be aware
+that it may receive a status in the response.
+
+For example:
+
+
+```protobuf
+service WidgetLookupProvider {
+ rpc streamingLookup(stream WidgetReq) returns (stream WidgetRsp) {}
+}
+
+message WidgetReq {
+ string widget_name = 1;
+}
+
+message WidgetRsp {
+ oneof message{
+ // details when ok
+ string widget_details = 1;
+ // or error details
+ google.rpc.Status status = 2;
+ }
+}
+```
+
+Note: the [status.proto](https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto)
+and [error_details.proto](https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto)
+files are provided in the `Google.Api.CommonProtos` NuGet package.
+
+Example server code fragment:
+```C#
+await foreach (var request in requestStream.ReadAllAsync())
+{
+ var response = new WidgetRsp();
+
+ // ... process the request ...
+
+ // to return an error
+ if (error)
+ {
+ response.Status = new Google.Rpc.Status { /* ... */ };
+ }
+ else
+ {
+ response.WidgetDetails = "the details";
+ }
+}
+```
+
+Example client code fragment:
+```C#
+// reading the responses
+var responseReaderTask = Task.Run(async () =>
+{
+ await foreach (var rsp in call.ResponseStream.ReadAllAsync())
+ {
+ switch (rsp.MessageCase)
+ {
+ case WidgetRsp.MessageOneofCase.WidgetDetails:
+ // ... processes the details ...
+ break;
+ case WidgetRsp.MessageOneofCase.Status:
+ // ... handle the error ...
+ break;
+ }
+ }
+});
+
+// sending the requests
+foreach (var request in requests)
+{
+ await call.RequestStream.WriteAsync(request);
+}
+```
+
+## See also
+* [gRPC richer error model](https://grpc.io/docs/guides/error/#richer-error-model)
+* [Google.Api.CommonProtos](https://cloud.google.com/dotnet/docs/reference/Google.Api.CommonProtos/latest/Google.Api)
diff --git a/src/Grpc.StatusProto/RpcExceptionExtensions.cs b/src/Grpc.StatusProto/RpcExceptionExtensions.cs
new file mode 100644
index 000000000..5a7cfa3f1
--- /dev/null
+++ b/src/Grpc.StatusProto/RpcExceptionExtensions.cs
@@ -0,0 +1,37 @@
+// Copyright 2023 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.
+
+using Grpc.Shared;
+
+namespace Grpc.Core;
+
+///
+/// Extensions to for handling rich error model.
+///
+public static class RpcExceptionExtensions
+{
+ ///
+ /// Retrieves the message containing extended error information
+ /// from the trailers in an , if present.
+ /// Note: experimental API that can change or be removed without any prior notice.
+ ///
+ /// The RPC exception to retrieve details from. Must not be null.
+ /// The message specified in the exception, or null
+ /// if there is no such information.
+ public static Google.Rpc.Status? GetRpcStatus(this RpcException ex)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(ex);
+ return ex.Trailers.GetRpcStatus();
+ }
+}
diff --git a/src/Grpc.StatusProto/RpcStatusExtensions.cs b/src/Grpc.StatusProto/RpcStatusExtensions.cs
new file mode 100644
index 000000000..a3c8efd6a
--- /dev/null
+++ b/src/Grpc.StatusProto/RpcStatusExtensions.cs
@@ -0,0 +1,112 @@
+// Copyright 2023 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.
+
+using Grpc.Shared;
+
+namespace Grpc.Core;
+
+///
+/// Extensions for to retrieve detailed error information.
+/// Based on ideas from:
+/// https://github.com/googleapis/gax-dotnet/blob/main/Google.Api.Gax.Grpc/RpcExceptionExtensions.cs
+///
+public static class RpcStatusExtensions
+{
+ ///
+ /// Create a from the
+ /// Note: experimental API that can change or be removed without any prior notice.
+ ///
+ ///
+ ///
+ /// The and in the
+ /// within the exception are populated from the details in the
+ ///
+ ///
+ ///
+ ///
+ /// Example:
+ ///
+ /// throw new Google.Rpc.Status {
+ /// Code = (int) StatusCode.NotFound,
+ /// Message = "Simple error message",
+ /// Details = {
+ /// Any.Pack(new ErrorInfo { Domain = "example", Reason = "some reason" })
+ /// }
+ /// }.ToRpcException();
+ ///
+ ///
+ ///
+ ///
+ /// The RPC status. Must not be null
+ /// A populated with the details from the status.
+ public static RpcException ToRpcException(this Google.Rpc.Status status)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(status);
+
+ // Both Grpc.Core.StatusCode and Google.Rpc.Code define enums for a common
+ // set of status codes such as "NotFound", "PermissionDenied", etc. They have the same
+ // values and are based on the codes defined "grpc/status.h"
+ //
+ // However applications can use a different domain of values if they want and and as
+ // long as their services are mutually compatible, things will work fine.
+ //
+ // If an application wants to explicitly set different status codes in Grpc.Core.Status
+ // and Google.Rpc.Status then use the ToRpcException below that takes additional parameters.
+ //
+ // Check here that we can convert Google.Rpc.Status.Code to Grpc.Core.StatusCode,
+ // and if not use StatusCode.Unknown.
+ var statusCode = System.Enum.IsDefined(typeof(StatusCode), status.Code) ? (StatusCode)status.Code : StatusCode.Unknown;
+ return status.ToRpcException(statusCode, status.Message);
+ }
+
+ ///
+ /// Create a from the
+ /// Note: experimental API that can change or be removed without any prior notice.
+ ///
+ ///
+ ///
+ /// The and in the
+ /// within the exception are populated from the details in the
+ ///
+ ///
+ ///
+ ///
+ /// Example:
+ ///
+ /// throw new Google.Rpc.Status {
+ /// Code = (int) StatusCode.NotFound,
+ /// Message = "Simple error message",
+ /// Details = {
+ /// Any.Pack(new ErrorInfo { Domain = "example", Reason = "some reason" })
+ /// }
+ /// }.ToRpcException(StatusCode.NotFound, "status message");
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// The status to set in the contained
+ /// The details to set in the contained
+ ///
+ public static RpcException ToRpcException(this Google.Rpc.Status status, StatusCode statusCode, string message)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(status);
+
+ var metadata = new Metadata();
+ metadata.SetRpcStatus(status);
+ return new RpcException(
+ new Grpc.Core.Status(statusCode, message),
+ metadata);
+ }
+}
diff --git a/test/Grpc.StatusProto.Tests/ExceptionExtensionsTest.cs b/test/Grpc.StatusProto.Tests/ExceptionExtensionsTest.cs
new file mode 100644
index 000000000..dc17d9a3d
--- /dev/null
+++ b/test/Grpc.StatusProto.Tests/ExceptionExtensionsTest.cs
@@ -0,0 +1,131 @@
+#region Copyright notice and license
+// Copyright 2023 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.Text;
+using Google.Rpc;
+using NUnit.Framework;
+
+namespace Grpc.Core.Tests;
+
+///
+/// Tests for ExceptionExtensions
+///
+[TestFixture]
+public class ExceptionExtensionsTest
+{
+ [Test]
+ public void ToRpcDebugInfoTest()
+ {
+ try
+ {
+ // Arrange and Act
+ ThrowException("extra details");
+ }
+ catch (Exception ex)
+ {
+ // Assert
+ var debugInfo = ex.ToRpcDebugInfo();
+ Assert.IsNotNull(debugInfo);
+ Assert.AreEqual("System.ArgumentException: extra details", debugInfo.Detail);
+
+ // Concatenate the returned stack traces into one string for checking
+ var stackTraces = ConcatStackTraces(debugInfo);
+ Console.WriteLine("Test stack trace data:");
+ Console.WriteLine(stackTraces);
+
+ // Test that some of the elements in the stack traces we expect are present.
+ // We are not doing a very strict comparision of the entire stack trace
+ // in case the format is slightly different in different environments.
+ Assert.IsTrue(stackTraces.Contains("ExceptionExtensionsTest.ThrowException"));
+ Assert.IsTrue(stackTraces.Contains("ExceptionExtensionsTest.ToRpcDebugInfoTest"));
+ Assert.IsFalse(stackTraces.Contains("InnerException:"));
+ }
+ }
+
+ [Test]
+ public void ToRpcDebugInfo_WithInnerExceptionTest()
+ {
+ try
+ {
+ // Arrange and Act
+ ThrowException("extra details");
+ }
+ catch (Exception ex)
+ {
+ // Assert
+ var debugInfo = ex.ToRpcDebugInfo(1);
+ Assert.IsNotNull(debugInfo);
+ Assert.AreEqual("System.ArgumentException: extra details", debugInfo.Detail);
+
+ // Concatenate the returned stack traces into one string for checking
+ var stackTraces = ConcatStackTraces(debugInfo);
+ Console.WriteLine("Test stack trace data:");
+ Console.WriteLine(stackTraces);
+
+ // Test that some of the elements in the stack traces we expect are present.
+ // We are not doing a very strict comparision of the entire stack trace
+ // in case the format is slightly different in different environments.
+ Assert.IsTrue(stackTraces.Contains("ExceptionExtensionsTest.ThrowException"));
+ Assert.IsTrue(stackTraces.Contains("ExceptionExtensionsTest.ToRpcDebugInfo_WithInnerExceptionTest"));
+ Assert.IsTrue(stackTraces.Contains("InnerException: System.ApplicationException: inner exception"));
+ }
+ }
+
+ ///
+ /// Throw an exception that contains an inner exception so that we
+ /// produce a stack trace for the tests.
+ ///
+ ///
+ ///
+ private void ThrowException(string message)
+ {
+ try
+ {
+ ThrowInnerException("inner exception");
+ }
+ catch (Exception ex)
+ {
+ throw new ArgumentException(message, ex);
+ }
+ }
+
+ ///
+ /// Throw an exception that will be the inner exception in the tests
+ ///
+ ///
+ ///
+ private void ThrowInnerException(string message)
+ {
+ throw new System.ApplicationException(message);
+ }
+
+ ///
+ /// Join the stack entries into one string
+ ///
+ ///
+ ///
+ private string ConcatStackTraces(DebugInfo debugInfo)
+ {
+ var sb = new StringBuilder();
+
+ foreach (var stackEntry in debugInfo.StackEntries)
+ {
+ sb.AppendLine(stackEntry);
+ }
+
+ return sb.ToString();
+ }
+}
diff --git a/test/Grpc.StatusProto.Tests/Grpc.StatusProto.Tests.csproj b/test/Grpc.StatusProto.Tests/Grpc.StatusProto.Tests.csproj
new file mode 100644
index 000000000..79555410c
--- /dev/null
+++ b/test/Grpc.StatusProto.Tests/Grpc.StatusProto.Tests.csproj
@@ -0,0 +1,13 @@
+
+
+ net462;net6.0;net7.0;net8.0
+ true
+
+
+
+
+
+
+
+
+
diff --git a/test/Grpc.StatusProto.Tests/MetadataExtensionsTest.cs b/test/Grpc.StatusProto.Tests/MetadataExtensionsTest.cs
new file mode 100644
index 000000000..a21b882d6
--- /dev/null
+++ b/test/Grpc.StatusProto.Tests/MetadataExtensionsTest.cs
@@ -0,0 +1,162 @@
+#region Copyright notice and license
+// Copyright 2023 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 Google.Protobuf.WellKnownTypes;
+using Google.Rpc;
+using NUnit.Framework;
+using Google.Protobuf;
+
+namespace Grpc.Core.Tests;
+
+///
+/// Tests for MetadataExtensions
+///
+[TestFixture]
+public class MetadataExtensionsTest
+{
+ // creates a status to use in the tests
+ private readonly Google.Rpc.Status status = new()
+ {
+ Code = (int)StatusCode.NotFound,
+ Message = "Simple error message",
+ Details =
+ {
+ Any.Pack(new ErrorInfo
+ {
+ Domain = "some domain",
+ Reason = "a reason"
+ }),
+ Any.Pack(new RequestInfo
+ {
+ RequestId = "request id",
+ ServingData = "data"
+ }),
+ }
+ };
+
+ [Test]
+ public void SetRpcStatusTest()
+ {
+ // Arrange
+ var metadata = new Metadata();
+
+ // Act
+ metadata.SetRpcStatus(status);
+
+ // Assert
+ var entry = metadata.Get(MetadataExtensions.StatusDetailsTrailerName);
+ Assert.IsNotNull(entry);
+ var sts = Google.Rpc.Status.Parser.ParseFrom(entry!.ValueBytes);
+ Assert.AreEqual(status, sts);
+ }
+
+ [Test]
+ public void SetRpcStatus_MultipleTimes()
+ {
+ // Arrange
+ Google.Rpc.Status status1 = new()
+ {
+ Code = (int)StatusCode.NotFound,
+ Message = "first"
+ };
+
+ Google.Rpc.Status status2 = new()
+ {
+ Code = (int)StatusCode.NotFound,
+ Message = "second"
+ };
+
+ Google.Rpc.Status status3 = new()
+ {
+ Code = (int)StatusCode.NotFound,
+ Message = "third"
+ };
+ var metadata = new Metadata();
+
+ // Act - set the status three times
+ metadata.SetRpcStatus(status1);
+ metadata.SetRpcStatus(status2);
+ metadata.SetRpcStatus(status3);
+
+ // Assert - only the last one should be in the metadata
+ Assert.AreEqual(1, metadata.Count);
+
+ var entry = metadata.Get(MetadataExtensions.StatusDetailsTrailerName);
+ Assert.IsNotNull(entry);
+ var sts = Google.Rpc.Status.Parser.ParseFrom(entry!.ValueBytes);
+ Assert.AreEqual(status3, sts);
+ }
+
+ [Test]
+ public void GetRpcStatus_OK()
+ {
+ // Arrange
+ var metadata = new Metadata();
+ metadata.SetRpcStatus(status);
+
+ // Act - retrieve the status from the metadata
+ var sts = metadata.GetRpcStatus();
+
+ // Assert - status retrieved ok
+ Assert.IsNotNull(sts);
+ Assert.AreEqual(status, sts);
+ }
+
+ [Test]
+ public void GetRpcStatus_NotFound()
+ {
+ // Arrange
+ var metadata = new Metadata();
+
+ // Act - try and retrieve the non-existent status from the metadata
+ var sts = metadata.GetRpcStatus();
+
+ // Assert - not found
+ Assert.IsNull(sts);
+ }
+
+ [Test]
+ public void GetRpcStatus_BadEncoding()
+ {
+ // Arrange - create badly encoded status in the metadata
+ var metadata = new Metadata
+ {
+ { MetadataExtensions.StatusDetailsTrailerName, new byte[] { 1, 2, 3 } }
+ };
+
+ // Act - try and retrieve the badly formed status from the metadata
+ var sts = metadata.GetRpcStatus(ignoreParseError: true);
+
+ // Assert - not found as it could not be decoded
+ Assert.IsNull(sts);
+ }
+
+ [Test]
+ public void GetRpcStatus_BadEncodingWithException()
+ {
+ // Arrange - create badly encoded status in the metadata
+ var metadata = new Metadata
+ {
+ { MetadataExtensions.StatusDetailsTrailerName, new byte[] { 1, 2, 3 } }
+ };
+
+ // Act and Assert
+ // Try and retrieve the status from the metadata and expect an exception
+ // because it could not be decoded
+ _ = Assert.Throws(() => metadata.GetRpcStatus(ignoreParseError: false));
+ }
+
+}
diff --git a/test/Grpc.StatusProto.Tests/RpcExceptionExtensionsTest.cs b/test/Grpc.StatusProto.Tests/RpcExceptionExtensionsTest.cs
new file mode 100644
index 000000000..80fdfa5ef
--- /dev/null
+++ b/test/Grpc.StatusProto.Tests/RpcExceptionExtensionsTest.cs
@@ -0,0 +1,87 @@
+#region Copyright notice and license
+// Copyright 2023 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 Google.Protobuf.WellKnownTypes;
+using Google.Rpc;
+using NUnit.Framework;
+
+namespace Grpc.Core.Tests;
+
+///
+/// Tests for RpcExceptionExtensions
+///
+[TestFixture]
+public class RpcExceptionExtensionsTest
+{
+ // creates a status to use in the tests
+ private readonly Google.Rpc.Status status = new()
+ {
+ Code = (int)StatusCode.NotFound,
+ Message = "Simple error message",
+ Details =
+ {
+ Any.Pack(new ErrorInfo
+ {
+ Domain = "some domain",
+ Reason = "a reason"
+ }),
+ Any.Pack(new RequestInfo
+ {
+ RequestId = "request id",
+ ServingData = "data"
+ }),
+ }
+ };
+
+ [Test]
+ public void GetRpcStatus_OK()
+ {
+ // Act
+ var exception = status.ToRpcException();
+
+ // Assert - check the contents of the exception
+ Assert.AreEqual(status.Code, (int)exception.StatusCode);
+ Assert.AreEqual(status.Message, exception.Status.Detail);
+ var sts = exception.GetRpcStatus();
+ Assert.IsNotNull(sts);
+ Assert.AreEqual(status, sts);
+ }
+
+ [Test]
+ public void GetRpcStatus_NotFound()
+ {
+ // Act
+ var exception = new RpcException(new Core.Status());
+
+ // Assert - the exception does not contain a RpcStatus
+ var sts = exception.GetRpcStatus();
+ Assert.IsNull(sts);
+ }
+
+ [Test]
+ public void GetRpcStatus_SetCodeAndMessage()
+ {
+ // Arrange and Act - create the exception with status code and message
+ var exception = status.ToRpcException(StatusCode.Aborted, "Different message");
+
+ // Assert - check the details in the exception
+ Assert.AreEqual(StatusCode.Aborted, exception.StatusCode);
+ Assert.AreEqual("Different message", exception.Status.Detail);
+ var sts = exception.GetRpcStatus();
+ Assert.IsNotNull(sts);
+ Assert.AreEqual(status, sts);
+ }
+}
diff --git a/test/Grpc.StatusProto.Tests/RpcStatusExtensionsTest.cs b/test/Grpc.StatusProto.Tests/RpcStatusExtensionsTest.cs
new file mode 100644
index 000000000..1d03e58c7
--- /dev/null
+++ b/test/Grpc.StatusProto.Tests/RpcStatusExtensionsTest.cs
@@ -0,0 +1,423 @@
+#region Copyright notice and license
+// Copyright 2023 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 Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Google.Rpc;
+using NUnit.Framework;
+
+namespace Grpc.Core.Tests;
+
+///
+/// Tests for RpcStatusExtensions
+///
+[TestFixture]
+public class RpcStatusExtensionsTest
+{
+ [Test]
+ public void ToRpcExceptionTest()
+ {
+ // Arrange - create a status
+ var status = CreateFullStatus();
+ // Act - get exception from the status
+ var ex = status.ToRpcException();
+
+ // Assert - check the details in the exception
+ Assert.IsNotNull(ex);
+
+ var grpcSts = ex.Status;
+ Assert.AreEqual(StatusCode.ResourceExhausted, grpcSts.StatusCode);
+ Assert.AreEqual("Test", grpcSts.Detail);
+
+ var sts = ex.GetRpcStatus();
+ Assert.IsNotNull(sts);
+ Assert.AreEqual(status, sts);
+ }
+
+ [Test]
+ public void ToRpcExceptionWithParamsTest()
+ {
+ // Arrange - create a status
+ var status = CreateFullStatus();
+
+ // Act - get exception from the status with specific parameters
+ var ex = status.ToRpcException(StatusCode.Cancelled, "status message");
+ Assert.IsNotNull(ex);
+
+ // Assert - check the details in the exception
+ var grpcSts = ex.Status;
+ Assert.AreEqual(StatusCode.Cancelled, grpcSts.StatusCode);
+ Assert.AreEqual("status message", grpcSts.Detail);
+
+ var sts = ex.GetRpcStatus();
+ Assert.IsNotNull(sts);
+ Assert.AreEqual(status, sts);
+ }
+
+ [Test]
+ public void GetStatusDetailTest()
+ {
+ // Arrange - create a status
+ // The detailsMap contains all the Messages added to the status so
+ // these can be used in the comparisions when then are retrieved later
+ var detailsMap = new Dictionary();
+ var status = CreateFullStatus(detailsMap);
+
+ // Act
+ var badRequest = status.GetDetail();
+ // Assert
+ Assert.IsNotNull(badRequest);
+ var expected = detailsMap["badRequest"];
+ Assert.AreEqual(expected, badRequest);
+
+ // Act
+ var errorInfo = status.GetDetail();
+ // Assert
+ Assert.IsNotNull(errorInfo);
+ expected = detailsMap["errorInfo"];
+ Assert.AreEqual(expected, errorInfo);
+
+ // Act
+ var retryInfo = status.GetDetail();
+ // Assert
+ Assert.IsNotNull(retryInfo);
+ expected = detailsMap["retryInfo"];
+ Assert.AreEqual(expected, retryInfo);
+
+ // Act
+ var debugInfo = status.GetDetail();
+ // Assert
+ Assert.IsNotNull(debugInfo);
+ expected = detailsMap["debugInfo"];
+ Assert.AreEqual(expected, debugInfo);
+
+ // Act
+ var quotaFailure = status.GetDetail();
+ // Assert
+ Assert.IsNotNull(quotaFailure);
+ expected = detailsMap["quotaFailure"];
+ Assert.AreEqual(expected, quotaFailure);
+
+ // Act
+ var preconditionFailure = status.GetDetail();
+ // Assert
+ Assert.IsNotNull(preconditionFailure);
+ expected = detailsMap["preconditionFailure"];
+ Assert.AreEqual(expected, preconditionFailure);
+
+ // Act
+ var requestInfo = status.GetDetail();
+ // Assert
+ Assert.IsNotNull(requestInfo);
+ expected = detailsMap["requestInfo"];
+ Assert.AreEqual(expected, requestInfo);
+
+ // Act
+ var help = status.GetDetail();
+ // Assert
+ Assert.IsNotNull(help);
+ expected = detailsMap["help"];
+ Assert.AreEqual(expected, help);
+
+ // Act
+ var localizedMessage = status.GetDetail();
+ // Assert
+ Assert.IsNotNull(localizedMessage);
+ expected = detailsMap["localizedMessage"];
+ Assert.AreEqual(expected, localizedMessage);
+ }
+
+ [Test]
+ public void GetStatusDetail_NotFound()
+ {
+ // Arrange - create a status with only a few details
+ // The detailsMap contains all the Messages added to the status so
+ // these can be used in the comparisions when then are retrieved later
+ var detailsMap = new Dictionary();
+ var status = CreatePartialStatus(detailsMap);
+
+ // Act - try and retieve non-existent BadRequest from the status
+ var badRequest = status.GetDetail();
+ // Assert
+ Assert.IsNull(badRequest);
+ }
+
+ [Test]
+ public void UnpackDetailMessageTest()
+ {
+ // Arrange - create a status
+ // The detailsMap contains all the Messages added to the status so
+ // these can be used in the comparisions when then are retrieved later
+ var detailsMap = new Dictionary();
+ var status = CreateFullStatus(detailsMap);
+
+ // foundSet will contain the messages found in the status so we can
+ // check all those expected were present
+ var foundSet = new HashSet();
+
+ // Act and Assert - iterate over all the messages in the status
+ // and check they contain what is expected
+ foreach (var msg in status.UnpackDetailMessages())
+ {
+ switch (msg)
+ {
+ case ErrorInfo errorInfo:
+ {
+ var expected = detailsMap["errorInfo"];
+ Assert.AreEqual(expected, errorInfo);
+ foundSet.Add("errorInfo");
+ break;
+ }
+
+ case BadRequest badRequest:
+ {
+ var expected = detailsMap["badRequest"];
+ Assert.AreEqual(expected, badRequest);
+ foundSet.Add("badRequest");
+ break;
+ }
+
+ case RetryInfo retryInfo:
+ {
+ var expected = detailsMap["retryInfo"];
+ Assert.AreEqual(expected, retryInfo);
+ foundSet.Add("retryInfo");
+ break;
+ }
+
+ case DebugInfo debugInfo:
+ {
+ var expected = detailsMap["debugInfo"];
+ Assert.AreEqual(expected, debugInfo);
+ foundSet.Add("debugInfo");
+ break;
+ }
+
+ case QuotaFailure quotaFailure:
+ {
+ var expected = detailsMap["quotaFailure"];
+ Assert.AreEqual(expected, quotaFailure);
+ foundSet.Add("quotaFailure");
+ break;
+ }
+
+ case PreconditionFailure preconditionFailure:
+ {
+ var expected = detailsMap["preconditionFailure"];
+ Assert.AreEqual(expected, preconditionFailure);
+ foundSet.Add("preconditionFailure");
+ break;
+ }
+
+ case RequestInfo requestInfo:
+ {
+ var expected = detailsMap["requestInfo"];
+ Assert.AreEqual(expected, requestInfo);
+ foundSet.Add("requestInfo");
+ break;
+ }
+
+ case ResourceInfo resourceInfo:
+ {
+ var expected = detailsMap["resourceInfo"];
+ Assert.AreEqual(expected, resourceInfo);
+ foundSet.Add("resourceInfo");
+ break;
+ }
+
+ case Help help:
+ {
+ var expected = detailsMap["help"];
+ Assert.AreEqual(expected, help);
+ foundSet.Add("help");
+ break;
+ }
+
+ case LocalizedMessage localizedMessage:
+ {
+ var expected = detailsMap["localizedMessage"];
+ Assert.AreEqual(expected, localizedMessage);
+ foundSet.Add("localizedMessage");
+ break;
+ }
+ }
+ }
+
+ // check everything was returned
+ Assert.AreEqual(detailsMap.Count, foundSet.Count);
+
+ }
+
+ private static Google.Rpc.Status CreatePartialStatus(Dictionary? detailsMap = null)
+ {
+ var retryInfo = new RetryInfo
+ {
+ RetryDelay = Duration.FromTimeSpan(new TimeSpan(0, 0, 5))
+ };
+
+ var debugInfo = new DebugInfo()
+ {
+ StackEntries = { "stack1", "stack2" },
+ Detail = "detail"
+ };
+
+ // add details to a map for later checking
+ if (detailsMap != null)
+ {
+ detailsMap.Clear();
+ detailsMap.Add("retryInfo", retryInfo);
+ detailsMap.Add("debugInfo", debugInfo);
+ }
+
+ var status = new Google.Rpc.Status()
+ {
+ Code = (int)StatusCode.Unavailable,
+ Message = "partial status",
+ Details =
+ {
+ Any.Pack(retryInfo),
+ Any.Pack(debugInfo),
+ }
+ };
+
+ return status;
+ }
+
+ static Google.Rpc.Status CreateFullStatus(Dictionary? detailsMap = null)
+ {
+ var errorInfo = new ErrorInfo()
+ {
+ Domain = "Rich Error Model Demo",
+ Reason = "Full error requested in the demo",
+ Metadata =
+ {
+ { "key1", "value1" },
+ { "key2", "value2" }
+ }
+ };
+
+ var badRequest = new BadRequest()
+ {
+ FieldViolations =
+ {
+ new BadRequest.Types.FieldViolation()
+ {
+ Field = "field", Description = "description"
+ }
+ }
+ };
+
+ var retryInfo = new RetryInfo
+ {
+ RetryDelay = Duration.FromTimeSpan(new TimeSpan(0, 0, 5))
+ };
+
+ var debugInfo = new DebugInfo()
+ {
+ StackEntries = { "stack1", "stack2" },
+ Detail = "detail"
+ };
+
+ var quotaFailure = new QuotaFailure()
+ {
+ Violations =
+ {
+ new QuotaFailure.Types.Violation()
+ {
+ Description = "Too much disk space used",
+ Subject = "Disk23"
+ }
+ }
+ };
+
+ var preconditionFailure = new PreconditionFailure()
+ {
+ Violations =
+ {
+ new PreconditionFailure.Types.Violation()
+ {
+ Type = "type", Subject = "subject", Description = "description"
+ }
+ }
+ };
+
+ var requestInfo = new RequestInfo()
+ {
+ RequestId = "reqId",
+ ServingData = "data"
+ };
+
+ var resourceInfo = new ResourceInfo()
+ {
+ ResourceType = "type",
+ ResourceName = "name",
+ Owner = "owner",
+ Description = "description"
+ };
+
+ var help = new Help()
+ {
+ Links =
+ {
+ new Help.Types.Link() { Url="url1", Description="desc1" },
+ new Help.Types.Link() { Url="url2", Description="desc2" },
+ }
+ };
+
+ var localizedMessage = new LocalizedMessage()
+ {
+ Locale = "en-GB",
+ Message = "Example localised error message"
+ };
+
+ // add details to a map for later checking
+ if (detailsMap != null)
+ {
+ detailsMap.Clear();
+ detailsMap.Add("badRequest", badRequest);
+ detailsMap.Add("errorInfo", errorInfo);
+ detailsMap.Add("retryInfo", retryInfo);
+ detailsMap.Add("debugInfo", debugInfo);
+ detailsMap.Add("quotaFailure", quotaFailure);
+ detailsMap.Add("preconditionFailure", preconditionFailure);
+ detailsMap.Add("requestInfo", requestInfo);
+ detailsMap.Add("resourceInfo", resourceInfo);
+ detailsMap.Add("help", help);
+ detailsMap.Add("localizedMessage", localizedMessage);
+ }
+
+ var status = new Google.Rpc.Status()
+ {
+ Code = (int)StatusCode.ResourceExhausted,
+ Message = "Test",
+ Details =
+ {
+ Any.Pack(badRequest),
+ Any.Pack(errorInfo),
+ Any.Pack(retryInfo),
+ Any.Pack(debugInfo),
+ Any.Pack(quotaFailure),
+ Any.Pack(preconditionFailure),
+ Any.Pack(requestInfo),
+ Any.Pack(resourceInfo),
+ Any.Pack(help),
+ Any.Pack(localizedMessage)
+ }
+ };
+
+ return status;
+ }
+}