From d748d9c01d4e5de93c0290401cfd8429dc89ef99 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Fri, 18 Jan 2019 21:41:42 +0100 Subject: [PATCH 1/5] Refactor ContextPropagationToken --- src/csharp/Grpc.Core.Tests/CallOptionsTest.cs | 6 +- .../Grpc.Core.Tests/ContextPropagationTest.cs | 2 +- src/csharp/Grpc.Core/CallOptions.cs | 12 +- .../Grpc.Core/ContextPropagationOptions.cs | 59 +++++++++ .../Grpc.Core/ContextPropagationToken.cs | 124 +----------------- src/csharp/Grpc.Core/Internal/AsyncCall.cs | 5 +- .../Internal/ContextPropagationFlags.cs | 34 +++++ .../Internal/ContextPropagationTokenImpl.cs | 118 +++++++++++++++++ .../Internal/DefaultServerCallContext.cs | 2 +- 9 files changed, 228 insertions(+), 134 deletions(-) create mode 100644 src/csharp/Grpc.Core/ContextPropagationOptions.cs create mode 100644 src/csharp/Grpc.Core/Internal/ContextPropagationFlags.cs create mode 100644 src/csharp/Grpc.Core/Internal/ContextPropagationTokenImpl.cs diff --git a/src/csharp/Grpc.Core.Tests/CallOptionsTest.cs b/src/csharp/Grpc.Core.Tests/CallOptionsTest.cs index 8e5c411cadd..1fd48812b54 100644 --- a/src/csharp/Grpc.Core.Tests/CallOptionsTest.cs +++ b/src/csharp/Grpc.Core.Tests/CallOptionsTest.cs @@ -45,7 +45,7 @@ namespace Grpc.Core.Tests var writeOptions = new WriteOptions(); Assert.AreSame(writeOptions, options.WithWriteOptions(writeOptions).WriteOptions); - var propagationToken = new ContextPropagationToken(CallSafeHandle.NullInstance, DateTime.UtcNow, + var propagationToken = new ContextPropagationTokenImpl(CallSafeHandle.NullInstance, DateTime.UtcNow, CancellationToken.None, ContextPropagationOptions.Default); Assert.AreSame(propagationToken, options.WithPropagationToken(propagationToken).PropagationToken); @@ -72,13 +72,13 @@ namespace Grpc.Core.Tests Assert.AreEqual(DateTime.MaxValue, new CallOptions().Normalize().Deadline.Value); var deadline = DateTime.UtcNow; - var propagationToken1 = new ContextPropagationToken(CallSafeHandle.NullInstance, deadline, CancellationToken.None, + var propagationToken1 = new ContextPropagationTokenImpl(CallSafeHandle.NullInstance, deadline, CancellationToken.None, new ContextPropagationOptions(propagateDeadline: true, propagateCancellation: false)); Assert.AreEqual(deadline, new CallOptions(propagationToken: propagationToken1).Normalize().Deadline.Value); Assert.Throws(typeof(ArgumentException), () => new CallOptions(deadline: deadline, propagationToken: propagationToken1).Normalize()); var token = new CancellationTokenSource().Token; - var propagationToken2 = new ContextPropagationToken(CallSafeHandle.NullInstance, deadline, token, + var propagationToken2 = new ContextPropagationTokenImpl(CallSafeHandle.NullInstance, deadline, token, new ContextPropagationOptions(propagateDeadline: false, propagateCancellation: true)); Assert.AreEqual(token, new CallOptions(propagationToken: propagationToken2).Normalize().CancellationToken); Assert.Throws(typeof(ArgumentException), () => new CallOptions(cancellationToken: token, propagationToken: propagationToken2).Normalize()); diff --git a/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs b/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs index c8bc372202d..9a878bde436 100644 --- a/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs +++ b/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs @@ -72,7 +72,7 @@ namespace Grpc.Core.Tests helper.ClientStreamingHandler = new ClientStreamingServerMethod(async (requestStream, context) => { var propagationToken = context.CreatePropagationToken(); - Assert.IsNotNull(propagationToken.ParentCall); + Assert.IsNotNull(propagationToken.AsImplOrNull().ParentCall); var callOptions = new CallOptions(propagationToken: propagationToken); try diff --git a/src/csharp/Grpc.Core/CallOptions.cs b/src/csharp/Grpc.Core/CallOptions.cs index 75d8906a691..89fd047a1ea 100644 --- a/src/csharp/Grpc.Core/CallOptions.cs +++ b/src/csharp/Grpc.Core/CallOptions.cs @@ -236,22 +236,24 @@ namespace Grpc.Core internal CallOptions Normalize() { var newOptions = this; + // silently ignore the context propagation token if it wasn't produced by "us" + var propagationTokenImpl = propagationToken.AsImplOrNull(); if (propagationToken != null) { - if (propagationToken.Options.IsPropagateDeadline) + if (propagationTokenImpl.Options.IsPropagateDeadline) { GrpcPreconditions.CheckArgument(!newOptions.deadline.HasValue, "Cannot propagate deadline from parent call. The deadline has already been set explicitly."); - newOptions.deadline = propagationToken.ParentDeadline; + newOptions.deadline = propagationTokenImpl.ParentDeadline; } - if (propagationToken.Options.IsPropagateCancellation) + if (propagationTokenImpl.Options.IsPropagateCancellation) { GrpcPreconditions.CheckArgument(!newOptions.cancellationToken.CanBeCanceled, "Cannot propagate cancellation token from parent call. The cancellation token has already been set to a non-default value."); - newOptions.cancellationToken = propagationToken.ParentCancellationToken; + newOptions.cancellationToken = propagationTokenImpl.ParentCancellationToken; } } - + newOptions.headers = newOptions.headers ?? Metadata.Empty; newOptions.deadline = newOptions.deadline ?? DateTime.MaxValue; return newOptions; diff --git a/src/csharp/Grpc.Core/ContextPropagationOptions.cs b/src/csharp/Grpc.Core/ContextPropagationOptions.cs new file mode 100644 index 00000000000..160d10dc82c --- /dev/null +++ b/src/csharp/Grpc.Core/ContextPropagationOptions.cs @@ -0,0 +1,59 @@ +#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; + +namespace Grpc.Core +{ + /// + /// Options for . + /// + public class ContextPropagationOptions + { + /// + /// The context propagation options that will be used by default. + /// + public static readonly ContextPropagationOptions Default = new ContextPropagationOptions(); + + bool propagateDeadline; + bool propagateCancellation; + + /// + /// Creates new context propagation options. + /// + /// If set to true parent call's deadline will be propagated to the child call. + /// If set to true parent call's cancellation token will be propagated to the child call. + public ContextPropagationOptions(bool propagateDeadline = true, bool propagateCancellation = true) + { + this.propagateDeadline = propagateDeadline; + this.propagateCancellation = propagateCancellation; + } + + /// true if parent call's deadline should be propagated to the child call. + public bool IsPropagateDeadline + { + get { return this.propagateDeadline; } + } + + /// true if parent call's cancellation token should be propagated to the child call. + public bool IsPropagateCancellation + { + get { return this.propagateCancellation; } + } + } +} diff --git a/src/csharp/Grpc.Core/ContextPropagationToken.cs b/src/csharp/Grpc.Core/ContextPropagationToken.cs index fe5c8a5c99a..d0c3f0cd1d3 100644 --- a/src/csharp/Grpc.Core/ContextPropagationToken.cs +++ b/src/csharp/Grpc.Core/ContextPropagationToken.cs @@ -16,12 +16,6 @@ #endregion -using System; -using System.Threading; - -using Grpc.Core.Internal; -using Grpc.Core.Utils; - namespace Grpc.Core { /// @@ -32,124 +26,10 @@ namespace Grpc.Core /// The gRPC native layer provides some other contexts (like tracing context) that /// are not accessible to explicitly C# layer, but this token still allows propagating them. /// - public class ContextPropagationToken - { - /// - /// Default propagation mask used by C core. - /// - private const ContextPropagationFlags DefaultCoreMask = (ContextPropagationFlags)0xffff; - - /// - /// Default propagation mask used by C# - we want to propagate deadline - /// and cancellation token by our own means. - /// - internal const ContextPropagationFlags DefaultMask = DefaultCoreMask - & ~ContextPropagationFlags.Deadline & ~ContextPropagationFlags.Cancellation; - - readonly CallSafeHandle parentCall; - readonly DateTime deadline; - readonly CancellationToken cancellationToken; - readonly ContextPropagationOptions options; - - internal ContextPropagationToken(CallSafeHandle parentCall, DateTime deadline, CancellationToken cancellationToken, ContextPropagationOptions options) - { - this.parentCall = GrpcPreconditions.CheckNotNull(parentCall); - this.deadline = deadline; - this.cancellationToken = cancellationToken; - this.options = options ?? ContextPropagationOptions.Default; - } - - /// - /// Gets the native handle of the parent call. - /// - internal CallSafeHandle ParentCall - { - get - { - return this.parentCall; - } - } - - /// - /// Gets the parent call's deadline. - /// - internal DateTime ParentDeadline - { - get - { - return this.deadline; - } - } - - /// - /// Gets the parent call's cancellation token. - /// - internal CancellationToken ParentCancellationToken - { - get - { - return this.cancellationToken; - } - } - - /// - /// Get the context propagation options. - /// - internal ContextPropagationOptions Options - { - get - { - return this.options; - } - } - } - - /// - /// Options for . - /// - public class ContextPropagationOptions + public abstract class ContextPropagationToken { - /// - /// The context propagation options that will be used by default. - /// - public static readonly ContextPropagationOptions Default = new ContextPropagationOptions(); - - bool propagateDeadline; - bool propagateCancellation; - - /// - /// Creates new context propagation options. - /// - /// If set to true parent call's deadline will be propagated to the child call. - /// If set to true parent call's cancellation token will be propagated to the child call. - public ContextPropagationOptions(bool propagateDeadline = true, bool propagateCancellation = true) - { - this.propagateDeadline = propagateDeadline; - this.propagateCancellation = propagateCancellation; - } - - /// true if parent call's deadline should be propagated to the child call. - public bool IsPropagateDeadline + internal ContextPropagationToken() { - get { return this.propagateDeadline; } } - - /// true if parent call's cancellation token should be propagated to the child call. - public bool IsPropagateCancellation - { - get { return this.propagateCancellation; } - } - } - - /// - /// Context propagation flags from grpc/grpc.h. - /// - [Flags] - internal enum ContextPropagationFlags - { - Deadline = 1, - CensusStatsContext = 2, - CensusTracingContext = 4, - Cancellation = 8 } } diff --git a/src/csharp/Grpc.Core/Internal/AsyncCall.cs b/src/csharp/Grpc.Core/Internal/AsyncCall.cs index b6d687f71e7..e2a018e871b 100644 --- a/src/csharp/Grpc.Core/Internal/AsyncCall.cs +++ b/src/csharp/Grpc.Core/Internal/AsyncCall.cs @@ -494,13 +494,14 @@ namespace Grpc.Core.Internal return injectedNativeCall; // allows injecting a mock INativeCall in tests. } - var parentCall = details.Options.PropagationToken != null ? details.Options.PropagationToken.ParentCall : CallSafeHandle.NullInstance; + var parentCall = details.Options.PropagationToken != null ? details.Options.PropagationToken.AsImplOrNull().ParentCall : CallSafeHandle.NullInstance; var credentials = details.Options.Credentials; using (var nativeCredentials = credentials != null ? credentials.ToNativeCredentials() : null) { + // TODO(jtattermusch): is the "DefaultMask" correct here?? var result = details.Channel.Handle.CreateCall( - parentCall, ContextPropagationToken.DefaultMask, cq, + parentCall, ContextPropagationTokenImpl.DefaultMask, cq, details.Method, details.Host, Timespec.FromDateTime(details.Options.Deadline.Value), nativeCredentials); return result; } diff --git a/src/csharp/Grpc.Core/Internal/ContextPropagationFlags.cs b/src/csharp/Grpc.Core/Internal/ContextPropagationFlags.cs new file mode 100644 index 00000000000..11a9c93a67d --- /dev/null +++ b/src/csharp/Grpc.Core/Internal/ContextPropagationFlags.cs @@ -0,0 +1,34 @@ +#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; + +namespace Grpc.Core.Internal +{ + /// + /// Context propagation flags from grpc/grpc.h. + /// + [Flags] + internal enum ContextPropagationFlags + { + Deadline = 1, + CensusStatsContext = 2, + CensusTracingContext = 4, + Cancellation = 8 + } +} diff --git a/src/csharp/Grpc.Core/Internal/ContextPropagationTokenImpl.cs b/src/csharp/Grpc.Core/Internal/ContextPropagationTokenImpl.cs new file mode 100644 index 00000000000..1fd994e607c --- /dev/null +++ b/src/csharp/Grpc.Core/Internal/ContextPropagationTokenImpl.cs @@ -0,0 +1,118 @@ +#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.Threading; + +using Grpc.Core.Utils; + +namespace Grpc.Core.Internal +{ + /// + /// Implementation of ContextPropagationToken that carries + /// all fields needed for context propagation by C-core based implementation of gRPC. + /// Instances of ContextPropagationToken that are not of this + /// type will be recognized as "foreign" and will be silently ignored + /// (treated as if null). + /// + internal class ContextPropagationTokenImpl : ContextPropagationToken + { + /// + /// Default propagation mask used by C core. + /// + private const ContextPropagationFlags DefaultCoreMask = (ContextPropagationFlags)0xffff; + + /// + /// Default propagation mask used by C# - we want to propagate deadline + /// and cancellation token by our own means. + /// + internal const ContextPropagationFlags DefaultMask = DefaultCoreMask + & ~ContextPropagationFlags.Deadline & ~ContextPropagationFlags.Cancellation; + + readonly CallSafeHandle parentCall; + readonly DateTime deadline; + readonly CancellationToken cancellationToken; + readonly ContextPropagationOptions options; + + internal ContextPropagationTokenImpl(CallSafeHandle parentCall, DateTime deadline, CancellationToken cancellationToken, ContextPropagationOptions options) + { + this.parentCall = GrpcPreconditions.CheckNotNull(parentCall); + this.deadline = deadline; + this.cancellationToken = cancellationToken; + this.options = options ?? ContextPropagationOptions.Default; + } + + /// + /// Gets the native handle of the parent call. + /// + internal CallSafeHandle ParentCall + { + get + { + return this.parentCall; + } + } + + /// + /// Gets the parent call's deadline. + /// + internal DateTime ParentDeadline + { + get + { + return this.deadline; + } + } + + /// + /// Gets the parent call's cancellation token. + /// + internal CancellationToken ParentCancellationToken + { + get + { + return this.cancellationToken; + } + } + + /// + /// Get the context propagation options. + /// + internal ContextPropagationOptions Options + { + get + { + return this.options; + } + } + } + + internal static class ContextPropagationTokenExtensions + { + /// + /// Converts given ContextPropagationToken to ContextPropagationTokenImpl + /// if possible or returns null. + /// Being able to convert means that the context propagation token is recognized as + /// "ours" (was created by this implementation). + /// + public static ContextPropagationTokenImpl AsImplOrNull(this ContextPropagationToken instanceOrNull) + { + return instanceOrNull as ContextPropagationTokenImpl; + } + } +} diff --git a/src/csharp/Grpc.Core/Internal/DefaultServerCallContext.cs b/src/csharp/Grpc.Core/Internal/DefaultServerCallContext.cs index 8220e599f92..b33cb631e26 100644 --- a/src/csharp/Grpc.Core/Internal/DefaultServerCallContext.cs +++ b/src/csharp/Grpc.Core/Internal/DefaultServerCallContext.cs @@ -63,7 +63,7 @@ namespace Grpc.Core protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions options) { - return new ContextPropagationToken(callHandle, deadline, cancellationToken, options); + return new ContextPropagationTokenImpl(callHandle, deadline, cancellationToken, options); } protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) From f019339cecd3b6b3972d6aa31d47e7f8d23ab89e Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Mon, 21 Jan 2019 14:42:23 +0100 Subject: [PATCH 2/5] improve ContextPropagationToken doc comment --- src/csharp/Grpc.Core/ContextPropagationToken.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/csharp/Grpc.Core/ContextPropagationToken.cs b/src/csharp/Grpc.Core/ContextPropagationToken.cs index d0c3f0cd1d3..60e407dc781 100644 --- a/src/csharp/Grpc.Core/ContextPropagationToken.cs +++ b/src/csharp/Grpc.Core/ContextPropagationToken.cs @@ -23,8 +23,8 @@ namespace Grpc.Core /// In situations when a backend is making calls to another backend, /// it makes sense to propagate properties like deadline and cancellation /// token of the server call to the child call. - /// The gRPC native layer provides some other contexts (like tracing context) that - /// are not accessible to explicitly C# layer, but this token still allows propagating them. + /// Underlying gRPC implementation may provide other "opaque" contexts (like tracing context) that + /// are not explicitly accesible via the public C# API, but this token still allows propagating them. /// public abstract class ContextPropagationToken { From 50854e952122176a241c708a28b412a3ceefc0f6 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Mon, 21 Jan 2019 14:51:30 +0100 Subject: [PATCH 3/5] remove unsubstantiated TODO --- src/csharp/Grpc.Core/Internal/AsyncCall.cs | 1 - src/csharp/Grpc.Core/Internal/ContextPropagationTokenImpl.cs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/csharp/Grpc.Core/Internal/AsyncCall.cs b/src/csharp/Grpc.Core/Internal/AsyncCall.cs index e2a018e871b..f80ac4a2f9d 100644 --- a/src/csharp/Grpc.Core/Internal/AsyncCall.cs +++ b/src/csharp/Grpc.Core/Internal/AsyncCall.cs @@ -499,7 +499,6 @@ namespace Grpc.Core.Internal var credentials = details.Options.Credentials; using (var nativeCredentials = credentials != null ? credentials.ToNativeCredentials() : null) { - // TODO(jtattermusch): is the "DefaultMask" correct here?? var result = details.Channel.Handle.CreateCall( parentCall, ContextPropagationTokenImpl.DefaultMask, cq, details.Method, details.Host, Timespec.FromDateTime(details.Options.Deadline.Value), nativeCredentials); diff --git a/src/csharp/Grpc.Core/Internal/ContextPropagationTokenImpl.cs b/src/csharp/Grpc.Core/Internal/ContextPropagationTokenImpl.cs index 1fd994e607c..e2528e84cff 100644 --- a/src/csharp/Grpc.Core/Internal/ContextPropagationTokenImpl.cs +++ b/src/csharp/Grpc.Core/Internal/ContextPropagationTokenImpl.cs @@ -39,7 +39,8 @@ namespace Grpc.Core.Internal /// /// Default propagation mask used by C# - we want to propagate deadline - /// and cancellation token by our own means. + /// and cancellation token by our own means, everything else will be propagated + /// by C core automatically (according to DefaultCoreMask). /// internal const ContextPropagationFlags DefaultMask = DefaultCoreMask & ~ContextPropagationFlags.Deadline & ~ContextPropagationFlags.Cancellation; From 222dd9f3402e013aa229b260ccc87d70f7c4831d Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 24 Jan 2019 16:05:24 -0500 Subject: [PATCH 4/5] fixes from code review --- src/csharp/Grpc.Core/CallOptions.cs | 2 +- src/csharp/Grpc.Core/Internal/AsyncCall.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/csharp/Grpc.Core/CallOptions.cs b/src/csharp/Grpc.Core/CallOptions.cs index 89fd047a1ea..a92caae917d 100644 --- a/src/csharp/Grpc.Core/CallOptions.cs +++ b/src/csharp/Grpc.Core/CallOptions.cs @@ -238,7 +238,7 @@ namespace Grpc.Core var newOptions = this; // silently ignore the context propagation token if it wasn't produced by "us" var propagationTokenImpl = propagationToken.AsImplOrNull(); - if (propagationToken != null) + if (propagationTokenImpl != null) { if (propagationTokenImpl.Options.IsPropagateDeadline) { diff --git a/src/csharp/Grpc.Core/Internal/AsyncCall.cs b/src/csharp/Grpc.Core/Internal/AsyncCall.cs index f80ac4a2f9d..785081c341a 100644 --- a/src/csharp/Grpc.Core/Internal/AsyncCall.cs +++ b/src/csharp/Grpc.Core/Internal/AsyncCall.cs @@ -494,7 +494,7 @@ namespace Grpc.Core.Internal return injectedNativeCall; // allows injecting a mock INativeCall in tests. } - var parentCall = details.Options.PropagationToken != null ? details.Options.PropagationToken.AsImplOrNull().ParentCall : CallSafeHandle.NullInstance; + var parentCall = details.Options.PropagationToken.AsImplOrNull()?.ParentCall ?? CallSafeHandle.NullInstance; var credentials = details.Options.Credentials; using (var nativeCredentials = credentials != null ? credentials.ToNativeCredentials() : null) From a682c75f6f7a33fa7ebf41fa7fa0f782a9d74c79 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Fri, 25 Jan 2019 03:50:45 -0500 Subject: [PATCH 5/5] add tests for foreign context propagation token --- .../Grpc.Core.Tests/ContextPropagationTest.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs b/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs index 9a878bde436..4158579194f 100644 --- a/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs +++ b/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs @@ -154,5 +154,28 @@ namespace Grpc.Core.Tests await call.RequestStream.CompleteAsync(); Assert.AreEqual("PASS", await call); } + + [Test] + public void ForeignPropagationTokenInterpretedAsNull() + { + Assert.IsNull(new ForeignContextPropagationToken().AsImplOrNull()); + } + + [Test] + public async Task ForeignPropagationTokenIsIgnored() + { + helper.UnaryHandler = new UnaryServerMethod((request, context) => + { + return Task.FromResult("PASS"); + }); + + var callOptions = new CallOptions(propagationToken: new ForeignContextPropagationToken()); + await Calls.AsyncUnaryCall(helper.CreateUnaryCall(callOptions), "xyz"); + } + + // For testing, represents context propagation token that's not generated by Grpc.Core + private class ForeignContextPropagationToken : ContextPropagationToken + { + } } }