-
Notifications
You must be signed in to change notification settings - Fork 51
/
ClientBuilderBase.cs
663 lines (600 loc) · 34.2 KB
/
ClientBuilderBase.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
/*
* Copyright 2019 Google LLC
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file or at
* https://developers.google.com/open-source/licenses/bsd
*/
using Google.Apis.Auth.OAuth2;
using Grpc.Auth;
using Grpc.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Google.Api.Gax.Grpc
{
/// <summary>
/// Base class for API-specific builders.
/// </summary>
/// <typeparam name="TClient">The type of client created by this builder.</typeparam>
public abstract class ClientBuilderBase<TClient>
{
/// <summary>
/// The default gRPC options.
/// </summary>
protected static GrpcChannelOptions DefaultOptions { get; } = GrpcChannelOptions.Empty
.WithKeepAliveTime(TimeSpan.FromMinutes(1))
.WithEnableServiceConfigResolution(false)
.WithMaxReceiveMessageSize(int.MaxValue);
/// <summary>
/// The metadata associated with the service that this client will make requests to.
/// </summary>
protected ServiceMetadata ServiceMetadata { get; }
/// <summary>
/// The endpoint to connect to, or null to use the default endpoint.
/// </summary>
public string Endpoint { get; set; }
/// <summary>
/// The logger to include in the client, if any.
/// </summary>
public ILogger Logger { get; set; }
/// <summary>
/// The scopes to use, or null to use the default scopes.
/// </summary>
public IReadOnlyList<string> Scopes { get; set; }
/// <summary>
/// The channel credentials to use, or null if credentials are being provided in a different way.
/// </summary>
public ChannelCredentials ChannelCredentials { get; set; }
/// <summary>
/// The path to the credentials file to use, or null if credentials are being provided in a different way.
/// </summary>
public string CredentialsPath { get; set; }
/// <summary>
/// The credentials to use as a JSON string, or null if credentials are being provided in a different way.
/// </summary>
public string JsonCredentials { get; set; }
/// <summary>
/// The credentials to use as a <see cref="GoogleCredential"/>, or null if credentials are being provided in
/// a different way. Note that unlike <see cref="ChannelCredentials"/> and <see cref="TokenAccessMethod"/>,
/// settings for <see cref="Scopes"/>, <see cref="QuotaProject"/> and self-signed JWTs will be applied to this
/// credential (creating a new one), in the same way as for application default credentials and credentials
/// specified using <see cref="CredentialsPath"/> or <see cref="JsonCredentials"/>.
/// </summary>
public GoogleCredential GoogleCredential { get; set; }
/// <summary>
/// The credentials to use in "raw" form, for conversion into channel credentials. No other settings
/// (e.g. <see cref="QuotaProject"/> or <see cref="Scopes"/>) are applied to these credentials.
/// </summary>
public ICredential Credential { get; set; }
/// <summary>
/// The token access method to use, or null if credentials are being provided in a different way.
/// </summary>
[Obsolete("The Credential property is preferred for simplicity. This property may be removed in a future major version.")]
public Func<string, CancellationToken, Task<string>> TokenAccessMethod { get; set; }
/// <summary>
/// The call invoker to use, or null to create the call invoker when the client is built.
/// </summary>
public CallInvoker CallInvoker { get; set; }
/// <summary>
/// A custom user agent to specify in the channel metadata, or null if no custom user agent is required.
/// </summary>
public string UserAgent { get; set; }
/// <summary>
/// The gRPC implementation to use, or null to use the default implementation.
/// </summary>
public GrpcAdapter GrpcAdapter { get; set; }
private EmulatorDetection _emulatorDetection = EmulatorDetection.None;
/// <summary>
/// The emulator detection policy to apply when building a client. Derived classes which support
/// emulators should create public properties which delegate to this one. The default value is
/// <see cref="EmulatorDetection.None"/>.
/// </summary>
protected EmulatorDetection EmulatorDetection
{
get => _emulatorDetection;
set => _emulatorDetection = GaxPreconditions.CheckEnumValue(value, nameof(value));
}
/// <summary>
/// The GCP project ID that should be used for quota and billing purposes.
/// May be null.
/// </summary>
public string QuotaProject { get; set; }
/// <summary>
/// Any custom channel options to merge with the default options.
/// If an option specified both here and in the default options, the custom option
/// will take priority. This property may be null (the default) in which case the default
/// options are used.
/// </summary>
public GrpcChannelOptions GrpcChannelOptions { get; set; }
// Note: when adding any more properties, CopyCommonSettings must also be updated,
// and potentially CopySettingsForEmulator.
/// <summary>
/// Returns the channel created last time any of the <see cref="Build()"/>-related methods
/// were called, or <code>null</code> if the last-created client did not require channel creation.
/// If a channel is obtained from a channel pool, this does not count as channel creation.
/// This property is useful when multiple clients are created and the calling code wishes to clean up
/// resources associated with the channel.
/// </summary>
public ChannelBase LastCreatedChannel { get; protected set; }
/// <summary>
/// Creates a new instance with no explicit settings.
/// This takes the value of <see cref="UseJwtAccessWithScopes" /> from <paramref name="serviceMetadata"/>.
/// </summary>
/// <param name="serviceMetadata">The metadata for the service that the client will be used with. Must not be null.</param>
protected ClientBuilderBase(ServiceMetadata serviceMetadata)
{
ServiceMetadata = serviceMetadata;
UseJwtAccessWithScopes = serviceMetadata.SupportsScopedJwts;
}
/// <summary>
/// Copies common settings from the specified builder into this one. This is a shallow copy.
/// </summary>
/// <typeparam name="TOther">The other client type</typeparam>
/// <param name="source">The builder to copy from.</param>
protected void CopyCommonSettings<TOther>(ClientBuilderBase<TOther> source)
{
GaxPreconditions.CheckNotNull(source, nameof(source));
Endpoint = source.Endpoint;
Scopes = source.Scopes;
ChannelCredentials = source.ChannelCredentials;
CredentialsPath = source.CredentialsPath;
JsonCredentials = source.JsonCredentials;
GoogleCredential = source.GoogleCredential;
Credential = source.Credential;
#pragma warning disable CS0618 // Type or member is obsolete
TokenAccessMethod = source.TokenAccessMethod;
#pragma warning restore CS0618 // Type or member is obsolete
CallInvoker = source.CallInvoker;
UserAgent = source.UserAgent;
GrpcAdapter = source.GrpcAdapter;
GrpcChannelOptions = source.GrpcChannelOptions;
QuotaProject = source.QuotaProject;
UseJwtAccessWithScopes = source.UseJwtAccessWithScopes;
Logger = source.Logger;
// Note that we may be copying from one type that supports emulators (e.g. FirestoreDbBuilder)
// to another type that doesn't (e.g. FirestoreClientBuilder). That ends up in a slightly odd situation,
// but the code in the emulator-unaware type won't use the property anyway. If we're ever copying from
// one type that supports emulators to another, it would make sense to propagate the setting anyway.
EmulatorDetection = source.EmulatorDetection;
}
/// <summary>
/// Copies common settings from the specified builder, expecting that any settings around
/// credentials and endpoints will be set by the caller, along with any client-specific settings.
/// Emulator detection is not copied, to avoid infinite recursion when building.
/// </summary>
protected void CopySettingsForEmulator(ClientBuilderBase<TClient> source)
{
GaxPreconditions.CheckNotNull(source, nameof(source));
UserAgent = source.UserAgent;
GrpcAdapter = source.GrpcAdapter;
Logger = source.Logger;
}
/// <summary>
/// Validates that the builder is in a consistent state for building. For example, it's invalid to call
/// <see cref="Build()"/> on an instance which has both JSON credentials and a credentials path specified.
/// </summary>
/// <exception cref="InvalidOperationException">The builder is in an invalid state.</exception>
protected virtual void Validate()
{
#pragma warning disable CS0618 // Type or member is obsolete
// If there's a call invoker, we shouldn't have any credentials-related information or an endpoint.
ValidateOptionExcludesOthers($"{nameof(CallInvoker)} cannot be specified with credentials settings or an endpoint", CallInvoker,
ChannelCredentials, CredentialsPath, JsonCredentials, Scopes, Endpoint, TokenAccessMethod, GoogleCredential, Credential);
ValidateAtMostOneNotNull("Only one source of credentials can be specified",
ChannelCredentials, CredentialsPath, JsonCredentials, TokenAccessMethod, GoogleCredential, Credential);
ValidateOptionExcludesOthers("Scopes are not relevant when a token access method, channel credentials or ICredential are supplied", Scopes,
TokenAccessMethod, ChannelCredentials, Credential);
#pragma warning restore CS0618 // Type or member is obsolete
ValidateOptionExcludesOthers($"{nameof(QuotaProject)} cannot be specified if a {nameof(CallInvoker)}, {nameof(ChannelCredentials)} or {nameof(Credential)} is specified", QuotaProject,
CallInvoker, ChannelCredentials, Credential);
}
/// <summary>
/// Performs basic emulator detection and validation based on the given environment variables.
/// This method is expected to be called by a derived class that supports emulators, in order to perform the common
/// work of checking whether the emulator is configured in the environment.
/// </summary>
/// <remarks>
/// <para>
/// If the emulator should not be used, either due to being disabled in <see cref="EmulatorDetection"/> or
/// the appropriate environment variables not being set, this method returns null.
/// </para>
/// <para>
/// Otherwise, a dictionary is returned mapping every value in <paramref name="allEmulatorEnvironmentVariables"/> to the value in
/// the environment. Any missing, empty or whitespace-only values are mapped to a null reference in the returned dictionary, but
/// the entry will still be present (so callers can use an indexer with the returned dictionary for every environment variable passed in).
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">The configuration is inconsistent, e.g. due to some environment variables
/// being set but not all the required ones, or any environment variables being set in a production-only environment.</exception>
/// <param name="requiredEmulatorEnvironmentVariables">Required emulator environment variables.</param>
/// <param name="allEmulatorEnvironmentVariables">All emulator environment variables.</param>
/// <param name="environmentVariableProvider">The provider used to retrieve environment variables. This is used to faciliate testing, and defaults
/// to using <see cref="Environment.GetEnvironmentVariable(string)"/>.</param>
/// <returns>A key/value mapping of the emulator environment variables to their values, or null if the emulator should not be used.</returns>
protected Dictionary<string, string> GetEmulatorEnvironment(
IEnumerable<string> requiredEmulatorEnvironmentVariables,
IEnumerable<string> allEmulatorEnvironmentVariables,
Func<string, string> environmentVariableProvider = null)
{
environmentVariableProvider ??= Environment.GetEnvironmentVariable;
var environment = allEmulatorEnvironmentVariables.ToDictionary(key => key, key => GetEnvironmentVariableOrNull(key));
switch (EmulatorDetection)
{
case EmulatorDetection.None:
return default;
case EmulatorDetection.ProductionOnly:
foreach (var variable in allEmulatorEnvironmentVariables)
{
GaxPreconditions.CheckState(
environment[variable] is null,
"Emulator environment variable '{0}' is set, contrary to use of {1}.{2}",
variable, nameof(EmulatorDetection), nameof(EmulatorDetection.ProductionOnly));
}
return default;
case EmulatorDetection.EmulatorOnly:
foreach (var variable in requiredEmulatorEnvironmentVariables)
{
GaxPreconditions.CheckState(
environment[variable] is object,
"Emulator environment variable '{0}' is not set, contrary to use of {1}.{2}",
variable, nameof(EmulatorDetection), nameof(EmulatorDetection.EmulatorOnly));
}
// When the settings *only* support the use of an emulator, the other properties shouldn't be set.
CheckNotSet(Endpoint, nameof(Endpoint));
CheckNotSet(CallInvoker, nameof(CallInvoker));
CheckNotSet(ChannelCredentials, nameof(ChannelCredentials));
CheckNotSet(CredentialsPath, nameof(CredentialsPath));
CheckNotSet(JsonCredentials, nameof(JsonCredentials));
CheckNotSet(Scopes, nameof(Scopes));
#pragma warning disable CS0618 // Type or member is obsolete
CheckNotSet(TokenAccessMethod, nameof(TokenAccessMethod));
#pragma warning restore CS0618 // Type or member is obsolete
CheckNotSet(QuotaProject, nameof(QuotaProject));
CheckNotSet(Credential, nameof(Credential));
CheckNotSet(GoogleCredential, nameof(GoogleCredential));
void CheckNotSet(object obj, string name)
{
GaxPreconditions.CheckState(obj is null, "{0} is set, contrary to use of {1}.{2}",
name, nameof(EmulatorDetection), nameof(EmulatorDetection.EmulatorOnly));
}
return environment;
case EmulatorDetection.EmulatorOrProduction:
bool anySet = allEmulatorEnvironmentVariables.Any(v => environment[v] is object);
if (!anySet)
{
return default;
}
bool allRequiredSet = requiredEmulatorEnvironmentVariables.All(v => environment[v] is object);
if (!allRequiredSet)
{
var sampleSet = allEmulatorEnvironmentVariables.First(v => environment[v] is object);
var sampleNotSet = requiredEmulatorEnvironmentVariables.First(v => environment[v] is null);
GaxPreconditions.CheckState(false,
"Emulator environment variable '{0}' is set, but '{1}' is not set.",
sampleSet, sampleNotSet);
}
// We allow other properties such as the endpoint to be set, although we expect them to be ignored
// by the calling code. This allows users to write code that has customizations in settings, but still doesn't need
// to be changed at all in order to use the emulator.
return environment;
default:
throw new InvalidOperationException($"Invalid emulator detection value: {EmulatorDetection}");
}
// Retrieves an environment variable from <see cref="EnvrionmentVariableProvider"/>, mapping empty or whitespace-only strings to null.
string GetEnvironmentVariableOrNull(string variable)
{
var value = environmentVariableProvider(variable);
return string.IsNullOrWhiteSpace(value) ? null : value;
}
}
/// <summary>
/// Validates that at most one of the given values is not null.
/// </summary>
/// <param name="message">The message if the condition is violated.</param>
/// <param name="values">The values to check for nullity.</param>
/// <exception cref="InvalidOperationException">More than one value is null.</exception>
protected void ValidateAtMostOneNotNull(string message, params object[] values)
{
int notNull = values.Count(v => v != null);
GaxPreconditions.CheckState(notNull < 2, message);
}
/// <summary>
/// Validates that if <paramref name="controlling"/> is not null, then every value in <paramref name="values"/> is null.
/// </summary>
/// <param name="message">The message if the condition is violated.</param>
/// <param name="controlling">The value controlling whether or not any other value can be non-null.</param>
/// <param name="values">The values checked for non-nullity if <paramref name="controlling"/> is non-null.</param>
protected void ValidateOptionExcludesOthers(string message, object controlling, params object[] values)
{
GaxPreconditions.CheckState(controlling == null || values.All(v => v == null), message);
}
/// <summary>
/// Creates a call invoker synchronously. Override this method in a concrete builder type if more
/// call invoker mechanisms are supported.
/// This implementation calls <see cref="GetChannelCredentials"/> if no call invoker is specified.
/// </summary>
protected virtual CallInvoker CreateCallInvoker()
{
LastCreatedChannel = null;
if (CallInvoker != null)
{
return CallInvoker;
}
var endpoint = Endpoint ?? ServiceMetadata.DefaultEndpoint;
ChannelBase channel;
if (CanUseChannelPool)
{
channel = GetChannelPool().GetChannel(EffectiveGrpcAdapter, endpoint, GetChannelOptions());
}
else
{
var credentials = GetChannelCredentials();
channel = CreateChannel(endpoint, credentials);
}
return channel.CreateCallInvoker();
}
/// <summary>
/// Creates a call invoker asynchronously. Override this method in a concrete builder type if more
/// call invoker mechanisms are supported.
/// This implementation calls <see cref="GetChannelCredentialsAsync(CancellationToken)"/> if no call
/// invoker is specified.
/// </summary>
protected virtual async Task<CallInvoker> CreateCallInvokerAsync(CancellationToken cancellationToken)
{
LastCreatedChannel = null;
if (CallInvoker != null)
{
return CallInvoker;
}
var endpoint = Endpoint ?? ServiceMetadata.DefaultEndpoint;
ChannelBase channel;
if (CanUseChannelPool)
{
channel = await GetChannelPool()
.GetChannelAsync(EffectiveGrpcAdapter, endpoint, GetChannelOptions(), cancellationToken)
.ConfigureAwait(false);
}
else
{
var credentials = await GetChannelCredentialsAsync(cancellationToken).ConfigureAwait(false);
channel = CreateChannel(endpoint, credentials);
}
return channel.CreateCallInvoker();
}
/// <summary>
/// Obtains channel credentials synchronously. Override this method in a concrete builder type if more
/// credential mechanisms are supported.
/// </summary>
protected virtual ChannelCredentials GetChannelCredentials() =>
MaybeGetSimpleChannelCredentials() ?? GetGoogleCredential().ToChannelCredentials();
/// <summary>
/// Obtains channel credentials asynchronously. Override this method in a concrete builder type if more
/// credential mechanisms are supported.
/// </summary>
protected async virtual Task<ChannelCredentials> GetChannelCredentialsAsync(CancellationToken cancellationToken) =>
MaybeGetSimpleChannelCredentials()
?? (await GetGoogleCredentialAsync(cancellationToken).ConfigureAwait(false)).ToChannelCredentials();
/// <summary>
/// Obtains channel credentials synchronously if they've been supplied in a ready-to-go fashion.
/// This avoids code duplication in the sync and async paths.
/// Returns null if the credentials aren't available.
/// </summary>
private ChannelCredentials MaybeGetSimpleChannelCredentials() =>
ChannelCredentials ?? Credential?.ToChannelCredentials() ??
#pragma warning disable CS0618 // Type or member is obsolete
(TokenAccessMethod is not null ? new DelegatedTokenAccess(TokenAccessMethod, QuotaProject).ToChannelCredentials() : null);
#pragma warning restore CS0618 // Type or member is obsolete
// Visible for testing
internal GoogleCredential GetGoogleCredential()
{
GoogleCredential unscoped =
GoogleCredential != null ? GoogleCredential :
CredentialsPath != null ? GoogleCredential.FromFile(CredentialsPath) :
JsonCredentials != null ? GoogleCredential.FromJson(JsonCredentials) :
GoogleCredential.GetApplicationDefault();
GoogleCredential scoped = unscoped.CreateScoped(Scopes ?? ServiceMetadata.DefaultScopes);
GoogleCredential maybeWithProject = QuotaProject is null ? scoped : scoped.CreateWithQuotaProject(QuotaProject);
if (maybeWithProject.UnderlyingCredential is ServiceAccountCredential serviceCredential
&& serviceCredential.UseJwtAccessWithScopes != UseJwtAccessWithScopes)
{
maybeWithProject = GoogleCredential.FromServiceAccountCredential(serviceCredential.WithUseJwtAccessWithScopes(UseJwtAccessWithScopes));
}
return maybeWithProject;
}
// Visible for testing
internal async Task<GoogleCredential> GetGoogleCredentialAsync(CancellationToken cancellationToken)
{
GoogleCredential unscoped =
GoogleCredential != null ? GoogleCredential :
CredentialsPath != null ? await GoogleCredential.FromFileAsync(CredentialsPath, cancellationToken).ConfigureAwait(false) :
JsonCredentials != null ? GoogleCredential.FromJson(JsonCredentials) :
await GoogleCredential.GetApplicationDefaultAsync(cancellationToken).ConfigureAwait(false);
GoogleCredential scoped = unscoped.CreateScoped(Scopes ?? ServiceMetadata.DefaultScopes);
GoogleCredential maybeWithProject = QuotaProject is null ? scoped : scoped.CreateWithQuotaProject(QuotaProject);
if (maybeWithProject.UnderlyingCredential is ServiceAccountCredential serviceCredential
&& serviceCredential.UseJwtAccessWithScopes != UseJwtAccessWithScopes)
{
maybeWithProject = GoogleCredential.FromServiceAccountCredential(serviceCredential.WithUseJwtAccessWithScopes(UseJwtAccessWithScopes));
}
return maybeWithProject;
}
/// <summary>
/// Returns the channel pool to use when no other options are specified. This method is not called unless
/// <see cref="CanUseChannelPool"/> returns true, so if a channel pool is unavailable, override that property
/// to return false and throw an exception from this method.
/// </summary>
protected abstract ChannelPool GetChannelPool();
/// <summary>
/// Returns the effective <see cref="GrpcAdapter"/> for this builder,
/// using the <see cref="GrpcAdapter"/> property if that is set, or the appropriate fallback adapter
/// for <see cref="ServiceMetadata"/> otherwise.
/// </summary>
protected GrpcAdapter EffectiveGrpcAdapter => GrpcAdapter ?? GrpcAdapter.GetFallbackAdapter(ServiceMetadata);
/// <summary>
/// Returns the options to use when creating a channel, taking <see cref="GrpcChannelOptions"/>
/// into account.
/// </summary>
/// <returns>The options to use when creating a channel.</returns>
protected virtual GrpcChannelOptions GetChannelOptions()
{
var defaultOptions = UserAgent == null
? DefaultOptions
: DefaultOptions.WithPrimaryUserAgent(UserAgent);
// While we could use the "CustomChannelOptions ?? GrpcChannelOptions.Empty"
// and merge unconditionally, there's no point in creating a new object
// if we have no options.
return GrpcChannelOptions is null
? defaultOptions
: defaultOptions.MergedWith(GrpcChannelOptions);
}
/// <summary>
/// Returns whether or not a channel pool can be used if a channel is required. The default behavior is to return
/// true if and only if no quota project, scopes, credentials or token access method have been specified and
/// if UseJwtAccessWithScopes flag matches the flag in ChannelPool.
/// Derived classes should override this property if there are other reasons why the channel pool should not be used.
/// </summary>
protected virtual bool CanUseChannelPool =>
ChannelCredentials == null &&
CredentialsPath == null &&
JsonCredentials == null &&
#pragma warning disable CS0618 // Type or member is obsolete
TokenAccessMethod == null &&
#pragma warning restore CS0618 // Type or member is obsolete
Scopes == null &&
QuotaProject == null &&
GoogleCredential == null &&
Credential == null &&
UseJwtAccessWithScopes == GetChannelPool().UseJwtAccessWithScopes;
/// <summary>
/// Returns whether or not self-signed JWTs will be used over OAuth tokens when OAuth scopes are explicitly set.
/// </summary>
/// <remarks>
/// In the base implementation, this defaults to <c>true</c>. Subclasses may add code in their own constructors
/// to make the default effectively <c>false</c>, however.
/// </remarks>
public bool UseJwtAccessWithScopes { get; set; }
// Note: The implementation is responsible for performing validation before constructing the client.
// The Validate method can be used as-is for most builders.
/// <summary>
/// Builds the resulting client.
/// </summary>
public abstract TClient Build();
/// <summary>
/// Populates properties based on those set via dependency injection.
/// </summary>
/// <remarks>
/// <para>
/// If gRPC adapters are configured in <paramref name="provider"/>, the first one that supports
/// the <see cref="ServiceMetadata"/> will be used.
/// </para>
/// <para>
/// Credentials are only requested from dependency injection if they are not already set
/// via any of <see cref="ChannelCredentials"/>, <see cref="CredentialsPath"/>,
/// <see cref="JsonCredentials"/>, <see cref="Credential"/>, <see cref="GoogleCredential"/>
/// or <see cref="TokenAccessMethod"/>.
/// </para>
/// <para>
/// If credentials are requested, they are tried in the following order:
/// </para>
/// <list type="bullet">
/// <item>ChannelCredentials</item>
/// <item>ICredential</item>
/// <item>GoogleCredential</item>
/// </list>
/// </remarks>
/// <param name="provider">The service provider to request dependencies from.</param>
protected virtual void Configure(IServiceProvider provider)
{
GaxPreconditions.CheckNotNull(provider, nameof(provider));
Logger ??= provider.GetService<ILogger<TClient>>();
CallInvoker ??= provider.GetService<CallInvoker>();
if (CallInvoker is object)
{
return;
}
GrpcAdapter ??= provider.GetServices<GrpcAdapter>().FirstOrDefault(adapter => adapter.SupportsApi(ServiceMetadata));
GrpcChannelOptions ??= provider.GetService<GrpcChannelOptions>();
#pragma warning disable CS0618 // Type or member is obsolete
if (ChannelCredentials is null && CredentialsPath is null && JsonCredentials is null && TokenAccessMethod is null &&
#pragma warning restore CS0618 // Type or member is obsolete
GoogleCredential is null && Credential is null)
{
if (provider.GetService<ChannelCredentials>() is ChannelCredentials channelCredentials)
{
ChannelCredentials = channelCredentials;
}
else if (provider.GetService<ICredential>() is ICredential credential)
{
Credential = credential;
}
else if (provider.GetService<GoogleCredential>() is GoogleCredential googleCredential)
{
GoogleCredential = googleCredential;
}
}
}
// Note: The implementation is responsible for performing validation before constructing the client.
// The Validate method can be used as-is for most builders.
/// <summary>
/// Builds the resulting client asynchronously.
/// </summary>
public abstract Task<TClient> BuildAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Populates properties supplied via dependency injection, then builds a client.
/// </summary>
/// <param name="provider">The service provider to request dependencies from. Must not be null.</param>
/// <returns>An API client configured from this builder.</returns>
public TClient Build(IServiceProvider provider)
{
Configure(provider);
return Build();
}
/// <summary>
/// Populates properties supplied via dependency injection, then builds a client asynchronously.
/// </summary>
/// <param name="provider">The service provider to request dependencies from. Must not be null.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An API client configured from this builder.</returns>
public async Task<TClient> BuildAsync(IServiceProvider provider, CancellationToken cancellationToken = default)
{
Configure(provider);
return await BuildAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Returns a <see cref="ChannelBase"/> as created by <see cref="EffectiveGrpcAdapter"/>
/// for the specified endpoint and credentials, using the gRPC channel options from
/// <see cref="GetChannelOptions"/>.
/// </summary>
/// <remarks>
/// This is only useful in very specific situations where a known channel is required;
/// <see cref="CreateCallInvoker"/> and its async equivalent are more usually useful.
/// This implementation sets the <see cref="LastCreatedChannel"/> property, and so should
/// any overriding implementations.
/// </remarks>
/// <param name="endpoint">The endpoint of the channel.</param>
/// <param name="credentials">The channel credentials.</param>
/// <returns>The channel created by the gRPC adapter.</returns>
protected virtual ChannelBase CreateChannel(string endpoint, ChannelCredentials credentials) =>
LastCreatedChannel = EffectiveGrpcAdapter.CreateChannel(ServiceMetadata, endpoint, credentials, GetChannelOptions());
private class DelegatedTokenAccess : ITokenAccessWithHeaders
{
private readonly Func<string, CancellationToken, Task<string>> _tokenAccessMethod;
private readonly string _quotaProject;
internal DelegatedTokenAccess(Func<string, CancellationToken, Task<string>> tokenAccessMethod, string quotaProject) =>
(_tokenAccessMethod, _quotaProject) = (tokenAccessMethod, quotaProject);
public Task<string> GetAccessTokenForRequestAsync(string authUri, CancellationToken cancellationToken) =>
_tokenAccessMethod(authUri, cancellationToken);
public async Task<AccessTokenWithHeaders> GetAccessTokenWithHeadersForRequestAsync(string authUri = null, CancellationToken cancellationToken = default)
{
string token = await _tokenAccessMethod(authUri, cancellationToken).ConfigureAwait(false);
return _quotaProject is null ?
new AccessTokenWithHeaders(token, null) :
new AccessTokenWithHeaders.Builder { QuotaProject = _quotaProject }.Build(token);
}
}
}
}