From e5c9b80566c3260035187b19e37c91f5cd3d4e2a Mon Sep 17 00:00:00 2001
From: Jan Tattermusch <jtattermusch@google.com>
Date: Tue, 14 Jul 2015 17:46:58 -0700
Subject: [PATCH] renaming stub to client and refactoring metadata class

---
 .../Grpc.Auth/OAuth2InterceptorFactory.cs     |   8 +-
 src/csharp/Grpc.Core/Calls.cs                 |   2 +-
 .../{Stub/AbstractStub.cs => ClientBase.cs}   |  34 +++-
 src/csharp/Grpc.Core/Grpc.Core.csproj         |   3 +-
 .../Internal/MetadataArraySafeHandle.cs       |   8 +-
 src/csharp/Grpc.Core/Metadata.cs              | 179 ++++++++++++++----
 .../Grpc.Core/Stub/StubConfiguration.cs       |  64 -------
 src/csharp/Grpc.Core/Version.cs               |   1 -
 .../Grpc.Examples.MathClient/MathClient.cs    |  14 +-
 src/csharp/Grpc.Examples/MathExamples.cs      |  26 +--
 src/csharp/Grpc.Examples/MathGrpc.cs          |   2 +-
 src/csharp/Grpc.HealthCheck/HealthGrpc.cs     |   2 +-
 .../Grpc.IntegrationTesting/TestGrpc.cs       |   2 +-
 13 files changed, 196 insertions(+), 149 deletions(-)
 rename src/csharp/Grpc.Core/{Stub/AbstractStub.cs => ClientBase.cs} (74%)
 delete mode 100644 src/csharp/Grpc.Core/Stub/StubConfiguration.cs

diff --git a/src/csharp/Grpc.Auth/OAuth2InterceptorFactory.cs b/src/csharp/Grpc.Auth/OAuth2InterceptorFactory.cs
index ca384d1a6e..420c4cb537 100644
--- a/src/csharp/Grpc.Auth/OAuth2InterceptorFactory.cs
+++ b/src/csharp/Grpc.Auth/OAuth2InterceptorFactory.cs
@@ -52,10 +52,10 @@ namespace Grpc.Auth
         /// <summary>
         /// Creates OAuth2 interceptor.
         /// </summary>
-        public static HeaderInterceptorDelegate Create(GoogleCredential googleCredential)
+        public static MetadataInterceptorDelegate Create(GoogleCredential googleCredential)
         {
             var interceptor = new OAuth2Interceptor(googleCredential.InternalCredential, SystemClock.Default);
-            return new HeaderInterceptorDelegate(interceptor.InterceptHeaders);
+            return new MetadataInterceptorDelegate(interceptor.InterceptHeaders);
         }
 
         /// <summary>
@@ -94,10 +94,10 @@ namespace Grpc.Auth
                 return credential.Token.AccessToken;
             }
 
-            public void InterceptHeaders(Metadata.Builder headerBuilder)
+            public void InterceptHeaders(Metadata metadata)
             {
                 var accessToken = GetAccessToken(CancellationToken.None);
-                headerBuilder.Add(new Metadata.MetadataEntry(AuthorizationHeader, Schema + " " + accessToken));
+                metadata.Add(new Metadata.Entry(AuthorizationHeader, Schema + " " + accessToken));
             }
         }
     }
diff --git a/src/csharp/Grpc.Core/Calls.cs b/src/csharp/Grpc.Core/Calls.cs
index 750282258f..9e95182c72 100644
--- a/src/csharp/Grpc.Core/Calls.cs
+++ b/src/csharp/Grpc.Core/Calls.cs
@@ -39,7 +39,7 @@ using Grpc.Core.Internal;
 namespace Grpc.Core
 {
     /// <summary>
-    /// Helper methods for generated client stubs to make RPC calls.
+    /// Helper methods for generated clients to make RPC calls.
     /// </summary>
     public static class Calls
     {
diff --git a/src/csharp/Grpc.Core/Stub/AbstractStub.cs b/src/csharp/Grpc.Core/ClientBase.cs
similarity index 74%
rename from src/csharp/Grpc.Core/Stub/AbstractStub.cs
rename to src/csharp/Grpc.Core/ClientBase.cs
index 4a8b254357..59dc40ba4e 100644
--- a/src/csharp/Grpc.Core/Stub/AbstractStub.cs
+++ b/src/csharp/Grpc.Core/ClientBase.cs
@@ -32,26 +32,39 @@
 #endregion
 
 using System;
+using System.Collections.Generic;
+
 using Grpc.Core.Internal;
 
 namespace Grpc.Core
 {
-    // TODO: support adding timeout to methods.
+    public delegate void MetadataInterceptorDelegate(Metadata metadata);
+
     /// <summary>
-    /// Base for client-side stubs.
+    /// Base class for client-side stubs.
     /// </summary>
-    public abstract class AbstractStub<TStub, TConfig>
-        where TConfig : StubConfiguration
+    public abstract class ClientBase
     {
         readonly Channel channel;
-        readonly TConfig config;
 
-        public AbstractStub(Channel channel, TConfig config)
+        public ClientBase(Channel channel)
         {
             this.channel = channel;
-            this.config = config;
         }
 
+        /// <summary>
+        /// Can be used to register a custom header (initial metadata) interceptor.
+        /// The delegate each time before a new call on this client is started.
+        /// </summary>
+        public MetadataInterceptorDelegate HeaderInterceptor
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// Channel associated with this client.
+        /// </summary>
         public Channel Channel
         {
             get
@@ -67,9 +80,10 @@ namespace Grpc.Core
             where TRequest : class
             where TResponse : class
         {
-            var headerBuilder = Metadata.CreateBuilder();
-            config.HeaderInterceptor(headerBuilder);
-            return new Call<TRequest, TResponse>(serviceName, method, channel, headerBuilder.Build());
+            var metadata = new Metadata();
+            HeaderInterceptor(metadata);
+            metadata.Freeze();
+            return new Call<TRequest, TResponse>(serviceName, method, channel, metadata);
         }
     }
 }
diff --git a/src/csharp/Grpc.Core/Grpc.Core.csproj b/src/csharp/Grpc.Core/Grpc.Core.csproj
index cde42c3b7e..a227fe5477 100644
--- a/src/csharp/Grpc.Core/Grpc.Core.csproj
+++ b/src/csharp/Grpc.Core/Grpc.Core.csproj
@@ -88,8 +88,7 @@
     <Compile Include="ServerCredentials.cs" />
     <Compile Include="Metadata.cs" />
     <Compile Include="Internal\MetadataArraySafeHandle.cs" />
-    <Compile Include="Stub\AbstractStub.cs" />
-    <Compile Include="Stub\StubConfiguration.cs" />
+    <Compile Include="ClientBase.cs" />
     <Compile Include="Internal\ServerCalls.cs" />
     <Compile Include="ServerMethods.cs" />
     <Compile Include="Internal\ClientRequestStream.cs" />
diff --git a/src/csharp/Grpc.Core/Internal/MetadataArraySafeHandle.cs b/src/csharp/Grpc.Core/Internal/MetadataArraySafeHandle.cs
index c9c4d954c9..80aa7f5603 100644
--- a/src/csharp/Grpc.Core/Internal/MetadataArraySafeHandle.cs
+++ b/src/csharp/Grpc.Core/Internal/MetadataArraySafeHandle.cs
@@ -54,11 +54,11 @@ namespace Grpc.Core.Internal
 
         public static MetadataArraySafeHandle Create(Metadata metadata)
         {
-            var entries = metadata.Entries;
-            var metadataArray = grpcsharp_metadata_array_create(new UIntPtr((ulong)entries.Count));
-            for (int i = 0; i < entries.Count; i++)
+            // TODO(jtattermusch): we might wanna check that the metadata is readonly 
+            var metadataArray = grpcsharp_metadata_array_create(new UIntPtr((ulong)metadata.Count));
+            for (int i = 0; i < metadata.Count; i++)
             {
-                grpcsharp_metadata_array_add(metadataArray, entries[i].Key, entries[i].ValueBytes, new UIntPtr((ulong)entries[i].ValueBytes.Length));
+                grpcsharp_metadata_array_add(metadataArray, metadata[i].Key, metadata[i].ValueBytes, new UIntPtr((ulong)metadata[i].ValueBytes.Length));
             }
             return metadataArray;
         }
diff --git a/src/csharp/Grpc.Core/Metadata.cs b/src/csharp/Grpc.Core/Metadata.cs
index eccec26a61..ddfe8488b7 100644
--- a/src/csharp/Grpc.Core/Metadata.cs
+++ b/src/csharp/Grpc.Core/Metadata.cs
@@ -30,55 +30,163 @@
 #endregion
 
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Immutable;
+using System.Collections.Specialized;
 using System.Runtime.InteropServices;
 using System.Text;
 
+using Grpc.Core.Utils;
+
 namespace Grpc.Core
 {
     /// <summary>
-    /// gRPC call metadata.
+    /// Provides access to read and write metadata values to be exchanged during a call.
     /// </summary>
-    public class Metadata
+    public sealed class Metadata : IList<Metadata.Entry>
     {
-        public static readonly Metadata Empty = new Metadata(ImmutableList<MetadataEntry>.Empty);
+        /// <summary>
+        /// An read-only instance of metadata containing no entries.
+        /// </summary>
+        public static readonly Metadata Empty = new Metadata().Freeze();
+
+        readonly List<Entry> entries;
+        bool readOnly;
+
+        public Metadata()
+        {
+            this.entries = new List<Entry>();
+        }
+
+        public Metadata(ICollection<Entry> entries)
+        {
+            this.entries = new List<Entry>(entries);
+        }
+
+        /// <summary>
+        /// Makes this object read-only.
+        /// </summary>
+        /// <returns>this object</returns>
+        public Metadata Freeze()
+        {
+            this.readOnly = true;
+            return this;
+        }
+
+        // TODO: add support for access by key
+
+        #region IList members
+
+        public int IndexOf(Metadata.Entry item)
+        {
+            return entries.IndexOf(item);
+        }
 
-        readonly ImmutableList<MetadataEntry> entries;
+        public void Insert(int index, Metadata.Entry item)
+        {
+            CheckWriteable();
+            entries.Insert(index, item);
+        }
 
-        public Metadata(ImmutableList<MetadataEntry> entries)
+        public void RemoveAt(int index)
         {
-            this.entries = entries;
+            CheckWriteable();
+            entries.RemoveAt(index);
         }
 
-        public ImmutableList<MetadataEntry> Entries
+        public Metadata.Entry this[int index]
         {
             get
             {
-                return this.entries;
+                return entries[index];
+            }
+
+            set
+            {
+                CheckWriteable();
+                entries[index] = value;
             }
         }
 
-        public static Builder CreateBuilder()
+        public void Add(Metadata.Entry item)
+        {
+            CheckWriteable();
+            entries.Add(item);
+        }
+
+        public void Clear()
+        {
+            CheckWriteable();
+            entries.Clear();
+        }
+
+        public bool Contains(Metadata.Entry item)
+        {
+            return entries.Contains(item);
+        }
+
+        public void CopyTo(Metadata.Entry[] array, int arrayIndex)
         {
-            return new Builder();
+            entries.CopyTo(array, arrayIndex);
         }
-       
-        public struct MetadataEntry
+
+        public int Count
+        {
+            get { return entries.Count; }
+        }
+
+        public bool IsReadOnly
+        {
+            get { return readOnly; }
+        }
+
+        public bool Remove(Metadata.Entry item)
+        {
+            CheckWriteable();
+            return entries.Remove(item);
+        }
+
+        public IEnumerator<Metadata.Entry> GetEnumerator()
+        {
+            return entries.GetEnumerator();
+        }
+
+        IEnumerator System.Collections.IEnumerable.GetEnumerator()
+        {
+            return entries.GetEnumerator();
+        }
+
+        private void CheckWriteable()
+        {
+            Preconditions.CheckState(!readOnly, "Object is read only");
+        }
+
+        #endregion
+
+        /// <summary>
+        /// Metadata entry
+        /// </summary>
+        public struct Entry
         {
+            private static readonly Encoding Encoding = Encoding.ASCII;
+
             readonly string key;
-            readonly byte[] valueBytes;
+            string value;
+            byte[] valueBytes;
 
-            public MetadataEntry(string key, byte[] valueBytes)
+            public Entry(string key, byte[] valueBytes)
             {
-                this.key = key;
-                this.valueBytes = valueBytes;
+                this.key = Preconditions.CheckNotNull(key);
+                this.value = null;
+                this.valueBytes = Preconditions.CheckNotNull(valueBytes);
             }
 
-            public MetadataEntry(string key, string value)
+            public Entry(string key, string value)
             {
-                this.key = key;
-                this.valueBytes = Encoding.ASCII.GetBytes(value);
+                this.key = Preconditions.CheckNotNull(key);
+                this.value = Preconditions.CheckNotNull(value);
+                this.valueBytes = null;
             }
 
             public string Key
@@ -89,38 +197,29 @@ namespace Grpc.Core
                 }
             }
 
-            // TODO: using ByteString would guarantee immutability.
             public byte[] ValueBytes
             {
                 get
                 {
-                    return this.valueBytes;
+                    if (valueBytes == null)
+                    {
+                        valueBytes = Encoding.GetBytes(value);
+                    }
+                    return valueBytes;
                 }
             }
-        }
 
-        public class Builder
-        {
-            readonly List<Metadata.MetadataEntry> entries = new List<Metadata.MetadataEntry>();
-
-            public List<MetadataEntry> Entries
+            public string Value
             {
                 get
                 {
-                    return entries;
+                    if (value == null)
+                    {
+                        value = Encoding.GetString(valueBytes);
+                    }
+                    return value;
                 }
             }
-
-            public Builder Add(MetadataEntry entry)
-            {
-                entries.Add(entry);
-                return this;
-            }
-
-            public Metadata Build()
-            {
-                return new Metadata(entries.ToImmutableList());
-            }
         }
-    }
+    }   
 }
diff --git a/src/csharp/Grpc.Core/Stub/StubConfiguration.cs b/src/csharp/Grpc.Core/Stub/StubConfiguration.cs
deleted file mode 100644
index 5bcb5b40d2..0000000000
--- a/src/csharp/Grpc.Core/Stub/StubConfiguration.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-#region Copyright notice and license
-
-// Copyright 2015, Google Inc.
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-//     * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-//     * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-//     * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-#endregion
-
-using System;
-using Grpc.Core.Internal;
-using Grpc.Core.Utils;
-
-namespace Grpc.Core
-{
-    public delegate void HeaderInterceptorDelegate(Metadata.Builder headerBuilder);
-
-    public class StubConfiguration
-    {
-        /// <summary>
-        /// The default stub configuration.
-        /// </summary>
-        public static readonly StubConfiguration Default = new StubConfiguration((headerBuilder) => { });
-
-        readonly HeaderInterceptorDelegate headerInterceptor;
-
-        public StubConfiguration(HeaderInterceptorDelegate headerInterceptor)
-        {
-            this.headerInterceptor = Preconditions.CheckNotNull(headerInterceptor);
-        }
-
-        public HeaderInterceptorDelegate HeaderInterceptor
-        {
-            get
-            {
-                return headerInterceptor;
-            }
-        }
-    }
-}
diff --git a/src/csharp/Grpc.Core/Version.cs b/src/csharp/Grpc.Core/Version.cs
index 972f495bd7..f1db1f6157 100644
--- a/src/csharp/Grpc.Core/Version.cs
+++ b/src/csharp/Grpc.Core/Version.cs
@@ -3,4 +3,3 @@ using System.Runtime.CompilerServices;
 
 // The current version of gRPC C#.
 [assembly: AssemblyVersion("0.6.0.*")]
-
diff --git a/src/csharp/Grpc.Examples.MathClient/MathClient.cs b/src/csharp/Grpc.Examples.MathClient/MathClient.cs
index b763721460..cfe2a06916 100644
--- a/src/csharp/Grpc.Examples.MathClient/MathClient.cs
+++ b/src/csharp/Grpc.Examples.MathClient/MathClient.cs
@@ -41,18 +41,18 @@ namespace math
         {
             using (Channel channel = new Channel("127.0.0.1", 23456))
             {
-                Math.IMathClient stub = new Math.MathClient(channel);
-                MathExamples.DivExample(stub);
+                Math.IMathClient client = new Math.MathClient(channel);
+                MathExamples.DivExample(client);
 
-                MathExamples.DivAsyncExample(stub).Wait();
+                MathExamples.DivAsyncExample(client).Wait();
 
-                MathExamples.FibExample(stub).Wait();
+                MathExamples.FibExample(client).Wait();
 
-                MathExamples.SumExample(stub).Wait();
+                MathExamples.SumExample(client).Wait();
 
-                MathExamples.DivManyExample(stub).Wait();
+                MathExamples.DivManyExample(client).Wait();
 
-                MathExamples.DependendRequestsExample(stub).Wait();
+                MathExamples.DependendRequestsExample(client).Wait();
             }
 
             GrpcEnvironment.Shutdown();
diff --git a/src/csharp/Grpc.Examples/MathExamples.cs b/src/csharp/Grpc.Examples/MathExamples.cs
index d2cfbee18f..7deb651689 100644
--- a/src/csharp/Grpc.Examples/MathExamples.cs
+++ b/src/csharp/Grpc.Examples/MathExamples.cs
@@ -38,29 +38,29 @@ namespace math
 {
     public static class MathExamples
     {
-        public static void DivExample(Math.IMathClient stub)
+        public static void DivExample(Math.IMathClient client)
         {
-            DivReply result = stub.Div(new DivArgs.Builder { Dividend = 10, Divisor = 3 }.Build());
+            DivReply result = client.Div(new DivArgs.Builder { Dividend = 10, Divisor = 3 }.Build());
             Console.WriteLine("Div Result: " + result);
         }
 
-        public static async Task DivAsyncExample(Math.IMathClient stub)
+        public static async Task DivAsyncExample(Math.IMathClient client)
         {
-            Task<DivReply> resultTask = stub.DivAsync(new DivArgs.Builder { Dividend = 4, Divisor = 5 }.Build());
+            Task<DivReply> resultTask = client.DivAsync(new DivArgs.Builder { Dividend = 4, Divisor = 5 }.Build());
             DivReply result = await resultTask;
             Console.WriteLine("DivAsync Result: " + result);
         }
 
-        public static async Task FibExample(Math.IMathClient stub)
+        public static async Task FibExample(Math.IMathClient client)
         {
-            using (var call = stub.Fib(new FibArgs.Builder { Limit = 5 }.Build()))
+            using (var call = client.Fib(new FibArgs.Builder { Limit = 5 }.Build()))
             {
                 List<Num> result = await call.ResponseStream.ToList();
                 Console.WriteLine("Fib Result: " + string.Join("|", result));
             }
         }
 
-        public static async Task SumExample(Math.IMathClient stub)
+        public static async Task SumExample(Math.IMathClient client)
         {
             var numbers = new List<Num>
             {
@@ -69,14 +69,14 @@ namespace math
                 new Num.Builder { Num_ = 3 }.Build()
             };
 
-            using (var call = stub.Sum())
+            using (var call = client.Sum())
             {
                 await call.RequestStream.WriteAll(numbers);
                 Console.WriteLine("Sum Result: " + await call.Result);
             }
         }
 
-        public static async Task DivManyExample(Math.IMathClient stub)
+        public static async Task DivManyExample(Math.IMathClient client)
         {
             var divArgsList = new List<DivArgs>
             {
@@ -84,14 +84,14 @@ namespace math
                 new DivArgs.Builder { Dividend = 100, Divisor = 21 }.Build(),
                 new DivArgs.Builder { Dividend = 7, Divisor = 2 }.Build()
             };
-            using (var call = stub.DivMany())
+            using (var call = client.DivMany())
             { 
                 await call.RequestStream.WriteAll(divArgsList);
                 Console.WriteLine("DivMany Result: " + string.Join("|", await call.ResponseStream.ToList()));
             }
         }
 
-        public static async Task DependendRequestsExample(Math.IMathClient stub)
+        public static async Task DependendRequestsExample(Math.IMathClient client)
         {
             var numbers = new List<Num>
             {
@@ -101,13 +101,13 @@ namespace math
             };
 
             Num sum;
-            using (var sumCall = stub.Sum())
+            using (var sumCall = client.Sum())
             {
                 await sumCall.RequestStream.WriteAll(numbers);
                 sum = await sumCall.Result;
             }
 
-            DivReply result = await stub.DivAsync(new DivArgs.Builder { Dividend = sum.Num_, Divisor = numbers.Count }.Build());
+            DivReply result = await client.DivAsync(new DivArgs.Builder { Dividend = sum.Num_, Divisor = numbers.Count }.Build());
             Console.WriteLine("Avg Result: " + result);
         }
     }
diff --git a/src/csharp/Grpc.Examples/MathGrpc.cs b/src/csharp/Grpc.Examples/MathGrpc.cs
index b9efc44e8c..bf820e2e3a 100644
--- a/src/csharp/Grpc.Examples/MathGrpc.cs
+++ b/src/csharp/Grpc.Examples/MathGrpc.cs
@@ -61,7 +61,7 @@ namespace math {
     }
 
     // client stub
-    public class MathClient : AbstractStub<MathClient, StubConfiguration>, IMathClient
+    public class MathClient : ClientBase<MathClient, StubConfiguration>, IMathClient
     {
       public MathClient(Channel channel) : this(channel, StubConfiguration.Default)
       {
diff --git a/src/csharp/Grpc.HealthCheck/HealthGrpc.cs b/src/csharp/Grpc.HealthCheck/HealthGrpc.cs
index ed9fc4ed77..d135ebd8c3 100644
--- a/src/csharp/Grpc.HealthCheck/HealthGrpc.cs
+++ b/src/csharp/Grpc.HealthCheck/HealthGrpc.cs
@@ -35,7 +35,7 @@ namespace Grpc.Health.V1Alpha {
     }
 
     // client stub
-    public class HealthClient : AbstractStub<HealthClient, StubConfiguration>, IHealthClient
+    public class HealthClient : ClientBase<HealthClient, StubConfiguration>, IHealthClient
     {
       public HealthClient(Channel channel) : this(channel, StubConfiguration.Default)
       {
diff --git a/src/csharp/Grpc.IntegrationTesting/TestGrpc.cs b/src/csharp/Grpc.IntegrationTesting/TestGrpc.cs
index ee077f9f56..35b9b3de40 100644
--- a/src/csharp/Grpc.IntegrationTesting/TestGrpc.cs
+++ b/src/csharp/Grpc.IntegrationTesting/TestGrpc.cs
@@ -81,7 +81,7 @@ namespace grpc.testing {
     }
 
     // client stub
-    public class TestServiceClient : AbstractStub<TestServiceClient, StubConfiguration>, ITestServiceClient
+    public class TestServiceClient : ClientBase<TestServiceClient, StubConfiguration>, ITestServiceClient
     {
       public TestServiceClient(Channel channel) : this(channel, StubConfiguration.Default)
       {
-- 
GitLab