PageRenderTime 51ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/Lib/StorageAccountInfo.cs

https://bitbucket.org/mh415/azure-storageclient
C# | 612 lines | 425 code | 52 blank | 135 comment | 109 complexity | 857d3af088d6462db5886a5086d1f2c9 MD5 | raw file
  1. //
  2. // <copyright file="StorageAccountInfo.cs" company="Microsoft">
  3. // Copyright (c) Microsoft Corporation. All rights reserved.
  4. // </copyright>
  5. //
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Linq;
  9. using System.Text;
  10. using System.Globalization;
  11. using System.Configuration;
  12. using System.Collections.Specialized;
  13. using System.Diagnostics;
  14. using Microsoft.ServiceHosting.ServiceRuntime;
  15. namespace Microsoft.Samples.ServiceHosting.StorageClient
  16. {
  17. /// <summary>
  18. /// Objects of this class encapsulate information about a storage account and endpoint configuration.
  19. /// Associated with a storage account is the account name, the base URI of the account and a shared key.
  20. /// </summary>
  21. public class StorageAccountInfo
  22. {
  23. /// <summary>
  24. /// The default configuration string in configuration files for setting the queue storage endpoint.
  25. /// </summary>
  26. public static readonly string DefaultQueueStorageEndpointConfigurationString = "QueueStorageEndpoint";
  27. /// <summary>
  28. /// The default configuration string in configuration files for setting the blob storage endpoint.
  29. /// </summary>
  30. public static readonly string DefaultBlobStorageEndpointConfigurationString = "BlobStorageEndpoint";
  31. /// <summary>
  32. /// The default configuration string in configuration files for setting the table storage endpoint.
  33. /// </summary>
  34. public static readonly string DefaultTableStorageEndpointConfigurationString = "TableStorageEndpoint";
  35. /// <summary>
  36. /// The default configuration string in configuration files for setting the storage account name.
  37. /// </summary>
  38. public static readonly string DefaultAccountNameConfigurationString = "AccountName";
  39. /// <summary>
  40. /// The default configuration string in configuration files for setting the shared key associated with a storage account.
  41. /// </summary>
  42. public static readonly string DefaultAccountSharedKeyConfigurationString = "AccountSharedKey";
  43. /// <summary>
  44. /// The default configuration string in configuration files for setting the UsePathStyleUris option.
  45. /// </summary>
  46. public static readonly string DefaultUsePathStyleUrisConfigurationString = "UsePathStyleUris";
  47. /// <summary>
  48. /// The default prefix string in application config and Web.config files to indicate that this setting should be looked up
  49. /// in the fabric's configuration system.
  50. /// </summary>
  51. public static readonly string CSConfigStringPrefix = "CSConfigName";
  52. private bool? _usePathStyleUris;
  53. /// <summary>
  54. /// Constructor for creating account info objects.
  55. /// </summary>
  56. /// <param name="baseUri">The account's base URI.</param>
  57. /// <param name="usePathStyleUris">If true, path-style URIs (http://baseuri/accountname/containername/objectname) are used.
  58. /// If false host-style URIs (http://accountname.baseuri/containername/objectname) are used,
  59. /// where baseuri is the URI of the service..
  60. /// If null, the choice is made automatically: path-style URIs if host name part of base URI is an
  61. /// IP addres, host-style otherwise.</param>
  62. /// <param name="accountName">The account name.</param>
  63. /// <param name="base64Key">The account's shared key.</param>
  64. public StorageAccountInfo(Uri baseUri, bool? usePathStyleUris, string accountName, string base64Key)
  65. : this(baseUri, usePathStyleUris, accountName, base64Key, false)
  66. {
  67. }
  68. /// <summary>
  69. /// Constructor for creating account info objects.
  70. /// </summary>
  71. /// <param name="baseUri">The account's base URI.</param>
  72. /// <param name="usePathStyleUris">If true, path-style URIs (http://baseuri/accountname/containername/objectname) are used.
  73. /// If false host-style URIs (http://accountname.baseuri/containername/objectname) are used,
  74. /// where baseuri is the URI of the service.
  75. /// If null, the choice is made automatically: path-style URIs if host name part of base URI is an
  76. /// IP addres, host-style otherwise.</param>
  77. /// <param name="accountName">The account name.</param>
  78. /// <param name="base64Key">The account's shared key.</param>
  79. /// <param name="allowIncompleteSettings">true if it shall be allowed to only set parts of the StorageAccountInfo properties.</param>
  80. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
  81. public StorageAccountInfo(Uri baseUri, bool? usePathStyleUris, string accountName, string base64Key, bool allowIncompleteSettings)
  82. {
  83. if (baseUri == null && !allowIncompleteSettings)
  84. {
  85. throw new ArgumentNullException("baseUri");
  86. }
  87. if (string.IsNullOrEmpty(base64Key) && !allowIncompleteSettings)
  88. {
  89. throw new ArgumentNullException("base64Key");
  90. }
  91. if (baseUri != null)
  92. {
  93. string newAccountName = null;
  94. Uri newBaseUri = null;
  95. if (IsStandardStorageEndpoint(baseUri, out newAccountName, out newBaseUri)) {
  96. if (!string.IsNullOrEmpty(newAccountName) &&
  97. !string.IsNullOrEmpty(accountName) &&
  98. string.Compare(accountName, newAccountName, StringComparison.Ordinal) != 0)
  99. {
  100. throw new ArgumentException("The configured base URI " + baseUri.AbsoluteUri + " for the storage service is incorrect. " +
  101. "The configured account name " + accountName + " must match the account name " + newAccountName +
  102. " as specified in the storage service base URI." +
  103. GeneralAccountConfigurationExceptionString);
  104. }
  105. Debug.Assert((newBaseUri == null && newAccountName == null) || (newBaseUri != null && newAccountName != null));
  106. if (newAccountName != null && newBaseUri != null) {
  107. accountName = newAccountName;
  108. baseUri = newBaseUri;
  109. }
  110. }
  111. }
  112. if (string.IsNullOrEmpty(accountName) && !allowIncompleteSettings)
  113. {
  114. throw new ArgumentNullException("accountName");
  115. }
  116. if (!string.IsNullOrEmpty(accountName) && accountName.ToLowerInvariant() != accountName) {
  117. throw new ArgumentException("The account name must not contain upper-case letters. " +
  118. "The account name is the first part of the URL for accessing the storage services as presented to you by the portal or " +
  119. "the predefined storage account name when using the development storage tool. " +
  120. GeneralAccountConfigurationExceptionString);
  121. }
  122. BaseUri = baseUri;
  123. AccountName = accountName;
  124. Base64Key = base64Key;
  125. if (usePathStyleUris == null && baseUri == null && !allowIncompleteSettings) {
  126. throw new ArgumentException("Cannot determine setting from empty URI.");
  127. }
  128. else if (usePathStyleUris == null)
  129. {
  130. if (baseUri != null)
  131. {
  132. _usePathStyleUris = Utilities.StringIsIPAddress(baseUri.Host);
  133. }
  134. else
  135. {
  136. _usePathStyleUris = null;
  137. }
  138. }
  139. else
  140. {
  141. UsePathStyleUris = usePathStyleUris.Value;
  142. }
  143. }
  144. /// <summary>
  145. /// The base URI of the account.
  146. /// </summary>
  147. public Uri BaseUri
  148. {
  149. get;
  150. set;
  151. }
  152. /// <summary>
  153. /// The account name.
  154. /// </summary>
  155. public string AccountName
  156. {
  157. get;
  158. set;
  159. }
  160. /// <summary>
  161. /// The account's key.
  162. /// </summary>
  163. public string Base64Key
  164. {
  165. get;
  166. set;
  167. }
  168. /// <summary>
  169. /// If set, returns the UsePathStyleUris properties. If the property has not been explicitly set,
  170. /// the implementation tries to derive the correct value from the base URI.
  171. /// </summary>
  172. public bool UsePathStyleUris
  173. {
  174. get
  175. {
  176. if (_usePathStyleUris == null)
  177. {
  178. if (BaseUri == null)
  179. {
  180. return false;
  181. }
  182. else
  183. {
  184. return Utilities.StringIsIPAddress(BaseUri.Host);
  185. }
  186. }
  187. else
  188. {
  189. return _usePathStyleUris.Value;
  190. }
  191. }
  192. set {
  193. _usePathStyleUris = value;
  194. }
  195. }
  196. /// <summary>
  197. /// Retrieves account settings for the queue service from the default settings.
  198. /// </summary>
  199. public static StorageAccountInfo GetDefaultQueueStorageAccountFromConfiguration(bool allowIncompleteSettings)
  200. {
  201. return GetAccountInfoFromConfiguration(DefaultQueueStorageEndpointConfigurationString, allowIncompleteSettings);
  202. }
  203. /// <summary>
  204. /// Retrieves account settings for the queue service from the default settings.
  205. /// Throws an exception in case of incomplete settings.
  206. /// </summary>
  207. public static StorageAccountInfo GetDefaultQueueStorageAccountFromConfiguration()
  208. {
  209. return GetAccountInfoFromConfiguration(DefaultQueueStorageEndpointConfigurationString, false);
  210. }
  211. /// <summary>
  212. /// Retrieves account settings for the table service from the default settings.
  213. /// </summary>
  214. public static StorageAccountInfo GetDefaultTableStorageAccountFromConfiguration(bool allowIncompleteSettings)
  215. {
  216. return GetAccountInfoFromConfiguration(DefaultTableStorageEndpointConfigurationString, allowIncompleteSettings);
  217. }
  218. /// <summary>
  219. /// Retrieves account settings for the table service from the default settings.
  220. /// Throws an exception in case of incomplete settings.
  221. /// </summary>
  222. public static StorageAccountInfo GetDefaultTableStorageAccountFromConfiguration()
  223. {
  224. return GetAccountInfoFromConfiguration(DefaultTableStorageEndpointConfigurationString, false);
  225. }
  226. /// <summary>
  227. /// Retrieves account settings for the blob service from the default settings.
  228. /// </summary>
  229. public static StorageAccountInfo GetDefaultBlobStorageAccountFromConfiguration(bool allowIncompleteSettings)
  230. {
  231. return GetAccountInfoFromConfiguration(DefaultBlobStorageEndpointConfigurationString, allowIncompleteSettings);
  232. }
  233. /// <summary>
  234. /// Retrieves account settings for the blob service from the default settings.
  235. /// Throws an exception in case of incomplete settings.
  236. /// </summary>
  237. public static StorageAccountInfo GetDefaultBlobStorageAccountFromConfiguration()
  238. {
  239. return GetAccountInfoFromConfiguration(DefaultBlobStorageEndpointConfigurationString, false);
  240. }
  241. /// <summary>
  242. /// Gets settings from default configuration names except for the endpoint configuration string.
  243. /// </summary>
  244. public static StorageAccountInfo GetAccountInfoFromConfiguration(string endpointConfiguration, bool allowIncompleteSettings)
  245. {
  246. return GetAccountInfoFromConfiguration(DefaultAccountNameConfigurationString,
  247. DefaultAccountSharedKeyConfigurationString,
  248. endpointConfiguration,
  249. DefaultUsePathStyleUrisConfigurationString,
  250. allowIncompleteSettings);
  251. }
  252. /// <summary>
  253. /// Gets settings from default configuration names except for the endpoint configuration string. Throws an exception
  254. /// in the case of incomplete settings.
  255. /// </summary>
  256. public static StorageAccountInfo GetAccountInfoFromConfiguration(string endpointConfiguration)
  257. {
  258. return GetAccountInfoFromConfiguration(DefaultAccountNameConfigurationString,
  259. DefaultAccountSharedKeyConfigurationString,
  260. endpointConfiguration,
  261. DefaultUsePathStyleUrisConfigurationString,
  262. false);
  263. }
  264. /// <summary>
  265. /// Gets a configuration setting from application settings in the Web.config or App.config file.
  266. /// When running in a hosted environment, configuration settings are read from .cscfg
  267. /// files.
  268. /// </summary>
  269. public static string GetConfigurationSetting(string configurationSetting, string defaultValue, bool throwIfNull)
  270. {
  271. if (string.IsNullOrEmpty(configurationSetting))
  272. {
  273. throw new ArgumentException("configurationSetting cannot be empty or null", "configurationSetting");
  274. }
  275. string ret = null;
  276. // first, try to read from appsettings
  277. ret = TryGetAppSetting(configurationSetting);
  278. // settings in the csc file overload settings in Web.config
  279. if (RoleManager.IsRoleManagerRunning)
  280. {
  281. string cscRet = TryGetConfigurationSetting(configurationSetting);
  282. if (!string.IsNullOrEmpty(cscRet))
  283. {
  284. ret = cscRet;
  285. }
  286. // if there is a csc config name in the app settings, this config name even overloads the
  287. // setting we have right now
  288. string refWebRet = TryGetAppSetting(StorageAccountInfo.CSConfigStringPrefix + configurationSetting);
  289. if (!string.IsNullOrEmpty(refWebRet))
  290. {
  291. cscRet = TryGetConfigurationSetting(refWebRet);
  292. if (!string.IsNullOrEmpty(cscRet))
  293. {
  294. ret = cscRet;
  295. }
  296. }
  297. }
  298. // if we could not retrieve any configuration string set return value to the default value
  299. if (string.IsNullOrEmpty(ret) && defaultValue != null)
  300. {
  301. ret = defaultValue;
  302. }
  303. if (string.IsNullOrEmpty(ret) && throwIfNull)
  304. {
  305. throw new ConfigurationErrorsException(
  306. string.Format(CultureInfo.InvariantCulture, "Cannot find configuration string {0}.", configurationSetting));
  307. }
  308. return ret;
  309. }
  310. /// <summary>
  311. /// Retrieves account information settings from configuration settings. First, the implementation checks for
  312. /// settings in an application config section of an app.config or Web.config file. These values are overwritten
  313. /// if the same settings appear in a .csdef file.
  314. /// The implementation also supports indirect settings. In this case, indirect settings overwrite all other settings.
  315. /// </summary>
  316. /// <param name="accountNameConfiguration">Configuration string for the account name.</param>
  317. /// <param name="accountSharedKeyConfiguration">Configuration string for the key.</param>
  318. /// <param name="endpointConfiguration">Configuration string for the endpoint.</param>
  319. /// <param name="usePathStyleUrisConfiguration">Configuration string for the path style.</param>
  320. /// <param name="allowIncompleteSettings">If false, an exception is thrown if not all settings are available.</param>
  321. /// <returns>StorageAccountInfo object containing the retrieved settings.</returns>
  322. public static StorageAccountInfo GetAccountInfoFromConfiguration(string accountNameConfiguration,
  323. string accountSharedKeyConfiguration,
  324. string endpointConfiguration,
  325. string usePathStyleUrisConfiguration,
  326. bool allowIncompleteSettings)
  327. {
  328. if (string.IsNullOrEmpty(endpointConfiguration))
  329. {
  330. throw new ArgumentException("Endpoint configuration is missing", "endpointConfiguration");
  331. }
  332. string endpoint = null;
  333. string name = null;
  334. string key = null;
  335. string pathStyle = null;
  336. name = TryGetAppSetting(accountNameConfiguration);
  337. key = TryGetAppSetting(accountSharedKeyConfiguration);
  338. endpoint = TryGetAppSetting(endpointConfiguration);
  339. pathStyle = TryGetAppSetting(usePathStyleUrisConfiguration);
  340. // settings in the csc file overload settings in Web.config
  341. if (RoleManager.IsRoleManagerRunning)
  342. {
  343. // get config settings from the csc file
  344. string cscName = TryGetConfigurationSetting(accountNameConfiguration);
  345. if (!string.IsNullOrEmpty(cscName))
  346. {
  347. name = cscName;
  348. }
  349. string cscKey = TryGetConfigurationSetting(accountSharedKeyConfiguration);
  350. if (!string.IsNullOrEmpty(cscKey))
  351. {
  352. key = cscKey;
  353. }
  354. string cscEndpoint = TryGetConfigurationSetting(endpointConfiguration);
  355. if (!string.IsNullOrEmpty(cscEndpoint))
  356. {
  357. endpoint = cscEndpoint;
  358. }
  359. string cscPathStyle = TryGetConfigurationSetting(usePathStyleUrisConfiguration);
  360. if (!string.IsNullOrEmpty(cscPathStyle))
  361. {
  362. pathStyle = cscPathStyle;
  363. }
  364. // the Web.config can have references to csc setting strings
  365. // these count event stronger than the direct settings in the csc file
  366. string refWebName = TryGetAppSetting(CSConfigStringPrefix + accountNameConfiguration);
  367. if (!string.IsNullOrEmpty(refWebName))
  368. {
  369. cscName = TryGetConfigurationSetting(refWebName);
  370. if (!string.IsNullOrEmpty(cscName))
  371. {
  372. name = cscName;
  373. }
  374. }
  375. string refWebKey = TryGetAppSetting(CSConfigStringPrefix + accountSharedKeyConfiguration);
  376. if (!string.IsNullOrEmpty(refWebKey))
  377. {
  378. cscKey = TryGetConfigurationSetting(refWebKey);
  379. if (!string.IsNullOrEmpty(cscKey))
  380. {
  381. key = cscKey;
  382. }
  383. }
  384. string refWebEndpoint = TryGetAppSetting(CSConfigStringPrefix + endpointConfiguration);
  385. if (!string.IsNullOrEmpty(refWebEndpoint))
  386. {
  387. cscEndpoint = TryGetConfigurationSetting(refWebEndpoint);
  388. if (!string.IsNullOrEmpty(cscEndpoint))
  389. {
  390. endpoint = cscEndpoint;
  391. }
  392. }
  393. string refWebPathStyle = TryGetAppSetting(CSConfigStringPrefix + usePathStyleUrisConfiguration);
  394. if (!string.IsNullOrEmpty(refWebPathStyle))
  395. {
  396. cscPathStyle = TryGetConfigurationSetting(refWebPathStyle);
  397. if (!string.IsNullOrEmpty(cscPathStyle))
  398. {
  399. pathStyle = cscPathStyle;
  400. }
  401. }
  402. }
  403. if (string.IsNullOrEmpty(key) && !allowIncompleteSettings)
  404. {
  405. throw new ArgumentException("No account key specified!");
  406. }
  407. if (string.IsNullOrEmpty(endpoint) && !allowIncompleteSettings)
  408. {
  409. throw new ArgumentException("No endpoint specified!");
  410. }
  411. if (string.IsNullOrEmpty(name))
  412. {
  413. // in this case let's try to derive the account name from the Uri
  414. string newAccountName = null;
  415. Uri newBaseUri = null;
  416. if (IsStandardStorageEndpoint(new Uri(endpoint), out newAccountName, out newBaseUri))
  417. {
  418. Debug.Assert((newAccountName != null && newBaseUri != null) || (newAccountName == null && newBaseUri == null));
  419. if (newAccountName != null && newBaseUri != null)
  420. {
  421. endpoint = newBaseUri.AbsoluteUri;
  422. name = newAccountName;
  423. }
  424. }
  425. if (string.IsNullOrEmpty(name) && !allowIncompleteSettings)
  426. {
  427. throw new ArgumentException("No account name specified.");
  428. }
  429. }
  430. bool? usePathStyleUris = null;
  431. if (!string.IsNullOrEmpty(pathStyle))
  432. {
  433. bool b;
  434. if (!bool.TryParse(pathStyle, out b))
  435. {
  436. throw new ConfigurationErrorsException("Cannot parse value of setting UsePathStyleUris as a boolean");
  437. }
  438. usePathStyleUris = b;
  439. }
  440. Uri tmpBaseUri = null;
  441. if (!string.IsNullOrEmpty(endpoint))
  442. {
  443. tmpBaseUri = new Uri(endpoint);
  444. }
  445. return new StorageAccountInfo(tmpBaseUri, usePathStyleUris, name, key, allowIncompleteSettings);
  446. }
  447. /// <summary>
  448. /// Checks whether all essential properties of this object are set. Only then, the account info object
  449. /// should be used in ohter APIs of this library.
  450. /// </summary>
  451. /// <returns></returns>
  452. public bool IsCompleteSetting()
  453. {
  454. return BaseUri != null && Base64Key != null && AccountName != null;
  455. }
  456. /// <summary>
  457. /// Checks whether this StorageAccountInfo object is complete in the sense that all properties are set.
  458. /// </summary>
  459. public void CheckComplete()
  460. {
  461. if (!IsCompleteSetting())
  462. {
  463. throw new ConfigurationErrorsException("Account information incomplete!");
  464. }
  465. }
  466. #region Private methods
  467. private static string TryGetConfigurationSetting(string configName)
  468. {
  469. string ret = null;
  470. try
  471. {
  472. ret = RoleManager.GetConfigurationSetting(configName);
  473. }
  474. catch (RoleException)
  475. {
  476. return null;
  477. }
  478. return ret;
  479. }
  480. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
  481. Justification = "Make sure that nothing prevents us to read from the fabric's configuration envrionment.")]
  482. private static string TryGetAppSetting(string configName)
  483. {
  484. string ret = null;
  485. try
  486. {
  487. ret = ConfigurationSettings.AppSettings[configName];
  488. }
  489. // some exception happened when accessing the app settings section
  490. // most likely this is because there is no app setting file
  491. // we assume that this is because there is no app settings file; this is not an error
  492. // and explicitly all exceptions are captured here
  493. catch (Exception)
  494. {
  495. return null;
  496. }
  497. return ret;
  498. }
  499. private static string GeneralAccountConfigurationExceptionString {
  500. get {
  501. return "If the portal defines http://test.blob.core.windows.net as your blob storage endpoint, the string \"test\" " +
  502. "is your account name, and you can specify http://blob.core.windows.net as the BlobStorageEndpoint in your " +
  503. "service's configuration file(s).";
  504. }
  505. }
  506. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
  507. private static bool IsStandardStorageEndpoint(Uri baseUri, out string newAccountName, out Uri newBaseUri) {
  508. if (baseUri == null) {
  509. throw new ArgumentNullException("baseUri");
  510. }
  511. newAccountName = null;
  512. newBaseUri = null;
  513. string host = baseUri.Host;
  514. if (string.IsNullOrEmpty(host)) {
  515. throw new ArgumentException("The host part of the Uri " + baseUri.AbsoluteUri + " must not be null or empty.");
  516. }
  517. if (host != host.ToLowerInvariant()) {
  518. throw new ArgumentException("The specified host string " + host + " must not contain upper-case letters.");
  519. }
  520. string suffix = null;
  521. if (host.EndsWith(StorageHttpConstants.StandardPortalEndpoints.TableStorageEndpoint, StringComparison.Ordinal)) {
  522. suffix = StorageHttpConstants.StandardPortalEndpoints.TableStorageEndpoint;
  523. }
  524. if (host.EndsWith(StorageHttpConstants.StandardPortalEndpoints.BlobStorageEndpoint, StringComparison.Ordinal))
  525. {
  526. suffix = StorageHttpConstants.StandardPortalEndpoints.BlobStorageEndpoint;
  527. }
  528. if (host.EndsWith(StorageHttpConstants.StandardPortalEndpoints.QueueStorageEndpoint, StringComparison.Ordinal))
  529. {
  530. suffix = StorageHttpConstants.StandardPortalEndpoints.QueueStorageEndpoint;
  531. }
  532. // a URL as presented on the portal was specified, lets find out whether it is in the correct format
  533. if (suffix != null) {
  534. int index = host.IndexOf(suffix, StringComparison.Ordinal);
  535. Debug.Assert(index != -1);
  536. if (index > 0) {
  537. string first = host.Substring(0, index);
  538. Debug.Assert(!string.IsNullOrEmpty(first));
  539. if (first[first.Length-1] != StorageHttpConstants.ConstChars.Dot[0]) {
  540. return false;
  541. }
  542. first = first.Substring(0, first.Length - 1);
  543. if (string.IsNullOrEmpty(first)) {
  544. throw new ArgumentException("The configured base URI " + baseUri.AbsoluteUri + " for the storage service is incorrect. " +
  545. GeneralAccountConfigurationExceptionString);
  546. }
  547. if (first.Contains(StorageHttpConstants.ConstChars.Dot)) {
  548. throw new ArgumentException("The configured base URI " + baseUri.AbsoluteUri + " for the storage service is incorrect. " +
  549. GeneralAccountConfigurationExceptionString);
  550. }
  551. newAccountName = first;
  552. newBaseUri = new Uri(baseUri.Scheme + Uri.SchemeDelimiter + suffix + baseUri.PathAndQuery);
  553. }
  554. return true;
  555. }
  556. return false;
  557. }
  558. #endregion
  559. }
  560. }